Skip to main content

git_workflow/
cli.rs

1//! CLI argument parsing with clap
2
3use clap::{Parser, Subcommand};
4
5#[derive(Parser)]
6#[command(name = "gw")]
7#[command(about = "Git workflow CLI - type-safe worktree-aware git operations")]
8#[command(version)]
9pub struct Cli {
10    #[command(subcommand)]
11    pub command: Commands,
12
13    /// Show verbose output (git commands being run)
14    #[arg(short, long, global = true)]
15    pub verbose: bool,
16}
17
18#[derive(Subcommand)]
19pub enum Commands {
20    /// Switch to home branch and sync with origin/main
21    Home,
22
23    /// Create new branch from origin/main
24    New {
25        /// Name of the branch to create (e.g., feature/add-login)
26        branch: Option<String>,
27    },
28
29    /// Delete merged branch and return to home
30    Cleanup {
31        /// Branch to delete (defaults to current branch if not on home)
32        branch: Option<String>,
33    },
34
35    /// Show current repository state
36    Status,
37
38    /// Pause current work: WIP commit and return to home branch
39    Pause {
40        /// Optional message describing why work is paused
41        message: Option<String>,
42    },
43
44    /// Abandon current changes and return to home branch
45    Abandon,
46
47    /// Undo the last commit (soft reset HEAD~1)
48    Undo,
49
50    /// Sync current branch after base PR is merged (update base to main, rebase, force push)
51    Sync,
52
53    /// Open the PR for the current branch in the browser
54    Open,
55
56    /// Watch a specific PR until merged or closed, then clean up its branch
57    Await {
58        /// PR number to watch (required so the watcher stays bound to one PR
59        /// even if you switch branches, e.g. while working a stacked PR)
60        pr: u64,
61
62        /// Also open the PR in the browser before watching
63        #[arg(long)]
64        open: bool,
65
66        /// Skip waiting for CI checks
67        #[arg(long = "no-wait")]
68        no_wait: bool,
69
70        /// Do not clean up the branch after the PR is merged
71        #[arg(long = "no-cleanup")]
72        no_cleanup: bool,
73
74        /// Seconds between merge-status polls
75        #[arg(long, default_value_t = 30)]
76        interval: u64,
77    },
78
79    /// Manage worktrees
80    Worktree {
81        #[command(subcommand)]
82        command: WorktreeCommands,
83    },
84}
85
86#[derive(Subcommand)]
87pub enum WorktreeCommands {
88    /// Manage a pre-warmed worktree pool
89    Pool {
90        #[command(subcommand)]
91        command: PoolCommands,
92    },
93}
94
95#[derive(Subcommand)]
96pub enum PoolCommands {
97    /// Pre-warm the pool with ready-to-use worktrees
98    Warm {
99        /// Target number of available worktrees in the pool
100        count: usize,
101    },
102
103    /// Acquire a worktree from the pool (prints path to stdout)
104    Acquire,
105
106    /// Show pool status
107    Status,
108
109    /// Release acquired worktree(s) back to the pool
110    Release {
111        /// Name of the worktree to release (defaults to all acquired)
112        name: Option<String>,
113    },
114
115    /// Remove all worktrees and clean up the pool
116    Drain {
117        /// Force drain even if worktrees are acquired
118        #[arg(long)]
119        force: bool,
120    },
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn await_requires_pr_number() {
129        // Without a PR number, parsing must fail.
130        assert!(Cli::try_parse_from(["gw", "await"]).is_err());
131    }
132
133    #[test]
134    fn await_defaults() {
135        let cli = Cli::try_parse_from(["gw", "await", "42"]).unwrap();
136        match cli.command {
137            Commands::Await {
138                pr,
139                open,
140                no_wait,
141                no_cleanup,
142                interval,
143            } => {
144                assert_eq!(pr, 42);
145                assert!(!open);
146                assert!(!no_wait);
147                assert!(!no_cleanup);
148                assert_eq!(interval, 30);
149            }
150            _ => panic!("expected Await command"),
151        }
152    }
153
154    #[test]
155    fn await_flags() {
156        let cli = Cli::try_parse_from([
157            "gw",
158            "await",
159            "42",
160            "--open",
161            "--no-wait",
162            "--no-cleanup",
163            "--interval",
164            "5",
165        ])
166        .unwrap();
167        match cli.command {
168            Commands::Await {
169                pr,
170                open,
171                no_wait,
172                no_cleanup,
173                interval,
174            } => {
175                assert_eq!(pr, 42);
176                assert!(open);
177                assert!(no_wait);
178                assert!(no_cleanup);
179                assert_eq!(interval, 5);
180            }
181            _ => panic!("expected Await command"),
182        }
183    }
184}