Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
@@ -20,6 +20,10 @@ toml = "0.8.19"
|
||||
gitea-rs = { git = "https://git.front.kjuulh.io/kjuulh/gitea-rs", version = "1.22.1" }
|
||||
url = "2.5.2"
|
||||
octocrab = "0.39.0"
|
||||
dirs = "5.0.1"
|
||||
prost = "0.13.2"
|
||||
prost-types = "0.13.2"
|
||||
bytes = "1.7.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
|
14
crates/gitnow/proto/gitnow/v1/gitnow.proto
Normal file
14
crates/gitnow/proto/gitnow/v1/gitnow.proto
Normal file
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package gitnow.v1;
|
||||
|
||||
message Repositories {
|
||||
repeated Repository repositories = 1;
|
||||
}
|
||||
|
||||
message Repository {
|
||||
string provider = 1;
|
||||
string owner = 2;
|
||||
string repo_name= 3;
|
||||
string ssh_url = 4;
|
||||
}
|
96
crates/gitnow/src/cache.rs
Normal file
96
crates/gitnow/src/cache.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::{app::App, cache_codec::CacheCodecApp, config::Config, git_provider::Repository};
|
||||
|
||||
pub struct Cache {
|
||||
app: &'static App,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn new(app: &'static App) -> Self {
|
||||
Self { app }
|
||||
}
|
||||
|
||||
pub async fn update(&self, repositories: &[Repository]) -> anyhow::Result<()> {
|
||||
tracing::debug!(repository_len = repositories.len(), "storing repositories");
|
||||
|
||||
let location = self.app.config.get_cache_file_location()?;
|
||||
tracing::trace!("found cache location: {}", location.display());
|
||||
|
||||
if let Some(parent) = location.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let cache_content = self
|
||||
.app
|
||||
.cache_codec()
|
||||
.serialize_repositories(repositories)?;
|
||||
|
||||
let mut cache_file = tokio::fs::File::create(location)
|
||||
.await
|
||||
.context("failed to create cache file")?;
|
||||
cache_file
|
||||
.write_all(&cache_content)
|
||||
.await
|
||||
.context("failed to write cache content to file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> anyhow::Result<Option<Vec<Repository>>> {
|
||||
tracing::debug!("fetching repositories");
|
||||
|
||||
let location = self.app.config.get_cache_file_location()?;
|
||||
if !location.exists() {
|
||||
tracing::debug!(
|
||||
location = location.display().to_string(),
|
||||
"cache doesn't exist"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file = tokio::fs::read(location).await?;
|
||||
if file.is_empty() {
|
||||
tracing::debug!("cache file appears to be empty");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let repos = match self.app.cache_codec().deserialize_repositories(file) {
|
||||
Ok(repos) => repos,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = e.to_string(), "failed to deserialize repositories");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(repos))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CacheApp {
|
||||
fn cache(&self) -> Cache;
|
||||
}
|
||||
|
||||
impl CacheApp for &'static App {
|
||||
fn cache(&self) -> Cache {
|
||||
Cache::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CacheConfig {
|
||||
fn get_cache_location(&self) -> anyhow::Result<PathBuf>;
|
||||
fn get_cache_file_location(&self) -> anyhow::Result<PathBuf>;
|
||||
}
|
||||
|
||||
impl CacheConfig for Config {
|
||||
fn get_cache_location(&self) -> anyhow::Result<PathBuf> {
|
||||
Ok(self.settings.cache.location.clone())
|
||||
}
|
||||
|
||||
fn get_cache_file_location(&self) -> anyhow::Result<PathBuf> {
|
||||
Ok(self.get_cache_location()?.join("cache.proto"))
|
||||
}
|
||||
}
|
61
crates/gitnow/src/cache_codec.rs
Normal file
61
crates/gitnow/src/cache_codec.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::Context;
|
||||
use prost::Message;
|
||||
|
||||
use crate::{app::App, git_provider::Repository};
|
||||
|
||||
mod proto_codec {
|
||||
include!("gen/gitnow.v1.rs");
|
||||
}
|
||||
|
||||
pub struct CacheCodec {}
|
||||
|
||||
impl CacheCodec {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn serialize_repositories(&self, repositories: &[Repository]) -> anyhow::Result<Vec<u8>> {
|
||||
let mut codec_repos = proto_codec::Repositories::default();
|
||||
|
||||
for repo in repositories.iter().cloned() {
|
||||
codec_repos.repositories.push(proto_codec::Repository {
|
||||
provider: repo.provider,
|
||||
owner: repo.owner,
|
||||
repo_name: repo.repo_name,
|
||||
ssh_url: repo.ssh_url,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(codec_repos.encode_to_vec())
|
||||
}
|
||||
|
||||
pub fn deserialize_repositories(&self, content: Vec<u8>) -> anyhow::Result<Vec<Repository>> {
|
||||
let codex_repos = proto_codec::Repositories::decode(&mut Cursor::new(content))
|
||||
.context("failed to decode protobuf repositories")?;
|
||||
|
||||
let mut repos = Vec::new();
|
||||
|
||||
for codec_repo in codex_repos.repositories {
|
||||
repos.push(Repository {
|
||||
provider: codec_repo.provider,
|
||||
owner: codec_repo.owner,
|
||||
repo_name: codec_repo.repo_name,
|
||||
ssh_url: codec_repo.ssh_url,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(repos)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CacheCodecApp {
|
||||
fn cache_codec(&self) -> CacheCodec;
|
||||
}
|
||||
|
||||
impl CacheCodecApp for &'static App {
|
||||
fn cache_codec(&self) -> CacheCodec {
|
||||
CacheCodec::new()
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
use crate::{ app::App, projects_list::ProjectsListApp};
|
||||
use crate::{app::App, cache::CacheApp, projects_list::ProjectsListApp};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RootCommand {
|
||||
@@ -14,7 +14,16 @@ impl RootCommand {
|
||||
pub async fn execute(&mut self) -> anyhow::Result<()> {
|
||||
tracing::debug!("executing");
|
||||
|
||||
let repositories = self.app.projects_list().get_projects().await?;
|
||||
let repositories = match self.app.cache().get().await? {
|
||||
Some(repos) => repos,
|
||||
None => {
|
||||
let repositories = self.app.projects_list().get_projects().await?;
|
||||
|
||||
self.app.cache().update(&repositories).await?;
|
||||
|
||||
repositories
|
||||
}
|
||||
};
|
||||
|
||||
for repo in &repositories {
|
||||
tracing::info!("repo: {}", repo.to_rel_path().display());
|
||||
|
@@ -15,12 +15,22 @@ pub struct Config {
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Settings {
|
||||
#[serde(default)]
|
||||
cache: Cache,
|
||||
pub cache: Cache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Cache {
|
||||
location: Option<PathBuf>,
|
||||
pub location: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
location: home.join(".cache/gitnow"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
@@ -162,6 +172,9 @@ mod test {
|
||||
#[test]
|
||||
fn test_can_parse_config() -> anyhow::Result<()> {
|
||||
let content = r#"
|
||||
[settings.cache]
|
||||
location = ".cache/gitnow"
|
||||
|
||||
[[providers.github]]
|
||||
current_user = "kjuulh"
|
||||
access_token = "some-token"
|
||||
@@ -237,7 +250,7 @@ mod test {
|
||||
},
|
||||
settings: Settings {
|
||||
cache: Cache {
|
||||
location: Some(PathBuf::from("$HOME/.cache/gitnow/"))
|
||||
location: PathBuf::from(".cache/gitnow/")
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -262,7 +275,7 @@ mod test {
|
||||
gitea: vec![]
|
||||
},
|
||||
settings: Settings {
|
||||
cache: Cache { location: None }
|
||||
cache: Cache::default()
|
||||
}
|
||||
},
|
||||
config
|
||||
|
21
crates/gitnow/src/gen/gitnow.v1.rs
Normal file
21
crates/gitnow/src/gen/gitnow.v1.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// @generated
|
||||
// This file is @generated by prost-build.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Repositories {
|
||||
#[prost(message, repeated, tag="1")]
|
||||
pub repositories: ::prost::alloc::vec::Vec<Repository>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Repository {
|
||||
#[prost(string, tag="1")]
|
||||
pub provider: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub owner: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="3")]
|
||||
pub repo_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="4")]
|
||||
pub ssh_url: ::prost::alloc::string::String,
|
||||
}
|
||||
// @@protoc_insertion_point(module)
|
@@ -6,6 +6,8 @@ use commands::root::RootCommand;
|
||||
use config::Config;
|
||||
|
||||
mod app;
|
||||
mod cache;
|
||||
mod cache_codec;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod git_provider;
|
||||
|
Reference in New Issue
Block a user