Auto-import from git on empty DB (bd-189)
- Add checkAndAutoImport() that detects empty DB with issues in git - Automatically imports from git HEAD:.beads/issues.jsonl - Integrated into list, ready, and stats commands - Zero cognitive load for agents in fresh clones - Makes JSONL truly the source of truth - DB becomes ephemeral cache that auto-rebuilds Also: - Update AGENTS.md onboarding section with import instructions - Merge PR #98 (enhanced .gitignore) Amp-Thread-ID: https://ampcode.com/threads/T-ffcb5e95-e5a0-486b-a0ae-ce8bd861ab9d Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -462,6 +462,13 @@ Happy coding! 🔗
|
|||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
|
|
||||||
|
**FIRST TIME?** Import existing issues from git:
|
||||||
|
```bash
|
||||||
|
bd import -i .beads/issues.jsonl --json
|
||||||
|
# Or if issues.jsonl is empty in working tree but exists in git:
|
||||||
|
git show HEAD:.beads/issues.jsonl | bd import -i /dev/stdin --json
|
||||||
|
```
|
||||||
|
|
||||||
**Check for ready work:**
|
**Check for ready work:**
|
||||||
```bash
|
```bash
|
||||||
bd ready --json
|
bd ready --json
|
||||||
|
|||||||
171
cmd/bd/autoimport.go
Normal file
171
cmd/bd/autoimport.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkAndAutoImport checks if the database is empty but git has issues.
|
||||||
|
// If so, it automatically imports them and returns true.
|
||||||
|
// Returns false if no import was needed or if import failed.
|
||||||
|
func checkAndAutoImport(ctx context.Context, store storage.Storage) bool {
|
||||||
|
// Don't auto-import if auto-import is explicitly disabled
|
||||||
|
if noAutoImport {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if database has any issues
|
||||||
|
stats, err := store.GetStatistics(ctx)
|
||||||
|
if err != nil || stats.TotalIssues > 0 {
|
||||||
|
// Either error checking or DB has issues - don't auto-import
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database is empty - check if git has issues
|
||||||
|
issueCount, jsonlPath := checkGitForIssues()
|
||||||
|
if issueCount == 0 {
|
||||||
|
// No issues in git either
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found issues in git! Auto-import them
|
||||||
|
if !jsonOutput {
|
||||||
|
fmt.Fprintf(os.Stderr, "Found 0 issues in database but %d in git. Importing...\n", issueCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import from git
|
||||||
|
if err := importFromGit(ctx, store, jsonlPath); err != nil {
|
||||||
|
if !jsonOutput {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, "Try manually: git show HEAD:%s | bd import -i /dev/stdin\n", jsonlPath)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !jsonOutput {
|
||||||
|
fmt.Fprintf(os.Stderr, "Successfully imported %d issues from git.\n\n", issueCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkGitForIssues checks if git has issues in HEAD:.beads/issues.jsonl
|
||||||
|
// Returns (issue_count, relative_jsonl_path)
|
||||||
|
func checkGitForIssues() (int, string) {
|
||||||
|
// Try to find .beads directory
|
||||||
|
beadsDir := findBeadsDir()
|
||||||
|
if beadsDir == "" {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct relative path to issues.jsonl from git root
|
||||||
|
gitRoot := findGitRoot()
|
||||||
|
if gitRoot == "" {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(gitRoot, filepath.Join(beadsDir, "issues.jsonl"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if git has this file with content
|
||||||
|
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", relPath))
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// File doesn't exist in git or other error
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count lines (rough estimate of issue count)
|
||||||
|
lines := bytes.Count(output, []byte("\n"))
|
||||||
|
if lines == 0 {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, relPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBeadsDir finds the .beads directory in current or parent directories
|
||||||
|
func findBeadsDir() string {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
beadsDir := filepath.Join(dir, ".beads")
|
||||||
|
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
||||||
|
return beadsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
// Reached root
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// findGitRoot finds the git repository root
|
||||||
|
func findGitRoot() string {
|
||||||
|
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(bytes.TrimSpace(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// importFromGit imports issues from git HEAD
|
||||||
|
func importFromGit(ctx context.Context, store storage.Storage, jsonlPath string) error {
|
||||||
|
// Get content from git
|
||||||
|
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", jsonlPath))
|
||||||
|
jsonlData, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read from git: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSONL data
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(jsonlData))
|
||||||
|
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 fmt.Errorf("failed to parse issue: %w", err)
|
||||||
|
}
|
||||||
|
issues = append(issues, &issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to scan JSONL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing import logic with auto-resolve collisions
|
||||||
|
opts := ImportOptions{
|
||||||
|
ResolveCollisions: true,
|
||||||
|
DryRun: false,
|
||||||
|
SkipUpdate: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = importIssuesCore(ctx, dbPath, store, issues, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -137,10 +137,22 @@ var listCmd = &cobra.Command{
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
issues, err := store.SearchIssues(ctx, "", filter)
|
issues, err := store.SearchIssues(ctx, "", filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no issues found, check if git has issues and auto-import
|
||||||
|
if len(issues) == 0 {
|
||||||
|
if checkAndAutoImport(ctx, store) {
|
||||||
|
// Re-run the query after import
|
||||||
|
issues, err = store.SearchIssues(ctx, "", filter)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle format flag
|
// Handle format flag
|
||||||
if formatStr != "" {
|
if formatStr != "" {
|
||||||
if err := outputFormattedList(ctx, store, issues, formatStr); err != nil {
|
if err := outputFormattedList(ctx, store, issues, formatStr); err != nil {
|
||||||
|
|||||||
@@ -91,10 +91,22 @@ var readyCmd = &cobra.Command{
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
issues, err := store.GetReadyWork(ctx, filter)
|
issues, err := store.GetReadyWork(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no ready work found, check if git has issues and auto-import
|
||||||
|
if len(issues) == 0 {
|
||||||
|
if checkAndAutoImport(ctx, store) {
|
||||||
|
// Re-run the query after import
|
||||||
|
issues, err = store.GetReadyWork(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
// Always output array, even if empty
|
// Always output array, even if empty
|
||||||
if issues == nil {
|
if issues == nil {
|
||||||
@@ -225,10 +237,22 @@ var statsCmd = &cobra.Command{
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
stats, err := store.GetStatistics(ctx)
|
stats, err := store.GetStatistics(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no issues found, check if git has issues and auto-import
|
||||||
|
if stats.TotalIssues == 0 {
|
||||||
|
if checkAndAutoImport(ctx, store) {
|
||||||
|
// Re-run the stats after import
|
||||||
|
stats, err = store.GetStatistics(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
outputJSON(stats)
|
outputJSON(stats)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user