diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 26c81e89..48042a20 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -19,7 +19,7 @@ on: tags: - "v*" env: - BIN_NAME: git-stack + CRATE_NAME: git-stack jobs: create-release: name: create-release @@ -95,17 +95,19 @@ jobs: shell: bash run: | outdir="./target/${{ env.TARGET_DIR }}/release" - staging="${{ env.BIN_NAME }}-${{ needs.create-release.outputs.release_version }}-${{ matrix.target }}" + staging="${{ env.CRATE_NAME }}-${{ needs.create-release.outputs.release_version }}-${{ matrix.target }}" mkdir -p "$staging"/{complete,doc} cp {README.md,LICENSE-*} "$staging/" cp {CHANGELOG.md,docs/*} "$staging/doc/" if [ "${{ matrix.os }}" = "windows-2019" ]; then - cp "target/${{ matrix.target }}/release/${{ env.BIN_NAME }}.exe" "$staging/" + cp "target/${{ matrix.target }}/release/git-stack.exe" "$staging/" + cp "target/${{ matrix.target }}/release/git-branch-backup.exe" "$staging/" cd "$staging" 7z a "../$staging.zip" . echo "ASSET=$staging.zip" >> $GITHUB_ENV else - cp "target/${{ matrix.target }}/release/${{ env.BIN_NAME }}" "$staging/" + cp "target/${{ matrix.target }}/release/git-stack" "$staging/" + cp "target/${{ matrix.target }}/release/git-branch-backup" "$staging/" tar czf "$staging.tar.gz" -C "$staging" . echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV fi diff --git a/Cargo.lock b/Cargo.lock index 95512647..e4baddfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,8 +334,10 @@ dependencies = [ "ignore", "itertools", "log", + "maplit", "proc-exit", "serde", + "serde_json", "structopt", "toml", "treeline", @@ -552,6 +554,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matches" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index b5770a1d..3853db1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,9 +40,11 @@ env_logger = { version = "0.9", default-features = false, features = ["termcolor atty = "0.2" itertools = "0.10" serde = { version = "1", features = ["derive"] } +serde_json = "1" toml = "0.5" ignore = "0.4" bstr = "0.2" +maplit = "1" [dev-dependencies] git-fixture = { version = "0.1", path = "crates/git-fixture" } diff --git a/src/backup/backup.rs b/src/backup/backup.rs new file mode 100644 index 00000000..2776b784 --- /dev/null +++ b/src/backup/backup.rs @@ -0,0 +1,91 @@ +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Backup { + pub branches: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")] + pub metadata: std::collections::BTreeMap, +} + +impl Backup { + pub fn load(path: &std::path::Path) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + let b = serde_json::from_reader(reader)?; + Ok(b) + } + + pub fn save(&self, path: &std::path::Path) -> Result<(), std::io::Error> { + let s = serde_json::to_string_pretty(self)?; + std::fs::write(path, &s)?; + Ok(()) + } + + pub fn from_repo(repo: &dyn crate::git::Repo) -> Result { + let mut branches: Vec<_> = repo + .local_branches() + .map(|b| { + let commit = repo.find_commit(b.id).unwrap(); + Branch { + name: b.name, + id: b.id, + metadata: maplit::btreemap! { + "summary".to_owned() => serde_json::Value::String( + String::from_utf8_lossy(commit.summary.as_slice()).into_owned() + ), + }, + } + }) + .collect(); + branches.sort_unstable(); + let metadata = Default::default(); + Ok(Self { branches, metadata }) + } + + pub fn apply(&self, repo: &mut dyn crate::git::Repo) -> Result<(), git2::Error> { + for branch in self.branches.iter() { + log::debug!("Restoring {}", branch.name); + repo.branch(&branch.name, branch.id)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Branch { + pub name: String, + #[serde(serialize_with = "serialize_oid")] + #[serde(deserialize_with = "deserialize_oid")] + pub id: git2::Oid, + #[serde(default)] + #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")] + pub metadata: std::collections::BTreeMap, +} + +fn serialize_oid(id: &git2::Oid, serializer: S) -> Result +where + S: serde::Serializer, +{ + let id = id.to_string(); + serializer.serialize_str(&id) +} + +fn deserialize_oid<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + let s = String::deserialize(deserializer)?; + git2::Oid::from_str(&s).map_err(serde::de::Error::custom) +} + +impl PartialOrd for Branch { + fn partial_cmp(&self, other: &Self) -> Option { + Some((&self.name, self.id).cmp(&(&other.name, other.id))) + } +} + +impl Ord for Branch { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (&self.name, self.id).cmp(&(&other.name, other.id)) + } +} diff --git a/src/backup/mod.rs b/src/backup/mod.rs new file mode 100644 index 00000000..a0369180 --- /dev/null +++ b/src/backup/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod backup; +mod stack; + +pub use backup::*; +pub use stack::*; diff --git a/src/backup/stack.rs b/src/backup/stack.rs new file mode 100644 index 00000000..a3749d46 --- /dev/null +++ b/src/backup/stack.rs @@ -0,0 +1,130 @@ +pub use super::Backup; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stack { + pub name: String, + root: std::path::PathBuf, + capacity: Option, +} + +impl Stack { + pub const DEFAULT_STACK: &'static str = "recent"; + const EXT: &'static str = "bak"; + + pub fn new(name: &str, repo: &crate::git::GitRepo) -> Self { + let root = stack_root(repo.raw().path(), name); + let name = name.to_owned(); + Self { + name, + root, + capacity: None, + } + } + + pub fn all(repo: &crate::git::GitRepo) -> impl Iterator { + let root = stacks_root(repo.raw().path()); + let mut stacks: Vec<_> = std::fs::read_dir(root) + .into_iter() + .flatten() + .filter_map(|e| { + let e = e.ok()?; + let e = e.file_type().ok()?.is_dir().then(|| e)?; + let p = e.path(); + let stack_name = p.file_name()?.to_str()?.to_owned(); + let stack_root = stack_root(repo.raw().path(), &stack_name); + Some(Self { + name: stack_name, + root: stack_root, + capacity: None, + }) + }) + .collect(); + if !stacks.iter().any(|v| v.name == Self::DEFAULT_STACK) { + stacks.insert(0, Self::new(Self::DEFAULT_STACK, repo)); + } + stacks.into_iter() + } + + pub fn capacity(&mut self, capacity: Option) { + self.capacity = capacity; + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + let mut elements: Vec<(usize, std::path::PathBuf)> = std::fs::read_dir(&self.root) + .into_iter() + .flatten() + .filter_map(|e| { + let e = e.ok()?; + let e = e.file_type().ok()?.is_file().then(|| e)?; + let p = e.path(); + let p = (p.extension()? == Self::EXT).then(|| p)?; + let index = p.file_stem()?.to_str()?.parse::().ok()?; + Some((index, p)) + }) + .collect(); + elements.sort_unstable(); + elements.into_iter().map(|(_, p)| p) + } + + pub fn push(&mut self, backup: Backup) -> Result { + let elems: Vec<_> = self.iter().collect(); + let last = elems.iter().last(); + let next_index = match last { + Some(last) => { + let current_index = last + .file_stem() + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap(); + current_index + 1 + } + None => 0, + }; + std::fs::create_dir_all(&self.root)?; + let new_path = self.root.join(format!("{}.{}", next_index, Self::EXT)); + backup.save(&new_path)?; + log::trace!("Backed up as {}", new_path.display()); + + if let Some(capacity) = self.capacity { + let len = elems.len(); + if capacity < len { + let remove = len - capacity; + log::warn!("Too many backups, clearing {} oldest", remove); + for backup_path in &elems[0..remove] { + if let Err(err) = std::fs::remove_file(&backup_path) { + log::trace!("Failed to remove {}: {}", backup_path.display(), err); + } else { + log::trace!("Removed {}", backup_path.display()); + } + } + } + } + + Ok(new_path) + } + + pub fn clear(&mut self) { + let _ = std::fs::remove_dir_all(&self.root); + } + + pub fn pop(&mut self) -> Option { + let mut elems: Vec<_> = self.iter().collect(); + let last = elems.pop()?; + std::fs::remove_file(&last).ok()?; + Some(last) + } + + pub fn peek(&mut self) -> Option { + self.iter().last() + } +} + +fn stacks_root(repo: &std::path::Path) -> std::path::PathBuf { + repo.join("branch-backup") +} + +fn stack_root(repo: &std::path::Path, stack: &str) -> std::path::PathBuf { + repo.join("branch-backup").join(stack) +} diff --git a/src/bin/git-branch-backup/args.rs b/src/bin/git-branch-backup/args.rs new file mode 100644 index 00000000..db1ff7db --- /dev/null +++ b/src/bin/git-branch-backup/args.rs @@ -0,0 +1,86 @@ +#[derive(structopt::StructOpt)] +#[structopt( + setting = structopt::clap::AppSettings::UnifiedHelpMessage, + setting = structopt::clap::AppSettings::DeriveDisplayOrder, + setting = structopt::clap::AppSettings::DontCollapseArgsInUsage + )] +pub struct Args { + #[structopt(subcommand)] + pub subcommand: Option, + + #[structopt(flatten)] + pub push: PushArgs, + + #[structopt(flatten)] + pub color: git_stack::color::ColorArgs, + + #[structopt(flatten)] + pub verbose: clap_verbosity_flag::Verbosity, +} + +#[derive(structopt::StructOpt)] +pub enum Subcommand { + /// Backup all branches + Push(PushArgs), + /// List all backups + List(ListArgs), + /// Clear all backups + Clear(ClearArgs), + /// Delete the last backup + Drop(DropArgs), + /// Apply the last backup, deleting it + Pop(PopArgs), + /// Apply the last backup + Apply(ApplyArgs), + /// List all backup stacks + Stacks(StacksArgs), +} + +#[derive(structopt::StructOpt)] +pub struct PushArgs { + /// Specify which backup stack to use + #[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)] + pub stack: String, + + /// Annotate the backup with the given message + #[structopt(short, long)] + pub message: Option, +} + +#[derive(structopt::StructOpt)] +pub struct ListArgs { + /// Specify which backup stack to use + #[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)] + pub stack: String, +} + +#[derive(structopt::StructOpt)] +pub struct ClearArgs { + /// Specify which backup stack to use + #[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)] + pub stack: String, +} + +#[derive(structopt::StructOpt)] +pub struct DropArgs { + /// Specify which backup stack to use + #[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)] + pub stack: String, +} + +#[derive(structopt::StructOpt)] +pub struct PopArgs { + /// Specify which backup stack to use + #[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)] + pub stack: String, +} + +#[derive(structopt::StructOpt)] +pub struct ApplyArgs { + /// Specify which backup stack to use + #[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)] + pub stack: String, +} + +#[derive(structopt::StructOpt)] +pub struct StacksArgs {} diff --git a/src/bin/git-branch-backup/main.rs b/src/bin/git-branch-backup/main.rs new file mode 100644 index 00000000..bf095cc8 --- /dev/null +++ b/src/bin/git-branch-backup/main.rs @@ -0,0 +1,215 @@ +#![allow(clippy::collapsible_else_if)] + +use std::io::Write; + +use proc_exit::WithCodeResultExt; +use structopt::StructOpt; + +mod args; + +fn main() { + human_panic::setup_panic!(); + let result = run(); + proc_exit::exit(result); +} + +fn run() -> proc_exit::ExitResult { + // clap's `get_matches` uses Failure rather than Usage, so bypass it for `get_matches_safe`. + let args = match args::Args::from_args_safe() { + Ok(args) => args, + Err(e) if e.use_stderr() => { + return Err(proc_exit::Code::USAGE_ERR.with_message(e)); + } + Err(e) => { + writeln!(std::io::stdout(), "{}", e)?; + return proc_exit::Code::SUCCESS.ok(); + } + }; + + let colored = args.color.colored().or_else(git_stack::color::colored_env); + let colored_stdout = colored + .or_else(git_stack::color::colored_stdout) + .unwrap_or(true); + let mut colored_stderr = colored + .or_else(git_stack::color::colored_stderr) + .unwrap_or(true); + if (colored_stdout || colored_stderr) && !yansi::Paint::enable_windows_ascii() { + colored_stderr = false; + } + + git_stack::log::init_logging(args.verbose.clone(), colored_stderr); + + let subcommand = args.subcommand; + let push_args = args.push; + match subcommand.unwrap_or(args::Subcommand::Push(push_args)) { + args::Subcommand::Push(sub_args) => push(sub_args), + args::Subcommand::List(sub_args) => list(sub_args), + args::Subcommand::Clear(sub_args) => clear(sub_args), + args::Subcommand::Drop(sub_args) => drop(sub_args), + args::Subcommand::Pop(sub_args) => pop(sub_args), + args::Subcommand::Apply(sub_args) => apply(sub_args), + args::Subcommand::Stacks(sub_args) => stacks(sub_args), + } +} + +fn push(args: args::PushArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git_stack::git::GitRepo::new(repo); + let mut stack = git_stack::backup::Stack::new(&args.stack, &repo); + + let repo_config = git_stack::config::RepoConfig::from_all(repo.raw()) + .with_code(proc_exit::Code::CONFIG_ERR)?; + let protected = git_stack::git::ProtectedBranches::new( + repo_config.protected_branches().iter().map(|s| s.as_str()), + ) + .with_code(proc_exit::Code::USAGE_ERR)?; + let branches = git_stack::git::Branches::new(repo.local_branches()); + let protected_branches = branches.protected(&protected); + + stack.capacity(repo_config.capacity()); + + let mut backup = + git_stack::backup::Backup::from_repo(&repo).with_code(proc_exit::Code::FAILURE)?; + if let Some(message) = args.message.as_deref() { + backup.metadata.insert( + "message".to_owned(), + serde_json::Value::String(message.to_owned()), + ); + } + for branch in backup.branches.iter_mut() { + if let Some(protected) = + git_stack::git::find_protected_base(&repo, &protected_branches, branch.id) + { + branch.metadata.insert( + "parent".to_owned(), + serde_json::Value::String(protected.name.clone()), + ); + } + } + stack.push(backup)?; + + Ok(()) +} + +fn list(args: args::ListArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git_stack::git::GitRepo::new(repo); + let stack = git_stack::backup::Stack::new(&args.stack, &repo); + + let backups: Vec<_> = stack.iter().collect(); + for backup_path in backups { + let backup = match git_stack::backup::Backup::load(&backup_path) { + Ok(backup) => backup, + Err(err) => { + log::error!("Failed to load backup {}: {}", backup_path.display(), err); + continue; + } + }; + match backup.metadata.get("message") { + Some(message) => { + writeln!(std::io::stdout(), "Message: {}", message)?; + } + None => { + writeln!(std::io::stdout(), "Path: {}", backup_path.display())?; + } + } + for branch in backup.branches.iter() { + let summary = if let Some(summary) = branch.metadata.get("summary") { + summary.to_string() + } else { + branch.id.to_string() + }; + let name = + if let Some(serde_json::Value::String(parent)) = branch.metadata.get("parent") { + format!("{}..{}", parent, branch.name) + } else { + branch.name.clone() + }; + writeln!(std::io::stdout(), "- {}: {}", name, summary)?; + } + writeln!(std::io::stdout())?; + } + + Ok(()) +} + +fn clear(args: args::ClearArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git_stack::git::GitRepo::new(repo); + let mut stack = git_stack::backup::Stack::new(&args.stack, &repo); + + stack.clear(); + + Ok(()) +} + +fn drop(args: args::DropArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git_stack::git::GitRepo::new(repo); + let mut stack = git_stack::backup::Stack::new(&args.stack, &repo); + + stack.pop(); + + Ok(()) +} + +fn pop(args: args::PopArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let mut repo = git_stack::git::GitRepo::new(repo); + let mut stack = git_stack::backup::Stack::new(&args.stack, &repo); + + match stack.peek() { + Some(last) => { + let backup = + git_stack::backup::Backup::load(&last).with_code(proc_exit::Code::FAILURE)?; + backup + .apply(&mut repo) + .with_code(proc_exit::Code::FAILURE)?; + let _ = std::fs::remove_file(&last); + } + None => { + log::warn!("Nothing to apply"); + } + } + + Ok(()) +} + +fn apply(args: args::ApplyArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let mut repo = git_stack::git::GitRepo::new(repo); + let mut stack = git_stack::backup::Stack::new(&args.stack, &repo); + + match stack.peek() { + Some(last) => { + let backup = + git_stack::backup::Backup::load(&last).with_code(proc_exit::Code::FAILURE)?; + backup + .apply(&mut repo) + .with_code(proc_exit::Code::FAILURE)?; + } + None => { + log::warn!("Nothing to apply"); + } + } + + Ok(()) +} + +fn stacks(_args: args::StacksArgs) -> proc_exit::ExitResult { + let cwd = std::env::current_dir().with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git2::Repository::discover(&cwd).with_code(proc_exit::Code::USAGE_ERR)?; + let repo = git_stack::git::GitRepo::new(repo); + + for stack in git_stack::backup::Stack::all(&repo) { + writeln!(std::io::stdout(), "{}", stack.name)?; + } + + Ok(()) +} diff --git a/src/bin/git-stack/args.rs b/src/bin/git-stack/args.rs index 67871366..34257d2c 100644 --- a/src/bin/git-stack/args.rs +++ b/src/bin/git-stack/args.rs @@ -73,6 +73,8 @@ impl Args { pull_remote: None, show_format: self.format, show_stacked: None, + + capacity: None, } } } diff --git a/src/bin/git-stack/stack.rs b/src/bin/git-stack/stack.rs index a07d6d0a..03893918 100644 --- a/src/bin/git-stack/stack.rs +++ b/src/bin/git-stack/stack.rs @@ -17,6 +17,7 @@ struct State { pull: bool, push: bool, dry_run: bool, + backup_capacity: Option, show_format: git_stack::config::Format, show_stacked: bool, @@ -43,6 +44,7 @@ impl State { ) .with_code(proc_exit::Code::CONFIG_ERR)?; let dry_run = args.dry_run; + let backup_capacity = repo_config.capacity(); let show_format = repo_config.show_format(); let show_stacked = repo_config.show_stacked(); @@ -154,6 +156,8 @@ impl State { pull, push, dry_run, + backup_capacity, + show_format, show_stacked, }) @@ -249,12 +253,31 @@ pub fn stack(args: &crate::args::Args, colored_stdout: bool) -> proc_exit::ExitR } } + const BACKUP_NAME: &str = "git-stack"; let mut success = true; if state.rebase { if state.repo.is_dirty() { return Err(proc_exit::Code::USAGE_ERR.with_message("Working tree is dirty, aborting")); } + let mut backups = git_stack::backup::Stack::new(BACKUP_NAME, &state.repo); + backups.capacity(state.backup_capacity); + let mut backup = git_stack::backup::Backup::from_repo(&state.repo) + .with_code(proc_exit::Code::FAILURE)?; + for branch in backup.branches.iter_mut() { + if let Some(protected) = git_stack::git::find_protected_base( + &state.repo, + &state.protected_branches, + branch.id, + ) { + branch.metadata.insert( + "parent".to_owned(), + serde_json::Value::String(protected.name.clone()), + ); + } + } + backups.push(backup)?; + let head_branch = state .repo .head_branch() @@ -292,6 +315,10 @@ pub fn stack(args: &crate::args::Args, colored_stdout: bool) -> proc_exit::ExitR show(&state, colored_stdout).with_code(proc_exit::Code::FAILURE)?; + if state.rebase { + log::info!("To undo, run `git branch-backup pop {}", BACKUP_NAME); + } + if !success { return proc_exit::Code::FAILURE.ok(); } diff --git a/src/config.rs b/src/config.rs index 867ec9e5..4776fae8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,6 +9,8 @@ pub struct RepoConfig { pub pull_remote: Option, pub show_format: Option, pub show_stacked: Option, + + pub capacity: Option, } static PROTECTED_STACK_FIELD: &str = "stack.protected-branch"; @@ -18,6 +20,8 @@ static PUSH_REMOTE_FIELD: &str = "stack.push-remote"; static PULL_REMOTE_FIELD: &str = "stack.pull-remote"; static FORMAT_FIELD: &str = "stack.show-format"; static STACKED_FIELD: &str = "stack.show-stacked"; +static BACKUP_CAPACITY_FIELD: &str = "branch-backup.capacity"; +const DEFAULT_CAPACITY: usize = 30; impl RepoConfig { pub fn from_all(repo: &git2::Repository) -> eyre::Result { @@ -122,6 +126,8 @@ impl RepoConfig { } } else if key == STACKED_FIELD { config.show_stacked = Some(value.as_ref().map(|v| v == "true").unwrap_or(true)); + } else if key == BACKUP_CAPACITY_FIELD { + config.capacity = value.as_deref().and_then(|s| s.parse::().ok()); } else { log::warn!( "Unsupported config: {}={}", @@ -153,6 +159,7 @@ impl RepoConfig { conf.pull_remote = Some(conf.pull_remote().to_owned()); conf.show_format = Some(conf.show_format()); conf.show_stacked = Some(conf.show_stacked()); + conf.capacity = Some(DEFAULT_CAPACITY); let mut protected_branches: Vec = Vec::new(); @@ -200,6 +207,10 @@ impl RepoConfig { .and_then(|s| FromStr::from_str(s).ok()); let show_stacked = config.get_bool(STACKED_FIELD).ok(); + let capacity = config + .get_i64(BACKUP_CAPACITY_FIELD) + .map(|i| i as usize) + .ok(); Self { protected_branches, @@ -208,6 +219,8 @@ impl RepoConfig { stack, show_format, show_stacked, + + capacity, } } @@ -243,6 +256,7 @@ impl RepoConfig { self.stack = other.stack.or(self.stack); self.show_format = other.show_format.or(self.show_format); self.show_stacked = other.show_stacked.or(self.show_stacked); + self.capacity = other.capacity.or(self.capacity); self } @@ -272,6 +286,11 @@ impl RepoConfig { pub fn show_stacked(&self) -> bool { self.show_stacked.unwrap_or(true) } + + pub fn capacity(&self) -> Option { + let capacity = self.capacity.unwrap_or(DEFAULT_CAPACITY); + (capacity != 0).then(|| capacity) + } } fn git_dir_config(repo: &git2::Repository) -> std::path::PathBuf { diff --git a/src/lib.rs b/src/lib.rs index 72362203..e81612da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate clap; +pub mod backup; pub mod color; pub mod config; pub mod git;