Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "gw")]
#[command(about = "Worktree-aware git workflow: branch -> PR -> cleanup, safely.")]
#[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.

Expand Down Expand Up @@ -39,42 +42,129 @@ pub struct Cli {
#[derive(Subcommand)]
pub enum Commands {
/// Switch to home branch and sync with origin/main
#[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,

/// Create new branch from origin/main
#[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 {
/// Name of the branch to create (e.g., feature/add-login)
branch: Option<String>,
},

/// Delete merged branch and return to home
#[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 to delete (defaults to current branch if not on home)
branch: Option<String>,
},

/// Show current repository state
#[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,

/// Pause current work: WIP commit and return to home branch
#[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 {
/// Optional message describing why work is paused
message: Option<String>,
},

/// Abandon current changes and return to home branch
#[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,

/// Undo the last commit (soft reset HEAD~1)
#[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,

/// Sync current branch after base PR is merged (update base to main, rebase, force push)
#[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,

/// Open the PR for the current branch in the browser
#[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,

/// Watch a specific PR until merged or closed, then clean up its branch
#[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 number to watch (required so the watcher stays bound to one PR
/// even if you switch branches, e.g. while working a stacked PR)
Expand Down Expand Up @@ -103,6 +193,14 @@ pub enum Commands {
},

/// Manage worktrees
#[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,
Expand All @@ -112,6 +210,18 @@ pub enum Commands {
#[derive(Subcommand)]
pub enum WorktreeCommands {
/// Manage a pre-warmed worktree pool
#[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,
Expand All @@ -121,24 +231,61 @@ pub enum WorktreeCommands {
#[derive(Subcommand)]
pub enum PoolCommands {
/// Pre-warm the pool with ready-to-use worktrees
#[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 {
/// Target number of available worktrees in the pool
count: usize,
},

/// Acquire a worktree from the pool (prints path to stdout)
#[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,

/// Show pool status
#[command(long_about = "\
Show how many pool worktrees exist and how many are available to acquire.

Example:
gw worktree pool status")]
Status,

/// Release acquired worktree(s) back to the pool
#[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 of the worktree to release (defaults to all acquired)
name: Option<String>,
},

/// Remove all worktrees and clean up the pool
#[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 {
/// Force drain even if worktrees are acquired
#[arg(long)]
Expand Down
Loading