fix(merge): preserve close_reason field during merge/sync (GH#891)

The close_reason and closed_by_session fields were being silently dropped
during 3-way merge operations because the simplified Issue struct in
internal/merge/merge.go was missing these fields.

Changes:
- Add CloseReason and ClosedBySession fields to merge.Issue struct
- Implement merge logic that preserves these fields when status is closed
- Use timestamp-based conflict resolution (later closed_at wins)
- Clear close metadata when status becomes non-closed

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
quartz
2026-01-04 11:28:39 -08:00
committed by Steve Yegge
parent c848c0fce2
commit 563f7e875d
2 changed files with 283 additions and 4 deletions
+21 -4
View File
@@ -48,10 +48,12 @@ type Issue struct {
Status string `json:"status,omitempty"`
Priority int `json:"priority"` // No omitempty: 0 is valid (P0/critical)
IssueType string `json:"issue_type,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
ClosedAt string `json:"closed_at,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
ClosedAt string `json:"closed_at,omitempty"`
CloseReason string `json:"close_reason,omitempty"` // Reason provided when closing (GH#891)
ClosedBySession string `json:"closed_by_session,omitempty"` // Session that closed this issue (GH#891)
CreatedBy string `json:"created_by,omitempty"`
Dependencies []Dependency `json:"dependencies,omitempty"`
RawLine string `json:"-"` // Store original line for conflict output
// Tombstone fields: inline soft-delete support for merge
@@ -596,8 +598,23 @@ func mergeIssue(base, left, right Issue) (Issue, string) {
// This prevents invalid state (status=open with closed_at set)
if result.Status == StatusClosed {
result.ClosedAt = maxTime(left.ClosedAt, right.ClosedAt)
// Merge close_reason and closed_by_session - use value from side with later closed_at (GH#891)
// This ensures we keep the most recent close action's metadata
if isTimeAfter(left.ClosedAt, right.ClosedAt) {
result.CloseReason = left.CloseReason
result.ClosedBySession = left.ClosedBySession
} else if right.ClosedAt != "" {
result.CloseReason = right.CloseReason
result.ClosedBySession = right.ClosedBySession
} else {
// Both empty or only left has value - prefer left
result.CloseReason = left.CloseReason
result.ClosedBySession = left.ClosedBySession
}
} else {
result.ClosedAt = ""
result.CloseReason = ""
result.ClosedBySession = ""
}
// Merge dependencies - proper 3-way merge where removals win