diff --git a/Cargo.lock b/Cargo.lock index c2534b6..f0587af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,14 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.33" @@ -55,16 +46,34 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "clap" -version = "2.33.3" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" dependencies = [ - "ansi_term", "atty", "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", "strsim", + "termcolor", "textwrap", "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -95,6 +104,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.17" @@ -115,6 +139,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "jobserver" version = "0.1.21" @@ -124,6 +158,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.79" @@ -204,6 +244,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -216,6 +262,30 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.24" @@ -265,9 +335,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -280,11 +350,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" dependencies = [ "unicode-width", ] @@ -322,6 +401,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + [[package]] name = "unicode-width" version = "0.1.8" @@ -351,6 +436,18 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "walkdir" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 56e1244..6d920b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" [dependencies] anyhow = "1.0.32" -clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } +clap = "3.0.0-beta.2" git2 = "0.13.11" serde = {version = "1.0.116", default-features = false, features = ["derive"] } serde_derive = "1.0.116" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..c3884cc --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,39 @@ +use clap::{crate_authors, crate_version, AppSettings, Clap}; + +#[derive(Clap)] +#[clap( + version = crate_version!(), + author = crate_authors!(), + setting = AppSettings::ColoredHelp, + setting = AppSettings::DeriveDisplayOrder, + setting = AppSettings::SubcommandRequiredElseHelp, +)] +pub(crate) struct Opts { + #[clap(subcommand)] + pub(crate) subcommand: SubCommand, +} + +#[derive(Clap)] +pub(crate) enum SubCommand { + Freeze(Freeze), + Thaw(Thaw), +} + +/// recursively find git repos and record their states into a lockfile +#[derive(Clap)] +#[clap(setting = AppSettings::ColoredHelp)] +pub(crate) struct Freeze { + /// directory to search and freeze repos from. + pub(crate) directory: String, +} + +/// takes the given +#[derive(Clap)] +#[clap(setting = AppSettings::ColoredHelp)] +pub(crate) struct Thaw { + /// directory to put cloned repos into. + pub(crate) directory: String, + /// the lockfile to restore repositories from. + #[clap(default_value = "gitice.lock")] + pub(crate) lockfile: String, +} diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..3aa83c0 --- /dev/null +++ b/src/git.rs @@ -0,0 +1,83 @@ +use crate::model::PersistableRepo; +use git2::Repository; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, + process::Command, +}; +use walkdir::WalkDir; + +pub(crate) fn freeze_repos(dir: &str) -> anyhow::Result<()> { + let mut repos: HashMap = HashMap::new(); + for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { + if entry.file_type().is_dir() { + let path = format!("{}/.git", entry.path().display()); + let git_dir = Path::new(&path); + + if git_dir.exists() { + let repo = Repository::open(git_dir)?; + if repo.is_empty()? { + continue; + } + + let head = repo.head()?; + if let Some(head) = head.name() { + if let Ok(upstream) = repo.branch_upstream_name(head) { + if let Ok(remote) = repo.find_remote( + // This is a rather ugly hack, but not sure how else to get the required name + // doesn't seem to work with the full name such as `refs/remotes/origin/master` + upstream.as_str().unwrap().split('/').collect::>()[2], + ) { + let path = entry + .path() + .strip_prefix(Path::new(dir))? + .to_str() + .unwrap() + .to_string(); + repos.insert( + path, + PersistableRepo { + remote_url: remote.url().unwrap_or("None").to_owned(), + head: head.to_owned(), + }, + ); + } + } + }; + } + }; + } + fs::write("gitice.lock", toml::to_string(&repos)?).expect("could not write to lockfile!"); + println!( + "Successfully generated lockfile with {} repos", + &repos.len() + ); + Ok(()) +} + +pub(crate) fn thaw_repos(dir: &str, lockfile: &str) -> anyhow::Result<()> { + let lockfile = fs::read_to_string(lockfile) + .unwrap_or_else(|_| panic!("unable to read lockfile from {}", lockfile)); + let repos: HashMap = toml::from_str(&lockfile)?; + + for (name, repo) in repos { + println!("Cloning {} from {}", &name, &repo.remote_url); + let output = Command::new("git") + .args(&[ + "clone", + &repo.remote_url, + PathBuf::from(&dir).join(&name).to_str().unwrap(), + ]) + .output() + .expect("Failed to run `git clone`. Perhaps git is not installed?"); + + if output.status.success() { + println!("Thawed {} successfully.", name) + } else { + println!("{}", std::str::from_utf8(&output.stderr)?) + } + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index f843c71..2311644 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,140 +1,19 @@ -use anyhow::anyhow; -use clap::{crate_version, App, AppSettings, Arg}; -use git2::Repository; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fs, - path::{Path, PathBuf}, - process::Command, -}; -use walkdir::WalkDir; +pub(crate) mod cli; +pub(crate) mod git; +pub(crate) mod model; -#[derive(Debug, Serialize, Deserialize)] -struct PersistableRepo { - pub(crate) remote_url: String, - pub(crate) head: String, -} +use clap::Clap; +use cli::Opts; +use cli::SubCommand; +use git::freeze_repos; +use git::thaw_repos; fn main() -> anyhow::Result<()> { - let matches = App::new("gitice") - .about("Command-line tool for backing up and restoring multiple Git repositories from a directory") - .version(crate_version!()) - .setting(AppSettings::ColoredHelp) - .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - App::new("freeze") - .about("Generate a gitice.lock file with all the repositories in the given directory") - .setting(AppSettings::ColoredHelp) - .args(&[Arg::with_name("directory") - .help("Directory to look for Git repos in") - .required(true) - .index(1)]), - ) - .subcommand( - App::new("thaw") - .about("Given a gitice.lock and a directory, clones back all the repositories from the lockfile in the directory") - .setting(AppSettings::ColoredHelp) - .args(&[ - Arg::with_name("directory") - .help("Directory to restore repositories in") - .required(true) - .index(1), - Arg::with_name("lockfile") - .help("The lockfile to restore repositories from") - .short("l") - .long("lockfile") - .required(false) - .default_value("gitice.lock") - ]), - ) - .get_matches(); + let opts = Opts::parse(); - match matches.subcommand() { - ("freeze", m) => freeze_repos(m.unwrap().value_of("directory").unwrap())?, - ("thaw", m) => { - let m = m.unwrap(); - thaw_repos( - m.value_of("directory").unwrap(), - m.value_of("lockfile").unwrap(), - )? - } - (cmd, _) => return Err(anyhow!("unknown subcommand: {}", cmd)), - } - - Ok(()) -} - -fn freeze_repos(dir: &str) -> anyhow::Result<()> { - let mut repos: HashMap = HashMap::new(); - for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { - if entry.file_type().is_dir() { - let path = format!("{}/.git", entry.path().display()); - let git_dir = Path::new(&path); - - if git_dir.exists() { - let repo = Repository::open(git_dir)?; - if repo.is_empty()? { - continue; - } - - let head = repo.head()?; - if let Some(head) = head.name() { - if let Ok(upstream) = repo.branch_upstream_name(head) { - if let Ok(remote) = repo.find_remote( - // This is a rather ugly hack, but not sure how else to get the required name - // doesn't seem to work with the full name such as `refs/remotes/origin/master` - upstream.as_str().unwrap().split('/').collect::>()[2], - ) { - let path = entry - .path() - .strip_prefix(Path::new(dir))? - .to_str() - .unwrap() - .to_string(); - repos.insert( - path, - PersistableRepo { - remote_url: remote.url().unwrap_or("None").to_owned(), - head: head.to_owned(), - }, - ); - } - } - }; - } - }; - } - fs::write("gitice.lock", toml::to_string(&repos)?).expect("could not write to lockfile!"); - println!( - "Successfully generated lockfile with {} repos", - &repos.len() - ); - Ok(()) -} - -fn thaw_repos(dir: &str, lockfile: &str) -> anyhow::Result<()> { - let lockfile = fs::read_to_string(lockfile) - .unwrap_or_else(|_| panic!("unable to read lockfile from {}", lockfile)); - let repos: HashMap = toml::from_str(&lockfile)?; - - for (name, repo) in repos { - println!("Cloning {} from {}", &name, &repo.remote_url); - let output = Command::new("git") - .args(&[ - "clone", - &repo.remote_url, - PathBuf::from(&dir).join(&name).to_str().unwrap(), - ]) - .output() - .expect("Failed to run `git clone`. Perhaps git is not installed?"); - - if output.status.success() { - println!("Thawed {} successfully.", name) - } else { - println!("{}", std::str::from_utf8(&output.stderr)?) - } + match opts.subcommand { + SubCommand::Freeze(p) => freeze_repos(&p.directory)?, + SubCommand::Thaw(p) => thaw_repos(&p.directory, &p.lockfile)?, } Ok(()) diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..a732ecc --- /dev/null +++ b/src/model.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct PersistableRepo { + pub(crate) remote_url: String, + pub(crate) head: String, +}