Fix gh-316: Prefer exact ID matches over prefix matches
The ID disambiguation logic treated 'offlinebrew-3d0' as ambiguous when child IDs like 'offlinebrew-3d0.1' existed. Now the system: 1. Checks for exact full ID matches first (issue.ID == input) 2. Checks for exact hash matches (handling cross-prefix scenarios) 3. Only falls back to substring matching if no exact match is found Added test cases verifying: - 'offlinebrew-3d0' matches exactly, not ambiguously with children - '3d0' without prefix still resolves to exact match Amp-Thread-ID: https://ampcode.com/threads/T-5358ea57-e9ea-49e9-aedf-7044ebf8b52a Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -81,14 +81,41 @@ func ResolvePartialID(ctx context.Context, store storage.Storage, input string)
|
|||||||
hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen)
|
hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen)
|
||||||
|
|
||||||
var matches []string
|
var matches []string
|
||||||
|
var exactMatch string
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issueHash := strings.TrimPrefix(issue.ID, prefixWithHyphen)
|
// Check for exact full ID match first (case: user typed full ID with different prefix)
|
||||||
|
if issue.ID == input {
|
||||||
|
exactMatch = issue.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hash from each issue, regardless of its prefix
|
||||||
|
// This handles cross-prefix matching (e.g., "3d0" matching "offlinebrew-3d0")
|
||||||
|
var issueHash string
|
||||||
|
if idx := strings.Index(issue.ID, "-"); idx >= 0 {
|
||||||
|
issueHash = issue.ID[idx+1:]
|
||||||
|
} else {
|
||||||
|
issueHash = issue.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for exact hash match (excluding hierarchical children)
|
||||||
|
if issueHash == hashPart {
|
||||||
|
exactMatch = issue.ID
|
||||||
|
// Don't break - keep searching in case there's a full ID match
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the issue hash contains the input hash as substring
|
// Check if the issue hash contains the input hash as substring
|
||||||
if strings.Contains(issueHash, hashPart) {
|
if strings.Contains(issueHash, hashPart) {
|
||||||
matches = append(matches, issue.ID)
|
matches = append(matches, issue.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer exact match over substring matches
|
||||||
|
if exactMatch != "" {
|
||||||
|
return exactMatch, nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return "", fmt.Errorf("no issue found matching %q", input)
|
return "", fmt.Errorf("no issue found matching %q", input)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,21 @@ func TestResolvePartialID(t *testing.T) {
|
|||||||
Priority: 1,
|
Priority: 1,
|
||||||
IssueType: types.TypeTask,
|
IssueType: types.TypeTask,
|
||||||
}
|
}
|
||||||
|
// Test hierarchical IDs - parent and child
|
||||||
|
parentIssue := &types.Issue{
|
||||||
|
ID: "offlinebrew-3d0",
|
||||||
|
Title: "Parent Epic",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeEpic,
|
||||||
|
}
|
||||||
|
childIssue := &types.Issue{
|
||||||
|
ID: "offlinebrew-3d0.1",
|
||||||
|
Title: "Child Task",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
}
|
||||||
|
|
||||||
if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
|
if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -100,6 +115,12 @@ func TestResolvePartialID(t *testing.T) {
|
|||||||
if err := store.CreateIssue(ctx, issue3, "test"); err != nil {
|
if err := store.CreateIssue(ctx, issue3, "test"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := store.CreateIssue(ctx, parentIssue, "test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := store.CreateIssue(ctx, childIssue, "test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Set config for prefix
|
// Set config for prefix
|
||||||
if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil {
|
if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil {
|
||||||
@@ -149,6 +170,16 @@ func TestResolvePartialID(t *testing.T) {
|
|||||||
input: "bd-1",
|
input: "bd-1",
|
||||||
expected: "bd-1", // Will match exactly, not ambiguously
|
expected: "bd-1", // Will match exactly, not ambiguously
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "exact match parent ID with hierarchical child - gh-316",
|
||||||
|
input: "offlinebrew-3d0",
|
||||||
|
expected: "offlinebrew-3d0", // Should match exactly, not be ambiguous with offlinebrew-3d0.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exact match parent without prefix - gh-316",
|
||||||
|
input: "3d0",
|
||||||
|
expected: "offlinebrew-3d0", // Should still prefer exact hash match
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user