Add debug logging for tombstone resurrection events
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user