fix(orphan): handle prefixes with dots in orphan detection (GH#508)

The orphan detection was incorrectly flagging issues with dots in their
prefix (e.g., "my.project-abc123") as orphans because it was looking for
any dot in the ID, treating everything before the first dot as the
parent ID.

The fix:
- Add IsHierarchicalID() helper that correctly detects hierarchical IDs
  by checking if the ID ends with .{digits} (e.g., "bd-abc.1")
- Update SQL query in orphan detection migration to use GLOB patterns
  that only match IDs ending with numeric suffixes
- Update all Go code that checks for hierarchical IDs to use the new
  helper function

Test cases added:
- Unit tests for IsHierarchicalID covering normal, dotted prefix, and
  edge cases
- Integration test verifying dotted prefixes do not trigger false
  positives

Fixes: #508
This commit is contained in:
Steve Yegge
2025-12-14 17:23:46 -08:00
parent 768db19635
commit fb20e43f5f
6 changed files with 270 additions and 17 deletions

View File

@@ -73,6 +73,38 @@ func isValidHex(s string) bool {
return true
}
// IsHierarchicalID checks if an issue ID is hierarchical (has a parent).
// Hierarchical IDs have the format {parentID}.{N} where N is a numeric child suffix.
// Returns true and the parent ID if hierarchical, false and empty string otherwise.
//
// This correctly handles prefixes that contain dots (e.g., "my.project-abc123"
// is NOT hierarchical, but "my.project-abc123.1" IS hierarchical with parent
// "my.project-abc123").
//
// The key insight is that hierarchical IDs always end with .{digits} where
// the digits represent the child number (1, 2, 3, etc.).
func IsHierarchicalID(id string) (isHierarchical bool, parentID string) {
lastDot := strings.LastIndex(id, ".")
if lastDot == -1 {
return false, ""
}
// Check if the suffix after the last dot is purely numeric
suffix := id[lastDot+1:]
if len(suffix) == 0 {
return false, ""
}
for _, c := range suffix {
if c < '0' || c > '9' {
return false, ""
}
}
// It's hierarchical - parent is everything before the last dot
return true, id[:lastDot]
}
// ValidateIssueIDPrefix validates that an issue ID matches the configured prefix
// Supports both top-level (bd-a3f8e9) and hierarchical (bd-a3f8e9.1) IDs
func ValidateIssueIDPrefix(id, prefix string) error {
@@ -203,11 +235,8 @@ func EnsureIDs(ctx context.Context, conn *sql.Conn, prefix string, issues []*typ
}
// For hierarchical IDs (bd-a3f8e9.1), ensure parent exists
if strings.Contains(issues[i].ID, ".") {
// Extract parent ID (everything before the last dot)
lastDot := strings.LastIndex(issues[i].ID, ".")
parentID := issues[i].ID[:lastDot]
// Use IsHierarchicalID to correctly handle prefixes with dots (GH#508)
if isHierarchical, parentID := IsHierarchicalID(issues[i].ID); isHierarchical {
var parentCount int
err := conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, parentID).Scan(&parentCount)
if err != nil {