use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "gw")]
#[command(about = "\
Worktree-aware git workflow: branch -> PR -> cleanup, safely.
Start here: gw new feature/login # the full flow is in --help")]
#[command(long_about = "\
Worktree-aware git workflow: branch -> PR -> cleanup, safely.
For developers using a PR-per-branch workflow. gw drives each change from a
fresh branch through review to a cleaned-up merge, and keeps parallel worktrees
in sync so day-to-day work never touches `main` directly.
\"home branch\": the branch a worktree returns to (the main worktree's home is
`main`). `gw home` switches to it and syncs with origin/main.
TYPICAL FLOW:
gw new feature/login # branch off a fresh origin/main
# ...edit, then: git commit -> git push -u origin <branch> -> gh pr create
gw await <pr> --open # wait for CI, open the PR, watch to merge, then clean up
COMMANDS BY SITUATION:
Everyday: new, status, open, sync, cleanup, home
Recovery: pause, abandon, undo
Worktrees: worktree pool (give parallel agents isolated worktrees)
Lost? Run `gw status` -- it prints the single next command for where you are.")]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(long_about = "\
Switch to your home branch and sync it with origin/main.
\"home branch\" = the branch a worktree returns to (the main worktree's home is
`main`). Use this instead of `git checkout main`, which conflicts across
worktrees.
Example:
gw home")]
Home,
#[command(long_about = "\
Create a new branch off a freshly fetched origin/main.
If you already edited on your home branch, gw new carries those changes onto the
new branch -- nothing is stranded on `main`.
Example:
gw new feature/add-login")]
New {
branch: Option<String>,
},
#[command(long_about = "\
Delete a merged branch and return to your home branch.
Verifies the PR was merged before deleting, then switches home and syncs with
origin/main. Usually run for you by `gw await` -- reach for it directly only to
clean up a leftover branch.
Example:
gw cleanup # the current branch
gw cleanup feature/old # a specific branch")]
Cleanup {
branch: Option<String>,
},
#[command(long_about = "\
Show the current repository state and the single next command to run.
Inspects your working dir, upstream sync, home branch, and PR state, then prints
one `Next:` line -- the engine of the gw workflow. When in doubt, run this.
Example:
gw status")]
Status,
#[command(long_about = "\
Pause current work: record a WIP commit, then return to your home branch.
A safe way to switch tasks mid-change -- the WIP commit travels across worktrees,
unlike `git stash`. Resume later by checking the branch back out.
Example:
gw pause \"investigating flaky test\"")]
Pause {
message: Option<String>,
},
#[command(long_about = "\
Discard all changes on the current branch and return to your home branch.
Destructive: both committed and uncommitted work on this branch is thrown away.
Use it when the branch is a dead end.
Example:
gw abandon")]
Abandon,
#[command(long_about = "\
Undo the last commit, keeping its changes as unstaged edits (soft reset HEAD~1).
Lets you re-stage or rewrite the most recent commit. Touches history only -- your
working files are left intact.
Example:
gw undo")]
Undo,
#[command(long_about = "\
Restack this branch after the PR it was based on merged.
Updates the PR's base to main on GitHub, rebases onto the latest main, and
force-pushes. Use this instead of hand-rebasing stacked PRs.
Rewrites history and force-pushes the branch.
Example:
gw sync")]
Sync,
#[command(long_about = "\
Open the current branch's PR in your browser.
The browser command is configured via GW_OPEN_URL_CMD in your dotfiles.
Example:
gw open")]
Open,
#[command(long_about = "\
Watch a specific PR until it merges or closes, then clean up its branch.
Takes a PR number (not a branch) so the watcher stays bound to that PR even if
you switch branches. It waits for CI, then -- with --open -- opens the PR,
watches it to merge, and runs `gw cleanup`. If CI fails it stops and reports, so
you can fix, push, and rerun. Launch it in the background right after creating
the PR.
Example:
gw await 41 --open")]
Await {
pr: u64,
#[arg(long)]
open: bool,
#[arg(long = "no-wait")]
no_wait: bool,
#[arg(long = "no-cleanup")]
no_cleanup: bool,
#[arg(long = "ignore-ci-failure")]
ignore_ci_failure: bool,
#[arg(long, default_value_t = 30)]
interval: u64,
},
#[command(long_about = "\
Manage git worktrees for running parallel work in isolation.
Subcommands live under `gw worktree pool` -- a pre-warmed set of ready-to-use
worktrees so parallel agents each get their own checkout.
Example:
gw worktree pool warm 3")]
Worktree {
#[command(subcommand)]
command: WorktreeCommands,
},
}
#[derive(Subcommand)]
pub enum WorktreeCommands {
#[command(long_about = "\
Manage a pre-warmed pool of worktrees for parallel, isolated work.
Warm the pool once, then acquire a worktree per task and release it when done so
the next task can reuse it. Always release, even on error -- a forgotten release
drains the pool.
Example:
gw worktree pool warm 3 # pre-create 3 worktrees
gw worktree pool acquire # take one (prints its path)
gw worktree pool release <name> # return it when done
gw worktree pool drain # remove them all")]
Pool {
#[command(subcommand)]
command: PoolCommands,
},
}
#[derive(Subcommand)]
pub enum PoolCommands {
#[command(long_about = "\
Pre-create worktrees so the pool has `count` ready to acquire.
Run once before fanning out parallel work.
Example:
gw worktree pool warm 3")]
Warm {
count: usize,
},
#[command(long_about = "\
Take a worktree from the pool and print its path to stdout.
Capture the path and run the task inside it; release it when done. Acquire fails
if the pool is empty -- `gw worktree pool warm <n>` first.
Example:
WORKTREE_PATH=$(gw worktree pool acquire)")]
Acquire,
#[command(long_about = "\
Show how many pool worktrees exist and how many are available to acquire.
Example:
gw worktree pool status")]
Status,
#[command(long_about = "\
Return an acquired worktree to the pool so it can be reused.
With no name, releases all worktrees acquired by this process. Always release,
even on error -- a forgotten release drains the pool.
Example:
gw worktree pool release # all acquired
gw worktree pool release wt-2 # a specific one")]
Release {
name: Option<String>,
},
#[command(long_about = "\
Remove every pool worktree and tear the pool down.
Refuses to drain while worktrees are still acquired unless you pass --force.
Example:
gw worktree pool drain
gw worktree pool drain --force")]
Drain {
#[arg(long)]
force: bool,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn await_requires_pr_number() {
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,
ignore_ci_failure,
interval,
} => {
assert_eq!(pr, 42);
assert!(!open);
assert!(!no_wait);
assert!(!no_cleanup);
assert!(!ignore_ci_failure);
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",
"--ignore-ci-failure",
"--interval",
"5",
])
.unwrap();
match cli.command {
Commands::Await {
pr,
open,
no_wait,
no_cleanup,
ignore_ci_failure,
interval,
} => {
assert_eq!(pr, 42);
assert!(open);
assert!(no_wait);
assert!(no_cleanup);
assert!(ignore_ci_failure);
assert_eq!(interval, 5);
}
_ => panic!("expected Await command"),
}
}
}