fix: support multi-part prefixes in issue ID extraction (#398)
This commit is contained in:
@@ -42,14 +42,18 @@ func TestExtractPrefix(t *testing.T) {
|
|||||||
{"bd-123", "bd"},
|
{"bd-123", "bd"},
|
||||||
{"custom-1", "custom"},
|
{"custom-1", "custom"},
|
||||||
{"TEST-999", "TEST"},
|
{"TEST-999", "TEST"},
|
||||||
{"no-number", "no"}, // Has hyphen, so "no" is prefix
|
{"no-number", "no"}, // Has hyphen, suffix not numeric, first hyphen
|
||||||
{"nonumber", ""}, // No hyphen
|
{"nonumber", ""}, // No hyphen
|
||||||
{"", ""},
|
{"", ""},
|
||||||
// Multi-part suffixes (bd-fasa regression tests)
|
// Multi-part non-numeric suffixes (bd-fasa regression tests)
|
||||||
{"vc-baseline-test", "vc"},
|
{"vc-baseline-test", "vc"},
|
||||||
{"vc-92cl-gate-test", "vc"},
|
{"vc-92cl-gate-test", "vc"},
|
||||||
{"bd-multi-part-id", "bd"},
|
{"bd-multi-part-id", "bd"},
|
||||||
{"prefix-a-b-c-d", "prefix"},
|
{"prefix-a-b-c-d", "prefix"},
|
||||||
|
// Multi-part prefixes with numeric suffixes
|
||||||
|
{"beads-vscode-1", "beads-vscode"},
|
||||||
|
{"alpha-beta-123", "alpha-beta"},
|
||||||
|
{"my-project-42", "my-project"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/steveyegge/beads/internal/debug"
|
"github.com/steveyegge/beads/internal/debug"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
|
"github.com/steveyegge/beads/internal/utils"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -335,8 +336,9 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
|
|
||||||
// Update last_import_hash metadata to enable content-based staleness detection (bd-khnb fix)
|
// Update last_import_hash metadata to enable content-based staleness detection (bd-khnb fix)
|
||||||
// This prevents git operations from resurrecting deleted issues by comparing content instead of mtime
|
// This prevents git operations from resurrecting deleted issues by comparing content instead of mtime
|
||||||
// When --force is true, ALWAYS update metadata even if no changes were made
|
// ALWAYS update metadata after successful import, even if no changes were made (fixes staleness check)
|
||||||
if input != "" && (result.Created > 0 || result.Updated > 0 || len(result.IDMapping) > 0 || force) {
|
// This ensures that running `bd import` marks the database as fresh for staleness detection
|
||||||
|
if input != "" {
|
||||||
if currentHash, err := computeJSONLHash(input); err == nil {
|
if currentHash, err := computeJSONLHash(input); err == nil {
|
||||||
if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
|
if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
|
||||||
// Non-fatal warning: Metadata update failures are intentionally non-fatal to prevent blocking
|
// Non-fatal warning: Metadata update failures are intentionally non-fatal to prevent blocking
|
||||||
@@ -694,7 +696,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detectPrefixFromIssues extracts the common prefix from issue IDs
|
// detectPrefixFromIssues extracts the common prefix from issue IDs
|
||||||
// Only considers the first hyphen, so "vc-baseline-test" -> "vc"
|
// Uses utils.ExtractIssuePrefix which handles multi-part prefixes correctly
|
||||||
func detectPrefixFromIssues(issues []*types.Issue) string {
|
func detectPrefixFromIssues(issues []*types.Issue) string {
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -703,10 +705,9 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
|
|||||||
// Count prefix occurrences
|
// Count prefix occurrences
|
||||||
prefixCounts := make(map[string]int)
|
prefixCounts := make(map[string]int)
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
// Extract prefix from issue ID using first hyphen only
|
prefix := utils.ExtractIssuePrefix(issue.ID)
|
||||||
idx := strings.Index(issue.ID, "-")
|
if prefix != "" {
|
||||||
if idx > 0 {
|
prefixCounts[prefix]++
|
||||||
prefixCounts[issue.ID[:idx]]++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -249,13 +249,33 @@ Examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractPrefix extracts the prefix from an issue ID (e.g., "bd-123" -> "bd")
|
// extractPrefix extracts the prefix from an issue ID (e.g., "bd-123" -> "bd")
|
||||||
// Only considers the first hyphen, so "vc-baseline-test" -> "vc"
|
// Uses the last hyphen before a numeric suffix, so "beads-vscode-1" -> "beads-vscode"
|
||||||
func extractPrefix(issueID string) string {
|
func extractPrefix(issueID string) string {
|
||||||
idx := strings.Index(issueID, "-")
|
// Try last hyphen first (handles multi-part prefixes like "beads-vscode-1")
|
||||||
if idx <= 0 {
|
lastIdx := strings.LastIndex(issueID, "-")
|
||||||
|
if lastIdx <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return issueID[:idx]
|
|
||||||
|
suffix := issueID[lastIdx+1:]
|
||||||
|
// Check if suffix is numeric
|
||||||
|
if len(suffix) > 0 {
|
||||||
|
numPart := suffix
|
||||||
|
if dotIdx := strings.Index(suffix, "."); dotIdx > 0 {
|
||||||
|
numPart = suffix[:dotIdx]
|
||||||
|
}
|
||||||
|
var num int
|
||||||
|
if _, err := fmt.Sscanf(numPart, "%d", &num); err == nil {
|
||||||
|
return issueID[:lastIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix is not numeric, fall back to first hyphen
|
||||||
|
firstIdx := strings.Index(issueID, "-")
|
||||||
|
if firstIdx <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return issueID[:firstIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionChange represents agent-relevant changes for a specific version
|
// VersionChange represents agent-relevant changes for a specific version
|
||||||
|
|||||||
@@ -177,12 +177,33 @@ func detectPrefix(_ string, memStore *memory.MemoryStorage) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
|
// extractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
|
||||||
|
// Uses the last hyphen before a numeric suffix, so "beads-vscode-1" -> "beads-vscode"
|
||||||
func extractIssuePrefix(issueID string) string {
|
func extractIssuePrefix(issueID string) string {
|
||||||
idx := strings.Index(issueID, "-")
|
// Try last hyphen first (handles multi-part prefixes like "beads-vscode-1")
|
||||||
if idx <= 0 {
|
lastIdx := strings.LastIndex(issueID, "-")
|
||||||
|
if lastIdx <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return issueID[:idx]
|
|
||||||
|
suffix := issueID[lastIdx+1:]
|
||||||
|
// Check if suffix is numeric
|
||||||
|
if len(suffix) > 0 {
|
||||||
|
numPart := suffix
|
||||||
|
if dotIdx := strings.Index(suffix, "."); dotIdx > 0 {
|
||||||
|
numPart = suffix[:dotIdx]
|
||||||
|
}
|
||||||
|
var num int
|
||||||
|
if _, err := fmt.Sscanf(numPart, "%d", &num); err == nil {
|
||||||
|
return issueID[:lastIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix is not numeric, fall back to first hyphen
|
||||||
|
firstIdx := strings.Index(issueID, "-")
|
||||||
|
if firstIdx <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return issueID[:firstIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeIssuesToJSONL writes all issues from memory storage to JSONL file atomically
|
// writeIssuesToJSONL writes all issues from memory storage to JSONL file atomically
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ func TestExtractIssuePrefix(t *testing.T) {
|
|||||||
{"standard ID", "bd-123", "bd"},
|
{"standard ID", "bd-123", "bd"},
|
||||||
{"custom prefix", "myproject-456", "myproject"},
|
{"custom prefix", "myproject-456", "myproject"},
|
||||||
{"hash ID", "bd-abc123def", "bd"},
|
{"hash ID", "bd-abc123def", "bd"},
|
||||||
{"multi-part suffix", "alpha-beta-1", "alpha"}, // Only first hyphen (bd-fasa)
|
{"multi-part prefix with numeric suffix", "alpha-beta-1", "alpha-beta"},
|
||||||
|
{"multi-part non-numeric suffix", "vc-baseline-test", "vc"}, // Falls back to first hyphen
|
||||||
|
{"beads-vscode style", "beads-vscode-42", "beads-vscode"},
|
||||||
{"no hyphen", "nohyphen", ""},
|
{"no hyphen", "nohyphen", ""},
|
||||||
{"empty", "", ""},
|
{"empty", "", ""},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,9 +353,19 @@ func TestExtractIssuePrefix(t *testing.T) {
|
|||||||
expected: "bd",
|
expected: "bd",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multi-part suffix",
|
name: "multi-part prefix with numeric suffix",
|
||||||
issueID: "alpha-beta-1",
|
issueID: "alpha-beta-1",
|
||||||
expected: "alpha", // Only first hyphen (bd-fasa)
|
expected: "alpha-beta", // Last hyphen before numeric suffix
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi-part non-numeric suffix",
|
||||||
|
issueID: "vc-baseline-test",
|
||||||
|
expected: "vc", // Falls back to first hyphen for non-numeric suffix
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "beads-vscode style prefix",
|
||||||
|
issueID: "beads-vscode-1",
|
||||||
|
expected: "beads-vscode", // Last hyphen before numeric suffix
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,36 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ExtractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
|
// ExtractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
|
||||||
// Only considers the first hyphen, so "vc-baseline-test" -> "vc"
|
// Uses the last hyphen before a numeric suffix, so "beads-vscode-1" -> "beads-vscode"
|
||||||
|
// For non-numeric suffixes like "vc-baseline-test", returns the first segment "vc"
|
||||||
func ExtractIssuePrefix(issueID string) string {
|
func ExtractIssuePrefix(issueID string) string {
|
||||||
idx := strings.Index(issueID, "-")
|
// Try last hyphen first (handles multi-part prefixes like "beads-vscode-1")
|
||||||
if idx <= 0 {
|
lastIdx := strings.LastIndex(issueID, "-")
|
||||||
|
if lastIdx <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return issueID[:idx]
|
|
||||||
|
suffix := issueID[lastIdx+1:]
|
||||||
|
// Check if suffix is numeric (or starts with a number for hierarchical IDs like "bd-123.1")
|
||||||
|
if len(suffix) > 0 {
|
||||||
|
// Extract just the numeric part (handle "123.1.2" -> check "123")
|
||||||
|
numPart := suffix
|
||||||
|
if dotIdx := strings.Index(suffix, "."); dotIdx > 0 {
|
||||||
|
numPart = suffix[:dotIdx]
|
||||||
|
}
|
||||||
|
var num int
|
||||||
|
if _, err := fmt.Sscanf(numPart, "%d", &num); err == nil {
|
||||||
|
// Suffix is numeric, use last hyphen
|
||||||
|
return issueID[:lastIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix is not numeric (e.g., "vc-baseline-test"), fall back to first hyphen
|
||||||
|
firstIdx := strings.Index(issueID, "-")
|
||||||
|
if firstIdx <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return issueID[:firstIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractIssueNumber extracts the number from an issue ID like "bd-123" -> 123
|
// ExtractIssueNumber extracts the number from an issue ID like "bd-123" -> 123
|
||||||
|
|||||||
Reference in New Issue
Block a user