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