fix(import): skip cross-prefix content matches instead of triggering rename

When importing issues, if an incoming issue has the same content hash as an
existing issue but a DIFFERENT prefix, this should not be treated as a rename.
Cross-prefix content matches occur when importing issues from other projects
that happen to have identical content.

Previously, the importer would call handleRename which tries to create an issue
with the incoming prefix, failing prefix validation ("does not match configured
prefix" error).

The fix checks if prefixes differ before calling handleRename:
- Same prefix, different ID suffix → true rename, call handleRename
- Different prefix → skip incoming issue, keep existing unchanged

Added test: TestImportCrossPrefixContentMatch reproduces the bug scenario
where alpha-* issues exist but beta-* issues are imported with same content.
This commit is contained in:
Ryan Snodgrass
2025-12-16 00:29:19 -08:00
parent 421d41dfa0
commit 627ac1afb8
2 changed files with 104 additions and 2 deletions

View File

@@ -593,8 +593,18 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
// Exact match (same content, same ID) - idempotent case
result.Unchanged++
} else {
// Same content, different ID - rename detected
if !opts.SkipUpdate {
// Same content, different ID - check if this is a rename or cross-prefix duplicate
existingPrefix := utils.ExtractIssuePrefix(existing.ID)
incomingPrefix := utils.ExtractIssuePrefix(incoming.ID)
if existingPrefix != incomingPrefix {
// Cross-prefix content match: same content but different projects/prefixes.
// This is NOT a rename - it's a duplicate from another project.
// Skip the incoming issue and keep the existing one unchanged.
// Calling handleRename would fail because CreateIssue validates prefix.
result.Skipped++
} else if !opts.SkipUpdate {
// Same prefix, different ID suffix - this is a true rename
deletedID, err := handleRename(ctx, sqliteStore, existing, incoming)
if err != nil {
return fmt.Errorf("failed to handle rename %s -> %s: %w", existing.ID, incoming.ID, err)