Files
beads/cmd/bd/sync_import.go
beads/crew/wolf 6ce36ab1f3 fix(lint): address G304 and unparam warnings
- Add #nosec G304 comment to parseJSONLFile in rename_prefix.go
- Mark unused noGitHistory param in importFromJSONLInline (mirrors import.go pattern)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 19:32:20 -08:00

224 lines
7.5 KiB
Go

package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/types"
)
// importFromJSONL imports the JSONL file by running the import command
// Optional parameters: noGitHistory, protectLeftSnapshot (bd-sync-deletion fix)
func importFromJSONL(ctx context.Context, jsonlPath string, renameOnImport bool, opts ...bool) error {
// Get current executable path to avoid "./bd" path issues
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("cannot resolve current executable: %w", err)
}
// Parse optional parameters
noGitHistory := false
protectLeftSnapshot := false
if len(opts) > 0 {
noGitHistory = opts[0]
}
if len(opts) > 1 {
protectLeftSnapshot = opts[1]
}
// Build args for import command
// Use --no-daemon to ensure subprocess uses direct mode, avoiding daemon connection issues
args := []string{"--no-daemon", "import", "-i", jsonlPath}
if renameOnImport {
args = append(args, "--rename-on-import")
}
if noGitHistory {
args = append(args, "--no-git-history")
}
// Add --protect-left-snapshot flag for post-pull imports (bd-sync-deletion fix)
if protectLeftSnapshot {
args = append(args, "--protect-left-snapshot")
}
// Run import command
cmd := exec.CommandContext(ctx, exe, args...) // #nosec G204 - bd import command from trusted binary
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("import failed: %w\n%s", err, output)
}
// Show output (import command provides the summary)
if len(output) > 0 {
fmt.Print(string(output))
}
return nil
}
// importFromJSONLInline imports the JSONL file directly without spawning a subprocess.
// This avoids path resolution issues when running from directories with .beads/redirect.
// The parent process's store and dbPath are used, ensuring consistent path resolution.
// (bd-ysal fix)
func importFromJSONLInline(ctx context.Context, jsonlPath string, renameOnImport bool, _ /* noGitHistory */ bool) error {
// Verify we have an active store
if store == nil {
return fmt.Errorf("no database store available for inline import")
}
// Read and parse the JSONL file
// #nosec G304 - jsonlPath is from findJSONLPath() which uses trusted paths
f, err := os.Open(jsonlPath)
if err != nil {
return fmt.Errorf("failed to open JSONL file: %w", err)
}
defer func() { _ = f.Close() }()
var allIssues []*types.Issue
scanner := bufio.NewScanner(f)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
if line == "" {
continue
}
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
return fmt.Errorf("error parsing line %d: %w", lineNum, err)
}
issue.SetDefaults()
allIssues = append(allIssues, &issue)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading JSONL: %w", err)
}
// Import using shared logic
opts := ImportOptions{
RenameOnImport: renameOnImport,
}
result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts)
if err != nil {
return fmt.Errorf("import failed: %w", err)
}
// Update staleness metadata (same as import.go lines 386-411)
// This is critical: without this, CheckStaleness will still report stale
if currentHash, hashErr := computeJSONLHash(jsonlPath); hashErr == nil {
if err := store.SetMetadata(ctx, "jsonl_content_hash", currentHash); err != nil {
debug.Logf("Warning: failed to update jsonl_content_hash: %v", err)
}
if err := store.SetJSONLFileHash(ctx, currentHash); err != nil {
debug.Logf("Warning: failed to update jsonl_file_hash: %v", err)
}
importTime := time.Now().Format(time.RFC3339Nano)
if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil {
debug.Logf("Warning: failed to update last_import_time: %v", err)
}
} else {
debug.Logf("Warning: failed to compute JSONL hash: %v", hashErr)
}
// Update database mtime
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
debug.Logf("Warning: failed to update database mtime: %v", err)
}
// Print summary
fmt.Fprintf(os.Stderr, "Import complete: %d created, %d updated", result.Created, result.Updated)
if result.Unchanged > 0 {
fmt.Fprintf(os.Stderr, ", %d unchanged", result.Unchanged)
}
if result.Skipped > 0 {
fmt.Fprintf(os.Stderr, ", %d skipped", result.Skipped)
}
fmt.Fprintf(os.Stderr, "\n")
return nil
}
// resolveNoGitHistoryForFromMain returns the resolved noGitHistory value for sync operations.
// When syncing from main (--from-main), noGitHistory is forced to true to prevent creating
// incorrect deletion records for locally-created beads that don't exist on main.
// See: https://github.com/steveyegge/beads/issues/417
func resolveNoGitHistoryForFromMain(fromMain, noGitHistory bool) bool {
if fromMain {
return true
}
return noGitHistory
}
// doSyncFromMain performs a one-way sync from the default branch (main/master)
// Used for ephemeral branches without upstream tracking.
// This fetches beads from main and imports them, discarding local beads changes.
// If sync.remote is configured (e.g., "upstream" for fork workflows), uses that remote
// instead of "origin".
func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool, dryRun bool, noGitHistory bool) error {
// Determine which remote to use (default: origin, but can be configured via sync.remote)
remote := "origin"
if err := ensureStoreActive(); err == nil && store != nil {
if configuredRemote, err := store.GetConfig(ctx, "sync.remote"); err == nil && configuredRemote != "" {
remote = configuredRemote
}
}
if dryRun {
fmt.Println("→ [DRY RUN] Would sync beads from main branch")
fmt.Printf(" 1. Fetch %s main\n", remote)
fmt.Printf(" 2. Checkout .beads/ from %s/main\n", remote)
fmt.Println(" 3. Import JSONL into database")
fmt.Println("\n✓ Dry run complete (no changes made)")
return nil
}
// Check if we're in a git repository
if !isGitRepo() {
return fmt.Errorf("not in a git repository")
}
// Check if remote exists
if !hasGitRemote(ctx) {
return fmt.Errorf("no git remote configured")
}
// Verify the configured remote exists
checkRemoteCmd := exec.CommandContext(ctx, "git", "remote", "get-url", remote)
if err := checkRemoteCmd.Run(); err != nil {
return fmt.Errorf("configured sync.remote '%s' does not exist (run 'git remote add %s <url>')", remote, remote)
}
defaultBranch := getDefaultBranchForRemote(ctx, remote)
// Step 1: Fetch from main
fmt.Printf("→ Fetching from %s/%s...\n", remote, defaultBranch)
fetchCmd := exec.CommandContext(ctx, "git", "fetch", remote, defaultBranch) //nolint:gosec // remote and defaultBranch from config
if output, err := fetchCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git fetch %s %s failed: %w\n%s", remote, defaultBranch, err, output)
}
// Step 2: Checkout .beads/ directory from main
fmt.Printf("→ Checking out beads from %s/%s...\n", remote, defaultBranch)
checkoutCmd := exec.CommandContext(ctx, "git", "checkout", fmt.Sprintf("%s/%s", remote, defaultBranch), "--", ".beads/") //nolint:gosec // remote and defaultBranch from config
if output, err := checkoutCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git checkout .beads/ from %s/%s failed: %w\n%s", remote, defaultBranch, err, output)
}
// Step 3: Import JSONL
fmt.Println("→ Importing JSONL...")
if err := importFromJSONL(ctx, jsonlPath, renameOnImport, noGitHistory); err != nil {
return fmt.Errorf("import failed: %w", err)
}
fmt.Println("\n✓ Sync from main complete")
return nil
}