fix: recreate a prunable worktree instead of no-op'ing on create#107
Conversation
When a worktree dir was deleted out-of-band (a partial teardown, a manual `rm -rf`, an interrupted run) while its git admin record survived, `git worktree list` still reports the path — as "prunable". AddOrAttach's idempotency check matched that path and returned `(false, base, nil)`, so `bough create` reported the repo "already registered" and never recreated its dir. A `claude --worktree <name>` over such a worktree came up half-materialised: only the sub-repos whose dirs happened to survive were present, the rest silently missing. Confirm the dir exists on disk (os.Stat) before treating a path match as a materialised no-op. A registered-but-missing entry now falls through to the existing prune + add recovery and is re-materialised on its existing branch (attach path). Adds TestRunner_AddOrAttach_RecreatesPrunableWorktree (real git: create → rm -rf the dir → re-add must recreate on the same branch). Verified end-to-end against a real `bough create`: the shipped v0.13.0 binary leaves the deleted repo missing; the fixed binary re-attaches it.
…linked temp /review findings on the prunable-worktree fix: - gitwt.go: os.Stat gated recovery on statErr==nil, so a non-ENOENT stat failure (EACCES on a parent, a stale NFS mount) on a *present* worktree misclassified it as prunable and turned a should-be resume no-op into a hard error. Gate recovery on errors.Is(statErr, os.ErrNotExist) only. - gitwt_test.go: TestRunner_AddOrAttach_RecreatesPrunableWorktree did not guard the regression on macOS: t.TempDir() under /var -> /private/var meant the post-rm literal dst never matched git's resolved worktree-list path, so the loop took the no-match fall-through and never exercised the os.Stat guard (the guard could be reverted and the test still passed). Resolve the temp root with EvalSymlinks so the match -> stat-fails -> break path runs on every OS. Proven: reverting the guard now fails the test.
| // momentarily-stale NFS mount) must keep the old no-op rather | ||
| // than tear down a worktree that is actually present — the | ||
| // intent is "the dir was deleted", not "the dir is unreadable". | ||
| if _, statErr := os.Stat(dst); !errors.Is(statErr, os.ErrNotExist) { |
There was a problem hiding this comment.
Retrospective review: this ENOENT-only guard only catches the "whole directory deleted" shape of prunability. git worktree list --porcelain also flags an entry prunable when the dir survives on disk but its own .git file (or git's admin-side gitdir record) was removed independently — verified against real git 2.53.0, which emits prunable gitdir file points to non-existent location for exactly that shape while os.Stat(dst) reports no error at all. So this guard silently no-ops on that case, reintroducing a variant of the half-materialised-worktree bug this PR fixed.
Filed as PR #119: adds a Prunable field to Worktree (parsed from the porcelain prunable line, which List() was discarding), and gates recovery on wt.Prunable || ENOENT instead of ENOENT alone — trusting git's own verdict while keeping the EACCES/NFS protection this commit added for the non-prunable case.
fix: retrospective review fixes for merged PR #107
When a worktree dir is deleted out-of-band (a partial teardown, a manual
rm -rf, an interrupted run) while its git admin record survives,git worktree liststill reports the path — as "prunable".AddOrAttach's idempotency check matched that path and returned(false, base, nil), sobough createreported the repo "already registered" and never recreated its dir. Aclaude --worktree <name>over such a worktree came up half-materialised — only the sub-repos whose dirs happened to survive were present.Confirm the dir exists on disk (
os.Stat) before treating a path match as a no-op. A registered-but-missing entry now falls through to the existing prune + add recovery and is re-materialised on its existing branch (attach path).go build/golangci-lint ./internal/gitwt/internal/ cmd/ pkg/)bough createbefore/afterNew test:
TestRunner_AddOrAttach_RecreatesPrunableWorktree(real git: create →rm -rfthe dir → re-add must recreate on the same branch).