Add merge driver auto-config to bd init

This commit is contained in:
Steve Yegge
2025-11-05 19:27:41 -08:00
parent 75dcb0da1c
commit 999ad0264d

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -29,6 +30,7 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite
branch, _ := cmd.Flags().GetString("branch") branch, _ := cmd.Flags().GetString("branch")
contributor, _ := cmd.Flags().GetBool("contributor") contributor, _ := cmd.Flags().GetBool("contributor")
team, _ := cmd.Flags().GetBool("team") team, _ := cmd.Flags().GetBool("team")
skipMergeDriver, _ := cmd.Flags().GetBool("skip-merge-driver")
// 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 {
@@ -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 // Skip output if quiet mode
if quiet { if quiet {
return 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")) 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().StringP("branch", "b", "", "Git branch for beads commits (default: current branch)")
initCmd.Flags().Bool("contributor", false, "Run OSS contributor setup wizard") initCmd.Flags().Bool("contributor", false, "Run OSS contributor setup wizard")
initCmd.Flags().Bool("team", false, "Run team workflow 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) rootCmd.AddCommand(initCmd)
} }
@@ -505,6 +540,75 @@ exit 0
return nil 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 // migrateOldDatabases detects and migrates old database files to beads.db
func migrateOldDatabases(targetPath string, quiet bool) error { func migrateOldDatabases(targetPath string, quiet bool) error {
targetDir := filepath.Dir(targetPath) targetDir := filepath.Dir(targetPath)