Implement Jira issue timestamp comparison for sync (bd-0qx5)
Add actual timestamp comparison to detect conflicts during Jira sync: - Add fetchJiraIssueTimestamp() to fetch a single issue's updated timestamp from Jira REST API - Add extractJiraKey() to parse Jira issue key from external_ref URLs - Add parseJiraTimestamp() to parse Jira's ISO 8601 timestamp format - Update detectJiraConflicts() to fetch and compare Jira timestamps instead of marking all locally-updated issues as conflicts - Update resolveConflictsByTimestamp() to show actual timestamp comparison results - Update reimportConflicts() to display both local and Jira timestamps Now only issues that have been modified on BOTH sides since the last sync are reported as conflicts, reducing false positives. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -181,3 +181,114 @@ func TestPushStats(t *testing.T) {
|
||||
t.Errorf("expected Errors to be 2, got %d", stats.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractJiraKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
externalRef string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "standard Jira Cloud URL",
|
||||
externalRef: "https://company.atlassian.net/browse/PROJ-123",
|
||||
want: "PROJ-123",
|
||||
},
|
||||
{
|
||||
name: "Jira Server URL",
|
||||
externalRef: "https://jira.company.com/browse/ISSUE-456",
|
||||
want: "ISSUE-456",
|
||||
},
|
||||
{
|
||||
name: "URL with trailing path",
|
||||
externalRef: "https://company.atlassian.net/browse/ABC-789/some/path",
|
||||
want: "ABC-789/some/path",
|
||||
},
|
||||
{
|
||||
name: "no browse pattern",
|
||||
externalRef: "https://github.com/org/repo/issues/123",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
externalRef: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "only browse",
|
||||
externalRef: "https://example.com/browse/",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractJiraKey(tt.externalRef)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractJiraKey(%q) = %q, want %q", tt.externalRef, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJiraTimestamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timestamp string
|
||||
wantErr bool
|
||||
wantYear int
|
||||
}{
|
||||
{
|
||||
name: "standard Jira Cloud format with milliseconds",
|
||||
timestamp: "2024-01-15T10:30:00.000+0000",
|
||||
wantErr: false,
|
||||
wantYear: 2024,
|
||||
},
|
||||
{
|
||||
name: "Jira format with Z suffix",
|
||||
timestamp: "2024-01-15T10:30:00.000Z",
|
||||
wantErr: false,
|
||||
wantYear: 2024,
|
||||
},
|
||||
{
|
||||
name: "without milliseconds",
|
||||
timestamp: "2024-01-15T10:30:00+0000",
|
||||
wantErr: false,
|
||||
wantYear: 2024,
|
||||
},
|
||||
{
|
||||
name: "RFC3339 format",
|
||||
timestamp: "2024-01-15T10:30:00Z",
|
||||
wantErr: false,
|
||||
wantYear: 2024,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
timestamp: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid format",
|
||||
timestamp: "not-a-timestamp",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with negative timezone offset",
|
||||
timestamp: "2024-06-15T10:30:00.000-0500",
|
||||
wantErr: false,
|
||||
wantYear: 2024,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseJiraTimestamp(tt.timestamp)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseJiraTimestamp(%q) error = %v, wantErr %v", tt.timestamp, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && got.Year() != tt.wantYear {
|
||||
t.Errorf("parseJiraTimestamp(%q) year = %d, want %d", tt.timestamp, got.Year(), tt.wantYear)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user