use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::{ArgValueCandidates, ArgValueCompleter, PathCompleter};
use crate::completion;
#[derive(Debug, Parser)]
#[command(
name = "gwx",
// Derived from `git describe` at build time; see build.rs.
version = env!("GWX_VERSION"),
about = "A friendly git worktree manager",
long_about = "gwx creates, lists and navigates git worktrees, with automatic \
path layout and per-repository hooks configured in .gwx.toml.",
subcommand_required = true,
arg_required_else_help = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Add(AddArgs),
#[command(visible_alias = "ls")]
List(ListArgs),
Cd(CdArgs),
#[command(visible_alias = "rm")]
Remove(RemoveArgs),
Clean(CleanArgs),
Init(InitArgs),
ShellInit(ShellArgs),
Completion(ShellArgs),
#[command(hide = true)]
Man(ManArgs),
}
#[derive(Debug, clap::Args)]
pub struct ManArgs {
#[arg(long, value_name = "DIR")]
pub out_dir: std::path::PathBuf,
}
#[derive(Debug, clap::Args)]
pub struct AddArgs {
#[arg(add = ArgValueCandidates::new(completion::addable_branches))]
pub branch: String,
#[arg(
long,
value_name = "COMMIT-ISH",
add = ArgValueCandidates::new(completion::start_points)
)]
pub from: Option<String>,
#[arg(
long,
value_name = "PATH",
add = ArgValueCompleter::new(PathCompleter::dir())
)]
pub path: Option<String>,
#[arg(long)]
pub no_create: bool,
#[arg(long)]
pub no_hooks: bool,
#[arg(long)]
pub force: bool,
#[arg(short, long)]
pub quiet: bool,
}
#[derive(Debug, clap::Args)]
pub struct ListArgs {
#[arg(long)]
pub paths: bool,
#[arg(long, conflicts_with = "paths")]
pub plain: bool,
#[arg(long, conflicts_with = "paths")]
pub no_header: bool,
}
#[derive(Debug, clap::Args)]
pub struct CdArgs {
#[arg(add = ArgValueCandidates::new(completion::worktrees))]
pub name: Option<String>,
}
#[derive(Debug, clap::Args)]
pub struct RemoveArgs {
#[arg(add = ArgValueCandidates::new(completion::removable_worktrees))]
pub name: String,
#[arg(long)]
pub with_branch: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub no_hooks: bool,
}
#[derive(Debug, clap::Args)]
pub struct CleanArgs {
#[arg(long)]
pub with_branch: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub no_hooks: bool,
}
#[derive(Debug, clap::Args)]
pub struct InitArgs {
#[arg(long)]
pub force: bool,
}
#[derive(Debug, clap::Args)]
pub struct ShellArgs {
#[arg(value_enum)]
pub shell: Shell,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Shell {
Bash,
Zsh,
Fish,
}
impl Shell {
pub fn completer(self) -> &'static dyn clap_complete::env::EnvCompleter {
match self {
Shell::Bash => &clap_complete::env::Bash,
Shell::Zsh => &clap_complete::env::Zsh,
Shell::Fish => &clap_complete::env::Fish,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn add_parses_flags() {
let cli = Cli::parse_from(["gwx", "add", "feat/x", "--from", "main", "--quiet"]);
match cli.command {
Command::Add(args) => {
assert_eq!(args.branch, "feat/x");
assert_eq!(args.from.as_deref(), Some("main"));
assert!(args.quiet);
}
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn list_alias_works() {
assert!(matches!(
Cli::parse_from(["gwx", "ls"]).command,
Command::List(_)
));
assert!(matches!(
Cli::parse_from(["gwx", "rm", "feat/x"]).command,
Command::Remove(_)
));
}
#[test]
fn cd_without_name_targets_the_main_worktree() {
match Cli::parse_from(["gwx", "cd"]).command {
Command::Cd(args) => assert!(args.name.is_none()),
other => panic!("unexpected command: {other:?}"),
}
}
}