fix(sync): tolerate "issue not found" during 3-way merge deletion

During sync, the 3-way merge logic tries to delete issues that were
removed remotely. If an issue is already gone (tombstoned or never
existed locally), that shouldn't be an error - the goal is just to
ensure the issue is deleted.

Changes:
- Add isIssueNotFoundError helper to detect missing issue errors
- Skip "issue not found" errors during merge deletion (count as success)
- Update stats output to show already-gone count when relevant
This commit is contained in:
Ryan Snodgrass
2025-12-16 00:15:35 -08:00
parent c7b45a8a40
commit e3d8119f8e

View File

@@ -5,12 +5,22 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/merge" "github.com/steveyegge/beads/internal/merge"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
) )
// isIssueNotFoundError checks if the error indicates the issue doesn't exist
// This is OK during merge - the issue may already be deleted/tombstoned
func isIssueNotFoundError(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "issue not found:")
}
// getVersion returns the current bd version // getVersion returns the current bd version
func getVersion() string { func getVersion() string {
return Version return Version
@@ -75,10 +85,16 @@ func merge3WayAndPruneDeletions(ctx context.Context, store storage.Storage, json
} }
// Prune accepted deletions from the database // Prune accepted deletions from the database
// Collect all deletion errors - fail the operation if any delete fails // "Issue not found" errors are OK - the issue may already be deleted/tombstoned
var deletionErrors []error var deletionErrors []error
var alreadyGone int
for _, id := range acceptedDeletions { for _, id := range acceptedDeletions {
if err := store.DeleteIssue(ctx, id); err != nil { if err := store.DeleteIssue(ctx, id); err != nil {
// If issue is already gone (tombstoned or never existed locally), that's fine
if isIssueNotFoundError(err) {
alreadyGone++
continue
}
deletionErrors = append(deletionErrors, fmt.Errorf("issue %s: %w", id, err)) deletionErrors = append(deletionErrors, fmt.Errorf("issue %s: %w", id, err))
} }
} }
@@ -89,9 +105,15 @@ func merge3WayAndPruneDeletions(ctx context.Context, store storage.Storage, json
// Print stats if deletions were found // Print stats if deletions were found
stats := sm.GetStats() stats := sm.GetStats()
if stats.DeletionsFound > 0 { actuallyDeleted := len(acceptedDeletions) - alreadyGone
fmt.Fprintf(os.Stderr, "3-way merge: pruned %d deleted issue(s) from database (base: %d, left: %d, merged: %d)\n", if stats.DeletionsFound > 0 || alreadyGone > 0 {
stats.DeletionsFound, stats.BaseCount, stats.LeftCount, stats.MergedCount) if alreadyGone > 0 {
fmt.Fprintf(os.Stderr, "3-way merge: pruned %d deleted issue(s) from database, %d already gone (base: %d, left: %d, merged: %d)\n",
actuallyDeleted, alreadyGone, stats.BaseCount, stats.LeftCount, stats.MergedCount)
} else {
fmt.Fprintf(os.Stderr, "3-way merge: pruned %d deleted issue(s) from database (base: %d, left: %d, merged: %d)\n",
actuallyDeleted, stats.BaseCount, stats.LeftCount, stats.MergedCount)
}
} }
return true, nil return true, nil