fix: bypass gitHasBeadsChanges when using sync-branch (#812)

When using sync-branch with aggressive gitignore on code branches
(.beads/* ignored), gitHasBeadsChanges() returns false because git
does not track the files. This caused bd sync to skip CommitToSyncBranch
entirely, even though CommitToSyncBranch has its own internal change
detection that checks the worktree where gitignore is different.

The fix bypasses gitHasBeadsChanges when useSyncBranch is true, letting
CommitToSyncBranch determine if there are actual changes in the worktree.

Closes #812

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 18:04:15 -08:00
parent 38a6097512
commit cf4aff1cb4
2 changed files with 118 additions and 3 deletions

View File

@@ -475,9 +475,21 @@ Use --merge to merge the sync branch back to main branch.`,
}
// Step 2: Check if there are changes to commit (check entire .beads/ directory)
hasChanges, err := gitHasBeadsChanges(ctx)
if err != nil {
FatalError("checking git status: %v", err)
// GH#812: When using sync-branch, skip this check - CommitToSyncBranch handles it internally.
// The main repo's .beads may be gitignored on code branches (valid per #797/#801).
// In that case, gitHasBeadsChanges returns false even when JSONL has changes,
// because git doesn't track the files. CommitToSyncBranch copies files to the
// worktree where they ARE tracked (different gitignore) and checks there.
var hasChanges bool
var err error
if !useSyncBranch {
hasChanges, err = gitHasBeadsChanges(ctx)
if err != nil {
FatalError("checking git status: %v", err)
}
} else {
// Let CommitToSyncBranch determine if there are actual changes in the worktree
hasChanges = true
}
// Track if we already pushed via worktree (to skip Step 5)

View File

@@ -146,3 +146,106 @@ func TestSyncBranchConfigPriorityOverUpstream(t *testing.T) {
}
})
}
// TestSyncBranchBypassesGitHasBeadsChanges tests that when sync.branch is configured,
// bd sync bypasses gitHasBeadsChanges and always calls CommitToSyncBranch.
// This is the regression test for GH#812: when .beads/ is gitignored on code branches
// (but tracked on the sync branch), gitHasBeadsChanges would return false, causing
// sync to skip CommitToSyncBranch entirely - even though CommitToSyncBranch has its
// own internal change detection that checks the worktree where gitignore is different.
func TestSyncBranchBypassesGitHasBeadsChanges(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
ctx := context.Background()
t.Run("CommitToSyncBranch detects changes even when main repo has gitignored beads", func(t *testing.T) {
// Setup: Create a git repo with sync-branch configured
tmpDir, cleanup := setupGitRepo(t)
defer cleanup()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0750); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create a sync branch with initial content
syncBranch := "beads-sync"
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
// Write initial content
if err := os.WriteFile(jsonlPath, []byte(`{"id":"test-1","title":"Initial"}`+"\n"), 0600); err != nil {
t.Fatalf("Failed to write initial JSONL: %v", err)
}
// Create sync branch and commit the initial content
if err := exec.Command("git", "checkout", "-b", syncBranch).Run(); err != nil {
t.Fatalf("Failed to create sync branch: %v", err)
}
if err := exec.Command("git", "add", ".").Run(); err != nil {
t.Fatalf("Failed to add files: %v", err)
}
if err := exec.Command("git", "commit", "-m", "initial sync").Run(); err != nil {
t.Fatalf("Failed to commit: %v", err)
}
if err := exec.Command("git", "checkout", "main").Run(); err != nil {
t.Fatalf("Failed to checkout main: %v", err)
}
// Recreate .beads directory on main branch (it may not exist after checkout)
if err := os.MkdirAll(beadsDir, 0750); err != nil {
t.Fatalf("Failed to recreate .beads dir: %v", err)
}
// Now add a gitignore that ignores all beads files on the code branch
// (simulating the pattern from GH#812)
gitignorePath := filepath.Join(beadsDir, ".gitignore")
gitignoreContent := `# On code branches, ignore all data files.
# The beads-sync branch tracks issues.jsonl, etc.
*
!.gitignore
`
if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0600); err != nil {
t.Fatalf("Failed to write .gitignore: %v", err)
}
if err := exec.Command("git", "add", gitignorePath).Run(); err != nil {
t.Fatalf("Failed to add .gitignore: %v", err)
}
if err := exec.Command("git", "commit", "-m", "add gitignore").Run(); err != nil {
t.Fatalf("Failed to commit .gitignore: %v", err)
}
// Now update the JSONL file (this change is gitignored on main branch)
newContent := `{"id":"test-1","title":"Initial"}` + "\n" + `{"id":"test-2","title":"New issue"}` + "\n"
if err := os.WriteFile(jsonlPath, []byte(newContent), 0600); err != nil {
t.Fatalf("Failed to update JSONL: %v", err)
}
// Verify gitHasBeadsChanges returns false (file is gitignored)
hasChanges, err := gitHasBeadsChanges(ctx)
if err != nil {
t.Fatalf("gitHasBeadsChanges error: %v", err)
}
if hasChanges {
t.Log("Note: gitHasBeadsChanges returned true (gitignore may not be working as expected in test env)")
// Continue test anyway - the key assertion is that CommitToSyncBranch works
} else {
t.Log("gitHasBeadsChanges correctly returned false (file is gitignored)")
}
// The key assertion: CommitToSyncBranch should still detect and commit changes
// because it checks the worktree where the gitignore is different
result, err := syncbranch.CommitToSyncBranch(ctx, tmpDir, syncBranch, jsonlPath, false)
if err != nil {
t.Fatalf("CommitToSyncBranch error: %v", err)
}
if !result.Committed {
t.Error("CommitToSyncBranch() Committed = false, want true")
t.Error("This indicates the GH#812 fix regression: sync branch worktree should detect changes even when main repo has files gitignored")
} else {
t.Log("CommitToSyncBranch correctly detected and committed changes to worktree")
}
})
}