diff --git a/Cargo.lock b/Cargo.lock index 0049ad6..498274b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,7 +128,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -186,7 +186,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -266,6 +266,27 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -466,11 +487,15 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bytes", "clap", + "dirs", "dotenv", "gitea-rs", "octocrab", "pretty_assertions", + "prost", + "prost-types", "serde", "tokio", "toml", @@ -727,6 +752,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -769,6 +803,16 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -993,6 +1037,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -1019,7 +1069,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1101,6 +1151,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.37" @@ -1119,6 +1201,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "reqwest" version = "0.12.7" @@ -2044,7 +2137,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2055,7 +2148,7 @@ checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ "windows-result", "windows-strings", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2064,7 +2157,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2074,7 +2167,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ "windows-result", - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] @@ -2083,7 +2185,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2092,7 +2194,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -2101,28 +2218,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2135,24 +2270,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..240fd97 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,10 @@ +version: v2 +managed: + enabled: true +plugins: + # dependencies +- remote: buf.build/community/neoeinstein-prost + out: crates/gitnow/src/gen + +inputs: + - directory: crates/gitnow/proto diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..5853bb2 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,4 @@ +version: v2 +modules: + - path: proto + name: buf.build/noschemaplz/gitnow diff --git a/crates/gitnow/Cargo.toml b/crates/gitnow/Cargo.toml index a1a080f..0ba6c4c 100644 --- a/crates/gitnow/Cargo.toml +++ b/crates/gitnow/Cargo.toml @@ -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" diff --git a/crates/gitnow/proto/gitnow/v1/gitnow.proto b/crates/gitnow/proto/gitnow/v1/gitnow.proto new file mode 100644 index 0000000..1e8bf57 --- /dev/null +++ b/crates/gitnow/proto/gitnow/v1/gitnow.proto @@ -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; +} diff --git a/crates/gitnow/src/cache.rs b/crates/gitnow/src/cache.rs new file mode 100644 index 0000000..9596627 --- /dev/null +++ b/crates/gitnow/src/cache.rs @@ -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>> { + 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; + fn get_cache_file_location(&self) -> anyhow::Result; +} + +impl CacheConfig for Config { + fn get_cache_location(&self) -> anyhow::Result { + Ok(self.settings.cache.location.clone()) + } + + fn get_cache_file_location(&self) -> anyhow::Result { + Ok(self.get_cache_location()?.join("cache.proto")) + } +} diff --git a/crates/gitnow/src/cache_codec.rs b/crates/gitnow/src/cache_codec.rs new file mode 100644 index 0000000..e5a6cd2 --- /dev/null +++ b/crates/gitnow/src/cache_codec.rs @@ -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> { + 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) -> anyhow::Result> { + 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() + } +} diff --git a/crates/gitnow/src/commands/root.rs b/crates/gitnow/src/commands/root.rs index 8005b73..1dad499 100644 --- a/crates/gitnow/src/commands/root.rs +++ b/crates/gitnow/src/commands/root.rs @@ -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()); diff --git a/crates/gitnow/src/config.rs b/crates/gitnow/src/config.rs index 904e5f1..3b31ded 100644 --- a/crates/gitnow/src/config.rs +++ b/crates/gitnow/src/config.rs @@ -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, + 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 diff --git a/crates/gitnow/src/gen/gitnow.v1.rs b/crates/gitnow/src/gen/gitnow.v1.rs new file mode 100644 index 0000000..2f60812 --- /dev/null +++ b/crates/gitnow/src/gen/gitnow.v1.rs @@ -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, +} +#[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) diff --git a/crates/gitnow/src/main.rs b/crates/gitnow/src/main.rs index e25a1b6..6146ae1 100644 --- a/crates/gitnow/src/main.rs +++ b/crates/gitnow/src/main.rs @@ -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;