Fix daemon auto-sync delete mutation not reflected in sync branch (#537)

Fix daemon auto-sync delete mutation not reflected in sync branch

When deleting an issue with `bd delete <id> --force`, the daemon auto-sync now properly removes the deleted issue from the sync branch.

**Problem:** The merge logic saw fewer local issues (due to deletion) and would re-add the deleted issue.

**Solution:** Add `ForceOverwrite` option to bypass merge logic when mutations occur. Mutation-triggered exports are authoritative and should overwrite, not merge.

Reviewed-by: stevey
This commit is contained in:
Charles P. Cross
2025-12-13 13:53:09 -05:00
committed by GitHub
parent 520e1007f1
commit eb988fcb21
5 changed files with 937 additions and 456 deletions

View File

@@ -455,7 +455,10 @@ func performExport(ctx context.Context, store storage.Storage, autoCommit, autoP
// Auto-commit if enabled (skip in git-free mode)
if autoCommit && !skipGit {
// Try sync branch commit first
committed, err := syncBranchCommitAndPush(exportCtx, store, autoPush, log)
// Use forceOverwrite=true because mutation-triggered exports (create, update, delete)
// mean the local state is authoritative and should not be merged with worktree.
// This is critical for delete mutations to be properly reflected in the sync branch.
committed, err := syncBranchCommitAndPushWithOptions(exportCtx, store, autoPush, true, log)
if err != nil {
log.log("Sync branch commit failed: %v", err)
return

View File

@@ -14,9 +14,18 @@ import (
"github.com/steveyegge/beads/internal/syncbranch"
)
// syncBranchCommitAndPush commits JSONL to the sync branch using a worktree
// Returns true if changes were committed, false if no changes or sync.branch not configured
// syncBranchCommitAndPush commits JSONL to the sync branch using a worktree.
// Returns true if changes were committed, false if no changes or sync.branch not configured.
// This is a convenience wrapper that calls syncBranchCommitAndPushWithOptions with default options.
func syncBranchCommitAndPush(ctx context.Context, store storage.Storage, autoPush bool, log daemonLogger) (bool, error) {
return syncBranchCommitAndPushWithOptions(ctx, store, autoPush, false, log)
}
// syncBranchCommitAndPushWithOptions commits JSONL to the sync branch using a worktree.
// Returns true if changes were committed, false if no changes or sync.branch not configured.
// If forceOverwrite is true, the local JSONL is copied to the worktree without merging,
// which is necessary for delete mutations to be properly reflected in the sync branch.
func syncBranchCommitAndPushWithOptions(ctx context.Context, store storage.Storage, autoPush, forceOverwrite bool, log daemonLogger) (bool, error) {
// Check if any remote exists (bd-biwp: support local-only repos)
if !hasGitRemote(ctx) {
return true, nil // Skip sync branch commit/push in local-only mode
@@ -77,7 +86,12 @@ func syncBranchCommitAndPush(ctx context.Context, store storage.Storage, autoPus
return false, fmt.Errorf("failed to get relative JSONL path: %w", err)
}
if err := wtMgr.SyncJSONLToWorktree(worktreePath, jsonlRelPath); err != nil {
// Use SyncJSONLToWorktreeWithOptions to pass forceOverwrite flag.
// When forceOverwrite is true (mutation-triggered sync, especially delete),
// the local JSONL is copied directly without merging, ensuring deletions
// are properly reflected in the sync branch.
syncOpts := git.SyncOptions{ForceOverwrite: forceOverwrite}
if err := wtMgr.SyncJSONLToWorktreeWithOptions(worktreePath, jsonlRelPath, syncOpts); err != nil {
return false, fmt.Errorf("failed to sync JSONL to worktree: %w", err)
}