mirror of
https://github.com/msfjarvis/gitice
synced 2025-08-14 12:57:00 +05:30
commit
90cba9587f
6 changed files with 255 additions and 150 deletions
129
Cargo.lock
generated
129
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
39
src/cli.rs
Normal file
39
src/cli.rs
Normal file
|
@ -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,
|
||||
}
|
83
src/git.rs
Normal file
83
src/git.rs
Normal file
|
@ -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<String, PersistableRepo> = 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::<Vec<&str>>()[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<String, PersistableRepo> = 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(())
|
||||
}
|
145
src/main.rs
145
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<String, PersistableRepo> = 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::<Vec<&str>>()[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<String, PersistableRepo> = 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(())
|
||||
|
|
7
src/model.rs
Normal file
7
src/model.rs
Normal file
|
@ -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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue