use clap::{Arg, ArgAction, Command, ValueHint};
use clap_complete::{Shell, generate};
use std::io::{self, Write};
pub fn dispatch(shell_raw: &str, extra: &[String]) -> i32 {
if !extra.is_empty() {
eprintln!("git-cli: error: expected `git-cli completion <bash|zsh>`");
return 1;
}
match shell_raw {
"bash" => generate_script(Shell::Bash),
"zsh" => generate_script(Shell::Zsh),
other => {
eprintln!("git-cli: error: unsupported completion shell '{other}'");
eprintln!("usage: git-cli completion <bash|zsh>");
1
}
}
}
fn generate_script(generator: Shell) -> i32 {
let mut command = build_command_model();
let bin_name = command.get_name().to_string();
if matches!(generator, Shell::Bash) {
let mut output = Vec::new();
generate(generator, &mut command, bin_name.clone(), &mut output);
let normalized = normalize_bash_completion(
String::from_utf8(output).expect("bash completion should be valid UTF-8"),
);
io::stdout()
.write_all(normalized.as_bytes())
.expect("failed to write bash completion");
return 0;
}
generate(generator, &mut command, bin_name, &mut io::stdout());
0
}
fn normalize_bash_completion(script: String) -> String {
script.replace("__subcmd__", "__")
}
fn build_command_model() -> Command {
Command::new("git-cli")
.version(env!("CARGO_PKG_VERSION"))
.about("Git helper CLI")
.disable_help_subcommand(true)
.subcommand(build_utils_group())
.subcommand(build_reset_group())
.subcommand(build_commit_group())
.subcommand(build_branch_group())
.subcommand(build_ci_group())
.subcommand(build_open_group())
.subcommand(Command::new("help").about("Display help message for git-cli"))
.subcommand(
Command::new("completion")
.about("Export shell completion script")
.arg(
Arg::new("shell")
.value_name("shell")
.value_parser(["bash", "zsh"])
.required(true),
),
)
}
fn build_utils_group() -> Command {
Command::new("utils")
.about("Utility helpers")
.subcommand(Command::new("zip").about("Create zip archive from HEAD"))
.subcommand(
Command::new("copy-staged")
.visible_alias("copy")
.about("Copy staged diff to clipboard")
.arg(
Arg::new("stdout")
.long("stdout")
.help("Print staged diff to stdout")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("print")
.short('p')
.long("print")
.help("Alias for --stdout")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("both")
.long("both")
.help("Print diff and copy it to clipboard")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("root").about("Jump to git root").arg(
Arg::new("shell")
.long("shell")
.help("Print shell command instead of plain output")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("commit-hash")
.visible_alias("hash")
.about("Resolve commit hash")
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(Command::new("help").about("Display help message for utils"))
}
fn build_reset_group() -> Command {
let count_arg = || Arg::new("count").value_name("count");
Command::new("reset")
.about("Reset helpers")
.subcommand(
Command::new("soft")
.about("Reset to HEAD~N (soft)")
.arg(count_arg()),
)
.subcommand(
Command::new("mixed")
.about("Reset to HEAD~N (mixed)")
.arg(count_arg()),
)
.subcommand(
Command::new("hard")
.about("Reset to HEAD~N (hard)")
.arg(count_arg()),
)
.subcommand(Command::new("undo").about("Undo last reset"))
.subcommand(Command::new("back-head").about("Checkout HEAD@{1}"))
.subcommand(Command::new("back-checkout").about("Return to previous branch"))
.subcommand(
Command::new("remote")
.about("Reset to remote branch")
.arg(
Arg::new("ref")
.long("ref")
.help("Remote ref in <remote>/<branch> form")
.value_name("ref"),
)
.arg(
Arg::new("remote")
.short('r')
.long("remote")
.help("Remote name")
.value_name("remote"),
)
.arg(
Arg::new("branch")
.short('b')
.long("branch")
.help("Remote branch name")
.value_name("branch"),
)
.arg(
Arg::new("no-fetch")
.long("no-fetch")
.help("Skip fetching remote refs")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("prune")
.long("prune")
.help("Run fetch with --prune")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("clean")
.long("clean")
.help("Run git clean -fd after reset")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("set-upstream")
.long("set-upstream")
.help("Set upstream to the target remote branch")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("yes")
.short('y')
.long("yes")
.help("Skip confirmation prompts")
.action(ArgAction::SetTrue),
),
)
.subcommand(Command::new("help").about("Display help message for reset"))
}
fn build_commit_group() -> Command {
Command::new("commit")
.about("Commit helpers")
.subcommand(
Command::new("context")
.about("Print commit context")
.arg(
Arg::new("stdout")
.long("stdout")
.help("Print report to stdout")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("both")
.long("both")
.help("Print report and write output file")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("no-color")
.long("no-color")
.help("Disable ANSI colors")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("include")
.long("include")
.help("Additional glob(s) to include")
.value_name("glob")
.num_args(1..),
),
)
.subcommand(
Command::new("context-json")
.visible_aliases(["context_json", "contextjson", "json"])
.about("Print commit context as JSON")
.arg(
Arg::new("stdout")
.long("stdout")
.help("Print JSON to stdout")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("both")
.long("both")
.help("Print JSON and write files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("pretty")
.long("pretty")
.help("Pretty-print JSON output")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("bundle")
.long("bundle")
.help("Write bundle files to output directory")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("out-dir")
.long("out-dir")
.help("Output directory for generated files")
.value_name("path"),
),
)
.subcommand(
Command::new("to-stash")
.visible_alias("stash")
.about("Create stash from commit")
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(Command::new("help").about("Display help message for commit"))
}
fn build_branch_group() -> Command {
Command::new("branch")
.about("Branch helpers")
.subcommand(
Command::new("cleanup")
.visible_alias("delete-merged")
.about("Delete merged branches")
.arg(
Arg::new("base")
.short('b')
.long("base")
.help("Base ref used to determine merged branches")
.value_name("base"),
)
.arg(
Arg::new("squash")
.short('s')
.long("squash")
.help("Include branches already applied via squash")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("remove-worktrees")
.short('w')
.long("remove-worktrees")
.help("Force-remove linked worktrees for candidate branches")
.action(ArgAction::SetTrue),
),
)
.subcommand(Command::new("help").about("Display help message for branch"))
}
fn build_ci_group() -> Command {
Command::new("ci")
.about("CI helpers")
.subcommand(
Command::new("pick")
.about("Cherry-pick into CI branch")
.arg(
Arg::new("remote")
.short('r')
.long("remote")
.help("Remote used for fetch/push")
.value_name("name"),
)
.arg(
Arg::new("no-fetch")
.long("no-fetch")
.help("Skip remote fetch before branch creation")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("force")
.short('f')
.long("force")
.help("Reset existing CI branch and force push")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("stay")
.long("stay")
.help("Stay on CI branch after push")
.action(ArgAction::SetTrue),
),
)
.subcommand(Command::new("help").about("Display help message for ci"))
}
fn build_open_group() -> Command {
Command::new("open")
.about("Open remote pages")
.subcommand(
Command::new("repo")
.about("Open repository page")
.arg(remotes_arg()),
)
.subcommand(
Command::new("branch")
.about("Open branch tree page")
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(
Command::new("default-branch")
.visible_alias("default")
.about("Open default branch tree page")
.arg(remotes_arg()),
)
.subcommand(
Command::new("commit")
.about("Open commit page")
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(
Command::new("compare")
.about("Open compare page")
.arg(Arg::new("from").value_name("from"))
.arg(Arg::new("to").value_name("to")),
)
.subcommand(
Command::new("pr")
.visible_aliases(["pull-request", "mr", "merge-request"])
.about("Open pull or merge request page")
.arg(Arg::new("id").value_name("id")),
)
.subcommand(
Command::new("pulls")
.visible_aliases(["prs", "merge-requests", "mrs"])
.about("Open pull or merge request list"),
)
.subcommand(
Command::new("issues")
.visible_alias("issue")
.about("Open issues list/page")
.arg(Arg::new("id").value_name("id")),
)
.subcommand(
Command::new("actions")
.visible_alias("action")
.about("Open actions page")
.arg(Arg::new("workflow").value_name("workflow")),
)
.subcommand(
Command::new("releases")
.visible_alias("release")
.about("Open releases list/page")
.arg(Arg::new("tag").value_name("tag")),
)
.subcommand(
Command::new("tags")
.visible_alias("tag")
.about("Open tags list/page")
.arg(Arg::new("tag").value_name("tag")),
)
.subcommand(
Command::new("commits")
.visible_alias("history")
.about("Open commit history page")
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(
Command::new("file")
.visible_alias("blob")
.about("Open file page")
.arg(
Arg::new("path")
.value_name("path")
.value_hint(ValueHint::FilePath),
)
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(
Command::new("blame")
.about("Open blame page")
.arg(
Arg::new("path")
.value_name("path")
.value_hint(ValueHint::FilePath),
)
.arg(Arg::new("ref").value_name("ref")),
)
.subcommand(Command::new("help").about("Display help message for open"))
}
fn remotes_arg() -> Arg {
Arg::new("remote").value_name("remote")
}