feat(import): add import.orphan_handling config with 4 modes
- Add GetOrphanHandling() helper to SQLiteStorage (reads from config table) - Add --orphan-handling flag to 'bd import' command - Wire OrphanHandling through ImportOptions -> importer.Options - Auto-read config if flag not provided (default: 'allow') - Document in CONFIG.md with detailed mode explanations Modes: - strict: Fail on missing parent (safest) - resurrect: Auto-create parent tombstones from JSONL - skip: Skip orphans with warning - allow: Import without validation (default, most permissive) Closes bd-8072, bd-b92a Amp-Thread-ID: https://ampcode.com/threads/T-fd18d4a5-06b3-4400-9073-194d570846d8 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
42
CONFIG.md
42
CONFIG.md
@@ -169,6 +169,7 @@ Configuration keys use dot-notation namespaces to organize settings:
|
||||
- `max_collision_prob` - Maximum collision probability for adaptive hash IDs (default: 0.25)
|
||||
- `min_hash_length` - Minimum hash ID length (default: 4)
|
||||
- `max_hash_length` - Maximum hash ID length (default: 8)
|
||||
- `import.orphan_handling` - How to handle hierarchical issues with missing parents during import (default: `allow`)
|
||||
|
||||
### Integration Namespaces
|
||||
|
||||
@@ -199,6 +200,47 @@ bd config set min_hash_length "5"
|
||||
|
||||
See [docs/ADAPTIVE_IDS.md](docs/ADAPTIVE_IDS.md) for detailed documentation.
|
||||
|
||||
### Example: Import Orphan Handling
|
||||
|
||||
Controls how imports handle hierarchical child issues when their parent is missing from the database:
|
||||
|
||||
```bash
|
||||
# Strictest: Fail import if parent is missing (safest, prevents orphans)
|
||||
bd config set import.orphan_handling "strict"
|
||||
|
||||
# Auto-resurrect: Search JSONL history and recreate missing parents as tombstones
|
||||
bd config set import.orphan_handling "resurrect"
|
||||
|
||||
# Skip: Skip orphaned issues with warning (partial import)
|
||||
bd config set import.orphan_handling "skip"
|
||||
|
||||
# Allow: Import orphans without validation (default, most permissive)
|
||||
bd config set import.orphan_handling "allow"
|
||||
```
|
||||
|
||||
**Mode details:**
|
||||
|
||||
- **`strict`** - Import fails immediately if a child's parent is missing. Use when database integrity is critical.
|
||||
- **`resurrect`** - Searches the full JSONL file for missing parents and recreates them as tombstones (Status=Closed, Priority=4). Preserves hierarchy with minimal data. Dependencies are also resurrected on best-effort basis.
|
||||
- **`skip`** - Skips orphaned children with a warning. Partial import succeeds but some issues are excluded.
|
||||
- **`allow`** - Imports orphans without parent validation. Most permissive, works around import bugs. **This is the default** because it ensures all data is imported even if hierarchy is temporarily broken.
|
||||
|
||||
**Override per command:**
|
||||
```bash
|
||||
# Override config for a single import
|
||||
bd import -i issues.jsonl --orphan-handling strict
|
||||
|
||||
# Auto-import (sync) uses config value
|
||||
bd sync # Respects import.orphan_handling setting
|
||||
```
|
||||
|
||||
**When to use each mode:**
|
||||
|
||||
- Use `allow` (default) for daily imports and auto-sync - ensures no data loss
|
||||
- Use `resurrect` when importing from another database that had parent deletions
|
||||
- Use `strict` only for controlled imports where you need to guarantee parent existence
|
||||
- Use `skip` rarely - only when you want to selectively import a subset
|
||||
|
||||
### Example: Jira Integration
|
||||
|
||||
```bash
|
||||
|
||||
@@ -33,6 +33,7 @@ Behavior:
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
renameOnImport, _ := cmd.Flags().GetBool("rename-on-import")
|
||||
dedupeAfter, _ := cmd.Flags().GetBool("dedupe-after")
|
||||
orphanHandling, _ := cmd.Flags().GetString("orphan-handling")
|
||||
|
||||
// Open input
|
||||
in := os.Stdin
|
||||
@@ -98,6 +99,7 @@ Behavior:
|
||||
SkipUpdate: skipUpdate,
|
||||
Strict: strict,
|
||||
RenameOnImport: renameOnImport,
|
||||
OrphanHandling: orphanHandling,
|
||||
}
|
||||
|
||||
result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts)
|
||||
@@ -263,6 +265,7 @@ func init() {
|
||||
importCmd.Flags().Bool("dedupe-after", false, "Detect and report content duplicates after import")
|
||||
importCmd.Flags().Bool("dry-run", false, "Preview collision detection without making changes")
|
||||
importCmd.Flags().Bool("rename-on-import", false, "Rename imported issues to match database prefix (updates all references)")
|
||||
importCmd.Flags().String("orphan-handling", "", "How to handle missing parent issues: strict/resurrect/skip/allow (default: use config or 'allow')")
|
||||
importCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output import statistics in JSON format")
|
||||
rootCmd.AddCommand(importCmd)
|
||||
}
|
||||
|
||||
@@ -158,11 +158,12 @@ func issueDataChanged(existing *types.Issue, updates map[string]interface{}) boo
|
||||
|
||||
// ImportOptions configures how the import behaves
|
||||
type ImportOptions struct {
|
||||
DryRun bool // Preview changes without applying them
|
||||
SkipUpdate bool // Skip updating existing issues (create-only mode)
|
||||
Strict bool // Fail on any error (dependencies, labels, etc.)
|
||||
RenameOnImport bool // Rename imported issues to match database prefix
|
||||
DryRun bool // Preview changes without applying them
|
||||
SkipUpdate bool // Skip updating existing issues (create-only mode)
|
||||
Strict bool // Fail on any error (dependencies, labels, etc.)
|
||||
RenameOnImport bool // Rename imported issues to match database prefix
|
||||
SkipPrefixValidation bool // Skip prefix validation (for auto-import)
|
||||
OrphanHandling string // Orphan handling mode: strict/resurrect/skip/allow (empty = use config)
|
||||
}
|
||||
|
||||
// ImportResult contains statistics about the import operation
|
||||
@@ -198,6 +199,7 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage,
|
||||
Strict: opts.Strict,
|
||||
RenameOnImport: opts.RenameOnImport,
|
||||
SkipPrefixValidation: opts.SkipPrefixValidation,
|
||||
OrphanHandling: importer.OrphanHandling(opts.OrphanHandling),
|
||||
}
|
||||
|
||||
// Delegate to the importer package
|
||||
|
||||
@@ -97,6 +97,11 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to clear export_hashes before import: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Read orphan handling from config if not explicitly set
|
||||
if opts.OrphanHandling == "" {
|
||||
opts.OrphanHandling = sqliteStore.GetOrphanHandling(ctx)
|
||||
}
|
||||
|
||||
// Check and handle prefix mismatches
|
||||
if err := handlePrefixMismatch(ctx, sqliteStore, issues, opts, result); err != nil {
|
||||
|
||||
@@ -1208,6 +1208,22 @@ func (s *SQLiteStorage) DeleteConfig(ctx context.Context, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOrphanHandling gets the import.orphan_handling config value
|
||||
// Returns OrphanAllow (the default) if not set or if value is invalid
|
||||
func (s *SQLiteStorage) GetOrphanHandling(ctx context.Context) OrphanHandling {
|
||||
value, err := s.GetConfig(ctx, "import.orphan_handling")
|
||||
if err != nil || value == "" {
|
||||
return OrphanAllow // Default
|
||||
}
|
||||
|
||||
switch OrphanHandling(value) {
|
||||
case OrphanStrict, OrphanResurrect, OrphanSkip, OrphanAllow:
|
||||
return OrphanHandling(value)
|
||||
default:
|
||||
return OrphanAllow // Invalid value, use default
|
||||
}
|
||||
}
|
||||
|
||||
// SetMetadata sets a metadata value (for internal state like import hashes)
|
||||
func (s *SQLiteStorage) SetMetadata(ctx context.Context, key, value string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
|
||||
Reference in New Issue
Block a user