fix: canonicalize path case on macOS for git worktree operations (GH#880)

bd sync fails with exit status 128 when the daemon is started from a
terminal with different path casing than what git has stored. This
happens on macOS case-insensitive filesystem when directory names
are renamed (e.g., MyProject to myproject) but terminal sessions
retain the old casing.

The fix uses realpath(1) on macOS to get the true filesystem case
when canonicalizing paths:
- CanonicalizePath() now calls realpath on macOS
- git.GetRepoRoot() canonicalizes repoRoot via canonicalizeCase()
- syncbranch.GetRepoRoot() uses utils.CanonicalizePath()

This ensures git worktree paths match exactly, preventing the
exit status 128 errors from git operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
beads/crew/dave
2026-01-04 13:52:38 -08:00
committed by Steve Yegge
parent fbc93e3de2
commit 7b90678afe
5 changed files with 163 additions and 11 deletions

View File

@@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
@@ -1233,6 +1234,60 @@ func TestCreateBeadsWorktree_MainRepoSparseCheckoutDisabled(t *testing.T) {
}
}
// TestGetRepoRootCanonicalCase tests that GetRepoRoot returns paths with correct
// filesystem case on case-insensitive filesystems (GH#880)
func TestGetRepoRootCanonicalCase(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("case canonicalization test only runs on macOS")
}
// Create a repo with mixed-case directory
tmpDir := t.TempDir()
mixedCaseDir := filepath.Join(tmpDir, "TestRepo")
if err := os.MkdirAll(mixedCaseDir, 0755); err != nil {
t.Fatal(err)
}
// Initialize git repo
cmd := exec.Command("git", "init")
cmd.Dir = mixedCaseDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to init git repo: %v\nOutput: %s", err, string(output))
}
// Save cwd and change to the repo using WRONG case
originalDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current dir: %v", err)
}
defer func() { _ = os.Chdir(originalDir) }()
// Access the repo with lowercase (wrong case)
wrongCasePath := filepath.Join(tmpDir, "testrepo")
// Verify the wrong case path works on macOS (case-insensitive)
if _, err := os.Stat(wrongCasePath); err != nil {
t.Fatalf("Wrong case path should be accessible on macOS: %v", err)
}
if err := os.Chdir(wrongCasePath); err != nil {
t.Fatalf("Failed to chdir with wrong case: %v", err)
}
ResetCaches() // Reset git context cache
// GetRepoRoot should return the canonical case (TestRepo, not testrepo)
repoRoot := GetRepoRoot()
if repoRoot == "" {
t.Fatal("GetRepoRoot returned empty string")
}
// The path should end with "TestRepo" (correct case), not "testrepo"
if !strings.HasSuffix(repoRoot, "TestRepo") {
t.Errorf("GetRepoRoot() = %q, want path ending in 'TestRepo' (correct case)", repoRoot)
}
}
// TestNormalizeBeadsRelPath tests path normalization for bare repo worktrees (GH#785, GH#810)
func TestNormalizeBeadsRelPath(t *testing.T) {
tests := []struct {