fix: tombstone/deletion overhaul for bd-4q8
- IsExpired(): Negative TTL means immediately expired (for --hard mode) - IsExpired(): ClockSkewGrace only added for TTLs > 1 hour - bd cleanup --hard: Use negative TTL to prune freshly created tombstones - bd delete --hard: New flag to immediately prune tombstones from JSONL - Import: Add early tombstone check before all phases to prevent resurrection The early tombstone check prevents ghost issues from being created when tombstones exist in the DB. However, a deeper git merge issue (bd-ncwo) can still cause resurrection when remote's status:closed wins the merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,8 +77,9 @@ SEE ALSO:
|
||||
if olderThanDays > 0 {
|
||||
customTTL = time.Duration(olderThanDays) * 24 * time.Hour
|
||||
} else {
|
||||
// --hard without --older-than: prune ALL tombstones (use 1 second TTL)
|
||||
customTTL = time.Second
|
||||
// --hard without --older-than: prune ALL tombstones immediately
|
||||
// Negative TTL means "immediately expired" (bd-4q8 fix)
|
||||
customTTL = -1
|
||||
}
|
||||
if !jsonOutput && !dryRun {
|
||||
fmt.Println(color.YellowString("⚠️ HARD DELETE MODE: Bypassing tombstone TTL safety"))
|
||||
@@ -186,7 +187,8 @@ SEE ALSO:
|
||||
}
|
||||
|
||||
// Use the existing batch deletion logic
|
||||
deleteBatch(cmd, issueIDs, force, dryRun, cascade, jsonOutput, "cleanup")
|
||||
// Note: cleanup always creates tombstones first; --hard prunes them after
|
||||
deleteBatch(cmd, issueIDs, force, dryRun, cascade, jsonOutput, false, "cleanup")
|
||||
|
||||
// Also prune expired tombstones (bd-08ea)
|
||||
// This runs after closed issues are converted to tombstones, cleaning up old ones
|
||||
|
||||
@@ -91,22 +91,36 @@ var deleteCmd = &cobra.Command{
|
||||
This command will:
|
||||
1. Remove all dependency links (any type, both directions) involving the issues
|
||||
2. Update text references to "[deleted:ID]" in directly connected issues
|
||||
3. Delete the issues from the database
|
||||
3. Delete the issues from the database (creates tombstones by default)
|
||||
|
||||
This is a destructive operation that cannot be undone. Use with caution.
|
||||
|
||||
BATCH DELETION:
|
||||
Delete multiple issues at once:
|
||||
bd delete bd-1 bd-2 bd-3 --force
|
||||
|
||||
Delete from file (one ID per line):
|
||||
bd delete --from-file deletions.txt --force
|
||||
|
||||
Preview before deleting:
|
||||
bd delete --from-file deletions.txt --dry-run
|
||||
|
||||
DEPENDENCY HANDLING:
|
||||
Default: Fails if any issue has dependents not in deletion set
|
||||
bd delete bd-1 bd-2
|
||||
|
||||
Cascade: Recursively delete all dependents
|
||||
bd delete bd-1 --cascade --force
|
||||
|
||||
Force: Delete and orphan dependents
|
||||
bd delete bd-1 --force`,
|
||||
bd delete bd-1 --force
|
||||
|
||||
PERMANENT DELETION:
|
||||
Use --hard to permanently delete (bypass tombstones):
|
||||
bd delete bd-1 bd-2 --hard --force
|
||||
|
||||
WARNING: --hard bypasses sync safety. Use only when you are certain
|
||||
the issues will not resurrect from remote branches.`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckReadonly("delete")
|
||||
@@ -114,6 +128,7 @@ Force: Delete and orphan dependents
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
cascade, _ := cmd.Flags().GetBool("cascade")
|
||||
hardDelete, _ := cmd.Flags().GetBool("hard")
|
||||
// Use global jsonOutput set by PersistentPreRun
|
||||
// Collect issue IDs from args and/or file
|
||||
issueIDs := make([]string, 0, len(args))
|
||||
@@ -150,7 +165,7 @@ Force: Delete and orphan dependents
|
||||
|
||||
// Handle batch deletion in direct mode
|
||||
if len(issueIDs) > 1 {
|
||||
deleteBatch(cmd, issueIDs, force, dryRun, cascade, jsonOutput, "batch delete")
|
||||
deleteBatch(cmd, issueIDs, force, dryRun, cascade, jsonOutput, hardDelete, "batch delete")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -409,7 +424,7 @@ func removeIssueFromJSONL(issueID string) error {
|
||||
}
|
||||
// deleteBatch handles deletion of multiple issues
|
||||
//nolint:unparam // cmd parameter required for potential future use
|
||||
func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, cascade bool, jsonOutput bool, reason string) {
|
||||
func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, cascade bool, jsonOutput bool, hardDelete bool, reason string) {
|
||||
// Ensure we have a direct store
|
||||
if store == nil {
|
||||
if err := ensureStoreActive(); err != nil {
|
||||
@@ -499,12 +514,30 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
|
||||
}
|
||||
}
|
||||
}
|
||||
// Actually delete
|
||||
// Actually delete (creates tombstones)
|
||||
result, err := d.DeleteIssues(ctx, issueIDs, cascade, force, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Hard delete: immediately prune tombstones from JSONL (bd-4q8)
|
||||
// Note: We keep tombstones in DB to prevent resurrection during sync.
|
||||
// The tombstones will be exported and synced to remote, blocking resurrection.
|
||||
// Use 'bd cleanup --hard' after syncing to fully purge old tombstones.
|
||||
if hardDelete {
|
||||
if !jsonOutput {
|
||||
fmt.Println(color.YellowString("⚠️ HARD DELETE MODE: Pruning tombstones from JSONL"))
|
||||
fmt.Println(" Note: Tombstones kept in DB to prevent resurrection. Run 'bd sync' then 'bd cleanup --hard' to fully purge.")
|
||||
}
|
||||
// Prune tombstones from JSONL using negative TTL (immediate expiration)
|
||||
if pruneResult, err := pruneExpiredTombstones(-1); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to prune tombstones from JSONL: %v\n", err)
|
||||
} else if pruneResult != nil && pruneResult.PrunedCount > 0 && !jsonOutput {
|
||||
fmt.Printf(" Pruned %d tombstone(s) from JSONL\n", pruneResult.PrunedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Update text references in connected issues (using pre-collected issues)
|
||||
updatedCount := updateTextReferencesInIssues(ctx, issueIDs, connectedIssues)
|
||||
// Note: No longer remove from JSONL - tombstones will be exported to JSONL (bd-3b4)
|
||||
@@ -663,5 +696,6 @@ func init() {
|
||||
deleteCmd.Flags().String("from-file", "", "Read issue IDs from file (one per line)")
|
||||
deleteCmd.Flags().Bool("dry-run", false, "Preview what would be deleted without making changes")
|
||||
deleteCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues")
|
||||
deleteCmd.Flags().Bool("hard", false, "Permanently delete (skip tombstone, cannot be recovered via sync)")
|
||||
rootCmd.AddCommand(deleteCmd)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user