use clap::{Parser, Subcommand, ValueEnum};
use crate::config::CleanBranchMode;
use crate::utils::EditorType;
#[derive(Parser, Debug)]
#[command(name = "gwm")]
#[command(author = "shutootaki")]
#[command(version)]
#[command(about = "Git Worktree Manager - Manage your Git worktrees with ease")]
#[command(long_about = None)]
#[command(disable_help_subcommand = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(alias = "ls")]
List(ListArgs),
Add(AddArgs),
#[command(alias = "rm")]
Remove(RemoveArgs),
Go(GoArgs),
#[command(hide = true)]
Init(InitArgs),
Clean(CleanArgs),
#[command(alias = "pull-main")]
Sync,
Help(HelpArgs),
#[command(hide = true)]
Completion(CompletionArgs),
}
#[derive(Parser, Debug)]
pub struct AddArgs {
#[arg()]
pub branch_name: Option<String>,
#[arg(short = 'r', long = "remote")]
pub remote: bool,
#[arg(long = "from")]
pub from_branch: Option<String>,
#[arg(short = 'o', long = "open", value_enum)]
pub open: Option<EditorArg>,
#[arg(long = "code", hide = true)]
pub open_code: bool,
#[arg(long = "cursor", hide = true)]
pub open_cursor: bool,
#[arg(long = "no-cd")]
pub no_cd: bool,
#[arg(long = "skip-hooks")]
pub skip_hooks: bool,
#[arg(long = "run-deferred-hooks", hide = true)]
pub run_deferred_hooks: Option<String>,
}
impl AddArgs {
fn has_editor_option(&self) -> bool {
self.open.is_some() || self.open_code || self.open_cursor
}
pub fn editor(&self) -> Option<EditorType> {
if let Some(editor) = self.open {
return Some(editor.to_editor_type());
}
if self.open_code {
eprintln!("\x1b[33mWarning: --code is deprecated. Use --open code instead.\x1b[0m");
return Some(EditorType::VsCode);
}
if self.open_cursor {
eprintln!("\x1b[33mWarning: --cursor is deprecated. Use --open cursor instead.\x1b[0m");
return Some(EditorType::Cursor);
}
None
}
pub fn should_output_path_only(&self) -> bool {
!self.no_cd && !self.has_editor_option()
}
}
#[derive(Parser, Debug)]
pub struct RemoveArgs {
#[arg()]
pub query: Option<String>,
#[arg(short = 'f', long = "force")]
pub force: bool,
#[arg(long = "clean-branch", value_parser = parse_clean_branch_mode)]
pub clean_branch: Option<CleanBranchMode>,
}
#[derive(Parser, Debug)]
pub struct GoArgs {
#[arg()]
pub query: Option<String>,
#[arg(short = 'o', long = "open", value_enum)]
pub open: Option<EditorArg>,
#[arg(short = 'c', long = "code", hide = true)]
pub open_code: bool,
#[arg(long = "cursor", hide = true)]
pub open_cursor: bool,
#[arg(long = "no-cd")]
pub no_cd: bool,
}
impl GoArgs {
fn has_editor_option(&self) -> bool {
self.open.is_some() || self.open_code || self.open_cursor
}
pub fn editor(&self) -> Option<EditorType> {
if let Some(editor) = self.open {
return Some(editor.to_editor_type());
}
if self.open_code {
eprintln!("\x1b[33mWarning: --code is deprecated. Use --open code instead.\x1b[0m");
return Some(EditorType::VsCode);
}
if self.open_cursor {
eprintln!("\x1b[33mWarning: --cursor is deprecated. Use --open cursor instead.\x1b[0m");
return Some(EditorType::Cursor);
}
None
}
pub fn should_output_path_only(&self) -> bool {
!self.no_cd && !self.has_editor_option()
}
}
#[derive(Parser, Debug)]
pub struct InitArgs {
#[arg(value_enum)]
pub shell: ShellType,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellType {
Bash,
Zsh,
Fish,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditorArg {
#[value(alias = "vscode")]
Code,
Cursor,
Zed,
}
impl EditorArg {
pub fn to_editor_type(self) -> EditorType {
match self {
EditorArg::Code => EditorType::VsCode,
EditorArg::Cursor => EditorType::Cursor,
EditorArg::Zed => EditorType::Zed,
}
}
}
#[derive(Parser, Debug)]
pub struct CleanArgs {
#[arg(short = 'n', long = "dry-run")]
pub dry_run: bool,
#[arg(long = "force")]
pub force: bool,
}
#[derive(Parser, Debug)]
pub struct HelpArgs {
#[arg()]
pub command: Option<String>,
}
#[derive(Parser, Debug)]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: CompletionShell,
#[arg(long = "with-dynamic")]
pub with_dynamic: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionShell {
Bash,
Zsh,
Fish,
}
#[derive(Parser, Debug, Default)]
pub struct ListArgs {
#[arg(long)]
pub compact: bool,
#[arg(long, value_enum, default_value = "table")]
pub format: OutputFormat,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Table,
Json,
Names,
}
fn parse_clean_branch_mode(s: &str) -> Result<CleanBranchMode, String> {
match s.to_lowercase().as_str() {
"auto" => Ok(CleanBranchMode::Auto),
"ask" => Ok(CleanBranchMode::Ask),
"never" => Ok(CleanBranchMode::Never),
_ => Err(format!(
"Invalid clean-branch mode: '{}'. Valid options: auto, ask, never",
s
)),
}
}
impl Cli {
pub fn parse_args() -> Self {
Self::parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
#[test]
fn test_parse_add_args() {
let cli = Cli::parse_from(["gwm", "add", "feature/test"]);
match cli.command {
Some(Commands::Add(args)) => {
assert_eq!(args.branch_name, Some("feature/test".to_string()));
assert!(!args.remote);
assert!(!args.open_code);
}
_ => panic!("Expected Add command"),
}
}
#[test]
fn test_parse_add_with_flags() {
let cli = Cli::parse_from(["gwm", "add", "-r", "--code"]);
match cli.command {
Some(Commands::Add(args)) => {
assert!(args.remote);
assert!(args.open_code);
}
_ => panic!("Expected Add command"),
}
}
#[test]
fn test_parse_remove_args() {
let cli = Cli::parse_from(["gwm", "remove", "--force", "--clean-branch", "auto"]);
match cli.command {
Some(Commands::Remove(args)) => {
assert!(args.force);
assert_eq!(args.clean_branch, Some(CleanBranchMode::Auto));
}
_ => panic!("Expected Remove command"),
}
}
#[test]
fn test_parse_init_args() {
let cli = Cli::parse_from(["gwm", "init", "bash"]);
match cli.command {
Some(Commands::Init(args)) => assert_eq!(args.shell, ShellType::Bash),
_ => panic!("Expected Init command"),
}
}
#[test]
fn test_list_alias_ls() {
let cli = Cli::parse_from(["gwm", "ls"]);
assert!(matches!(cli.command, Some(Commands::List(_))));
}
#[test]
fn test_list_compact_flag() {
let cli = Cli::parse_from(["gwm", "list", "--compact"]);
if let Some(Commands::List(args)) = cli.command {
assert!(args.compact);
} else {
panic!("Expected List command");
}
}
#[test]
fn test_list_json_format() {
let cli = Cli::parse_from(["gwm", "list", "--format", "json"]);
if let Some(Commands::List(args)) = cli.command {
assert_eq!(args.format, OutputFormat::Json);
} else {
panic!("Expected List command");
}
}
#[test]
fn test_remove_alias_rm() {
let cli = Cli::parse_from(["gwm", "rm"]);
assert!(matches!(cli.command, Some(Commands::Remove(_))));
}
#[test]
fn test_sync_command() {
let cli = Cli::parse_from(["gwm", "sync"]);
assert!(matches!(cli.command, Some(Commands::Sync)));
}
#[test]
fn test_sync_alias_pull_main() {
let cli = Cli::parse_from(["gwm", "pull-main"]);
assert!(matches!(cli.command, Some(Commands::Sync)));
}
#[test]
fn test_parse_clean_branch_mode() {
assert_eq!(parse_clean_branch_mode("auto"), Ok(CleanBranchMode::Auto));
assert_eq!(parse_clean_branch_mode("AUTO"), Ok(CleanBranchMode::Auto));
assert_eq!(parse_clean_branch_mode("ask"), Ok(CleanBranchMode::Ask));
assert_eq!(parse_clean_branch_mode("never"), Ok(CleanBranchMode::Never));
assert!(parse_clean_branch_mode("invalid").is_err());
}
#[test]
fn test_add_should_output_path_only() {
let cli = Cli::parse_from(["gwm", "add", "feature/test"]);
if let Some(Commands::Add(args)) = cli.command {
assert!(args.should_output_path_only());
}
let cli = Cli::parse_from(["gwm", "add", "feature/test", "--no-cd"]);
if let Some(Commands::Add(args)) = cli.command {
assert!(!args.should_output_path_only());
}
let cli = Cli::parse_from(["gwm", "add", "feature/test", "--code"]);
if let Some(Commands::Add(args)) = cli.command {
assert!(!args.should_output_path_only());
}
let cli = Cli::parse_from(["gwm", "add", "feature/test", "--cursor"]);
if let Some(Commands::Add(args)) = cli.command {
assert!(!args.should_output_path_only());
}
}
#[test]
fn test_go_should_output_path_only() {
let cli = Cli::parse_from(["gwm", "go", "feature/test"]);
if let Some(Commands::Go(args)) = cli.command {
assert!(
args.should_output_path_only(),
"Default go should output path only"
);
}
let cli = Cli::parse_from(["gwm", "go", "feature/test", "--no-cd"]);
if let Some(Commands::Go(args)) = cli.command {
assert!(
!args.should_output_path_only(),
"--no-cd should disable path-only output"
);
}
let cli = Cli::parse_from(["gwm", "go", "feature/test", "--code"]);
if let Some(Commands::Go(args)) = cli.command {
assert!(
!args.should_output_path_only(),
"--code should disable path-only output"
);
}
let cli = Cli::parse_from(["gwm", "go", "feature/test", "--cursor"]);
if let Some(Commands::Go(args)) = cli.command {
assert!(
!args.should_output_path_only(),
"--cursor should disable path-only output"
);
}
}
#[test]
fn test_editor_arg_to_editor_type() {
assert_eq!(EditorArg::Code.to_editor_type(), EditorType::VsCode);
assert_eq!(EditorArg::Cursor.to_editor_type(), EditorType::Cursor);
assert_eq!(EditorArg::Zed.to_editor_type(), EditorType::Zed);
}
#[test]
fn test_parse_add_with_open_option() {
let cli = Cli::parse_from(["gwm", "add", "test", "--open", "code"]);
if let Some(Commands::Add(args)) = cli.command {
assert_eq!(args.open, Some(EditorArg::Code));
assert!(!args.should_output_path_only());
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_parse_add_with_open_short_form() {
let cli = Cli::parse_from(["gwm", "add", "test", "-o", "zed"]);
if let Some(Commands::Add(args)) = cli.command {
assert_eq!(args.open, Some(EditorArg::Zed));
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_parse_add_with_open_vscode_alias() {
let cli = Cli::parse_from(["gwm", "add", "test", "--open", "vscode"]);
if let Some(Commands::Add(args)) = cli.command {
assert_eq!(args.open, Some(EditorArg::Code));
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_parse_add_with_open_cursor() {
let cli = Cli::parse_from(["gwm", "add", "test", "--open", "cursor"]);
if let Some(Commands::Add(args)) = cli.command {
assert_eq!(args.open, Some(EditorArg::Cursor));
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_parse_go_with_open_option() {
let cli = Cli::parse_from(["gwm", "go", "main", "--open", "cursor"]);
if let Some(Commands::Go(args)) = cli.command {
assert_eq!(args.open, Some(EditorArg::Cursor));
assert!(!args.should_output_path_only());
} else {
panic!("Expected Go command");
}
}
#[test]
fn test_parse_go_with_open_short_form() {
let cli = Cli::parse_from(["gwm", "go", "main", "-o", "code"]);
if let Some(Commands::Go(args)) = cli.command {
assert_eq!(args.open, Some(EditorArg::Code));
} else {
panic!("Expected Go command");
}
}
#[test]
fn test_legacy_code_flag_still_works() {
let cli = Cli::parse_from(["gwm", "add", "test", "--code"]);
if let Some(Commands::Add(args)) = cli.command {
assert!(args.open_code);
assert!(!args.should_output_path_only());
} else {
panic!("Expected Add command");
}
}
#[test]
fn test_legacy_cursor_flag_still_works() {
let cli = Cli::parse_from(["gwm", "go", "main", "--cursor"]);
if let Some(Commands::Go(args)) = cli.command {
assert!(args.open_cursor);
assert!(!args.should_output_path_only());
} else {
panic!("Expected Go command");
}
}
#[test]
fn test_legacy_short_c_flag_still_works() {
let cli = Cli::parse_from(["gwm", "go", "main", "-c"]);
if let Some(Commands::Go(args)) = cli.command {
assert!(args.open_code);
} else {
panic!("Expected Go command");
}
}
#[test]
fn test_parse_completion_bash() {
let cli = Cli::parse_from(["gwm", "completion", "bash"]);
match cli.command {
Some(Commands::Completion(args)) => {
assert_eq!(args.shell, CompletionShell::Bash);
assert!(!args.with_dynamic);
}
_ => panic!("Expected Completion command"),
}
}
#[test]
fn test_parse_completion_with_dynamic() {
let cli = Cli::parse_from(["gwm", "completion", "zsh", "--with-dynamic"]);
match cli.command {
Some(Commands::Completion(args)) => {
assert_eq!(args.shell, CompletionShell::Zsh);
assert!(args.with_dynamic);
}
_ => panic!("Expected Completion command"),
}
}
#[test]
fn test_list_format_names() {
let cli = Cli::parse_from(["gwm", "list", "--format", "names"]);
if let Some(Commands::List(args)) = cli.command {
assert_eq!(args.format, OutputFormat::Names);
} else {
panic!("Expected List command");
}
}
}