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