Skip to content

Tags: shhac/git-wt

Tags

v0.13.1

Toggle v0.13.1's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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.

v0.13.0

Toggle v0.13.0's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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.

v0.12.0

Toggle v0.12.0's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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)

v0.11.0

Toggle v0.11.0's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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`.

v0.10.0

Toggle v0.10.0's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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.

v0.9.0

Toggle v0.9.0's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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.

v0.8.3

Toggle v0.8.3's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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

v0.8.2

Toggle v0.8.2's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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).

v0.8.1

Toggle v0.8.1's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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.

v0.8.0

Toggle v0.8.0's commit message

Verified

This commit was signed with the committer’s verified signature.
shhac Paul Somers
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).