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        /// Continue watching for merge even if CI checks fail
75        /// (default: stop and report the failure)
76        #[arg(long = "ignore-ci-failure")]
77        ignore_ci_failure: bool,
78
79        /// Seconds between merge-status polls
80        #[arg(long, default_value_t = 30)]
81        interval: u64,
82    },
83
84    /// Manage worktrees
85    Worktree {
86        #[command(subcommand)]
87        command: WorktreeCommands,
88    },
89}
90
91#[derive(Subcommand)]
92pub enum WorktreeCommands {
93    /// Manage a pre-warmed worktree pool
94    Pool {
95        #[command(subcommand)]
96        command: PoolCommands,
97    },
98}
99
100#[derive(Subcommand)]
101pub enum PoolCommands {
102    /// Pre-warm the pool with ready-to-use worktrees
103    Warm {
104        /// Target number of available worktrees in the pool
105        count: usize,
106    },
107
108    /// Acquire a worktree from the pool (prints path to stdout)
109    Acquire,
110
111    /// Show pool status
112    Status,
113
114    /// Release acquired worktree(s) back to the pool
115    Release {
116        /// Name of the worktree to release (defaults to all acquired)
117        name: Option<String>,
118    },
119
120    /// Remove all worktrees and clean up the pool
121    Drain {
122        /// Force drain even if worktrees are acquired
123        #[arg(long)]
124        force: bool,
125    },
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn await_requires_pr_number() {
134        // Without a PR number, parsing must fail.
135        assert!(Cli::try_parse_from(["gw", "await"]).is_err());
136    }
137
138    #[test]
139    fn await_defaults() {
140        let cli = Cli::try_parse_from(["gw", "await", "42"]).unwrap();
141        match cli.command {
142            Commands::Await {
143                pr,
144                open,
145                no_wait,
146                no_cleanup,
147                ignore_ci_failure,
148                interval,
149            } => {
150                assert_eq!(pr, 42);
151                assert!(!open);
152                assert!(!no_wait);
153                assert!(!no_cleanup);
154                assert!(!ignore_ci_failure);
155                assert_eq!(interval, 30);
156            }
157            _ => panic!("expected Await command"),
158        }
159    }
160
161    #[test]
162    fn await_flags() {
163        let cli = Cli::try_parse_from([
164            "gw",
165            "await",
166            "42",
167            "--open",
168            "--no-wait",
169            "--no-cleanup",
170            "--ignore-ci-failure",
171            "--interval",
172            "5",
173        ])
174        .unwrap();
175        match cli.command {
176            Commands::Await {
177                pr,
178                open,
179                no_wait,
180                no_cleanup,
181                ignore_ci_failure,
182                interval,
183            } => {
184                assert_eq!(pr, 42);
185                assert!(open);
186                assert!(no_wait);
187                assert!(no_cleanup);
188                assert!(ignore_ci_failure);
189                assert_eq!(interval, 5);
190            }
191            _ => panic!("expected Await command"),
192        }
193    }
194}