Tags: shhac/git-wt
Tags
ci[release]: tag-triggered build + GitHub release + Homebrew bump Push a v* tag to release: calls the shared shhac/homebrew-tap go-release workflow to cross-build every platform, publish the GitHub Release, and regenerate + push Formula/git-wt.rb. The tap deploy key lives in a homebrew-tap Environment gated to v* tag refs, so branch builds never see the secret. Rewrites the /release command to the automated tag-push flow.
feat[rm]: fast, robust worktree removal with progress git worktree remove deletes the directory itself, serially and silently, and aborts mid-delete when it hits a read-only directory or an immutable (uchg) file — leaving the worktree unregistered but the directory half gone, with no way to retry through normal commands. Build caches and package managers routinely create exactly those files. rm now runs git's cleanliness check itself, renames the directory aside (instant, freeing the path), unregisters it via worktree prune, then deletes the renamed tree with a walker that restores directory permissions, clears BSD immutable flags, never follows symlinks, and fans out across a worker pool — roughly 2x faster than rm -rf on a 50k-file tree, with a live progress line on a TTY. Locked worktrees and anything the fast path cannot establish fall back to plain git worktree remove. rm <name> also rescues an unregistered leftover directory under the trees dir — the debris an interrupted older removal could strand.
feat[completion]: shell tab-completion + dynamic candidate completion
Cobra-generated bash/zsh/fish/pwsh completion scripts plus dynamic
ValidArgsFunction completion on the commands where it's useful:
gwt go <TAB> → existing worktree branches (excludes current)
gwt rm <TAB> → removable worktrees (excludes main + already-typed
args, so a second TAB after `rm feat-a ` offers the
remainder)
gwt add <TAB> → local branches + <remote>/<rest> refs that don't
already have a worktree
Candidates carry descriptions matching `gwt ls` columns (DisplayPath
+ HumanSince) so zsh / fish / bash-v2 / pwsh show e.g.
`feat-x -- #feat-x 2h 3m` next to each branch. bash-v1 falls back
to plain branch names — graceful degradation.
The shell function generated by `git-wt alias <name>` now appends a
two-line completion-binding so `eval "$(git-wt alias gwt)"` wires both
the wrapper AND completion in one step. The binding is no-op-safe
when the completion script hasn't been sourced yet. Opt out with
`git-wt alias gwt --no-completion`.
Setup:
git-wt completion bash > ~/.local/share/bash-completion/completions/git-wt
git-wt completion zsh > "${fpath[1]}/_git-wt"
git-wt completion fish > ~/.config/fish/completions/git-wt.fish
Implementation notes:
- internal/cli/completion.go — Cobra subcommand
- internal/cli/completion_helpers.go — pure helpers + 3 ValidArgsFuncs
- internal/cli/alias.go — completion-binding template +
--no-completion flag
Helpers reuse the existing columnWidths/padRight + ui.HumanSince so
completion descriptions stay in lockstep with `gwt ls` layout —
location padded to max width within the batch, recency at its native
fixed-width unit-pair format so columns line up across candidates.
Tests:
- 6 unit cases (worktreeBranchesForGo/Rm, addCandidates, descriptions,
splitNonEmptyLines)
- 7 e2e cases (script render per shell, __complete go/rm against a
real repo, description content, alias binding on/off)
feat[config]: persistent settings via git config wt.* + ${...} templa…
…ting
Add a typed config layer on top of `git config wt.*`, a new `git-wt config`
command for managing it, and `${...}` template expansion for path-shaped
values. BREAKING: default `parentDir` renamed from `.gwt` to `.worktrees`.
New surface:
- `git-wt config` lists every key with effective value + source scope.
- `git-wt config <key>` shows raw + (for templated values) resolved.
- `git-wt config <key> <value>` writes to --local by default; --global flag.
- `git-wt config --unset <key>` is idempotent.
- Three keys: `wt.parentDir` (templated string), `wt.plain` (bool),
`wt.fd` (int, 3-9).
- Template vars: ${repo}, ${repoPath}, ${repoParent}, ${home}. Unknown
vars error at set time AND surface at use time (no silent fallback).
- Precedence: CLI flag > --local > --global > built-in default.
Internal layering:
- New `internal/config/` package with typed Key registry (incl. per-Key
Validator field, e.g. FD's 3-9 range lives next to FD's declaration).
- New `internal/cli/config_resolve.go` (parent-dir flag/config/default
precedence helper).
- `applyConfigDefaults` in root.go reads wt.plain / wt.fd as flag defaults
via small helpers (applyBoolFlagDefault / applyIntFlagDefault) that
route through config.Validate so per-key constraints have one source
of truth.
- `wt.ResolveParentDir` now does template expansion and resolves relative
paths against mainRoot (was: against CWD, which surprised users running
`--parent-dir trees` from a subdir).
- `internal/git` exposes a typed `*ExitError` (with ExitCode + Unwrap)
so callers can branch on exit codes (e.g. `git config --unset` returns
5 when missing) without string-matching error messages.
- `internal/testutil` package with SetupTempGitconfig shared across the
cli and config test suites (was: two near-identical copies).
Breaking default:
- `<repo>/.gwt/` → `<repo>/.worktrees/`. Existing users can pin the old
path: `git config --global wt.parentDir '${repoPath}/.gwt'`. Old
worktrees in `.gwt/` remain tracked by git and visible in `gwt list`;
only the default destination for new worktrees moves.
Headline use case:
git config --global wt.parentDir '${repoParent}/${repo}.worktrees'
# every repo now uses sibling `<reponame>.worktrees/` directories
Tests:
- ~25 new unit tests (config package, cli helpers, git ExitError contract).
- 11 new E2E (set/get/unset, literal + templated values, flag-vs-config
precedence, validation, bypass-validation surfaces at use time,
wt.plain runtime effect, unknown key).
- All e2e and unit tests previously referencing `.gwt` updated to
`.worktrees`.
refactor[eject]: extract planEject + ejectPlan struct
runEject was a 56-line slab of orchestration mixing arg parsing,
repo inspection, branch/base detection, leaf defaulting, parent +
path prep, dirty check, confirm, and dispatch. Lift the eight
gather-and-validate steps into planEject returning a *ejectPlan,
and pass that single value through confirmEject and executeEject.
runEject: 56 lines → 14 lines (orchestration only)
confirmEject: takes *ejectPlan instead of 4 positional args
executeEject: takes *ejectPlan instead of 7 positional args
(the smell the analyst flagged)
Pure structural change; no behaviour difference.
test[e2e]: pin which error path fires for bare-repo refusal The bare-repo refusal currently flows through wt.Inspect's "not in a git repository" error — `git rev-parse --show-toplevel` errors out in a bare repo before the explicit Bare guard inside requireMutableRepo can fire. Phase 3 dead-code audit confirmed the guard is unreachable today but is kept as defensive policy. Asserting on the message means that if Inspect is ever reordered so the Bare guard starts firing (with its "cannot create worktrees in a bare repository" wording), these tests will fail loudly — exposing the change rather than letting it land silently.
fix[fd]: require write mode for wrapper-protocol fd
Container runtimes (Docker, runc, GitHub Actions Linux runners) leak a
read-only /sys/fs/cgroup/cpu.max as fd 3 in every child process. The
previous fstat-only check accepted this as a valid wrapper fd, so
git-wt would attempt to write the worktree path and fail with EBADF
("error: write fd3: bad file descriptor"). Every CI push since
2026-05-08 was red for this reason.
fd.Open / fd.Available now require O_WRONLY or O_RDWR via fcntl(F_GETFL).
The shell wrapper passes the write end of a captured pipe (O_WRONLY),
which still qualifies; the cgroup leak (O_RDONLY) is correctly rejected
and the binary falls through to bare mode.
Also:
- debug.Logf for one-shot notes; emit.go logs the bare-mode fallback
under --debug for diagnostics
- e2e harness pins --fd 9 across all subcommands so bare-mode tests
probe a reliably-closed fd regardless of environment
- TestAlias_DefaultFD bypasses the harness wrapper to pin the
user-facing default of fd 3
fix[alias]: detect subcommand after leading global flags The wrapper's `case "$1"` only matched when the subcommand was the literal first arg, so `gwt --debug go <branch>` (and any pre-subcommand global flag like --plain, -n, --fd) fell through to the pass-through branch. That branch has no fd redirect, so emitTarget hit the bare-mode path (stdout + arrow hint) and the parent shell never cd'd. The wrapper now walks $@ skipping leading flags (and --fd's separate value arg) to find the subcommand, then routes go/new/rm through the cd-capable branch as intended. The workaround of putting the flag after the subcommand (`gwt go --debug`) still works and is now redundant. E2E coverage: TestAlias_WrapperCdWorksWithLeadingGlobalFlag evals the generated wrapper in bash and asserts pwd lands in the worktree after `gwt --debug go`, `gwt --plain go`, `gwt -n go`, and the trailing-flag form (regression guard).
feat[debug]: tab-separate columns with '-' placeholder for empties Switches the debug timeline from space-separated prose to tab-separated columns so awk/cut/sort can extract fields cleanly: [elapsed] op#N status took name detail [err] Non-trailing empties (the took column on start lines, detail when an op has no args but a non-nil error) are filled with "-" so default awk's \$N stays stable across start/done/failed rows. Trailing empties are omitted so success lines stop at 6 columns. ui.Dim now sets TabWidth(-1) — lipgloss otherwise expands embedded tabs to 4 spaces during render and silently mangles column boundaries when output goes to a TTY. The other two ui.Dim callers (list, picker) pass single-field strings with no tabs, so no behavioural change for them.
feat: wire --debug flag to operation timeline Introduces internal/debug with Op(name, detail...) that records start/done pairs to stderr (op#N ids, durations, error state). The previously dead --debug flag now drives this. Every git subprocess is instrumented at the wrapper, plus targeted spans in rm (chdir bounce, execute) and new (config copy, check-ignore probe).
PreviousNext