fix(syncbranch): support bare repos and worktrees with git-common-dir (#641)

* fix(syncbranch): support bare repos and worktrees with git-common-dir

Replace hardcoded .git/beads-worktrees/ path with dynamic detection using
git rev-parse --git-common-dir. This correctly handles:

- Regular repositories (.git is a directory)
- Git worktrees (.git is a file pointing elsewhere)
- Bare repositories (no .git directory, repo IS the git dir)
- Worktrees of bare repositories

The new getBeadsWorktreePath() helper uses git's native API to find the
shared git directory, ensuring beads worktrees are created in the correct
location regardless of repository structure.

Updated functions:
- CommitToSyncBranch
- PullFromSyncBranch
- CheckDivergence
- ResetToRemote

Fixes #639

* test(syncbranch): add regression tests for getBeadsWorktreePath

Add comprehensive tests for the worktree path calculation to ensure proper
handling of various repository structures:

- Regular repos: uses .git/beads-worktrees path
- Bare repos: uses <bare-repo>/beads-worktrees (no .git subdirectory)
- Worktrees: uses main repo's .git/beads-worktrees (git-common-dir)
- Fallback: legacy behavior when git command fails
- Relative paths: ensures absolute path conversion

These tests ensure the fix for GH#639 doesn't regress.

---------

Co-authored-by: Charles P. Cross <cpdata@users.noreply.github.com>
This commit is contained in:
Charles P. Cross
2025-12-19 20:52:40 -05:00
committed by GitHub
parent e309b1c1aa
commit 81aa301649
2 changed files with 204 additions and 8 deletions

View File

@@ -77,8 +77,8 @@ func CommitToSyncBranch(ctx context.Context, repoRoot, syncBranch, jsonlPath str
Branch: syncBranch,
}
// Worktree path is under .git/beads-worktrees/<branch>
worktreePath := filepath.Join(repoRoot, ".git", "beads-worktrees", syncBranch)
// GH#639: Use git-common-dir for worktree path to support bare repos
worktreePath := getBeadsWorktreePath(ctx, repoRoot, syncBranch)
// Initialize worktree manager
wtMgr := git.NewWorktreeManager(repoRoot)
@@ -240,8 +240,8 @@ func PullFromSyncBranch(ctx context.Context, repoRoot, syncBranch, jsonlPath str
JSONLPath: jsonlPath,
}
// Worktree path is under .git/beads-worktrees/<branch>
worktreePath := filepath.Join(repoRoot, ".git", "beads-worktrees", syncBranch)
// GH#639: Use git-common-dir for worktree path to support bare repos
worktreePath := getBeadsWorktreePath(ctx, repoRoot, syncBranch)
// Initialize worktree manager
wtMgr := git.NewWorktreeManager(repoRoot)
@@ -469,8 +469,8 @@ func CheckDivergence(ctx context.Context, repoRoot, syncBranch string) (*Diverge
Branch: syncBranch,
}
// Worktree path is under .git/beads-worktrees/<branch>
worktreePath := filepath.Join(repoRoot, ".git", "beads-worktrees", syncBranch)
// GH#639: Use git-common-dir for worktree path to support bare repos
worktreePath := getBeadsWorktreePath(ctx, repoRoot, syncBranch)
// Initialize worktree manager
wtMgr := git.NewWorktreeManager(repoRoot)
@@ -526,8 +526,8 @@ func CheckDivergence(ctx context.Context, repoRoot, syncBranch string) (*Diverge
//
// Returns error if reset fails.
func ResetToRemote(ctx context.Context, repoRoot, syncBranch, jsonlPath string) error {
// Worktree path is under .git/beads-worktrees/<branch>
worktreePath := filepath.Join(repoRoot, ".git", "beads-worktrees", syncBranch)
// GH#639: Use git-common-dir for worktree path to support bare repos
worktreePath := getBeadsWorktreePath(ctx, repoRoot, syncBranch)
// Initialize worktree manager
wtMgr := git.NewWorktreeManager(repoRoot)
@@ -858,6 +858,28 @@ func PushSyncBranch(ctx context.Context, repoRoot, syncBranch string) error {
return pushFromWorktree(ctx, worktreePath, syncBranch)
}
// getBeadsWorktreePath returns the path where beads worktrees should be stored.
// GH#639: Uses git rev-parse --git-common-dir to correctly handle bare repos and worktrees.
// For regular repos, this is typically .git/beads-worktrees/<branch>.
// For bare repos or worktrees of bare repos, this uses the common git directory.
func getBeadsWorktreePath(ctx context.Context, repoRoot, syncBranch string) string {
// Try to get the git common directory using git's native API
// This handles all cases: regular repos, worktrees, bare repos
cmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", "--git-common-dir")
output, err := cmd.Output()
if err == nil {
gitCommonDir := strings.TrimSpace(string(output))
// Make path absolute if it's relative
if !filepath.IsAbs(gitCommonDir) {
gitCommonDir = filepath.Join(repoRoot, gitCommonDir)
}
return filepath.Join(gitCommonDir, "beads-worktrees", syncBranch)
}
// Fallback to legacy behavior for compatibility
return filepath.Join(repoRoot, ".git", "beads-worktrees", syncBranch)
}
// getRemoteForBranch gets the remote name for a branch, defaulting to "origin"
func getRemoteForBranch(ctx context.Context, worktreePath, branch string) string {
remoteCmd := exec.CommandContext(ctx, "git", "-C", worktreePath, "config", "--get", fmt.Sprintf("branch.%s.remote", branch))