diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 92ed856a..39d9b517 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -380,12 +380,20 @@ Use --merge to merge the sync branch back to main branch.`, if err := ensureStoreActive(); err == nil && store != nil { syncBranchName, _ = syncbranch.Get(ctx, store) if syncBranchName != "" && syncbranch.HasGitRemote(ctx) { - repoRoot, err = syncbranch.GetRepoRoot(ctx) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: sync.branch configured but failed to get repo root: %v\n", err) - fmt.Fprintf(os.Stderr, "Falling back to current branch commits\n") + // GH#519: Check if sync.branch equals current branch + // If so, we can't use a worktree (git doesn't allow same branch in multiple worktrees) + // Fall back to direct commits on the current branch + if syncbranch.IsSyncBranchSameAsCurrent(ctx, syncBranchName) { + // sync.branch == current branch - use regular commits, not worktree + useSyncBranch = false } else { - useSyncBranch = true + repoRoot, err = syncbranch.GetRepoRoot(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: sync.branch configured but failed to get repo root: %v\n", err) + fmt.Fprintf(os.Stderr, "Falling back to current branch commits\n") + } else { + useSyncBranch = true + } } } } diff --git a/internal/syncbranch/worktree.go b/internal/syncbranch/worktree.go index de272575..88d3da7c 100644 --- a/internal/syncbranch/worktree.go +++ b/internal/syncbranch/worktree.go @@ -916,3 +916,25 @@ func HasGitRemote(ctx context.Context) bool { } return len(strings.TrimSpace(string(output))) > 0 } + +// GetCurrentBranch returns the name of the current git branch +func GetCurrentBranch(ctx context.Context) (string, error) { + cmd := exec.CommandContext(ctx, "git", "symbolic-ref", "--short", "HEAD") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get current branch: %w", err) + } + return strings.TrimSpace(string(output)), nil +} + +// IsSyncBranchSameAsCurrent returns true if the sync branch is the same as the current branch. +// This is used to detect the case where we can't use a worktree because the branch is already +// checked out. In this case, we should commit directly to the current branch instead. +// See: https://github.com/steveyegge/beads/issues/519 +func IsSyncBranchSameAsCurrent(ctx context.Context, syncBranch string) bool { + currentBranch, err := GetCurrentBranch(ctx) + if err != nil { + return false + } + return currentBranch == syncBranch +} diff --git a/internal/syncbranch/worktree_divergence_test.go b/internal/syncbranch/worktree_divergence_test.go index 96289e57..99060477 100644 --- a/internal/syncbranch/worktree_divergence_test.go +++ b/internal/syncbranch/worktree_divergence_test.go @@ -683,3 +683,70 @@ func TestCountIssuesInContent(t *testing.T) { }) } } + +// TestIsSyncBranchSameAsCurrent tests detection of sync.branch == current branch (GH#519) +func TestIsSyncBranchSameAsCurrent(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + ctx := context.Background() + + t.Run("returns true when sync branch equals current branch", func(t *testing.T) { + repoDir := setupTestRepo(t) + defer os.RemoveAll(repoDir) + + // Create initial commit so we can get current branch + writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`) + runGit(t, repoDir, "add", ".") + runGit(t, repoDir, "commit", "-m", "initial commit") + + // Get current branch name + currentBranch := strings.TrimSpace(getGitOutput(t, repoDir, "symbolic-ref", "--short", "HEAD")) + + // Save original dir and change to test repo + origDir, _ := os.Getwd() + os.Chdir(repoDir) + defer os.Chdir(origDir) + + // Should return true when sync branch == current branch + if !IsSyncBranchSameAsCurrent(ctx, currentBranch) { + t.Errorf("IsSyncBranchSameAsCurrent(%q) = false, want true", currentBranch) + } + }) + + t.Run("returns false when sync branch differs from current branch", func(t *testing.T) { + repoDir := setupTestRepo(t) + defer os.RemoveAll(repoDir) + + // Create initial commit + writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`) + runGit(t, repoDir, "add", ".") + runGit(t, repoDir, "commit", "-m", "initial commit") + + // Save original dir and change to test repo + origDir, _ := os.Getwd() + os.Chdir(repoDir) + defer os.Chdir(origDir) + + // Should return false when sync branch != current branch + if IsSyncBranchSameAsCurrent(ctx, "beads-sync") { + t.Error("IsSyncBranchSameAsCurrent(\"beads-sync\") = true, want false") + } + }) + + t.Run("returns false on error getting current branch", func(t *testing.T) { + // Test in a non-git directory + tmpDir, _ := os.MkdirTemp("", "non-git-*") + defer os.RemoveAll(tmpDir) + + origDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(origDir) + + // Should return false when not in a git repo + if IsSyncBranchSameAsCurrent(ctx, "any-branch") { + t.Error("IsSyncBranchSameAsCurrent in non-git dir = true, want false") + } + }) +}