resume picker: make worktree porcelain parsing block-scoped (trail finding) · Entire
resume picker: make worktree porcelain parsing block-scoped (trail finding)
5ef1842→main·
Soph·1mo ago·2 files·+59 added/-5 removed
Factor the git worktree list --porcelain parsing in branchCheckedOutElsewhere into parseWorktreeForBranch and make it block-scoped: reset the worktree path at each blank-line block boundary and only consider a branch line when a worktree line was seen in the same block. A detached worktree (no branch line) can no longer pair a branch with a stale path or return an empty path. Also drops the now-dead rawRoot!="" guard (the empty case returns earlier).
Adds TestParseWorktreeForBranch covering a detached worktree.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Changes
2
cmd/entire/cli
Mresume_picker.go+17/-5
Mresume_picker_test.go+42
440 unmodified lines
```go
dec := exec.CommandContext(ctx, "git", "worktree", "list", "--porcelain")
if rawRoot != "" {
gitCmd.Dir = rawRoot
}
gitCmd.Dir = rawRoot
out, err := gitCmd.Output()
if err != nil {
return "", false
}
return parseWorktreeForBranch(string(out), branch, currentRoot)
}
// parseWorktreeForBranch scans `git worktree list --porcelain` output and returns
// the path of a worktree (other than currentRoot) that has branch checked out.
//
// Each worktree is a block beginning with a `worktree <path>` line and separated
// by a blank line; a `branch <ref>` line only appears for non-detached worktrees.
// curPath is reset at each block boundary and a branch line is only considered
// when a worktree line was seen in the same block, so a detached worktree (no
// branch line) can never pair a branch with a stale path or return an empty one.
func parseWorktreeForBranch(porcelain, branch, currentRoot string) (string, bool) {
var curPath string
for _, line := range strings.Split(string(out), "\n") {
for _, line := range strings.Split(porcelain, "\n") {
switch {
case line == "":
curPath = "" // block boundary
case strings.HasPrefix(line, "worktree "):
curPath = strings.TrimPrefix(line, "worktree ")
case strings.HasPrefix(line, "branch "):
case strings.HasPrefix(line, "branch ") && curPath != "":
name := strings.TrimPrefix(strings.TrimPrefix(line, "branch "), "refs/heads/")
if name == branch && normalizeWorktreePath(curPath) != currentRoot {
return curPath, true
}
}
}
}
Mcmd/entire/cli/resume_picker.go+17/-5
480 unmodified lines
// TestParseWorktreeForBranch covers the porcelain parser, including a detached
// worktree (no branch line) that must not cause a stale or empty-path match.
func TestParseWorktreeForBranch(t *testing.T) {
t.Parallel()
porcelain := strings.Join([]string{
"worktree /repo/main",
"HEAD 1111111111111111111111111111111111111111",
"branch refs/heads/main",
"",
"worktree /repo/wt-feat",
"HEAD 2222222222222222222222222222222222222222",
"branch refs/heads/feat",
"",
"worktree /repo/wt-detached",
"HEAD 3333333333333333333333333333333333333333",
"detached",
"",
}, "\n")
// A branch checked out in another worktree is found, with its path.
if path, ok := parseWorktreeForBranch(porcelain, "feat", "/repo/main"); !ok || path != "/repo/wt-feat" {
t.Errorf("feat: got (%q, %v), want (/repo/wt-feat, true)", path, ok)
}
// The current worktree's own branch is not "elsewhere".
if path, ok := parseWorktreeForBranch(porcelain, "main", "/repo/main"); ok {
t.Errorf("main from its own worktree should not match, got (%q, %v)", path, ok)
}
// A branch on no worktree is not found.
if _, ok := parseWorktreeForBranch(porcelain, "nope", "/repo/main"); ok {
t.Error("unknown branch should not match")
}
// A name equal to the detached worktree's path must not match (the detached
// block has no branch line, so nothing is attributed to it).
if _, ok := parseWorktreeForBranch(porcelain, "/repo/wt-detached", "/repo/main"); ok {
t.Error("detached worktree must not produce a branch match")
}
}
// TestBranchCheckedOutElsewhere verifies worktree awareness: a branch checked
// out in another worktree is detected (with its path), while the current
// worktree's own branch and unknown branches are not flagged.