A Go CLI for managing git worktrees: create, navigate, list, multi-select remove, clean stale worktrees, generate a shell-integration wrapper.
End-user docs live in README.md. This file is the working-copy reference for anyone (human or agent) modifying the codebase.
- Language: Go 1.22+ (module
github.com/shhac/git-wt) - CLI framework: Cobra
- Picker: custom bubbletea components in
internal/picker/(we tried huh initially but its option- rendering pipeline strips per-column ANSI; built our own to keep full control) - Styling: lipgloss
- TTY detection: mattn/go-isatty
cmd/git-wt/main.go # entry point
internal/
cli/ # cobra subcommands + per-command helpers
root.go # global flags, Execute()
list.go new.go add.go eject.go # the ten commands
go_cmd.go rm.go clean.go alias.go config_cmd.go completion.go
emit.go # fd<N> / bare-mode path emission
picker.go # adapts internal/picker to worktrees
worktrees.go # findByBranch, filterOutCurrent, ...
config_resolve.go # layer git-config wt.* under CLI flags
completion_helpers.go # ValidArgsFunction candidates for go/rm/add
picker/ # bubbletea SelectOne / SelectMany / Confirm
wt/ # worktree types, parsing, validation
config/ # wt.* key registry + git-config IO + ${...} expansion
copyspec/ # .git-wt-copy-files parser
ui/ # lipgloss styles + duration formatting
fd/ # wrapper-protocol fd open/check
git/ # thin git subprocess wrapper
version/ # build-time version string
e2e/ # Go-native E2E (drives the built binary)
Each command's RunE is small: it gathers inputs (repo, worktree list,
flags) and delegates to focused helpers. The pure helpers live alongside
the command file, are unit-tested directly, and don't depend on Cobra or
the picker.
The shell function generated by git-wt alias gwt opens fd N (default 3)
via N>&1 1>&2 and captures whatever the binary writes there. The binary:
- Wrapper mode (
fd.Open(N)succeeds): writes the destination path to fd N. - Bare mode (no fd N): writes the path to stdout with a
→ cd '...'hint on stderr. Supportscd "$(git-wt go branch)".
The fd number is baked literally into the redirect at alias-generation time because bash parses redirect operators before variable expansion.
| Command | Description |
|---|---|
new <branch> |
Create worktree at <repo>/.worktrees/<branch>/ (or --parent-dir). Copies files matching .git-wt-copy-files (or built-in defaults). Hints if <parent>/ isn't gitignored when it lives inside the repo. |
add [<leaf>] <branch|remote-ref> |
Worktree for an existing branch. Resolver: <remote>/<rest> (with matching remote ref) ⇒ remote DWIM via --track -b <rest>; else local branch lookup. Optional <leaf> overrides the leaf directory name (defaults to the local branch name). Never creates new branches. |
eject [<leaf>] |
Move HEAD's branch out of the main working tree into a new worktree. Stashes (incl. untracked), switches main tree to main/master/--base, runs the add pipeline, then stash apply --index in the new worktree. Interactive confirm by default. Must run from the main tree, must be on a branch, current branch must not be the base. |
rm [branch...] |
Remove worktree(s). Interactive multi-select if no args. Confirm step is also the type-of-rm choice (worktree only / worktree+branch / cancel). Bounces to main if you remove your current worktree. Fast path (rm_remove.go): clean-check → rename aside → worktree prune → parallel permission-fixing delete (wt.DeleteTree) with a TTY progress line; falls back to git worktree remove for locked/edge cases. Args also match unregistered leftover dirs under the trees dir (rescue). |
go [branch] |
Navigate. Direct branch lookup with unique-suffix fallback (auth → paul/auth). Interactive picker if no arg. |
list (ls) |
Print the table. Columns: marker, branch, location (#name inside .worktrees/, rel-to-repo elsewhere, abs outside), mtime. |
clean |
Remove worktrees whose local branch is gone OR whose upstream is gone. Both run by default; narrow with --orphaned-only / --upstream-gone-only. |
alias <name> |
Print a POSIX shell function. Configurable fd (--fd N, range 3-9), baked flags (--plain, -n, --debug), and an optional tab-completion binding (default on; suppress with --no-completion). |
completion <shell> |
Print a Cobra-generated completion script for bash/zsh/fish/powershell. Dynamic argument completion via ValidArgsFunction on go / rm / add. |
config [<key> [<value>]] |
Show/set persistent settings stored in git config wt.*. List bare; show one key with template + resolved value; set with <key> <value> (default --local, --global flag for user-wide); --unset to remove. Validates type and template vars at set time. |
-h --help, -v --version, --debug, --plain (also honours NO_COLOR),
-n --non-interactive (auto-detected when stdin isn't a TTY), --fd <N>.
--plain and --fd layer over their wt.* config defaults — when the
user doesn't pass the flag (cmd.Flags().Changed), PersistentPreRun
reads the corresponding key. --parent-dir is checked the same way in
cli.resolveParentDir. Templating (${repo}, ${repoPath},
${repoParent}, ${home}) is applied by wt.ResolveParentDir whenever
the value is non-empty, so values from CLI or config can both use it.
go build -o git-wt ./cmd/git-wt
go test -race -count=1 ./... # ~95 unit + 17 e2e
go vet ./...E2E tests run inside t.TempDir() (under /tmp/), never the project repo.
CI mirrors this on Linux + macOS.
- Branches that touch flags should make them explicit parameters of the
helper they configure (e.g.
chooseRmAction(targets, keepBranch, deleteBranch)). Avoid reading package-level flag vars from inside helpers — it makes them un-unit-testable. - Pure functions get unit tests in the same package; commands get E2E
coverage in
e2e/. - The picker package is generic —
SelectOne/SelectMany/Confirmtake row/option slices and return(value, ok, err).ok=falsemeans the user cancelled (ESC, Ctrl-C, q); the caller should exit silently.
main— the Go implementation. The Go rewrite (formerly on amigrate-to-gobranch) landed here in 2026; branch frommainfor all new work.zig-cli— frozen snapshot of the pre-migration Zig codebase, retained for reference.- The migration spec lives at
.ai-cache/plan-go-migration.md.