Skip to main content

git_worktree_manager/
cli.rs

1/// CLI definitions using clap derive.
2///
3/// Mirrors the Typer-based CLI in src/git_worktree_manager/cli.py.
4pub mod completions;
5pub mod global;
6
7use clap::{Parser, Subcommand, ValueHint};
8
9/// Validate config key (accepts any string but provides completion hints).
10fn parse_config_key(s: &str) -> Result<String, String> {
11    Ok(s.to_string())
12}
13
14/// Parse duration strings like "30", "30d", "2w", "1m" into days.
15fn parse_duration_days(s: &str) -> Result<u64, String> {
16    let s = s.trim();
17    if s.is_empty() {
18        return Err("empty duration".into());
19    }
20
21    // Pure number = days
22    if let Ok(n) = s.parse::<u64>() {
23        return Ok(n);
24    }
25
26    let (num_str, suffix) = s.split_at(s.len() - 1);
27    let n: u64 = num_str
28        .parse()
29        .map_err(|_| format!("invalid duration: '{}'. Use e.g. 30, 7d, 2w, 1m", s))?;
30
31    match suffix {
32        "d" => Ok(n),
33        "w" => Ok(n * 7),
34        "m" => Ok(n * 30),
35        "y" => Ok(n * 365),
36        _ => Err(format!(
37            "unknown duration suffix '{}'. Use d (days), w (weeks), m (months), y (years)",
38            suffix
39        )),
40    }
41}
42
43/// Git worktree manager CLI.
44#[derive(Parser, Debug)]
45#[command(
46    name = "gw",
47    version,
48    about = "git worktree manager — AI coding assistant integration",
49    long_about = None,
50    arg_required_else_help = true,
51)]
52pub struct Cli {
53    /// Run in global mode (across all registered repositories)
54    #[arg(short = 'g', long = "global", global = true)]
55    pub global: bool,
56
57    /// Generate shell completions for the given shell
58    #[arg(long, value_name = "SHELL", value_parser = clap::builder::PossibleValuesParser::new(["bash", "zsh", "fish", "powershell", "elvish"]))]
59    pub generate_completion: Option<String>,
60
61    #[command(subcommand)]
62    pub command: Option<Commands>,
63}
64
65#[derive(Subcommand, Debug)]
66pub enum Commands {
67    /// Create new worktree for feature branch
68    New {
69        /// Branch name for the new worktree
70        name: String,
71
72        /// Custom worktree path (default: ../<repo>-<branch>)
73        #[arg(short, long, value_hint = ValueHint::DirPath)]
74        path: Option<String>,
75
76        /// Base branch to create from (default: from config)
77        #[arg(short = 'b', long = "base")]
78        base: Option<String>,
79
80        /// Skip AI tool launch
81        #[arg(long = "no-term")]
82        no_term: bool,
83
84        /// Terminal launch method (e.g., tmux, iterm-tab, zellij)
85        #[arg(short = 'T', long)]
86        term: Option<String>,
87
88        /// Launch AI tool in background
89        #[arg(long)]
90        bg: bool,
91
92        /// Initial prompt to pass to the AI tool (starts interactive session with task)
93        #[arg(long)]
94        prompt: Option<String>,
95    },
96
97    /// Create GitHub Pull Request from worktree
98    Pr {
99        /// Branch name (default: current worktree branch)
100        branch: Option<String>,
101
102        /// PR title
103        #[arg(short, long)]
104        title: Option<String>,
105
106        /// PR body
107        #[arg(short = 'B', long)]
108        body: Option<String>,
109
110        /// Create as draft PR
111        #[arg(short, long)]
112        draft: bool,
113
114        /// Skip pushing to remote
115        #[arg(long)]
116        no_push: bool,
117
118        /// Resolve target as worktree name (instead of branch)
119        #[arg(short, long)]
120        worktree: bool,
121
122        /// Resolve target as branch name (instead of worktree)
123        #[arg(short = 'b', long = "by-branch", conflicts_with = "worktree")]
124        by_branch: bool,
125    },
126
127    /// Merge feature branch into base branch
128    Merge {
129        /// Branch name (default: current worktree branch)
130        branch: Option<String>,
131
132        /// Interactive rebase
133        #[arg(short, long)]
134        interactive: bool,
135
136        /// Dry run (show what would happen)
137        #[arg(long)]
138        dry_run: bool,
139
140        /// Push to remote after merge
141        #[arg(long)]
142        push: bool,
143
144        /// Use AI to resolve merge conflicts
145        #[arg(long)]
146        ai_merge: bool,
147
148        /// Resolve target as worktree name (instead of branch)
149        #[arg(short, long)]
150        worktree: bool,
151    },
152
153    /// Resume AI work in a worktree
154    Resume {
155        /// Branch name to resume (default: current worktree)
156        branch: Option<String>,
157
158        /// Terminal launch method
159        #[arg(short = 'T', long)]
160        term: Option<String>,
161
162        /// Launch AI tool in background
163        #[arg(long)]
164        bg: bool,
165
166        /// Resolve target as worktree name (instead of branch)
167        #[arg(short, long)]
168        worktree: bool,
169
170        /// Resolve target as branch name (instead of worktree)
171        #[arg(short, long, conflicts_with = "worktree")]
172        by_branch: bool,
173    },
174
175    /// Open interactive shell or execute command in a worktree
176    Shell {
177        /// Worktree branch to shell into
178        worktree: Option<String>,
179
180        /// Command and arguments to execute
181        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
182        args: Vec<String>,
183    },
184
185    /// Show current worktree status
186    Status,
187
188    /// Delete a worktree
189    Delete {
190        /// Branch name or path of worktree to delete (default: current worktree)
191        target: Option<String>,
192
193        /// Keep the branch (only remove worktree)
194        #[arg(short = 'k', long)]
195        keep_branch: bool,
196
197        /// Also delete the remote branch
198        #[arg(short = 'r', long)]
199        delete_remote: bool,
200
201        /// Force remove: also bypasses the busy-detection gate (skips the
202        /// "worktree is in use" check and deletes anyway)
203        #[arg(short, long, conflicts_with = "no_force")]
204        force: bool,
205
206        /// Don't use --force flag
207        #[arg(long)]
208        no_force: bool,
209
210        /// Resolve target as worktree name (instead of branch)
211        #[arg(short, long)]
212        worktree: bool,
213
214        /// Resolve target as branch name (instead of worktree)
215        #[arg(short, long, conflicts_with = "worktree")]
216        branch: bool,
217    },
218
219    /// List all worktrees
220    #[command(alias = "ls")]
221    List,
222
223    /// Batch cleanup of worktrees
224    Clean {
225        /// Delete worktrees for branches already merged to base
226        #[arg(long)]
227        merged: bool,
228
229        /// Delete worktrees older than duration (e.g., 7, 30d, 2w, 1m)
230        #[arg(long, value_name = "DURATION", value_parser = parse_duration_days)]
231        older_than: Option<u64>,
232
233        /// Interactive selection UI
234        #[arg(short, long)]
235        interactive: bool,
236
237        /// Show what would be deleted without deleting
238        #[arg(long)]
239        dry_run: bool,
240
241        /// Bypass the busy-detection gate: delete busy worktrees too
242        /// (default: skip worktrees another session is using)
243        #[arg(short, long)]
244        force: bool,
245    },
246
247    /// Display worktree hierarchy as a tree
248    Tree,
249
250    /// Show worktree statistics
251    Stats,
252
253    /// Compare two branches
254    Diff {
255        /// First branch
256        branch1: String,
257        /// Second branch
258        branch2: String,
259        /// Show statistics only
260        #[arg(short, long)]
261        summary: bool,
262        /// Show changed files only
263        #[arg(short, long)]
264        files: bool,
265    },
266
267    /// Sync worktree with base branch
268    Sync {
269        /// Branch name (default: current worktree)
270        branch: Option<String>,
271
272        /// Sync all worktrees
273        #[arg(long)]
274        all: bool,
275
276        /// Only fetch updates without rebasing
277        #[arg(long)]
278        fetch_only: bool,
279
280        /// Use AI to resolve merge conflicts
281        #[arg(long)]
282        ai_merge: bool,
283
284        /// Resolve target as worktree name (instead of branch)
285        #[arg(short, long)]
286        worktree: bool,
287
288        /// Resolve target as branch name (instead of worktree)
289        #[arg(short, long, conflicts_with = "worktree")]
290        by_branch: bool,
291    },
292
293    /// Change base branch for a worktree
294    ChangeBase {
295        /// New base branch
296        new_base: String,
297        /// Branch name (default: current worktree)
298        branch: Option<String>,
299
300        /// Dry run (show what would happen)
301        #[arg(long)]
302        dry_run: bool,
303
304        /// Interactive rebase
305        #[arg(short, long)]
306        interactive: bool,
307
308        /// Resolve target as worktree name (instead of branch)
309        #[arg(short, long)]
310        worktree: bool,
311
312        /// Resolve target as branch name (instead of worktree)
313        #[arg(short, long, conflicts_with = "worktree")]
314        by_branch: bool,
315    },
316
317    /// Configuration management
318    Config {
319        #[command(subcommand)]
320        action: ConfigAction,
321    },
322
323    /// Backup and restore worktrees
324    Backup {
325        #[command(subcommand)]
326        action: BackupAction,
327    },
328
329    /// Stash management (worktree-aware)
330    Stash {
331        #[command(subcommand)]
332        action: StashAction,
333    },
334
335    /// Manage lifecycle hooks
336    Hook {
337        #[command(subcommand)]
338        action: HookAction,
339    },
340
341    /// Export worktree configuration to a file
342    Export {
343        /// Output file path
344        #[arg(short, long)]
345        output: Option<String>,
346    },
347
348    /// Import worktree configuration from a file
349    Import {
350        /// Path to the configuration file to import
351        import_file: String,
352
353        /// Apply the imported configuration (default: preview only)
354        #[arg(long)]
355        apply: bool,
356    },
357
358    /// Scan for repositories (global mode)
359    Scan {
360        /// Base directory to scan (default: home directory)
361        #[arg(short, long, value_hint = ValueHint::DirPath)]
362        dir: Option<std::path::PathBuf>,
363    },
364
365    /// Clean up stale registry entries (global mode)
366    Prune,
367
368    /// Run diagnostics
369    Doctor,
370
371    /// Check for updates / upgrade
372    Upgrade,
373
374    /// Install Claude Code skill for worktree task delegation
375    #[command(name = "setup-claude")]
376    SetupClaude,
377
378    /// Interactive shell integration setup
379    ShellSetup,
380
381    /// [Internal] Get worktree path for a branch
382    #[command(name = "_path", hide = true)]
383    Path {
384        /// Branch name
385        branch: Option<String>,
386
387        /// List branch names (for tab completion)
388        #[arg(long)]
389        list_branches: bool,
390
391        /// Interactive worktree selection
392        #[arg(short, long)]
393        interactive: bool,
394    },
395
396    /// Generate shell function for gw-cd / cw-cd
397    #[command(name = "_shell-function", hide = true)]
398    ShellFunction {
399        /// Shell type: bash, zsh, fish, or powershell
400        shell: String,
401    },
402
403    /// List config keys (for tab completion)
404    #[command(name = "_config-keys", hide = true)]
405    ConfigKeys,
406
407    /// Refresh update cache (background process)
408    #[command(name = "_update-cache", hide = true)]
409    UpdateCache,
410
411    /// List terminal launch method values (for tab completion)
412    #[command(name = "_term-values", hide = true)]
413    TermValues,
414
415    /// List preset names (for tab completion)
416    #[command(name = "_preset-names", hide = true)]
417    PresetNames,
418
419    /// List hook event names (for tab completion)
420    #[command(name = "_hook-events", hide = true)]
421    HookEvents,
422}
423
424#[derive(Subcommand, Debug)]
425pub enum ConfigAction {
426    /// Show current configuration summary
427    Show,
428    /// List all configuration keys, values, and descriptions
429    #[command(alias = "ls")]
430    List,
431    /// Get a configuration value
432    Get {
433        /// Dot-separated config key (e.g., ai_tool.command)
434        #[arg(value_parser = parse_config_key)]
435        key: String,
436    },
437    /// Set a configuration value
438    Set {
439        /// Dot-separated config key (e.g., ai_tool.command)
440        #[arg(value_parser = parse_config_key)]
441        key: String,
442        /// Value to set
443        value: String,
444    },
445    /// Use a predefined AI tool preset
446    UsePreset {
447        /// Preset name (e.g., claude, codex, no-op)
448        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::PRESET_NAMES))]
449        name: String,
450    },
451    /// List available presets
452    ListPresets,
453    /// Reset configuration to defaults
454    Reset,
455}
456
457#[derive(Subcommand, Debug)]
458pub enum BackupAction {
459    /// Create backup of worktree(s) using git bundle
460    Create {
461        /// Branch name to backup (default: current worktree)
462        branch: Option<String>,
463
464        /// Backup all worktrees
465        #[arg(long)]
466        all: bool,
467
468        /// Output directory for backups
469        #[arg(short, long)]
470        output: Option<String>,
471    },
472    /// List available backups
473    List {
474        /// Filter by branch name
475        branch: Option<String>,
476
477        /// Show all backups (not just current repo)
478        #[arg(short, long)]
479        all: bool,
480    },
481    /// Restore worktree from backup
482    Restore {
483        /// Branch name to restore
484        branch: String,
485
486        /// Custom path for restored worktree
487        #[arg(short, long)]
488        path: Option<String>,
489
490        /// Backup ID (timestamp) to restore (default: latest)
491        #[arg(long)]
492        id: Option<String>,
493    },
494}
495
496#[derive(Subcommand, Debug)]
497pub enum StashAction {
498    /// Save changes in current worktree to stash
499    Save {
500        /// Optional message to describe the stash
501        message: Option<String>,
502    },
503    /// List all stashes organized by worktree/branch
504    List,
505    /// Apply a stash to a different worktree
506    Apply {
507        /// Branch name of worktree to apply stash to
508        target_branch: String,
509
510        /// Stash reference (default: stash@{0})
511        #[arg(short, long, default_value = "stash@{0}")]
512        stash: String,
513    },
514}
515
516#[derive(Subcommand, Debug)]
517pub enum HookAction {
518    /// Add a new hook for an event
519    Add {
520        /// Hook event (e.g., worktree.post_create, merge.pre)
521        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
522        event: String,
523        /// Shell command to execute
524        command: String,
525        /// Custom hook identifier
526        #[arg(long)]
527        id: Option<String>,
528        /// Human-readable description
529        #[arg(short, long)]
530        description: Option<String>,
531    },
532    /// Remove a hook
533    Remove {
534        /// Hook event
535        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
536        event: String,
537        /// Hook identifier to remove
538        hook_id: String,
539    },
540    /// List all hooks
541    List {
542        /// Filter by event
543        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
544        event: Option<String>,
545    },
546    /// Enable a disabled hook
547    Enable {
548        /// Hook event
549        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
550        event: String,
551        /// Hook identifier
552        hook_id: String,
553    },
554    /// Disable a hook without removing it
555    Disable {
556        /// Hook event
557        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
558        event: String,
559        /// Hook identifier
560        hook_id: String,
561    },
562    /// Manually run all hooks for an event
563    Run {
564        /// Hook event to run
565        #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
566        event: String,
567        /// Show what would be executed without running
568        #[arg(long)]
569        dry_run: bool,
570    },
571}