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)
|
||||
func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage, jsonlPath, gitRef string) error {
|
||||
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 --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,6 +390,25 @@ With --stealth: configures per-repository git settings for invisible beads usage
|
||||
}
|
||||
|
||||
// Check if git has existing issues to import (fresh clone scenario)
|
||||
// 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)
|
||||
}
|
||||
} else if !quiet {
|
||||
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 {
|
||||
@@ -401,6 +425,7 @@ With --stealth: configures per-repository git settings for invisible beads usage
|
||||
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run contributor wizard if --contributor flag is set
|
||||
if contributor {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user