diff --git a/internal/utils/id_parser.go b/internal/utils/id_parser.go index 7c5b7cc8..a1a27bf9 100644 --- a/internal/utils/id_parser.go +++ b/internal/utils/id_parser.go @@ -81,14 +81,41 @@ func ResolvePartialID(ctx context.Context, store storage.Storage, input string) hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen) var matches []string + var exactMatch string + 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 if strings.Contains(issueHash, hashPart) { matches = append(matches, issue.ID) } } + // Prefer exact match over substring matches + if exactMatch != "" { + return exactMatch, nil + } + if len(matches) == 0 { return "", fmt.Errorf("no issue found matching %q", input) } diff --git a/internal/utils/id_parser_test.go b/internal/utils/id_parser_test.go index 5c265983..bb1b4606 100644 --- a/internal/utils/id_parser_test.go +++ b/internal/utils/id_parser_test.go @@ -90,6 +90,21 @@ func TestResolvePartialID(t *testing.T) { Priority: 1, 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 { t.Fatal(err) @@ -100,6 +115,12 @@ func TestResolvePartialID(t *testing.T) { if err := store.CreateIssue(ctx, issue3, "test"); err != nil { 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 if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { @@ -149,6 +170,16 @@ func TestResolvePartialID(t *testing.T) { input: "bd-1", 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 {