use std::io;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{Shell, generate};
#[derive(Parser)]
#[command(name = "rsmultigit")]
#[command(version = concat!(env!("CARGO_PKG_VERSION"), " by ", env!("CARGO_PKG_AUTHORS")))]
#[command(about = "Manage multiple git repositories at once")]
#[command(help_template = "\
{about}
Usage: {usage}
Commands:
{subcommands}
Options:
-h, --help Print help
-V, --version Print version
Use `rsmultigit <command> --help` for more options.")]
pub struct Cli {
#[arg(long, global = true, default_value_t = false)]
pub terse: bool,
#[arg(long, global = true, default_value_t = false)]
pub no_header: bool,
#[arg(long, global = true, default_value_t = false)]
pub no_output: bool,
#[arg(short, long, global = true, default_value_t = false)]
pub verbose: bool,
#[arg(long, global = true, default_value_t = false)]
pub print_not: bool,
#[arg(long, global = true, default_value_t = false)]
pub no_stop: bool,
#[arg(short = 'j', long, global = true, default_value_t = 1)]
pub jobs: usize,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Age,
Authors,
Blame {
file: String,
},
Branch {
#[arg(value_enum)]
what: BranchWhat,
},
Build {
#[arg(value_enum)]
what: BuildWhat,
},
Checkout {
branch: String,
},
CheckSame {
#[arg(long, num_args = 1.., value_delimiter = ' ')]
checks: Vec<String>,
#[arg(long, num_args = 1.., value_delimiter = ' ')]
checks_re: Vec<String>,
#[arg(long, default_value_t = false)]
only_failed: bool,
#[arg(long, default_value_t = false)]
diff: bool,
#[arg(long, default_value_t = false)]
copy: bool,
#[arg(long, default_value_t = false)]
allow_empty: bool,
#[arg(long, default_value_t = false)]
fix_missing: bool,
},
Clean {
#[arg(value_enum)]
what: CleanWhat,
},
Commit {
#[arg(short, long)]
message: String,
},
Complete {
#[arg(value_enum)]
shell: Shell,
},
Config {
key: String,
},
ConfigExample,
Count {
#[arg(value_enum)]
what: CountWhat,
},
Diff,
Dirty,
Fetch,
Gc,
Grep {
regexp: String,
#[arg(short = 'l', long, default_value_t = false)]
files: bool,
},
LastTag,
ListRepos,
ListChecks,
Log {
#[arg(long, default_value_t = 10)]
count: u32,
},
Prune,
Pull {
#[arg(long, default_value_t = false)]
quiet: bool,
},
Push,
Remote,
Reset {
#[arg(value_enum)]
what: ResetWhat,
},
#[command(alias = "exec")]
Run {
#[arg(required = true, num_args = 1.., trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
Size,
Stash {
#[arg(value_enum)]
what: StashWhat,
},
Status,
SubmoduleUpdate,
Tag {
#[arg(value_enum)]
what: TagWhat,
},
Version,
}
#[derive(Clone, ValueEnum)]
pub enum CleanWhat {
Hard,
Soft,
Make,
Git,
Cargo,
}
#[derive(Clone, ValueEnum)]
pub enum CountWhat {
Dirty,
Untracked,
Synchronized,
}
#[derive(Clone, ValueEnum)]
pub enum BranchWhat {
Local,
Remote,
Github,
}
#[derive(Clone, ValueEnum)]
pub enum StashWhat {
Push,
Pop,
}
#[derive(Clone, ValueEnum)]
pub enum TagWhat {
Local,
Remote,
HasLocal,
HasRemote,
}
#[derive(Clone, ValueEnum)]
pub enum ResetWhat {
Hard,
Soft,
Mixed,
}
#[derive(Clone, ValueEnum)]
pub enum BuildWhat {
Bootstrap,
Pydmt,
Make,
VenvMake,
VenvPydmt,
PydmtBuildVenv,
Rsconstruct,
Cargo,
CargoPublish,
}
pub fn print_completions(shell: Shell) {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "rsmultigit", &mut io::stdout());
match shell {
Shell::Bash => print!("{}", CHECKS_COMPLETION_BASH),
Shell::Zsh => print!("{}", CHECKS_COMPLETION_ZSH),
_ => {}
}
}
const CHECKS_COMPLETION_BASH: &str = r#"
# rsmultigit: dynamic --checks completion (appended by `rsmultigit complete bash`)
if declare -F _rsmultigit >/dev/null; then
eval "$(declare -f _rsmultigit | sed '1 s/^_rsmultigit /_rsmultigit_clap /')"
_rsmultigit() {
local i cur prev
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
local in_checks=0
local saw_check_same=0
for ((i=1; i<COMP_CWORD; i++)); do
local w="${COMP_WORDS[i]}"
case "$w" in
check-same) saw_check_same=1 ;;
--checks) in_checks=1 ;;
--*) in_checks=0 ;;
esac
done
if [[ "$prev" == "--checks" ]]; then
in_checks=1
fi
if (( saw_check_same && in_checks )); then
local names
names=$(rsmultigit list-checks 2>/dev/null)
if [[ -n "$names" ]]; then
# shellcheck disable=SC2207
COMPREPLY=($(compgen -W "$names" -- "$cur"))
return 0
fi
fi
# Bash passes (cmd_name, current_word, previous_word) as "$@" to the
# completion function; clap's generated code reads them via $2/$3, so
# we must forward all args intact.
_rsmultigit_clap "$@"
}
fi
"#;
const CHECKS_COMPLETION_ZSH: &str = r#"
# rsmultigit: dynamic --checks completion (appended by `rsmultigit complete zsh`)
if (( ${+functions[_rsmultigit]} )); then
functions[_rsmultigit_clap]="${functions[_rsmultigit]}"
_rsmultigit() {
local prev=${words[$CURRENT-1]}
local seen_checks=0
local seen_check_same=0
local i
for ((i=1; i<CURRENT; i++)); do
case "${words[i]}" in
check-same) seen_check_same=1 ;;
--checks) seen_checks=1 ;;
--*) seen_checks=0 ;;
esac
done
if [[ "$prev" == "--checks" ]]; then
seen_checks=1
fi
if (( seen_check_same && seen_checks )); then
local -a names
names=(${(f)"$(rsmultigit list-checks 2>/dev/null)"})
if (( ${#names} )); then
_describe 'check name' names
return 0
fi
fi
_rsmultigit_clap "$@"
}
fi
"#;
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Cli {
Cli::parse_from(args)
}
#[test]
fn parse_count_dirty() {
let cli = parse(&["rsmultigit", "count", "dirty"]);
assert!(matches!(
cli.command,
Commands::Count {
what: CountWhat::Dirty
}
));
}
#[test]
fn parse_all_subcommands() {
let subcommands = [
"status",
"dirty",
"list-repos",
"age",
"authors",
"size",
"last-tag",
"pull",
"push",
"fetch",
"diff",
"remote",
"prune",
"gc",
"submodule-update",
"version",
];
for sub in subcommands {
let result = Cli::try_parse_from(["rsmultigit", sub]);
assert!(result.is_ok(), "subcommand {sub} should parse");
}
let clean_whats = ["hard", "soft", "make", "git", "cargo"];
for what in clean_whats {
let result = Cli::try_parse_from(["rsmultigit", "clean", what]);
assert!(result.is_ok(), "clean {what} should parse");
}
let count_whats = ["dirty", "untracked", "synchronized"];
for what in count_whats {
let result = Cli::try_parse_from(["rsmultigit", "count", what]);
assert!(result.is_ok(), "count {what} should parse");
}
let branch_whats = ["local", "remote", "github"];
for what in branch_whats {
let result = Cli::try_parse_from(["rsmultigit", "branch", what]);
assert!(result.is_ok(), "branch {what} should parse");
}
let tag_whats = ["local", "remote", "has-local", "has-remote"];
for what in tag_whats {
let result = Cli::try_parse_from(["rsmultigit", "tag", what]);
assert!(result.is_ok(), "tag {what} should parse");
}
let reset_whats = ["hard", "soft", "mixed"];
for what in reset_whats {
let result = Cli::try_parse_from(["rsmultigit", "reset", what]);
assert!(result.is_ok(), "reset {what} should parse");
}
let result = Cli::try_parse_from(["rsmultigit", "log"]);
assert!(result.is_ok(), "log should parse without args");
let result = Cli::try_parse_from(["rsmultigit", "log", "--count", "5"]);
assert!(result.is_ok(), "log --count 5 should parse");
let result = Cli::try_parse_from(["rsmultigit", "checkout", "main"]);
assert!(result.is_ok(), "checkout main should parse");
let result = Cli::try_parse_from(["rsmultigit", "commit", "-m", "test"]);
assert!(result.is_ok(), "commit -m test should parse");
let result = Cli::try_parse_from(["rsmultigit", "config", "user.email"]);
assert!(result.is_ok(), "config user.email should parse");
let result = Cli::try_parse_from(["rsmultigit", "blame", "README.md"]);
assert!(result.is_ok(), "blame README.md should parse");
let stash_whats = ["push", "pop"];
for what in stash_whats {
let result = Cli::try_parse_from(["rsmultigit", "stash", what]);
assert!(result.is_ok(), "stash {what} should parse");
}
let build_whats = [
"bootstrap",
"pydmt",
"make",
"venv-make",
"venv-pydmt",
"pydmt-build-venv",
"rsconstruct",
"cargo",
"cargo-publish",
];
for what in build_whats {
let result = Cli::try_parse_from(["rsmultigit", "build", what]);
assert!(result.is_ok(), "build {what} should parse");
}
let complete_shells = ["bash", "zsh", "fish", "elvish", "powershell"];
for shell in complete_shells {
let result = Cli::try_parse_from(["rsmultigit", "complete", shell]);
assert!(result.is_ok(), "complete {shell} should parse");
}
}
#[test]
fn parse_complete_bash() {
let cli = parse(&["rsmultigit", "complete", "bash"]);
match &cli.command {
Commands::Complete { shell } => {
assert!(matches!(shell, Shell::Bash));
}
_ => panic!("expected Complete"),
}
}
#[test]
fn parse_grep_with_regexp() {
let cli = parse(&["rsmultigit", "grep", "TODO"]);
match &cli.command {
Commands::Grep { regexp, files } => {
assert_eq!(regexp, "TODO");
assert!(!files);
}
_ => panic!("expected Grep"),
}
}
#[test]
fn parse_grep_with_files_flag() {
let cli = parse(&["rsmultigit", "grep", "--files", "TODO"]);
match &cli.command {
Commands::Grep { regexp, files } => {
assert_eq!(regexp, "TODO");
assert!(files);
}
_ => panic!("expected Grep"),
}
}
#[test]
fn parse_grep_with_short_files_flag() {
let cli = parse(&["rsmultigit", "grep", "-l", "TODO"]);
match &cli.command {
Commands::Grep { regexp, files } => {
assert_eq!(regexp, "TODO");
assert!(files);
}
_ => panic!("expected Grep"),
}
}
#[test]
fn parse_pull_quiet() {
let cli = parse(&["rsmultigit", "pull", "--quiet"]);
match &cli.command {
Commands::Pull { quiet } => assert!(quiet),
_ => panic!("expected Pull"),
}
}
#[test]
fn parse_global_flags() {
let cli = parse(&[
"rsmultigit",
"--terse",
"--no-output",
"--verbose",
"--print-not",
"--no-stop",
"count",
"dirty",
]);
assert!(cli.terse);
assert!(cli.no_output);
assert!(cli.verbose);
assert!(cli.print_not);
assert!(cli.no_stop);
}
#[test]
fn parse_jobs_flag() {
let cli = parse(&["rsmultigit", "-j", "4", "list-repos"]);
assert_eq!(cli.jobs, 4);
let cli = parse(&["rsmultigit", "--jobs", "8", "list-repos"]);
assert_eq!(cli.jobs, 8);
}
#[test]
fn default_jobs_is_one() {
let cli = parse(&["rsmultigit", "list-repos"]);
assert_eq!(cli.jobs, 1);
}
#[test]
fn parse_run_command() {
let cli = parse(&["rsmultigit", "run", "git", "status"]);
match &cli.command {
Commands::Run { command } => {
assert_eq!(command, &vec!["git", "status"]);
}
_ => panic!("expected Run"),
}
}
#[test]
fn parse_exec_command_alias() {
let cli = parse(&["rsmultigit", "exec", "ls", "-la"]);
match &cli.command {
Commands::Run { command } => {
assert_eq!(command, &vec!["ls", "-la"]);
}
_ => panic!("expected Run"),
}
}
#[test]
fn unknown_subcommand_fails() {
let result = Cli::try_parse_from(["rsmultigit", "nonexistent"]);
assert!(result.is_err());
}
}