From 0f21a80fe0a2cd7a12ec3fb857411520747f3ec9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 30 Dec 2025 15:56:53 -0800 Subject: [PATCH] Add debug logging for tombstone resurrection events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When debug mode is enabled, the merge system now logs a message when an expired tombstone loses to a live issue (resurrection occurs). This helps understand why previously closed issues reappear. Example output: "Issue bd-abc resurrected (tombstone expired)" Changes: - Add debug parameter to Merge3WayWithTTL and merge3Way functions - Add debug logging in all 4 resurrection code paths - Update tests to pass new debug parameter (bd-nl2) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/merge/merge.go | 21 +++++++++-- internal/merge/merge_test.go | 72 ++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/internal/merge/merge.go b/internal/merge/merge.go index 7ce376e6..27ed02a0 100644 --- a/internal/merge/merge.go +++ b/internal/merge/merge.go @@ -113,7 +113,7 @@ func Merge3Way(outputPath, basePath, leftPath, rightPath string, debug bool) err } // Perform 3-way merge - result, conflicts := merge3Way(baseIssues, leftIssues, rightIssues) + result, conflicts := merge3Way(baseIssues, leftIssues, rightIssues, debug) if debug { fmt.Fprintf(os.Stderr, "Merge complete:\n") @@ -294,15 +294,16 @@ func IsExpiredTombstone(issue Issue, ttl time.Duration) bool { return time.Now().After(expirationTime) } -func merge3Way(base, left, right []Issue) ([]Issue, []string) { - return Merge3WayWithTTL(base, left, right, DefaultTombstoneTTL) +func merge3Way(base, left, right []Issue, debug bool) ([]Issue, []string) { + return Merge3WayWithTTL(base, left, right, DefaultTombstoneTTL, debug) } // Merge3WayWithTTL performs a 3-way merge with configurable tombstone TTL. // This is the core merge function that handles tombstone semantics. // Use this when you need to configure TTL for testing, debugging, or // per-repository configuration. For default TTL behavior, use merge3Way. -func Merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, []string) { +// When debug is true, logs resurrection events to stderr. +func Merge3WayWithTTL(base, left, right []Issue, ttl time.Duration, debug bool) ([]Issue, []string) { // Build maps for quick lookup by IssueKey baseMap := make(map[IssueKey]Issue) for _, issue := range base { @@ -416,6 +417,9 @@ func Merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, [] if leftTombstone && !rightTombstone { if IsExpiredTombstone(leftIssue, ttl) { // Tombstone expired - resurrection allowed, keep live issue + if debug { + fmt.Fprintf(os.Stderr, "Issue %s resurrected (tombstone expired)\n", rightIssue.ID) + } result = append(result, rightIssue) } else { // Tombstone wins @@ -428,6 +432,9 @@ func Merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, [] if rightTombstone && !leftTombstone { if IsExpiredTombstone(rightIssue, ttl) { // Tombstone expired - resurrection allowed, keep live issue + if debug { + fmt.Fprintf(os.Stderr, "Issue %s resurrected (tombstone expired)\n", leftIssue.ID) + } result = append(result, leftIssue) } else { // Tombstone wins @@ -456,6 +463,9 @@ func Merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, [] // CASE: Left is tombstone, right is live if leftTombstone && !rightTombstone { if IsExpiredTombstone(leftIssue, ttl) { + if debug { + fmt.Fprintf(os.Stderr, "Issue %s resurrected (tombstone expired)\n", rightIssue.ID) + } result = append(result, rightIssue) } else { result = append(result, leftIssue) @@ -466,6 +476,9 @@ func Merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, [] // CASE: Right is tombstone, left is live if rightTombstone && !leftTombstone { if IsExpiredTombstone(rightIssue, ttl) { + if debug { + fmt.Fprintf(os.Stderr, "Issue %s resurrected (tombstone expired)\n", leftIssue.ID) + } result = append(result, leftIssue) } else { result = append(result, rightIssue) diff --git a/internal/merge/merge_test.go b/internal/merge/merge_test.go index 93a497f3..a9d03dcf 100644 --- a/internal/merge/merge_test.go +++ b/internal/merge/merge_test.go @@ -444,7 +444,7 @@ func TestMerge3Way_SimpleUpdates(t *testing.T) { } right := base - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -471,7 +471,7 @@ func TestMerge3Way_SimpleUpdates(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -509,7 +509,7 @@ func TestMerge3Way_SimpleUpdates(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -681,7 +681,7 @@ func TestMerge3Way_AutoResolve(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts with auto-resolution, got %d", len(conflicts)) } @@ -723,7 +723,7 @@ func TestMerge3Way_AutoResolve(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts with auto-resolution, got %d", len(conflicts)) } @@ -765,7 +765,7 @@ func TestMerge3Way_AutoResolve(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts with auto-resolution, got %d", len(conflicts)) } @@ -808,7 +808,7 @@ func TestMerge3Way_AutoResolve(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts with auto-resolution, got %d", len(conflicts)) } @@ -837,7 +837,7 @@ func TestMerge3Way_Deletions(t *testing.T) { left := []Issue{} // Deleted in left right := base // Unchanged in right - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -859,7 +859,7 @@ func TestMerge3Way_Deletions(t *testing.T) { left := base // Unchanged in left right := []Issue{} // Deleted in right - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -891,7 +891,7 @@ func TestMerge3Way_Deletions(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts, got %d", len(conflicts)) } @@ -923,7 +923,7 @@ func TestMerge3Way_Deletions(t *testing.T) { } right := []Issue{} // Deleted in right - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts, got %d", len(conflicts)) } @@ -948,7 +948,7 @@ func TestMerge3Way_Additions(t *testing.T) { } right := []Issue{} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -973,7 +973,7 @@ func TestMerge3Way_Additions(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -999,7 +999,7 @@ func TestMerge3Way_Additions(t *testing.T) { left := []Issue{issueData} right := []Issue{issueData} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1031,7 +1031,7 @@ func TestMerge3Way_Additions(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("expected no conflicts with auto-resolution, got %d", len(conflicts)) } @@ -1078,7 +1078,7 @@ func TestMerge3Way_ResurrectionPrevention(t *testing.T) { }, } - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1132,7 +1132,7 @@ func TestMerge3Way_ResurrectionPrevention(t *testing.T) { // Right: issue is still open (stale) right := base - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1413,7 +1413,7 @@ func TestMerge3Way_TombstoneVsLive(t *testing.T) { left := []Issue{recentTombstone} right := []Issue{modifiedLive} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1430,7 +1430,7 @@ func TestMerge3Way_TombstoneVsLive(t *testing.T) { left := []Issue{modifiedLive} right := []Issue{recentTombstone} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1447,7 +1447,7 @@ func TestMerge3Way_TombstoneVsLive(t *testing.T) { left := []Issue{expiredTombstone} right := []Issue{modifiedLive} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1467,7 +1467,7 @@ func TestMerge3Way_TombstoneVsLive(t *testing.T) { left := []Issue{modifiedLive} right := []Issue{expiredTombstone} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1516,7 +1516,7 @@ func TestMerge3Way_TombstoneVsTombstone(t *testing.T) { left := []Issue{leftTombstone} right := []Issue{rightTombstone} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1545,7 +1545,7 @@ func TestMerge3Way_TombstoneNoBase(t *testing.T) { DeletedBy: "user1", } - result, conflicts := merge3Way([]Issue{}, []Issue{tombstone}, []Issue{}) + result, conflicts := merge3Way([]Issue{}, []Issue{tombstone}, []Issue{}, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1568,7 +1568,7 @@ func TestMerge3Way_TombstoneNoBase(t *testing.T) { DeletedBy: "user1", } - result, conflicts := merge3Way([]Issue{}, []Issue{}, []Issue{tombstone}) + result, conflicts := merge3Way([]Issue{}, []Issue{}, []Issue{tombstone}, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1598,7 +1598,7 @@ func TestMerge3Way_TombstoneNoBase(t *testing.T) { CreatedBy: "user1", } - result, conflicts := merge3Way([]Issue{}, []Issue{recentTombstone}, []Issue{live}) + result, conflicts := merge3Way([]Issue{}, []Issue{recentTombstone}, []Issue{live}, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1648,7 +1648,7 @@ func TestMerge3WayWithTTL(t *testing.T) { left := []Issue{tombstone} right := []Issue{liveIssue} - result, _ := Merge3WayWithTTL(base, left, right, shortTTL) + result, _ := Merge3WayWithTTL(base, left, right, shortTTL, false) if len(result) != 1 { t.Fatalf("expected 1 issue, got %d", len(result)) } @@ -1665,7 +1665,7 @@ func TestMerge3WayWithTTL(t *testing.T) { left := []Issue{tombstone} right := []Issue{liveIssue} - result, _ := Merge3WayWithTTL(base, left, right, longTTL) + result, _ := Merge3WayWithTTL(base, left, right, longTTL, false) if len(result) != 1 { t.Fatalf("expected 1 issue, got %d", len(result)) } @@ -1753,7 +1753,7 @@ func TestMerge3Way_TombstoneWithImplicitDeletion(t *testing.T) { left := []Issue{tombstone} right := []Issue{} // Implicitly deleted in right - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1773,7 +1773,7 @@ func TestMerge3Way_TombstoneWithImplicitDeletion(t *testing.T) { left := []Issue{} // Implicitly deleted in left right := []Issue{tombstone} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -1797,7 +1797,7 @@ func TestMerge3Way_TombstoneWithImplicitDeletion(t *testing.T) { left := []Issue{modifiedLive} right := []Issue{} // Implicitly deleted in right - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) } @@ -2149,7 +2149,7 @@ func TestMerge3Way_TombstoneBaseBothLiveResurrection(t *testing.T) { left := []Issue{leftLive} right := []Issue{rightLive} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) // Should not have conflicts - merge rules apply if len(conflicts) != 0 { @@ -2202,7 +2202,7 @@ func TestMerge3Way_TombstoneBaseBothLiveResurrection(t *testing.T) { left := []Issue{leftOpen} right := []Issue{rightOpen} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) @@ -2229,7 +2229,7 @@ func TestMerge3Way_TombstoneBaseBothLiveResurrection(t *testing.T) { left := []Issue{leftOpen} right := []Issue{rightClosed} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) @@ -2293,7 +2293,7 @@ func TestMerge3Way_TombstoneVsLiveTimestampPrecisionMismatch(t *testing.T) { left := []Issue{tombstone} right := []Issue{closedIssue} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) @@ -2340,7 +2340,7 @@ func TestMerge3Way_TombstoneVsLiveTimestampPrecisionMismatch(t *testing.T) { left := []Issue{tombstone} right := []Issue{closedIssue} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts) @@ -2379,7 +2379,7 @@ func TestMerge3Way_TombstoneVsLiveTimestampPrecisionMismatch(t *testing.T) { left := []Issue{liveLeft} right := []Issue{liveRight} - result, conflicts := merge3Way(base, left, right) + result, conflicts := merge3Way(base, left, right, false) if len(conflicts) != 0 { t.Errorf("unexpected conflicts: %v", conflicts)