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:
@@ -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)
|
// 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 {
|
func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage, jsonlPath, gitRef string) error {
|
||||||
jsonlData, err := readFromGitRef(jsonlPath, gitRef)
|
jsonlData, err := readFromGitRef(jsonlPath, gitRef)
|
||||||
|
|||||||
@@ -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 --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:
|
With --stealth: configures per-repository git settings for invisible beads usage:
|
||||||
• .git/info/exclude to prevent beads files from being committed
|
• .git/info/exclude to prevent beads files from being committed
|
||||||
• Claude Code settings with bd onboard instruction
|
• 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")
|
skipMergeDriver, _ := cmd.Flags().GetBool("skip-merge-driver")
|
||||||
skipHooks, _ := cmd.Flags().GetBool("skip-hooks")
|
skipHooks, _ := cmd.Flags().GetBool("skip-hooks")
|
||||||
force, _ := cmd.Flags().GetBool("force")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
fromJSONL, _ := cmd.Flags().GetBool("from-jsonl")
|
||||||
|
|
||||||
// Initialize config (PersistentPreRun doesn't run for init command)
|
// Initialize config (PersistentPreRun doesn't run for init command)
|
||||||
if err := config.Initialize(); err != nil {
|
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)
|
// Check if git has existing issues to import (fresh clone scenario)
|
||||||
issueCount, jsonlPath, gitRef := checkGitForIssues()
|
// With --from-jsonl: import from local file instead of git history
|
||||||
if issueCount > 0 {
|
if fromJSONL {
|
||||||
if !quiet {
|
// Import from current working tree's JSONL file
|
||||||
fmt.Fprintf(os.Stderr, "\n✓ Database initialized. Found %d issues in git, importing...\n", issueCount)
|
localJSONLPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||||
}
|
if _, err := os.Stat(localJSONLPath); err == nil {
|
||||||
|
issueCount, err := importFromLocalJSONL(ctx, initDBPath, store, localJSONLPath)
|
||||||
if err := importFromGit(ctx, initDBPath, store, jsonlPath, gitRef); err != nil {
|
if err != nil {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: import from local JSONL 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 && 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 {
|
} 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-hooks", false, "Skip git hooks installation")
|
||||||
initCmd.Flags().Bool("skip-merge-driver", false, "Skip git merge driver setup")
|
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("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)
|
rootCmd.AddCommand(initCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user