docs: add detailed comments explaining ID formats and merge deletion logic
Document the intent and nuances of recent fixes: internal/importer/utils.go: - RenameImportedIssuePrefixes: explain the three ID formats (sequential, hash-based, hierarchical) and how prefix renaming preserves identity - isValidIDSuffix: document why dots are allowed (hierarchical parent-child relationships) and what characters are rejected cmd/bd/deletion_tracking.go: - isIssueNotFoundError: explain why "not found" is success during merge (issue may be tombstoned, never existed locally, or manually deleted) - Deletion loop: document what "accepted deletions" means and why we tolerate missing issues during the pruning phase
This commit is contained in:
@@ -12,8 +12,16 @@ import (
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
)
|
||||
|
||||
// isIssueNotFoundError checks if the error indicates the issue doesn't exist
|
||||
// This is OK during merge - the issue may already be deleted/tombstoned
|
||||
// isIssueNotFoundError checks if the error indicates the issue doesn't exist in the database.
|
||||
//
|
||||
// During 3-way merge, we try to delete issues that were removed remotely. However, the issue
|
||||
// may already be gone from the local database due to:
|
||||
// - Already tombstoned by a previous sync/import
|
||||
// - Never existed locally (multi-repo scenarios, partial clones)
|
||||
// - Deleted by user between export and import phases
|
||||
//
|
||||
// In all these cases, "issue not found" is success from the merge's perspective - the goal
|
||||
// is to ensure the issue is deleted, and it already is. We only fail on actual database errors.
|
||||
func isIssueNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@@ -84,13 +92,22 @@ func merge3WayAndPruneDeletions(ctx context.Context, store storage.Storage, json
|
||||
return false, fmt.Errorf("failed to compute accepted deletions: %w", err)
|
||||
}
|
||||
|
||||
// Prune accepted deletions from the database
|
||||
// "Issue not found" errors are OK - the issue may already be deleted/tombstoned
|
||||
// Prune accepted deletions from the database.
|
||||
//
|
||||
// "Accepted deletions" are issues that:
|
||||
// 1. Existed in the base snapshot (last successful import)
|
||||
// 2. Were NOT modified locally (still in left snapshot, unchanged)
|
||||
// 3. Are NOT in the merged result (deleted remotely)
|
||||
//
|
||||
// We tolerate "issue not found" errors because the issue may already be gone:
|
||||
// - Tombstoned by auto-import's git-history-backfill
|
||||
// - Deleted manually by the user
|
||||
// - Never existed in this clone (multi-repo, partial history)
|
||||
// The goal is ensuring deletion, so already-deleted is success.
|
||||
var deletionErrors []error
|
||||
var alreadyGone int
|
||||
for _, id := range acceptedDeletions {
|
||||
if err := store.DeleteIssue(ctx, id); err != nil {
|
||||
// If issue is already gone (tombstoned or never existed locally), that's fine
|
||||
if isIssueNotFoundError(err) {
|
||||
alreadyGone++
|
||||
continue
|
||||
|
||||
@@ -139,7 +139,19 @@ func (fc *fieldComparator) checkFieldChanged(key string, existing *types.Issue,
|
||||
}
|
||||
}
|
||||
|
||||
// RenameImportedIssuePrefixes renames all issues and their references to match the target prefix
|
||||
// RenameImportedIssuePrefixes renames all issues and their references to match the target prefix.
|
||||
//
|
||||
// This function handles three ID formats:
|
||||
// - Sequential numeric IDs: "old-123" → "new-123"
|
||||
// - Hash-based IDs: "old-abc1" → "new-abc1"
|
||||
// - Hierarchical IDs: "old-abc1.2.3" → "new-abc1.2.3"
|
||||
//
|
||||
// The suffix (everything after "prefix-") is preserved during rename, only the prefix changes.
|
||||
// This preserves issue identity across prefix renames while maintaining parent-child relationships
|
||||
// in hierarchical IDs (dots denote subtask nesting, e.g., bd-abc1.2 is child 2 of bd-abc1).
|
||||
//
|
||||
// All text references to old IDs in issue fields (title, description, notes, etc.) and
|
||||
// dependency relationships are updated to use the new IDs.
|
||||
func RenameImportedIssuePrefixes(issues []*types.Issue, targetPrefix string) error {
|
||||
// Build a mapping of old IDs to new IDs
|
||||
idMapping := make(map[string]string)
|
||||
@@ -270,9 +282,18 @@ func isBoundary(c byte) bool {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}'
|
||||
}
|
||||
|
||||
// isValidIDSuffix checks if a string is a valid issue ID suffix
|
||||
// Accepts: digits (0-9), lowercase letters (a-z), and dots (.) for hierarchy
|
||||
// Examples: "123", "abc1", "6we", "6we.2", "abc.1.2"
|
||||
// isValidIDSuffix validates the suffix portion of an issue ID (everything after "prefix-").
|
||||
//
|
||||
// Beads supports three ID formats, all of which this function must accept:
|
||||
// - Sequential numeric: "123", "999" (legacy format)
|
||||
// - Hash-based (base36): "abc1", "6we", "zzz" (current format, content-addressed)
|
||||
// - Hierarchical: "abc1.2", "6we.2.3" (subtasks, dot-separated child counters)
|
||||
//
|
||||
// The dot separator in hierarchical IDs represents parent-child relationships:
|
||||
// "bd-abc1.2" means child #2 of parent "bd-abc1". Maximum depth is 3 levels.
|
||||
//
|
||||
// Rejected: uppercase letters, hyphens (would be confused with prefix separator),
|
||||
// and special characters.
|
||||
func isValidIDSuffix(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user