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:
@@ -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"}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user