diff --git a/.gitignore b/.gitignore index fd65bdd..7910302 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target -/tst/test -/tst/test.yaml +/src/test/test +/src/test/test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1bc9e..ea9b2be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,40 @@ All notable changes to this project will be documented in this file. +## [0.2.0] - 2023-07-07 + +### Bug Fixes + +- Made categories with only links possible + +### Features + +- [**breaking**] Put links in categories + +### Miscellaneous Tasks + +- Filled out Cargo.toml +- Added test.yaml to gitignore +- Fixed up code, roadmap for bump + +### Refactor + +- Simple code quality changes + +### Testing + +- Refactored testing, added tests dir + ## [0.1.2] - 2023-07-03 ### Features - Implemented quiet flag +### Miscellaneous Tasks + +- Bump to v0.1.2 + ## [0.1.1] - 2023-07-03 ### Bug Fixes @@ -24,6 +52,7 @@ All notable changes to this project will be documented in this file. ### Miscellaneous Tasks +- Bump v0.1.0, housekeeping, #8 from cafkafk/dev - Bump to v0.1.1 ## [0.1.0] - 2023-07-03 @@ -42,6 +71,7 @@ All notable changes to this project will be documented in this file. ### Miscellaneous Tasks +- Bump to 0.0.7 #7 from cafkafk/dev - Bump v0.1.0, housekeeping ### Refactor @@ -95,6 +125,7 @@ All notable changes to this project will be documented in this file. - Version bump to v0.0.3 - Moved install scripts to ./bin - Merge 0.0.6 +- Merge 0.0.6 #6 from cafkafk/dev - Bump to 0.0.7 ### Refactor diff --git a/Cargo.lock b/Cargo.lock index 9415f31..a07e1d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,7 +179,7 @@ dependencies = [ [[package]] name = "gg" -version = "0.1.2" +version = "0.2.0" dependencies = [ "clap", "clap_mangen", diff --git a/Cargo.toml b/Cargo.toml index 2af5769..b882576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,30 @@ [package] name = "gg" -version = "0.1.2" -edition = "2021" +version = "0.2.0" authors = ["Christina Sørensen"] +edition = "2021" +rust-version = "1.72.0" +description = "A Rust GitOps/symlinkfarm orchestrator inspired by GNU Stow." +documentation = "https://github.com/cafkafk/gg" +readme = "./README.org" +homepage = "https://github.com/cafkafk/gg" repository = "https://github.com/cafkafk/gg" license = "GPL-3.0-only" +keywords = ["git", "declarative", "cli", "devops", "terminal"] +categories = ["command-line-interface", "command-line-utilities"] +# workspace = "idk, I have no idea how to use this" +# build = "build.rs" +# links = "git2" +# exclude = "./vacation_photos" +# include = "./gg_memes" +publish = false +# metadata +# deafult-run +# autobins +# autoexamples +# autotests +# autobenches +# resolver # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/doc/roadmap.org b/doc/roadmap.org index 194466f..159ff0e 100644 --- a/doc/roadmap.org +++ b/doc/roadmap.org @@ -1,7 +1,9 @@ #+title: Roadmap * 0.2.0 (maybe) -- [ ] Links in categories? +- [X] Links in categories? +- [X] Fix category with no links +- [-] Refactor * 0.1.2 - [X] Implement Quiet flag * 0.1.1 diff --git a/src/git.rs b/src/git.rs index 8734bc3..cfb237c 100644 --- a/src/git.rs +++ b/src/git.rs @@ -54,16 +54,12 @@ pub enum RepoFlags { /// Represents the config.toml file. /// /// For diagrams of the underlying architecture, consult ARCHITECHTURE.md -/// -/// #[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] pub struct Config { /// map of all categories /// /// Key should conceptually be seen as the name of the category. pub categories: HashMap, - /// A vector containing links - pub links: Vec, } /// Represents a category of repositories @@ -73,16 +69,22 @@ pub struct Config { pub struct Category { #[serde(skip_serializing_if = "Option::is_none")] pub flags: Option>, // FIXME: not implemented - /// map of all categories + /// map of all repos in category /// /// Key should conceptually be seen as the name of the category. #[serde(skip_serializing_if = "Option::is_none")] pub repos: Option>, + + /// map of all links in category + /// + /// Key should conceptually be seen as the name of the category. + #[serde(skip_serializing_if = "Option::is_none")] + pub links: Option>, } /// Contain fields for a single link. #[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] -pub struct Links { +pub struct Link { /// The name of the link pub name: String, pub rx: String, @@ -115,7 +117,7 @@ pub struct SeriesItem<'series> { //////////////////////////////////// //////////////////////////////////// -fn handle_file_exists(selff: &Links, tx_path: &Path, rx_path: &Path) { +fn handle_file_exists(selff: &Link, tx_path: &Path, rx_path: &Path) -> bool { match rx_path.read_link() { Ok(file) if file.canonicalize().expect("failed to canonicalize file") @@ -125,22 +127,25 @@ fn handle_file_exists(selff: &Links, tx_path: &Path, rx_path: &Path) { "Linking {} -> {} failed: file already linked", &selff.tx, &selff.rx ); + false } Ok(file) => { error!( "Linking {} -> {} failed: link to different file exists", &selff.tx, &selff.rx ); + false } Err(error) => { error!("Linking {} -> {} failed: file exists", &selff.tx, &selff.rx); + false } } } -impl Links { +impl Link { /// Creates the link from the link struct - pub fn link(&self) { + pub fn link(&self) -> bool { let tx_path: &Path = std::path::Path::new(&self.tx); let rx_path: &Path = std::path::Path::new(&self.rx); match rx_path.try_exists() { @@ -150,14 +155,17 @@ impl Links { "Linking {} -> {} failed: broken symlink", &self.tx, &self.rx ); + false } Ok(false) => { symlink(&self.tx, &self.rx).expect("failed to create link"); + true } Err(error) => { error!("Linking {} -> {} failed: {}", &self.tx, &self.rx, error); + false } - }; + } } } @@ -305,7 +313,6 @@ impl GitRepo { } impl Config { - /* GIT RELATED */ /// Loads the configuration toml from a path in to the Config struct. pub fn new(path: &String) -> Self { debug!("initializing new Config struct"); @@ -320,15 +327,9 @@ impl Config { ) }) } - ////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////// /// Runs associated function on all repos in config /// - /// TODO: need to be made over a generic repo type - /// /// NOTE: currently unused - /// fn on_all(&self, f: F) where F: Fn(&GitRepo), @@ -339,28 +340,88 @@ impl Config { } } } + // /// Runs associated function on all repos in config + // fn on_all_spinner(&self, op: &str, f: F) + // where + // F: Fn(&GitRepo) -> bool, + // { + // for category in self.categories.values() { + // for (_, repo) in category.repos.as_ref().expect("failed to get repos").iter() { + // if !settings::QUIET.load(std::sync::atomic::Ordering::Relaxed) { + // let mut sp = Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op)); + // if f(repo) { + // sp.stop_and_persist(success_str(), format!("{}: {}", repo.name, op)); + // } else { + // sp.stop_and_persist(failure_str(), format!("{}: {}", repo.name, op)); + // } + // } else { + // f(repo); + // } + // } + // } + // } /// Runs associated function on all repos in config - /// - /// TODO: need to be made over a generic repo type - /// - /// - fn on_all_spinner(&self, op: &str, f: F) + fn on_all_repos_spinner(&self, op: &str, f: F) where F: Fn(&GitRepo) -> bool, { for category in self.categories.values() { - for (_, repo) in category.repos.as_ref().expect("failed to get repos").iter() { - if !settings::QUIET.load(std::sync::atomic::Ordering::Relaxed) { - let mut sp = Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op)); - if f(repo) { - sp.stop_and_persist(success_str(), format!("{}: {}", repo.name, op)); - } else { - sp.stop_and_persist(failure_str(), format!("{}: {}", repo.name, op)); + match category.repos.as_ref() { + Some(repos) => { + for repo in repos.values() { + if !settings::QUIET.load(std::sync::atomic::Ordering::Relaxed) { + let mut sp = + Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op)); + if f(repo) { + sp.stop_and_persist( + success_str(), + format!("{}: {}", repo.name, op), + ); + } else { + sp.stop_and_persist( + failure_str(), + format!("{}: {}", repo.name, op), + ); + } + } else { + f(repo); + } } - } else { - f(repo); } - } + None => continue, + }; + } + } + /// Runs associated function on all links in config + fn on_all_links_spinner(&self, op: &str, f: F) + where + F: Fn(&Link) -> bool, + { + for category in self.categories.values() { + match category.links.as_ref() { + Some(links) => { + for link in links.values() { + if !settings::QUIET.load(std::sync::atomic::Ordering::Relaxed) { + let mut sp = + Spinner::new(Spinners::Dots10, format!("{}: {}", link.name, op)); + if f(link) { + sp.stop_and_persist( + success_str(), + format!("{}: {}", link.name, op), + ); + } else { + sp.stop_and_persist( + failure_str(), + format!("{}: {}", link.name, op), + ); + } + } else { + f(link); + } + } + } + None => continue, + }; } } /// Runs associated function on all repos in config @@ -476,33 +537,61 @@ impl Config { } } } + pub fn get_repo(&self, cat_name: &str, repo_name: &str, f: F) + where + F: FnOnce(&GitRepo), + { + f(&self + .categories + .get(cat_name) + .expect("failed to get category") + .repos + .as_ref() + .expect("failed to get repo") + .get(repo_name) + .expect("failed to get category")) + } + pub fn get_link(&self, cat_name: &str, link_name: &str, f: F) + where + F: FnOnce(&Link), + { + f(&self + .categories + .get(cat_name) + .expect("failed to get category") + .links + .as_ref() + .expect("failed to get repo") + .get(link_name) + .expect("failed to get category")) + } ////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////// /// Tries to pull all repositories, skips if fail. pub fn pull_all(&self) { debug!("exectuting pull_all"); - self.on_all_spinner("pull", GitRepo::pull); + self.on_all_repos_spinner("pull", GitRepo::pull); } - /// Tries to clone all repositories, skips if fail. + /// Tries to clone all repossitories, skips if fail. pub fn clone_all(&self) { debug!("exectuting clone_all"); - self.on_all_spinner("clone", GitRepo::clone); + self.on_all_repos_spinner("clone", GitRepo::clone); } - /// Tries to add all work in all repositories, skips if fail. + /// Tries to add all work in all repossitories, skips if fail. pub fn add_all(&self) { debug!("exectuting clone_all"); - self.on_all_spinner("add", GitRepo::add_all); + self.on_all_repos_spinner("add", GitRepo::add_all); } - /// Tries to commit all repositories one at a time, skips if fail. + /// Tries to commit all repossitories one at a time, skips if fail. pub fn commit_all(&self) { debug!("exectuting clone_all"); - self.on_all_spinner("commit", GitRepo::commit); + self.on_all_repos_spinner("commit", GitRepo::commit); } - /// Tries to commit all repositories with msg, skips if fail. + /// Tries to commit all repossitories with msg, skips if fail. pub fn commit_all_msg(&self, msg: &str) { debug!("exectuting clone_all"); - self.on_all_spinner("commit", |repo| repo.commit_with_msg(msg)); + self.on_all_repos_spinner("commit", |repo| repo.commit_with_msg(msg)); } /// Tries to pull, add all, commit with msg "quick commit", and push all /// repositories, skips if fail. @@ -558,8 +647,6 @@ impl Config { /// Tries to link all repositories, skips if fail. pub fn link_all(&self) { debug!("exectuting link_all"); - for link in &self.links { - link.link(); - } + self.on_all_links_spinner("link", Link::link); } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..667db91 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +// #[allow(unused)] +// pub mod git; +// #[allow(unused)] +// mod settings; +// #[allow(unused)] +// mod utils; diff --git a/src/main.rs b/src/main.rs index 1503283..e830ccd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -126,7 +126,6 @@ mod config { fn init_config() { let _config = Config { categories: HashMap::new(), - links: vec![], }; } #[test] @@ -134,10 +133,10 @@ mod config { let default_category = Category { flags: Some(vec![]), repos: Some(HashMap::new()), + links: Some(HashMap::new()), }; let mut config = Config { categories: HashMap::new(), - links: vec![], }; config .categories @@ -192,23 +191,10 @@ mod config { let test_config = Config::new(&RelativePath::new("./src/test/test.yaml").to_string()); assert_eq!(config, test_config); } + #[allow(dead_code)] fn get_category<'cat>(config: &'cat Config, name: &'cat str) -> &'cat Category { config.categories.get(name).expect("failed to get category") } - fn get_repo(config: &Config, cat_name: &str, repo_name: &str, f: F) - where - F: FnOnce(&GitRepo), - { - f(config - .categories - .get(cat_name) - .expect("failed to get category") - .repos - .as_ref() - .expect("failed to get repo") - .get(repo_name) - .expect("failed to get category")) - } #[test] fn is_config_readable() { let root = current_dir().expect("failed to get current dir"); @@ -220,24 +206,20 @@ mod config { .expect("failed to turnn config into string"), ); - let flags = vec![Clone, Push]; - // FIXME not very extensive + let _flags = vec![Clone, Push]; + // NOTE not very extensive #[allow(clippy::bool_assert_comparison)] { - get_repo(&config, "config", "qmk_firmware", |repo| { + (&config).get_repo("config", "qmk_firmware", |repo| { assert_eq!(repo.name, "qmk_firmware"); assert_eq!(repo.path, "/home/ces/org/src/git/"); assert_eq!(repo.url, "git@github.com:cafkafk/qmk_firmware.git"); - }) - } - { - assert_eq!(config.links[0].name, "gg"); - assert_eq!(config.links[0].rx, "/home/ces/.config/gg"); - assert_eq!(config.links[0].tx, "/home/ces/.dots/gg"); - assert_eq!(config.links[1].name, "starship"); - assert_eq!(config.links[1].rx, "/home/ces/.config/starship.toml"); - assert_eq!(config.links[1].tx, "/home/ces/.dots/starship.toml"); - // FIXME doesn't check repoflags + }); + (&config).get_link("stuff", "gg", |link| { + assert_eq!(link.name, "gg"); + assert_eq!(link.tx, "/home/ces/.dots/gg"); + assert_eq!(link.rx, "/home/ces/.config/gg"); + }); } } } diff --git a/src/test/config.yaml b/src/test/config.yaml index 54dcaac..46994cb 100644 --- a/src/test/config.yaml +++ b/src/test/config.yaml @@ -36,10 +36,23 @@ categories: name: li path: /home/ces/org/src/git/ url: git@github.com:cafkafk/li.git -links: - - name: gg - rx: /home/ces/.config/gg - tx: /home/ces/.dots/gg - - name: starship - rx: /home/ces/.config/starship.toml - tx: /home/ces/.dots/starship.toml + links: + gg: + name: gg + rx: /home/ces/.config/gg + tx: /home/ces/.dots/gg + starship: + name: starship + rx: /home/ces/.config/starship.toml + tx: /home/ces/.dots/starship.toml + fluff: + flags: [] + links: + gg: + name: gg + rx: /home/ces/.config/gg + tx: /home/ces/.dots/gg + starship: + name: starship + rx: /home/ces/.config/starship.toml + tx: /home/ces/.dots/starship.toml diff --git a/src/test/test.yaml b/src/test/test.yaml deleted file mode 100644 index 56c844b..0000000 --- a/src/test/test.yaml +++ /dev/null @@ -1,53 +0,0 @@ -categories: - config: - flags: [] - repos: - starship: - name: starship - path: /home/ces/org/src/git/ - url: https://github.com/starship/starship.git - flags: - - Clone - - Push - qmk_firmware: - name: qmk_firmware - path: /home/ces/org/src/git/ - url: git@github.com:cafkafk/qmk_firmware.git - flags: - - Clone - - Push - stuff: - flags: [] - repos: - li: - name: li - path: /home/ces/org/src/git/ - url: git@github.com:cafkafk/li.git - gg: - name: gg - path: /home/ces/.dots/ - url: git@github.com:cafkafk/gg.git - utils: - repos: - gg: - name: gg - path: /home/ces/.dots/ - url: git@github.com:cafkafk/gg.git - flags: - - Clone - - Push - li: - name: li - path: /home/ces/org/src/git/ - url: git@github.com:cafkafk/li.git - flags: - - Clone - - Push - empty: {} -links: -- name: gg - rx: /home/ces/.config/gg - tx: /home/ces/.dots/gg -- name: starship - rx: /home/ces/.config/starship.toml - tx: /home/ces/.dots/starship.toml diff --git a/src/utils/strings.rs b/src/utils/strings.rs index b11eebc..55b28bf 100644 --- a/src/utils/strings.rs +++ b/src/utils/strings.rs @@ -77,17 +77,17 @@ pub const SUCCESS_STRING: &str = "SUCC"; pub const FAILURE_STRING: &str = "FAIL"; pub fn success_str() -> &'static str { - if !settings::EMOJIS.load(Ordering::Relaxed) { - SUCCESS_EMOJI - } else { + if settings::EMOJIS.load(Ordering::Relaxed) { SUCCESS_STRING + } else { + SUCCESS_EMOJI } } pub fn failure_str() -> &'static str { - if !settings::EMOJIS.load(Ordering::Relaxed) { - FAILURE_EMOJI - } else { + if settings::EMOJIS.load(Ordering::Relaxed) { FAILURE_STRING + } else { + FAILURE_EMOJI } } diff --git a/tests/main.rs b/tests/main.rs new file mode 100644 index 0000000..ddc61f5 --- /dev/null +++ b/tests/main.rs @@ -0,0 +1,201 @@ +#[test] +fn main() { + assert!(true); +} + +/* +#[cfg(test)] +mod config { + use gg::git::RepoFlags::{Clone, Push}; + use gg::git::{Category, Config, GitRepo, Link}; + use relative_path::RelativePath; + use std::collections::HashMap; + use std::env::current_dir; + use std::fs::File; + use std::io::prelude::*; + #[test] + fn init_config() { + let _config = Config { + categories: HashMap::new(), + }; + } + #[test] + fn init_config_populate() { + let default_category = Category { + flags: Some(vec![]), + repos: Some(HashMap::new()), + links: Some(HashMap::new()), + }; + let mut config = Config { + categories: HashMap::new(), + }; + config + .categories + .insert(format!("{}", 0).to_string(), default_category); + for i in 0..=5 { + config + .categories + .get_mut(&format!("{}", 0).to_string()) + .expect("category not found") + .repos + .as_mut() + .expect("failed to get repo") + .insert( + format!("{}", i).to_string(), + GitRepo { + name: "test repo".to_string(), + path: "/tmp".to_string(), + url: "https://github.com/cafkafk/gg".to_string(), + flags: Some(vec![Clone, Push]), + }, + ); + } + } + #[test] + fn read_config_populate() { + let _config = Config::new(&RelativePath::new("./src/test/config.yaml").to_string()); + } + #[test] + fn write_config() { + let root = current_dir().expect("failed to get current dir"); + let config = Config::new( + &RelativePath::new("./src/test/config.yaml") + .to_logical_path(&root) + .into_os_string() + .into_string() + .expect("failed to turn config into string"), + ); + + let mut test_file = File::create( + RelativePath::new("./src/test/test.yaml") + .to_logical_path(&root) + .into_os_string() + .into_string() + .expect("failed to turn config into string"), + ) + .expect("failed to create test file"); + let contents = serde_yaml::to_string(&config).expect("failed to turn config into string"); + test_file + .write_all(contents.as_bytes()) + .expect("failed to write contents of config into file"); + + let test_config = Config::new(&RelativePath::new("./src/test/test.yaml").to_string()); + assert_eq!(config, test_config); + } + #[allow(dead_code)] + fn get_category<'cat>(config: &'cat Config, name: &'cat str) -> &'cat Category { + config.categories.get(name).expect("failed to get category") + } + fn get_repo(config: &Config, cat_name: &str, repo_name: &str, f: F) + where + F: FnOnce(&GitRepo), + { + f(config + .categories + .get(cat_name) + .expect("failed to get category") + .repos + .as_ref() + .expect("failed to get repo") + .get(repo_name) + .expect("failed to get category")) + } + fn get_link(config: &Config, cat_name: &str, link_name: &str, f: F) + where + F: FnOnce(&Link), + { + f(config + .categories + .get(cat_name) + .expect("failed to get category") + .links + .as_ref() + .expect("failed to get repo") + .get(link_name) + .expect("failed to get category")) + } + #[test] + fn is_config_readable() { + let root = current_dir().expect("failed to get current dir"); + let config = Config::new( + &RelativePath::new("./src/test/config.yaml") + .to_logical_path(root) + .into_os_string() + .into_string() + .expect("failed to turnn config into string"), + ); + + let _flags = vec![Clone, Push]; + // FIXME not very extensive + #[allow(clippy::bool_assert_comparison)] + { + get_repo(&config, "config", "qmk_firmware", |repo| { + assert_eq!(repo.name, "qmk_firmware"); + assert_eq!(repo.path, "/home/ces/org/src/git/"); + assert_eq!(repo.url, "git@github.com:cafkafk/qmk_firmware.git"); + }); + get_link(&config, "stuff", "gg", |link| { + assert_eq!(link.name, "gg"); + assert_eq!(link.tx, "/home/ces/.dots/gg"); + assert_eq!(link.rx, "/home/ces/.config/gg"); + }); + } + /* + { + assert_eq!(config.links[0].name, "gg"); + assert_eq!(config.links[0].rx, "/home/ces/.config/gg"); + assert_eq!(config.links[0].tx, "/home/ces/.dots/gg"); + assert_eq!(config.links[1].name, "starship"); + assert_eq!(config.links[1].rx, "/home/ces/.config/starship.toml"); + assert_eq!(config.links[1].tx, "/home/ces/.dots/starship.toml"); + // FIXME doesn't check repoflags + }*/ + } +}*/ + +/* +#[cfg(test)] +mod repo_actions { + use gg::git::GitRepo; + use gg::relative_path::RelativePath; + use gg::std::env::current_dir; + use gg::std::process::Command; + #[test] + #[allow(clippy::redundant_clone)] + fn test_repo_actions() { + let test_repo_name: String = "test".to_string(); + let root = current_dir().unwrap(); + let test_repo_dir: String = RelativePath::new("./src/test") + .to_logical_path(&root) + .into_os_string() + .into_string() + .unwrap(); + let test_repo_url: String = "git@github.com:cafkafk/test.git".to_string(); + println!("{}", test_repo_dir); + let mut config = Config { + repos: vec![], + links: vec![], + }; + let repo = GitRepo { + name: test_repo_name.to_owned(), + path: test_repo_dir.to_owned(), + url: test_repo_url.to_owned(), + clone: true, + }; + config.repos.push(repo); + // BUG FIXME can't do this in flake + // should have a good alternative + // config.clone_all(); + // config.pull_all(); + for r in config.repos.iter() { + Command::new("touch") + .current_dir(&(r.path.to_owned() + &r.name)) + .arg("test") + .status() + .expect("failed to create test file"); + } + config.add_all(); + config.commit_all_msg(&"test".to_string()); + } +} +*/