chore: merge 0.0.6 #6 from cafkafk/dev

Of major notice is:
- Categories
- Repo flags
- Architectural overhaul
- Increased documentation
- Improved UX

- Changed config.yaml location
- Increased scope of push field
- Remove potentially destructive operation
- Fixed mini-license typos
- Fixed testing with hashmap arch
- Spinner on all repoactions
- Fixed commit in quick
- [**breaking**] Fixed quick, fast messages
- Fixed commit with editor regression

- Architectural Overview
- Moved charts to doc/img
- Update image locations
- Moved ARCHITECTURE.md to doc/
- Added some documentation
- Added roadmap

- Started flakification
- Added nix flake #5
- [**breaking**] Add push field
- [**breaking**] Add repo flags
- [**breaking**] Implemented naive categories
- Started work on using spinners
- Added pull flag
- React to exit code of git
- Started adding multi instruction logic
- Added fast subcommand

- Version bump to v0.0.3
- Moved install scripts to ./bin

- Fixed various clippy errors
- Removed unused code from flake
- Improved GitRepo assoc. function debug
- Removed redundant line in Cargo.toml
- Created on_all for config struct
- Naive nested hashmap
- Generic refactor

- Removed atty dependency

- Removed unused ./test dir

- Mvp flake working

<!-- generated by git-cliff -->

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
This commit is contained in:
Christina Sørensen 2023-07-02 08:35:50 +00:00 committed by GitHub
commit 8476b343ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 713 additions and 159 deletions

69
Cargo.lock generated
View file

@ -118,7 +118,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -179,7 +179,7 @@ dependencies = [
[[package]] [[package]]
name = "gg" name = "gg"
version = "0.0.3" version = "0.0.6"
dependencies = [ dependencies = [
"clap", "clap",
"clap_mangen", "clap_mangen",
@ -188,6 +188,7 @@ dependencies = [
"relative-path", "relative-path",
"serde", "serde",
"serde_yaml", "serde_yaml",
"spinners",
] ]
[[package]] [[package]]
@ -253,6 +254,12 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.144" version = "0.2.144"
@ -274,6 +281,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"
@ -357,6 +370,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "rustversion"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.13" version = "1.0.13"
@ -380,7 +399,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -396,12 +415,56 @@ dependencies = [
"unsafe-libyaml", "unsafe-libyaml",
] ]
[[package]]
name = "spinners"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08615eea740067d9899969bc2891c68a19c315cb1f66640af9a9ecb91b13bcab"
dependencies = [
"lazy_static",
"maplit",
"strum",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.18" version = "2.0.18"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "gg" name = "gg"
version = "0.0.3" version = "0.0.6"
edition = "2021" edition = "2021"
authors = ["Christina Sørensen <christina@cafkafk.com>"] authors = ["Christina Sørensen <christina@cafkafk.com>"]
repository = "https://github.com/cafkafk/gg" repository = "https://github.com/cafkafk/gg"
@ -15,6 +15,7 @@ clap = { version = "4.0.22", features = ["derive"] }
log = "0.4" log = "0.4"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
relative-path = "1.8.0" relative-path = "1.8.0"
spinners = "4.1.0"
[build-dependencies] [build-dependencies]
clap = { version = "4.3.2", features = ["derive", "cargo", "env", "help"] } clap = { version = "4.3.2", features = ["derive", "cargo", "env", "help"] }

View file

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cargo rustc --release -- -C target-cpu=native cargo rustc --release -- -C target-cpu=native
cp ./target/release/gg ~/.local/bin cargo install --path .

3
bin/install_debug Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
# cargo rustc
cargo install --debug --path .

103
doc/ARCHITECTURE.org Normal file
View file

@ -0,0 +1,103 @@
#+title: Architecture
** Architecture
*** Config datastructure
There were 3 major designs considered so far (here in chronological order).
**** Vec Based
Code sketch in https://github.com/cafkafk/gg/commit/3d3b6d6646bda84333018cd621cd8bd6348b9cef
#+begin_src mermaid :file ./doc/img/config-struct-vec.png :width 4000px
flowchart LR
Co[config]
Ca["categories (vec<category>)"]
L[links]
Co ==> Ca & L
Ca ----> c1(category 1) & c2(category 2) & c3(category 3)
subgraph Categories Vec
c1(category 1) ==> flags1 & repos1("repos (vec<GitRepo>)")
c2(category 2) ==> flags2 & repos2("repos (vec<GitRepo>)")
c3(category 3) ==> flags3 & repos3("repos (vec<GitRepo>)")
direction TB
subgraph GitRepos
repos1 --> gr1 & gr2 & gr3
repos2 --> gr4 & gr5 & gr6
repos3 --> gr7 & gr8 & gr9
end
direction TB
subgraph Flags Enum
flags1 & flags2 & flags3 -. any of .- Push & Clone
end
end
#+end_src
#+RESULTS:
[[file:./doc/img/config-struct-vec.png]]
**** BTreeMap Based (nested)
#+begin_src mermaid :file ./doc/img/config-struct-nested.png :width 4000px
flowchart LR
Co[config]
Ca["categories (BTreeMap)"]
L[links]
Co ==> Ca & L
Ca -- "unique_name/key" --> c1(category 1) & c2(category 2) & c3(category 3)
subgraph Categories BTreeMap
c1(category 1) ==> flags1 & repos1("repos (BTreeMap)")
c2(category 2) ==> flags2 & repos2("repos (BTreeMap)")
c3(category 3) ==> flags3 & repos3("repos (BTreeMap)")
direction TB
subgraph GitRepos
repos1 -- "unique_name/key" --> gr1 & gr2 & gr3
repos2 -- "unique_name/key" --> gr4 & gr5 & gr6
repos3 -- "unique_name/key" --> gr7 & gr8 & gr9
end
direction TB
subgraph Flags Enum
flags1 & flags2 & flags3 -. any of .- Push & Clone
end
end
#+end_src
#+RESULTS:
[[file:./doc/img/config-struct-nested.png]]
**** BTreeMap Based (Store)
#+begin_src mermaid :file ./doc/img/config-struct-store.png :width 4000px
flowchart LR
S[(Store)]
subgraph Repo Store BMapTree
S -- "unique_name/key" ----> gr1 & gr2 & gr3
S -- "unique_name/key" ----> gr4 & gr5 & gr6
S -- "unique_name/key" ----> gr7 & gr8 & gr9
end
Co[config]
Ca["categories (BTreeMap)"]
L[links]
Co ==> Ca & L
Ca -- "unique_name/key" --> c1(category 1) & c2(category 2) & c3(category 3)
subgraph Categories BTreeMap
c1(category 1) ==> flags1 & repos1("repos (vec<keys>)")
c2(category 2) ==> flags2 & repos2("repos (vec<keys>)")
c3(category 3) ==> flags3 & repos3("repos (vec<keys>)")
direction TB
subgraph GitRepos
repos1 <-. "unique_name/key" .-> gr1 & gr2 & gr3 & gr4 & gr5 & gr6 & gr7 & gr8 & gr9
repos2 <-. "unique_name/key" .-> gr1 & gr2 & gr3 & gr4 & gr5 & gr6 & gr7 & gr8 & gr9
repos3 <-. "unique_name/key" .-> gr1 & gr2 & gr3 & gr4 & gr5 & gr6 & gr7 & gr8 & gr9
end
direction TB
subgraph Flags Enum
flags1 & flags2 & flags3 -. any of .- Push & Clone
end
end
#+end_src
#+RESULTS:
[[file:./doc/img/config-struct-store.png]]
**** Discussion

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

15
doc/roadmap.org Normal file
View file

@ -0,0 +1,15 @@
#+title: Roadmap
* Roadmap [0%] [0/4]
- [ ] Custom operation sequences
- [ ] Generic repositories
- [ ] Version pinning
- [ ] libgit2 (maybe)
* 0.1.0 [20%] [1/5]
- [X] No functionality regressions
- [X] commit works in quick, fast
- [X] commit with edit works
- [ ] Repo Flags Finished
- [ ] Category Flags Finished
- [ ] Optional Fields
- [ ] Subcommands

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
cargo rustc
cp ./target/debug/gg ~/.local/bin

View file

@ -13,6 +13,8 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/gpl-3.0.html. // along with this program. If not, see https://www.gnu.org/gpl-3.0.html.
//
//! Handles command line input
use crate::utils::dir::home_dir; use crate::utils::dir::home_dir;
use crate::utils::strings::INTERACTIVE_NOTICE; use crate::utils::strings::INTERACTIVE_NOTICE;
@ -75,6 +77,10 @@ pub enum Commands {
#[command(visible_alias = "q")] #[command(visible_alias = "q")]
Quick { msg: Option<String> }, Quick { msg: Option<String> },
/// Do fast pull-commit-push with msg for commit, skipping repo on failure
#[command(visible_alias = "f")]
Fast { msg: Option<String> },
/// Clone all repositories /// Clone all repositories
#[command(visible_alias = "c")] #[command(visible_alias = "c")]
Clone { msg: Option<String> }, Clone { msg: Option<String> },

View file

@ -13,24 +13,60 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/gpl-3.0.html. // along with this program. If not, see https://www.gnu.org/gpl-3.0.html.
//
//! Git repositories
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use spinners::{Spinner, Spinners};
use std::collections::HashMap;
use std::fs::canonicalize; use std::fs::canonicalize;
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
use std::path::Path; use std::path::Path;
use std::{fs, process::Command}; use std::{fs, process::Command};
/// An enum containing flags that change behaviour of repos and categories
#[derive(PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum RepoFlags {
/// If push is set, the repository should respond to the push subcommand
Push,
/// If clone is set, the repository should respond to the clone subcommand
Clone,
/// If pull is set, the repository should respond to the pull subcommand
Pull,
}
/// Represents the config.toml file. /// Represents the config.toml file.
///
/// For diagrams of the underlying architecture, consult ARCHITECHTURE.md
///
///
#[derive(PartialEq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub repos: Vec<GitRepo>, /// map of all categories
///
/// Key should conceptually be seen as the name of the category.
pub categories: HashMap<String, Category>,
/// A vector containing links
pub links: Vec<Links>, pub links: Vec<Links>,
} }
/// Represents a category of repositories
///
/// This allows you to organize your repositories into categories
#[derive(PartialEq, Debug, Serialize, Deserialize)]
pub struct Category {
pub flags: Vec<RepoFlags>, // FIXME: not implemented
/// map of all categories
///
/// Key should conceptually be seen as the name of the category.
pub repos: HashMap<String, GitRepo>,
}
/// Contain fields for a single link. /// Contain fields for a single link.
#[derive(PartialEq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
pub struct Links { pub struct Links {
/// The name of the link
pub name: String, pub name: String,
pub rx: String, pub rx: String,
pub tx: String, pub tx: String,
@ -42,9 +78,25 @@ pub struct GitRepo {
pub name: String, pub name: String,
pub path: String, pub path: String,
pub url: String, pub url: String,
pub clone: bool, pub flags: Vec<RepoFlags>,
} }
////////////////////////////////////
////////////////////////////////////
////////////////////////////////////
/// Represents a single operation on a repository
struct SeriesItem<'series> {
/// The string to be displayed to the user
operation: &'series str,
/// The closure representing the actual operation
closure: Box<dyn Fn(&GitRepo) -> (bool)>,
}
////////////////////////////////////
////////////////////////////////////
////////////////////////////////////
fn handle_file_exists(selff: &Links, tx_path: &Path, rx_path: &Path) { fn handle_file_exists(selff: &Links, tx_path: &Path, rx_path: &Path) {
match rx_path.read_link() { match rx_path.read_link() {
Ok(file) if file.canonicalize().unwrap() == tx_path.canonicalize().unwrap() => { Ok(file) if file.canonicalize().unwrap() == tx_path.canonicalize().unwrap() => {
@ -66,8 +118,8 @@ fn handle_file_exists(selff: &Links, tx_path: &Path, rx_path: &Path) {
} }
impl Links { impl Links {
/// Creates a link from a file /// Creates the link from the link struct
fn link(&self) { pub fn link(&self) {
let tx_path: &Path = std::path::Path::new(&self.tx); let tx_path: &Path = std::path::Path::new(&self.tx);
let rx_path: &Path = std::path::Path::new(&self.rx); let rx_path: &Path = std::path::Path::new(&self.rx);
match rx_path.try_exists() { match rx_path.try_exists() {
@ -90,81 +142,115 @@ impl Links {
impl GitRepo { impl GitRepo {
/// Clones the repository to its specified folder. /// Clones the repository to its specified folder.
fn clone(&self) { fn clone(&self) -> bool {
if self.clone { if self.flags.contains(&RepoFlags::Clone) {
// TODO: check if &self.name already exists in dir // TODO: check if &self.name already exists in dir
let out = Command::new("git") let output = Command::new("git")
.current_dir(&self.path) .current_dir(&self.path)
.arg("clone") .arg("clone")
.arg(&self.url) .arg(&self.url)
.arg(&self.name) .arg(&self.name)
.status() .output()
.unwrap_or_else(|_| panic!("git repo failed to clone: {:?}", &self,)); .unwrap_or_else(|_| panic!("git repo failed to clone: {:?}", &self,));
info!("{out}"); output.status.success()
} else { } else {
info!("{} has clone set to false, not cloned", &self.name); info!("{} has clone set to false, not cloned", &self.name);
false
} }
} }
/// Pulls the repository if able. /// Pulls the repository if able.
fn pull(&self) { fn pull(&self) -> bool {
let out = Command::new("git") if self.flags.contains(&RepoFlags::Pull) {
let output = Command::new("git")
.current_dir(format!("{}{}", &self.path, &self.name)) .current_dir(format!("{}{}", &self.path, &self.name))
.arg("pull") .arg("pull")
.status() .output()
.unwrap_or_else(|_| panic!("git repo failed to pull: {:?}", &self,)); .unwrap_or_else(|_| panic!("git repo failed to pull: {:?}", &self,));
info!("{out}"); output.status.success()
} else {
info!("{} has clone set to false, not pulled", &self.name);
false
}
} }
/// Adds all files in the repository. /// Adds all files in the repository.
fn add_all(&self) { fn add_all(&self) -> bool {
let out = Command::new("git") if self.flags.contains(&RepoFlags::Push) {
let output = Command::new("git")
.current_dir(format!("{}{}", &self.path, &self.name)) .current_dir(format!("{}{}", &self.path, &self.name))
.arg("add") .arg("add")
.arg(".") .arg(".")
.status() .output()
.unwrap_or_else(|_| panic!("git repo failed to add: {:?}", &self,)); .unwrap_or_else(|_| panic!("git repo failed to add: {:?}", &self,));
info!("{out}"); output.status.success()
} else {
info!("{} has clone set to false, not cloned", &self.name);
false
}
} }
/// Tries to commit changes in the repository. /// Tries to commit changes in the repository.
///
/// # Development
///
/// - FIXME: this prints extra information to terminal this is because we
/// use status() instead of output(), as that makes using the native editor
/// easy
#[allow(dead_code)] #[allow(dead_code)]
fn commit(&self) { fn commit(&self) -> bool {
let out = Command::new("git") if self.flags.contains(&RepoFlags::Push) {
let status = Command::new("git")
.current_dir(format!("{}{}", &self.path, &self.name)) .current_dir(format!("{}{}", &self.path, &self.name))
.arg("commit") .arg("commit")
.status() .status()
.unwrap_or_else(|_| panic!("git repo failed to commit: {:?}", &self,)); .unwrap_or_else(|_| panic!("git repo failed to commit: {:?}", &self,));
info!("{out}"); status.success()
} else {
info!("{} has push set to false, not cloned", &self.name);
false
}
} }
/// Tries to commit changes with a message argument. /// Tries to commit changes with a message argument.
fn commit_with_msg(&self, msg: &String) { fn commit_with_msg(&self, msg: &str) -> bool {
let out = Command::new("git") if self.flags.contains(&RepoFlags::Push) {
let output = Command::new("git")
.current_dir(format!("{}{}", &self.path, &self.name)) .current_dir(format!("{}{}", &self.path, &self.name))
.arg("commit") .arg("commit")
.arg("-m") .arg("-m")
.arg(msg) .arg(msg)
.status() .output()
.unwrap_or_else(|_| panic!("git repo failed to commit: {:?}", &self,)); .unwrap_or_else(|_| panic!("git repo failed to commit: {:?}", &self,));
info!("{out}"); output.status.success()
} else {
info!("{} has clone set to false, not cloned", &self.name);
false
}
} }
/// Attempts to push the repository. /// Attempts to push the repository.
fn push(&self) { fn push(&self) -> bool {
let out = Command::new("git") if self.flags.contains(&RepoFlags::Push) {
let output = Command::new("git")
.current_dir(format!("{}{}", &self.path, &self.name)) .current_dir(format!("{}{}", &self.path, &self.name))
.arg("push") .arg("push")
.status() .output()
.unwrap_or_else(|_| panic!("git repo failed to push: {:?}", &self,)); .unwrap_or_else(|_| panic!("git repo failed to push: {:?}", &self,));
info!("{out}"); output.status.success()
} else {
info!("{} has clone set to false, not cloned", &self.name);
false
} }
/// Removes repository }
/// Removes a repository (not implemented)
///
/// Kept here as a reminder that we probably shouldn't do this
fn remove(&self) -> Result<(), std::io::Error> { fn remove(&self) -> Result<(), std::io::Error> {
// https://doc.rust-lang.org/std/fs/fn.remove_dir_all.html // https://doc.rust-lang.org/std/fs/fn.remove_dir_all.html
unimplemented!("This seems to easy to missuse/exploit"); unimplemented!("This seems to easy to missuse/exploit");
fs::remove_dir_all(format!("{}{}", &self.path, &self.name)) // fs::remove_dir_all(format!("{}{}", &self.path, &self.name))
} }
} }
impl Config { impl Config {
/* GIT RELATED */ /* GIT RELATED */
/// Reads the configuration toml from a path. /// Loads the configuration toml from a path in to the Config struct.
pub fn new(path: &String) -> Self { pub fn new(path: &String) -> Self {
debug!("initializing new Config struct"); debug!("initializing new Config struct");
let yaml = fs::read_to_string(path).unwrap_or_else(|_| { let yaml = fs::read_to_string(path).unwrap_or_else(|_| {
@ -178,54 +264,230 @@ 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<F>(&self, f: F)
where
F: Fn(&GitRepo),
{
for (_, category) in self.categories.iter() {
for (_, repo) in category.repos.iter() {
f(repo);
}
}
}
/// Runs associated function on all repos in config
///
/// TODO: need to be made over a generic repo type
///
///
fn on_all_spinner<F>(&self, op: &str, f: F)
where
F: Fn(&GitRepo) -> bool,
{
for (_, category) in self.categories.iter() {
for (_, repo) in category.repos.iter() {
let mut sp =
Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op).into());
if f(repo) {
sp.stop_and_persist("", format!("{}: {}", repo.name, op).into());
} else {
sp.stop_and_persist("", format!("{}: {}", repo.name, op).into());
}
}
}
}
/// Runs associated function on all repos in config
///
/// TODO: need to be made over a generic repo type
///
/// # Current Problem
///
/// The goal of this function is that it should run some function on all
/// repos but stop executing further functions on any repo that fails,
/// without blocking the repos that don't have an issue.
///
/// This is actually somewhat hairy to do, at least at 6:16 am :S
///
/// However, at 6:24, we're so ready! Let's go!
///
/// Fun fact: only the last element of a tuple must have a dynamically typed size
///
/// # Usage
///
/// Here is an example of how an associated method could use this function.
///
/// ```
/// let series: Vec<SeriesItem> = vec![
/// SeriesItem {
/// operation: "pull",
/// closure: Box::new(move |repo: &GitRepo| repo.pull()),
/// },
/// SeriesItem {
/// operation: "add",
/// closure: Box::new(move |repo: &GitRepo| repo.add_all()),
/// },
/// SeriesItem {
/// operation: "commit",
/// closure: Box::new(move |repo: &GitRepo| repo.commit()),
/// },
/// SeriesItem {
/// operation: "push",
/// closure: Box::new(move |repo: &GitRepo| repo.push()),
/// },
/// ];
/// self.series_on_all(series);
/// ```
pub fn series_on_all(&self, closures: Vec<SeriesItem>) {
for (_, category) in self.categories.iter() {
for (_, repo) in category.repos.iter() {
for instruction in closures.iter() {
let f = &instruction.closure;
let op = instruction.operation;
let mut sp =
Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op).into());
if f(repo) {
sp.stop_and_persist("", format!("{}: {}", repo.name, op).into());
} else {
sp.stop_and_persist("", format!("{}: {}", repo.name, op).into());
break;
}
}
}
}
}
/// Runs associated function on all repos in config
///
/// Unlike `series_on_all`, this does not stop if it encounters an error
///
/// # Usage
///
/// Here is an example of how an associated method could use this function.
///
/// ```
/// let series: Vec<SeriesItem> = vec![
/// SeriesItem {
/// operation: "pull",
/// closure: Box::new(move |repo: &GitRepo| repo.pull()),
/// },
/// SeriesItem {
/// operation: "add",
/// closure: Box::new(move |repo: &GitRepo| repo.add_all()),
/// },
/// SeriesItem {
/// operation: "commit",
/// closure: Box::new(move |repo: &GitRepo| repo.commit()),
/// },
/// SeriesItem {
/// operation: "push",
/// closure: Box::new(move |repo: &GitRepo| repo.push()),
/// },
/// ];
/// self.all_on_all(series);
/// ```
pub fn all_on_all(&self, closures: Vec<SeriesItem>) {
for (_, category) in self.categories.iter() {
for (_, repo) in category.repos.iter() {
for instruction in closures.iter() {
let f = &instruction.closure;
let op = instruction.operation;
let mut sp =
Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op).into());
if f(repo) {
sp.stop_and_persist("", format!("{}: {}", repo.name, op).into());
} else {
sp.stop_and_persist("", format!("{}: {}", repo.name, op).into());
}
}
}
}
}
//////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
/// Tries to pull all repositories, skips if fail. /// Tries to pull all repositories, skips if fail.
pub fn pull_all(&self) { pub fn pull_all(&self) {
debug!("exectuting pull_all"); debug!("exectuting pull_all");
for r in self.repos.iter() { self.on_all_spinner("pull", |repo| repo.pull());
r.pull();
}
} }
/// Tries to clone all repositories, skips if fail. /// Tries to clone all repositories, skips if fail.
pub fn clone_all(&self) { pub fn clone_all(&self) {
debug!("exectuting clone_all"); debug!("exectuting clone_all");
for r in self.repos.iter() { self.on_all_spinner("clone", |repo| repo.clone());
r.clone();
}
} }
/// Tries to add all work in all repositories, skips if fail. /// Tries to add all work in all repositories, skips if fail.
pub fn add_all(&self) { pub fn add_all(&self) {
debug!("exectuting clone_all"); debug!("exectuting clone_all");
for r in self.repos.iter() { self.on_all_spinner("add", |repo| repo.add_all());
r.add_all();
}
} }
/// Tries to commit all repositories one at a time, skips if fail. /// Tries to commit all repositories one at a time, skips if fail.
pub fn commit_all(&self) { pub fn commit_all(&self) {
debug!("exectuting clone_all"); debug!("exectuting clone_all");
for r in self.repos.iter() { self.on_all_spinner("commit", |repo| repo.commit());
r.commit();
}
} }
/// Tries to commit all repositories with msg, skips if fail. /// Tries to commit all repositories with msg, skips if fail.
pub fn commit_all_msg(&self, msg: &String) { pub fn commit_all_msg(&self, msg: &str) {
debug!("exectuting clone_all"); debug!("exectuting clone_all");
for r in self.repos.iter() { self.on_all_spinner("commit", |repo| repo.commit_with_msg(msg));
r.commit_with_msg(msg);
}
} }
/// Tries to pull, add all, commit with msg "quick commit", and push all /// Tries to pull, add all, commit with msg "quick commit", and push all
/// repositories, skips if fail. /// repositories, skips if fail.
pub fn quick(&self, msg: &String) { pub fn quick(&self, msg: &'static str) {
debug!("exectuting quick"); debug!("exectuting quick");
for r in self.repos.iter() { let series: Vec<SeriesItem> = vec![
r.pull(); SeriesItem {
r.add_all(); operation: "pull",
r.commit_with_msg(msg); closure: Box::new(move |repo: &GitRepo| repo.pull()),
r.push(); },
SeriesItem {
operation: "add",
closure: Box::new(move |repo: &GitRepo| repo.add_all()),
},
SeriesItem {
operation: "commit",
closure: Box::new(move |repo: &GitRepo| repo.commit_with_msg(msg)),
},
SeriesItem {
operation: "push",
closure: Box::new(move |repo: &GitRepo| repo.push()),
},
];
self.all_on_all(series);
} }
/// Tries to pull, add all, commit with msg "quick commit", and push all
/// repositories, skips if fail.
pub fn fast(&self, msg: &'static str) {
debug!("exectuting fast");
let series: Vec<SeriesItem> = vec![
SeriesItem {
operation: "pull",
closure: Box::new(move |repo: &GitRepo| repo.pull()),
},
SeriesItem {
operation: "add",
closure: Box::new(move |repo: &GitRepo| repo.add_all()),
},
SeriesItem {
operation: "commit",
closure: Box::new(move |repo: &GitRepo| repo.commit()),
},
SeriesItem {
operation: "push",
closure: Box::new(move |repo: &GitRepo| repo.push()),
},
];
self.series_on_all(series);
} }
//////////////////////////////////////////////////////////////
/* LINK RELATED */ //////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
/// Tries to link all repositories, skips if fail. /// Tries to link all repositories, skips if fail.
pub fn link_all(&self) { pub fn link_all(&self) {
debug!("exectuting link_all"); debug!("exectuting link_all");

View file

@ -13,6 +13,23 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/gpl-3.0.html. // along with this program. If not, see https://www.gnu.org/gpl-3.0.html.
//
//! A Rust GitOps/symlinkfarm orchestrator inspired by GNU Stow.
//!
//! # What is?
//!
//! A Rust GitOps/symlinkfarm orchestrator inspired by GNU Stow. Useful for dealing
//! with "dotfiles", and with git support as a first class feature. Configuration is
//! done throug a single yaml file, giving it a paradigm that should bring joy to
//! those that use declarative operating systems and package managers.
//!
//! Although this isn't really a case where it matters *that* much for performance,
//! being written in rust instead of e.g. /janky/ scripting languages does also mean
//! it is snappy and reliable, and the /extensive/ testing helps ensure regressions
//! aren't introduced.
//!
//! That said, we're in 0.0.Z, *here be dragons* for now.
#![feature(unsized_tuple_coercion)]
extern crate log; extern crate log;
extern crate pretty_env_logger; extern crate pretty_env_logger;
@ -26,14 +43,20 @@ mod utils;
use cli::{Args, Commands}; use cli::{Args, Commands};
use git::Config; use git::Config;
use utils::strings::{FAST_COMMIT, QUICK_COMMIT};
use clap::Parser; use clap::Parser;
#[allow(unused)] #[allow(unused)]
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
/// The main loop of the binary
///
/// Here, we handle parsing the configuration file, as well as matching commands
/// to the relavant operations.
fn main() { fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
let args = Args::parse(); let mut args = Args::parse();
let config = Config::new(&args.config); let config = Config::new(&args.config);
match &args { match &args {
args if args.license => println!("{}", utils::strings::INTERACTIVE_LICENSE), args if args.license => println!("{}", utils::strings::INTERACTIVE_LICENSE),
@ -41,12 +64,27 @@ fn main() {
args if args.code_of_conduct => unimplemented!(), args if args.code_of_conduct => unimplemented!(),
_ => (), _ => (),
} }
match &args.command { match &mut args.command {
Some(Commands::Link { msg: _ }) => { Some(Commands::Link { msg: _ }) => {
config.link_all(); config.link_all();
} }
Some(Commands::Quick { msg }) => { Some(Commands::Quick { msg }) => {
config.quick(msg.as_ref().get_or_insert(&"gg: quick commit".to_string())); let s = Box::leak(
msg.as_mut()
.get_or_insert(&mut QUICK_COMMIT.to_string())
.clone()
.into_boxed_str(),
);
config.quick(s);
}
Some(Commands::Fast { msg }) => {
let s = Box::leak(
msg.as_mut()
.get_or_insert(&mut FAST_COMMIT.to_string())
.clone()
.into_boxed_str(),
);
config.fast(s);
} }
Some(Commands::Clone { msg: _ }) => { Some(Commands::Clone { msg: _ }) => {
config.clone_all(); config.clone_all();
@ -71,35 +109,51 @@ fn main() {
#[cfg(test)] #[cfg(test)]
mod config { mod config {
use crate::*; use crate::*;
use git::GitRepo; use git::RepoFlags::{Clone, Push};
use git::{Category, GitRepo};
use relative_path::RelativePath; use relative_path::RelativePath;
use std::collections::HashMap;
use std::env::current_dir; use std::env::current_dir;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
#[test] #[test]
fn init_config() { fn init_config() {
let _config = Config { let _config = Config {
repos: vec![], categories: HashMap::new(),
links: vec![], links: vec![],
}; };
} }
#[test] #[test]
fn init_config_populate() { fn init_config_populate() {
let default_category = Category {
flags: vec![],
repos: HashMap::new(),
};
let mut config = Config { let mut config = Config {
repos: vec![], categories: HashMap::new(),
links: vec![], links: vec![],
}; };
for _ in 0..=5 { config
let repo = GitRepo { .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
.insert(
format!("{}", i).to_string(),
GitRepo {
name: "test repo".to_string(), name: "test repo".to_string(),
path: "/tmp".to_string(), path: "/tmp".to_string(),
url: "https://github.com/cafkafk/gg".to_string(), url: "https://github.com/cafkafk/gg".to_string(),
clone: false, flags: vec![Clone, Push],
}; },
config.repos.push(repo); );
} }
let yaml = serde_yaml::to_string(&config).unwrap(); // let yaml = serde_yaml::to_string(&config).unwrap();
println!("{}", yaml); // println!("{}", yaml);
} }
#[test] #[test]
fn read_config_populate() { fn read_config_populate() {
@ -130,8 +184,23 @@ mod config {
let test_config = Config::new(&RelativePath::new("./src/test/test.yaml").to_string()); let test_config = Config::new(&RelativePath::new("./src/test/test.yaml").to_string());
assert_eq!(config, test_config); assert_eq!(config, test_config);
} }
fn get_category<'cat>(config: &'cat Config, name: &'cat str) -> &'cat Category {
config.categories.get(name).expect("failed to get category")
}
fn get_repo<F>(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
.get(repo_name)
.expect("failed to get category"))
}
#[test] #[test]
fn read_and_verify_config() { fn is_config_readable() {
let root = current_dir().unwrap(); let root = current_dir().unwrap();
let config = Config::new( let config = Config::new(
&RelativePath::new("./src/test/config.yaml") &RelativePath::new("./src/test/config.yaml")
@ -140,31 +209,16 @@ mod config {
.into_string() .into_string()
.unwrap(), .unwrap(),
); );
// FIXME This is unnecessarily terse
let flags = vec![Clone, Push];
// FIXME not very extensive
#[allow(clippy::bool_assert_comparison)] #[allow(clippy::bool_assert_comparison)]
{ {
assert_eq!(config.repos[0].name, "gg"); get_repo(&config, "config", "qmk_firmware", |repo| {
assert_eq!(config.repos[0].path, "/home/ces/.dots/"); assert_eq!(repo.name, "qmk_firmware");
assert_eq!(config.repos[0].url, "git@github.com:cafkafk/gg.git"); assert_eq!(repo.path, "/home/ces/org/src/git/");
assert_eq!(config.repos[0].clone, true); assert_eq!(repo.url, "git@github.com:cafkafk/qmk_firmware.git");
assert_eq!(config.repos[1].name, "li"); })
assert_eq!(config.repos[1].path, "/home/ces/org/src/git/");
assert_eq!(config.repos[1].url, "git@github.com:cafkafk/li.git");
assert_eq!(config.repos[1].clone, true);
assert_eq!(config.repos[2].name, "qmk_firmware");
assert_eq!(config.repos[2].path, "/home/ces/org/src/git/");
assert_eq!(
config.repos[2].url,
"git@github.com:cafkafk/qmk_firmware.git"
);
assert_eq!(config.repos[2].clone, true);
assert_eq!(config.repos[3].name, "starship");
assert_eq!(config.repos[3].path, "/home/ces/org/src/git/");
assert_eq!(
config.repos[3].url,
"https://github.com/starship/starship.git"
);
assert_eq!(config.repos[3].clone, true);
} }
{ {
assert_eq!(config.links[0].name, "gg"); assert_eq!(config.links[0].name, "gg");
@ -173,6 +227,7 @@ mod config {
assert_eq!(config.links[1].name, "starship"); assert_eq!(config.links[1].name, "starship");
assert_eq!(config.links[1].rx, "/home/ces/.config/starship.toml"); assert_eq!(config.links[1].rx, "/home/ces/.config/starship.toml");
assert_eq!(config.links[1].tx, "/home/ces/.dots/starship.toml"); assert_eq!(config.links[1].tx, "/home/ces/.dots/starship.toml");
// FIXME doesn't check repoflags
} }
} }
} }

View file

@ -1,20 +1,30 @@
repos: categories:
- name: gg config:
path: /home/ces/.dots/ flags: []
url: git@github.com:cafkafk/gg.git repos:
clone: true qmk_firmware:
- name: li name: qmk_firmware
path: /home/ces/org/src/git/
url: git@github.com:cafkafk/li.git
clone: true
- name: qmk_firmware
path: /home/ces/org/src/git/ path: /home/ces/org/src/git/
url: git@github.com:cafkafk/qmk_firmware.git url: git@github.com:cafkafk/qmk_firmware.git
clone: true flags: [Clone, Push]
- name: starship starship:
name: starship
path: /home/ces/org/src/git/ path: /home/ces/org/src/git/
url: https://github.com/starship/starship.git url: https://github.com/starship/starship.git
clone: true flags: [Clone, Push]
utils:
flags: []
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]
links: links:
- name: gg - name: gg
rx: /home/ces/.config/gg rx: /home/ces/.config/gg

View file

@ -1,20 +1,38 @@
repos: categories:
- name: gg utils:
path: /home/ces/.dots/ flags: []
url: git@github.com:cafkafk/gg.git repos:
clone: true li:
- name: li name: li
path: /home/ces/org/src/git/ path: /home/ces/org/src/git/
url: git@github.com:cafkafk/li.git url: git@github.com:cafkafk/li.git
clone: true flags:
- name: qmk_firmware - Clone
- Push
gg:
name: gg
path: /home/ces/.dots/
url: git@github.com:cafkafk/gg.git
flags:
- Clone
- Push
config:
flags: []
repos:
qmk_firmware:
name: qmk_firmware
path: /home/ces/org/src/git/ path: /home/ces/org/src/git/
url: git@github.com:cafkafk/qmk_firmware.git url: git@github.com:cafkafk/qmk_firmware.git
clone: true flags:
- name: starship - Clone
- Push
starship:
name: starship
path: /home/ces/org/src/git/ path: /home/ces/org/src/git/
url: https://github.com/starship/starship.git url: https://github.com/starship/starship.git
clone: true flags:
- Clone
- Push
links: links:
- name: gg - name: gg
rx: /home/ces/.config/gg rx: /home/ces/.config/gg

View file

@ -13,6 +13,8 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/gpl-3.0.html. // along with this program. If not, see https://www.gnu.org/gpl-3.0.html.
//
//! Sublibrary for useful functions
pub mod dir; pub mod dir;
pub mod strings; pub mod strings;

View file

@ -13,6 +13,8 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/gpl-3.0.html. // along with this program. If not, see https://www.gnu.org/gpl-3.0.html.
//
//! Nice helpers for dealing with filesystem environment.
#![feature(stmt_expr_attributes)] #![feature(stmt_expr_attributes)]
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
@ -20,6 +22,9 @@ use log::{debug, error, info, trace, warn};
use std::env; use std::env;
use std::path::Path; use std::path::Path;
/// Returns the users current dir
///
/// Does not work on Windows
pub fn current_dir() -> String { pub fn current_dir() -> String {
#[allow(deprecated)] // NOTE we don't care about windows , we don't support it #[allow(deprecated)] // NOTE we don't care about windows , we don't support it
env::current_dir() env::current_dir()
@ -29,6 +34,9 @@ pub fn current_dir() -> String {
.expect("Failed to turn home_dir into a valid string") .expect("Failed to turn home_dir into a valid string")
} }
/// Returns the users home dir
///
/// Does not work on Windows
pub fn home_dir() -> String { pub fn home_dir() -> String {
#[allow(deprecated)] // NOTE we don't care about windows , we don't support it #[allow(deprecated)] // NOTE we don't care about windows , we don't support it
env::home_dir() env::home_dir()

View file

@ -13,14 +13,19 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/gpl-3.0.html. // along with this program. If not, see https://www.gnu.org/gpl-3.0.html.
//
//! Module for chunk of text
//!
//! Ideally, at a VERY long term scale, this should be a nice pattern for
//! possible translations.
/// Contains the notice for interactive programs from the GPLv3's "How to Apply /// Contains the notice for interactive programs from the GPLv3's "How to Apply
/// These Terms to Your New Programs" /// These Terms to Your New Programs"
pub const INTERACTIVE_NOTICE: &str = "\ pub const INTERACTIVE_NOTICE: &str = "\
gg Copyright (C) 2023 Christina Sørensen <christina@cafkafk.com> gg Copyright (C) 2023 Christina Sørensen <christina@cafkafk.com>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `gg --warranty'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `gg --license' for details.
"; ";
/// Contains the license part of the long notice for interactive programs from /// Contains the license part of the long notice for interactive programs from
@ -40,3 +45,9 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
"; ";
/// Contains the message for quick commit subcommand
pub const QUICK_COMMIT: &str = "git: quick commit";
/// Contains the message for fast commit subcommand
pub const FAST_COMMIT: &str = "git: fast commit";