diff --git a/cmd/bd/init.go b/cmd/bd/init.go index 679f1c6f..4463a548 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" "strings" @@ -29,6 +30,7 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite branch, _ := cmd.Flags().GetString("branch") contributor, _ := cmd.Flags().GetBool("contributor") team, _ := cmd.Flags().GetBool("team") + skipMergeDriver, _ := cmd.Flags().GetBool("skip-merge-driver") // Initialize config (PersistentPreRun doesn't run for init command) if err := config.Initialize(); err != nil { @@ -307,6 +309,17 @@ if isGitRepo() && !hooksInstalled() { } } +// Check if we're in a git repo and merge driver isn't configured +// Do this BEFORE quiet mode return so merge driver gets configured for agents +if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() { + if quiet { + // Auto-install merge driver silently in quiet mode (best default for agents) + _ = installMergeDriver() // Ignore errors in quiet mode + } else { + // Defer to interactive prompt below + } +} + // Skip output if quiet mode if quiet { return @@ -343,6 +356,27 @@ if quiet { } } + // Interactive git merge driver prompt for humans + if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() { + fmt.Printf("%s Git merge driver not configured\n", yellow("⚠")) + fmt.Printf(" bd merge provides intelligent JSONL merging to prevent conflicts.\n") + fmt.Printf(" This will configure git to use 'bd merge' for .beads/beads.jsonl\n\n") + + // Prompt to install + fmt.Printf("Configure git merge driver now? [Y/n] ") + var response string + _, _ = fmt.Scanln(&response) // ignore EOF on empty input + response = strings.ToLower(strings.TrimSpace(response)) + + if response == "" || response == "y" || response == "yes" { + if err := installMergeDriver(); err != nil { + fmt.Fprintf(os.Stderr, "Error configuring merge driver: %v\n", err) + } else { + fmt.Printf("%s Git merge driver configured successfully!\n\n", green("✓")) + } + } + } + fmt.Printf("Run %s to get started.\n\n", cyan("bd quickstart")) }, } @@ -353,6 +387,7 @@ func init() { initCmd.Flags().StringP("branch", "b", "", "Git branch for beads commits (default: current branch)") initCmd.Flags().Bool("contributor", false, "Run OSS contributor setup wizard") initCmd.Flags().Bool("team", false, "Run team workflow setup wizard") + initCmd.Flags().Bool("skip-merge-driver", false, "Skip git merge driver setup (non-interactive)") rootCmd.AddCommand(initCmd) } @@ -505,6 +540,75 @@ exit 0 return nil } +// mergeDriverInstalled checks if bd merge driver is configured +func mergeDriverInstalled() bool { + // Check git config for merge driver + cmd := exec.Command("git", "config", "merge.beads.driver") + output, err := cmd.Output() + if err != nil || len(output) == 0 { + return false + } + + // Check if .gitattributes has the merge driver configured + gitattributesPath := ".gitattributes" + content, err := os.ReadFile(gitattributesPath) + if err != nil { + return false + } + + // Look for beads JSONL merge attribute + return strings.Contains(string(content), ".beads/beads.jsonl") && + strings.Contains(string(content), "merge=beads") +} + +// installMergeDriver configures git to use bd merge for JSONL files +func installMergeDriver() error { + // Configure git merge driver + cmd := exec.Command("git", "config", "merge.beads.driver", "bd merge %A %O %L %R") + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to configure git merge driver: %w\n%s", err, output) + } + + cmd = exec.Command("git", "config", "merge.beads.name", "bd JSONL merge driver") + if output, err := cmd.CombinedOutput(); err != nil { + // Non-fatal, the name is just descriptive + fmt.Fprintf(os.Stderr, "Warning: failed to set merge driver name: %v\n%s", err, output) + } + + // Create or update .gitattributes + gitattributesPath := ".gitattributes" + + // Read existing .gitattributes if it exists + var existingContent string + content, err := os.ReadFile(gitattributesPath) + if err == nil { + existingContent = string(content) + } + + // Check if beads merge driver is already configured + hasBeadsMerge := strings.Contains(existingContent, ".beads/beads.jsonl") && + strings.Contains(existingContent, "merge=beads") + + if !hasBeadsMerge { + // Append beads merge driver configuration + beadsMergeAttr := "\n# Use bd merge for beads JSONL files\n.beads/beads.jsonl merge=beads\n" + + newContent := existingContent + if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 { + newContent += "\n" + } + newContent += beadsMergeAttr + + // Write updated .gitattributes (0644 is standard for .gitattributes) + // #nosec G306 - .gitattributes needs to be readable + if err := os.WriteFile(gitattributesPath, []byte(newContent), 0644); err != nil { + return fmt.Errorf("failed to update .gitattributes: %w", err) + } + } + + return nil +} + // migrateOldDatabases detects and migrates old database files to beads.db func migrateOldDatabases(targetPath string, quiet bool) error { targetDir := filepath.Dir(targetPath)