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}