diff --git a/cmd/bd/migrate_sync.go b/cmd/bd/migrate_sync.go index e12b4740..e4f8af4f 100644 --- a/cmd/bd/migrate_sync.go +++ b/cmd/bd/migrate_sync.go @@ -155,12 +155,12 @@ func runMigrateSync(ctx context.Context, branchName string, dryRun, force bool) fmt.Printf("→ Would create new branch '%s'\n", branchName) } - // Use worktree-aware git directory detection - gitDir, err := git.GetGitDir() + // Use git-common-dir for worktree path to support bare repos and worktrees (GH#639) + gitCommonDir, err := git.GetGitCommonDir() if err != nil { return fmt.Errorf("not a git repository: %w", err) } - worktreePath := filepath.Join(gitDir, "beads-worktrees", branchName) + worktreePath := filepath.Join(gitCommonDir, "beads-worktrees", branchName) fmt.Printf("→ Would create worktree at: %s\n", worktreePath) fmt.Println("\n=== END DRY RUN ===") @@ -195,12 +195,12 @@ func runMigrateSync(ctx context.Context, branchName string, dryRun, force bool) } // Step 2: Create the worktree - // Use worktree-aware git directory detection - gitDir, err := git.GetGitDir() + // Use git-common-dir for worktree path to support bare repos and worktrees (GH#639) + gitCommonDir, err := git.GetGitCommonDir() if err != nil { return fmt.Errorf("not a git repository: %w", err) } - worktreePath := filepath.Join(gitDir, "beads-worktrees", branchName) + worktreePath := filepath.Join(gitCommonDir, "beads-worktrees", branchName) fmt.Printf("→ Creating worktree at %s...\n", worktreePath) wtMgr := git.NewWorktreeManager(repoRoot) diff --git a/internal/git/gitdir.go b/internal/git/gitdir.go index 116fe910..dd11b531 100644 --- a/internal/git/gitdir.go +++ b/internal/git/gitdir.go @@ -100,6 +100,23 @@ func GetGitDir() (string, error) { return ctx.gitDir, nil } +// GetGitCommonDir returns the common git directory shared across all worktrees. +// For regular repos, this equals GetGitDir(). For worktrees, this returns +// the main repository's .git directory where shared data (like worktree +// registrations, hooks, and objects) lives. +// +// Use this instead of GetGitDir() when you need to create new worktrees or +// access shared git data that should not be scoped to a single worktree. +// GH#639: This is critical for bare repo setups where GetGitDir() returns +// a worktree-specific path that cannot host new worktrees. +func GetGitCommonDir() (string, error) { + ctx, err := getGitContext() + if err != nil { + return "", err + } + return ctx.commonDir, nil +} + // GetGitHooksDir returns the path to the Git hooks directory. // This function is worktree-aware and handles both regular repos and worktrees. func GetGitHooksDir() (string, error) { diff --git a/internal/syncbranch/worktree.go b/internal/syncbranch/worktree.go index 379829d8..134b34c5 100644 --- a/internal/syncbranch/worktree.go +++ b/internal/syncbranch/worktree.go @@ -987,8 +987,9 @@ func GetRepoRoot(ctx context.Context) (string, error) { line := strings.TrimSpace(string(content)) if strings.HasPrefix(line, "gitdir: ") { gitDir := strings.TrimPrefix(line, "gitdir: ") - // Remove /worktrees/* part - if idx := strings.Index(gitDir, "/worktrees/"); idx > 0 { + // Remove /worktrees/* part - use LastIndex to handle user paths containing "worktrees" + // e.g., /Users/foo/worktrees/project/.bare/worktrees/main should strip at .bare/worktrees/ + if idx := strings.LastIndex(gitDir, "/worktrees/"); idx > 0 { gitDir = gitDir[:idx] } repoRoot = filepath.Dir(gitDir)