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