feat: Add bd init --from-jsonl for preserving manual cleanups

Adds --from-jsonl flag that imports from the current working tree's
.beads/issues.jsonl file instead of scanning git history. This prevents
deleted issues from being resurrected during re-initialization.

Use case: After running bd compact --purge-tombstones and committing
the cleaned JSONL, a subsequent bd init would previously re-import
all historical issues from git, defeating the cleanup.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-29 12:34:57 -08:00
parent 6c42b461a4
commit bb5c27c81b
2 changed files with 101 additions and 12 deletions

View File

@@ -206,6 +206,69 @@ func getLocalSyncBranch(beadsDir string) string {
// importFromLocalJSONL imports issues from a local JSONL file on disk.
// Unlike importFromGit, this reads from the current working tree, preserving
// any manual cleanup done to the JSONL file (e.g., via bd compact --purge-tombstones).
// Returns the number of issues imported and any error.
func importFromLocalJSONL(ctx context.Context, dbFilePath string, store storage.Storage, localPath string) (int, error) {
// #nosec G304 -- path provided by bd init command
jsonlData, err := os.ReadFile(localPath)
if err != nil {
return 0, fmt.Errorf("failed to read local JSONL file: %w", err)
}
// Parse JSONL data
scanner := bufio.NewScanner(bytes.NewReader(jsonlData))
// Increase buffer size to handle large JSONL lines (e.g., big descriptions)
scanner.Buffer(make([]byte, 0, 1024*1024), 64*1024*1024) // allow up to 64MB per line
var issues []*types.Issue
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
return 0, fmt.Errorf("failed to parse issue: %w", err)
}
issue.SetDefaults() // Apply defaults for omitted fields
issues = append(issues, &issue)
}
if err := scanner.Err(); err != nil {
return 0, fmt.Errorf("failed to scan JSONL: %w", err)
}
// Set issue_prefix from first imported issue if missing
if len(issues) > 0 {
configuredPrefix, err := store.GetConfig(ctx, "issue_prefix")
if err == nil && strings.TrimSpace(configuredPrefix) == "" {
firstPrefix := utils.ExtractIssuePrefix(issues[0].ID)
if firstPrefix != "" {
if err := store.SetConfig(ctx, "issue_prefix", firstPrefix); err != nil {
return 0, fmt.Errorf("failed to set issue_prefix from imported issues: %w", err)
}
}
}
}
// Use existing import logic
opts := ImportOptions{
DryRun: false,
SkipUpdate: false,
SkipPrefixValidation: true,
}
_, err = importIssuesCore(ctx, dbFilePath, store, issues, opts)
if err != nil {
return 0, err
}
return len(issues), nil
}
// importFromGit imports issues from git at the specified ref (bd-0is: supports sync-branch)
func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage, jsonlPath, gitRef string) error {
jsonlData, err := readFromGitRef(jsonlPath, gitRef)

View File

@@ -31,6 +31,10 @@ and database file. Optionally specify a custom issue prefix.
With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite database.
With --from-jsonl: imports from the current .beads/issues.jsonl file on disk instead
of scanning git history. Use this after manual JSONL cleanup (e.g., bd compact --purge-tombstones)
to prevent deleted issues from being resurrected during re-initialization.
With --stealth: configures per-repository git settings for invisible beads usage:
• .git/info/exclude to prevent beads files from being committed
• Claude Code settings with bd onboard instruction
@@ -45,6 +49,7 @@ With --stealth: configures per-repository git settings for invisible beads usage
skipMergeDriver, _ := cmd.Flags().GetBool("skip-merge-driver")
skipHooks, _ := cmd.Flags().GetBool("skip-hooks")
force, _ := cmd.Flags().GetBool("force")
fromJSONL, _ := cmd.Flags().GetBool("from-jsonl")
// Initialize config (PersistentPreRun doesn't run for init command)
if err := config.Initialize(); err != nil {
@@ -385,20 +390,40 @@ With --stealth: configures per-repository git settings for invisible beads usage
}
// Check if git has existing issues to import (fresh clone scenario)
issueCount, jsonlPath, gitRef := checkGitForIssues()
if issueCount > 0 {
if !quiet {
fmt.Fprintf(os.Stderr, "\n✓ Database initialized. Found %d issues in git, importing...\n", issueCount)
}
if err := importFromGit(ctx, initDBPath, store, jsonlPath, gitRef); err != nil {
if !quiet {
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
fmt.Fprintf(os.Stderr, "Try manually: git show %s:%s | bd import -i /dev/stdin\n", gitRef, jsonlPath)
// With --from-jsonl: import from local file instead of git history
if fromJSONL {
// Import from current working tree's JSONL file
localJSONLPath := filepath.Join(beadsDir, "issues.jsonl")
if _, err := os.Stat(localJSONLPath); err == nil {
issueCount, err := importFromLocalJSONL(ctx, initDBPath, store, localJSONLPath)
if err != nil {
if !quiet {
fmt.Fprintf(os.Stderr, "Warning: import from local JSONL failed: %v\n", err)
}
// Non-fatal - continue with empty database
} else if !quiet && issueCount > 0 {
fmt.Fprintf(os.Stderr, "✓ Imported %d issues from local %s\n\n", issueCount, localJSONLPath)
}
// Non-fatal - continue with empty database
} else if !quiet {
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
fmt.Fprintf(os.Stderr, "Warning: --from-jsonl specified but %s not found\n", localJSONLPath)
}
} else {
// Default: import from git history
issueCount, jsonlPath, gitRef := checkGitForIssues()
if issueCount > 0 {
if !quiet {
fmt.Fprintf(os.Stderr, "\n✓ Database initialized. Found %d issues in git, importing...\n", issueCount)
}
if err := importFromGit(ctx, initDBPath, store, jsonlPath, gitRef); err != nil {
if !quiet {
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
fmt.Fprintf(os.Stderr, "Try manually: git show %s:%s | bd import -i /dev/stdin\n", gitRef, jsonlPath)
}
// Non-fatal - continue with empty database
} else if !quiet {
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
}
}
}
@@ -511,6 +536,7 @@ func init() {
initCmd.Flags().Bool("skip-hooks", false, "Skip git hooks installation")
initCmd.Flags().Bool("skip-merge-driver", false, "Skip git merge driver setup")
initCmd.Flags().Bool("force", false, "Force re-initialization even if JSONL already has issues (may cause data loss)")
initCmd.Flags().Bool("from-jsonl", false, "Import from current .beads/issues.jsonl file instead of git history (preserves manual cleanups)")
rootCmd.AddCommand(initCmd)
}