harden worktree checks from review feedback · Entire
harden worktree checks from review feedback
11febb6→main·pfleidi·1w ago·2 files·+43 added/-1 removed
Use Lstat and require a directory when reusing a registered worktree path, so a planted symlink is rejected instead of passing the health check. Run 'git worktree list' from the main root and include git's stderr in the failure message.
Sessions
01KX6XRJEFTVG0ABV9GDFE0G00View transcript
?\ Add trail checkout --worktree SupportClaude Code·2 steps
Changes
2
cmd/entire/cli
Mtrail_checkout_worktree.go+10/-1
Mtrail_checkout_worktree_test.go+33
313 unmodified lines
314
315
316
317
317
318
319
320
321
322
323
324
97 unmodified lines
422
423
424
425
426
427
428
429
430
431
432
433
434
313 unmodified lines
return err
}
if found {
switch _, statErr := os.Stat(match.path); {
// Lstat: a symlink planted at the registered path must not pass as a
// healthy worktree directory.
switch info, statErr := os.Lstat(match.path); {
case statErr == nil && !info.IsDir():
return fmt.Errorf("branch %q is registered to %s, which is not a directory", branch, match.path)
case statErr == nil:
if !match.managed {
return fmt.Errorf("branch %q is already checked out at %s", branch, match.path)
}
97 unmodified lines
// with found reporting whether any worktree does.
func findWorktreeForBranch(ctx context.Context, branch, root string) (match trailWorktreeMatch, found bool, err error) {
cmd := exec.CommandContext(ctx, "git", "worktree", "list", "--porcelain")
cmd.Dir = root
output, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && len(bytes.TrimSpace(exitErr.Stderr)) > 0 {
return trailWorktreeMatch{}, false, fmt.Errorf("failed to list worktrees: %s: %w", bytes.TrimSpace(exitErr.Stderr), err)
}
return trailWorktreeMatch{}, false, fmt.Errorf("failed to list worktrees: %w", err)
}
// Empty currentRoot: match any worktree, including the current checkout.
Mcmd/entire/cli/trail_checkout_worktree.go+10/-1
601 unmodified lines
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
601 unmodified lines
}
}
func TestCheckoutTrailWorktree_RegisteredPathNotADirectory(t *testing.T) {
repoDir := newTrailWorktreeTestRepo(t)
runGit(t, repoDir, "branch", "feature/swapped")
t.Chdir(repoDir)
var out1, err1 bytes.Buffer
if err := checkoutTrailWorktree(context.Background(), &out1, &err1, "feature/swapped", false, 6); err != nil {
t.Fatalf("first checkout: %v; stderr: %s", err, err1.String())
}
worktreePath := filepath.Join(repoDir, ".entire", "worktrees", "trail-6-feature-swapped")
if err := os.RemoveAll(worktreePath); err != nil {
t.Fatalf("remove worktree dir: %v", err)
}
if err := os.Symlink(repoDir, worktreePath); err != nil {
t.Skipf("symlinks unsupported: %v", err)
}
var out2, err2 bytes.Buffer
err := checkoutTrailWorktree(context.Background(), &out2, &err2, "feature/swapped", false, 6)
if err == nil || !strings.Contains(err.Error(), "is not a directory") {
t.Fatalf("error = %v, want not-a-directory rejection", err)
}
}
func TestFindWorktreeForBranch_SurfacesGitError(t *testing.T) {
testutil.IsolateGitConfigEnv(t)
_, _, err := findWorktreeForBranch(context.Background(), "any", t.TempDir())
if err == nil || !strings.Contains(err.Error(), "not a git repository") {
t.Fatalf("error = %v, want git stderr in message", err)
}
}
func TestCheckoutTrailWorktree_RejectsInvalidBranch(t *testing.T) {
t.Parallel()