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}