fix(import): add warning when issues are skipped due to deletions manifest

When importing JSONL that contains issues in the deletions manifest,
import now:
- Filters out deleted issues before import
- Prints per-issue warning with deletion details (date, actor)
- Shows count of skipped issues in summary
- Suggests --ignore-deletions flag to force import

The new --ignore-deletions flag allows importing issues that are in the
deletions manifest, useful for recovering accidentally deleted issues.

Fixes bd-4zy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-30 21:21:47 -08:00
parent 3df138445a
commit f7dea354d8
4 changed files with 46 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"id":"bd-4zy","title":"Import silently skips issues in deletions manifest","description":"**Problem**: When JSONL has valid issues but the deletions manifest lists them as deleted, bd import returns '0 created, 0 updated' with NO warning. This is confusing because issues exist in JSONL but aren't imported.\n\n**Expected behavior**: \n- At minimum: warn that N issues were skipped due to deletions manifest\n- Better: offer to restore them or show what's being skipped\n\n**Requirements**:\n1. Add warning message when issues are skipped due to deletions\n2. Show count and optionally list IDs of skipped issues\n3. Consider --force flag to import anyway (ignoring deletions)\n\n**Files to check**: cmd/bd/import.go, internal/importer/","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-30T21:14:47.146103-08:00","updated_at":"2025-11-30T21:21:33.428328-08:00","closed_at":"2025-11-30T21:21:33.428328-08:00","close_reason":"Implemented filtering of deleted issues during import with warning message and --ignore-deletions flag"}

View File

@@ -96,6 +96,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
orphanHandling, _ := cmd.Flags().GetString("orphan-handling") orphanHandling, _ := cmd.Flags().GetString("orphan-handling")
force, _ := cmd.Flags().GetBool("force") force, _ := cmd.Flags().GetBool("force")
noGitHistory, _ := cmd.Flags().GetBool("no-git-history") noGitHistory, _ := cmd.Flags().GetBool("no-git-history")
ignoreDeletions, _ := cmd.Flags().GetBool("ignore-deletions")
// Check if stdin is being used interactively (not piped) // Check if stdin is being used interactively (not piped)
if input == "" && term.IsTerminal(int(os.Stdin.Fd())) { if input == "" && term.IsTerminal(int(os.Stdin.Fd())) {
@@ -255,6 +256,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
ClearDuplicateExternalRefs: clearDuplicateExternalRefs, ClearDuplicateExternalRefs: clearDuplicateExternalRefs,
OrphanHandling: orphanHandling, OrphanHandling: orphanHandling,
NoGitHistory: noGitHistory, NoGitHistory: noGitHistory,
IgnoreDeletions: ignoreDeletions,
} }
result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts) result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts)
@@ -402,8 +404,18 @@ NOTE: Import requires direct database access and does not work with daemon mode.
if len(result.IDMapping) > 0 { if len(result.IDMapping) > 0 {
fmt.Fprintf(os.Stderr, ", %d issues remapped", len(result.IDMapping)) fmt.Fprintf(os.Stderr, ", %d issues remapped", len(result.IDMapping))
} }
if result.SkippedDeleted > 0 {
fmt.Fprintf(os.Stderr, ", %d skipped (deleted)", result.SkippedDeleted)
}
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
// Print skipped deleted issues summary if any (bd-4zy)
if result.SkippedDeleted > 0 {
fmt.Fprintf(os.Stderr, "\n⚠ Skipped %d issue(s) found in deletions manifest\n", result.SkippedDeleted)
fmt.Fprintf(os.Stderr, " These issues were previously deleted and will not be resurrected.\n")
fmt.Fprintf(os.Stderr, " Use --ignore-deletions to force import anyway.\n")
}
// Print skipped dependencies summary if any // Print skipped dependencies summary if any
if len(result.SkippedDependencies) > 0 { if len(result.SkippedDependencies) > 0 {
fmt.Fprintf(os.Stderr, "\n⚠ Warning: Skipped %d dependencies due to missing references:\n", len(result.SkippedDependencies)) fmt.Fprintf(os.Stderr, "\n⚠ Warning: Skipped %d dependencies due to missing references:\n", len(result.SkippedDependencies))
@@ -759,6 +771,7 @@ func init() {
importCmd.Flags().String("orphan-handling", "", "How to handle missing parent issues: strict/resurrect/skip/allow (default: use config or 'allow')") importCmd.Flags().String("orphan-handling", "", "How to handle missing parent issues: strict/resurrect/skip/allow (default: use config or 'allow')")
importCmd.Flags().Bool("force", false, "Force metadata update even when database is already in sync with JSONL") importCmd.Flags().Bool("force", false, "Force metadata update even when database is already in sync with JSONL")
importCmd.Flags().Bool("no-git-history", false, "Skip git history backfill for deletions (use during JSONL filename migrations)") importCmd.Flags().Bool("no-git-history", false, "Skip git history backfill for deletions (use during JSONL filename migrations)")
importCmd.Flags().Bool("ignore-deletions", false, "Import issues even if they're in the deletions manifest")
importCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output import statistics in JSON format") importCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output import statistics in JSON format")
rootCmd.AddCommand(importCmd) rootCmd.AddCommand(importCmd)
} }

View File

@@ -166,6 +166,7 @@ type ImportOptions struct {
ClearDuplicateExternalRefs bool // Clear duplicate external_ref values instead of erroring ClearDuplicateExternalRefs bool // Clear duplicate external_ref values instead of erroring
OrphanHandling string // Orphan handling mode: strict/resurrect/skip/allow (empty = use config) OrphanHandling string // Orphan handling mode: strict/resurrect/skip/allow (empty = use config)
NoGitHistory bool // Skip git history backfill for deletions (prevents spurious deletion during JSONL migrations) NoGitHistory bool // Skip git history backfill for deletions (prevents spurious deletion during JSONL migrations)
IgnoreDeletions bool // Import issues even if they're in the deletions manifest
} }
// ImportResult contains statistics about the import operation // ImportResult contains statistics about the import operation
@@ -183,6 +184,8 @@ type ImportResult struct {
SkippedDependencies []string // Dependencies skipped due to FK constraint violations SkippedDependencies []string // Dependencies skipped due to FK constraint violations
Purged int // Issues purged from DB (found in deletions manifest) Purged int // Issues purged from DB (found in deletions manifest)
PurgedIDs []string // IDs that were purged PurgedIDs []string // IDs that were purged
SkippedDeleted int // Issues skipped because they're in deletions manifest
SkippedDeletedIDs []string // IDs that were skipped due to deletions manifest
} }
// importIssuesCore handles the core import logic used by both manual and auto-import. // importIssuesCore handles the core import logic used by both manual and auto-import.
@@ -223,6 +226,7 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage,
ClearDuplicateExternalRefs: opts.ClearDuplicateExternalRefs, ClearDuplicateExternalRefs: opts.ClearDuplicateExternalRefs,
OrphanHandling: importer.OrphanHandling(orphanHandling), OrphanHandling: importer.OrphanHandling(orphanHandling),
NoGitHistory: opts.NoGitHistory, NoGitHistory: opts.NoGitHistory,
IgnoreDeletions: opts.IgnoreDeletions,
} }
// Delegate to the importer package // Delegate to the importer package
@@ -246,6 +250,8 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage,
SkippedDependencies: result.SkippedDependencies, SkippedDependencies: result.SkippedDependencies,
Purged: result.Purged, Purged: result.Purged,
PurgedIDs: result.PurgedIDs, PurgedIDs: result.PurgedIDs,
SkippedDeleted: result.SkippedDeleted,
SkippedDeletedIDs: result.SkippedDeletedIDs,
}, nil }, nil
} }

View File

@@ -43,6 +43,7 @@ type Options struct {
OrphanHandling OrphanHandling // How to handle missing parent issues (default: allow) OrphanHandling OrphanHandling // How to handle missing parent issues (default: allow)
ClearDuplicateExternalRefs bool // Clear duplicate external_ref values instead of erroring ClearDuplicateExternalRefs bool // Clear duplicate external_ref values instead of erroring
NoGitHistory bool // Skip git history backfill for deletions (prevents spurious deletion during JSONL migrations) NoGitHistory bool // Skip git history backfill for deletions (prevents spurious deletion during JSONL migrations)
IgnoreDeletions bool // Import issues even if they're in the deletions manifest
} }
// Result contains statistics about the import operation // Result contains statistics about the import operation
@@ -60,6 +61,8 @@ type Result struct {
SkippedDependencies []string // Dependencies skipped due to FK constraint violations SkippedDependencies []string // Dependencies skipped due to FK constraint violations
Purged int // Issues purged from DB (found in deletions manifest) Purged int // Issues purged from DB (found in deletions manifest)
PurgedIDs []string // IDs that were purged PurgedIDs []string // IDs that were purged
SkippedDeleted int // Issues skipped because they're in deletions manifest
SkippedDeletedIDs []string // IDs that were skipped due to deletions manifest
} }
// ImportIssues handles the core import logic used by both manual and auto-import. // ImportIssues handles the core import logic used by both manual and auto-import.
@@ -114,6 +117,29 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss
opts.OrphanHandling = sqliteStore.GetOrphanHandling(ctx) opts.OrphanHandling = sqliteStore.GetOrphanHandling(ctx)
} }
// Filter out issues that are in the deletions manifest (bd-4zy)
// Unless IgnoreDeletions is set, skip importing deleted issues
if !opts.IgnoreDeletions && dbPath != "" {
beadsDir := filepath.Dir(dbPath)
deletionsPath := deletions.DefaultPath(beadsDir)
loadResult, err := deletions.LoadDeletions(deletionsPath)
if err == nil && len(loadResult.Records) > 0 {
var filteredIssues []*types.Issue
for _, issue := range issues {
if del, found := loadResult.Records[issue.ID]; found {
// Issue is in deletions manifest - skip it
result.SkippedDeleted++
result.SkippedDeletedIDs = append(result.SkippedDeletedIDs, issue.ID)
fmt.Fprintf(os.Stderr, "Skipping %s (in deletions manifest: deleted %s by %s)\n",
issue.ID, del.Timestamp.Format("2006-01-02"), del.Actor)
} else {
filteredIssues = append(filteredIssues, issue)
}
}
issues = filteredIssues
}
}
// Check and handle prefix mismatches // Check and handle prefix mismatches
if err := handlePrefixMismatch(ctx, sqliteStore, issues, opts, result); err != nil { if err := handlePrefixMismatch(ctx, sqliteStore, issues, opts, result); err != nil {
return result, err return result, err