fix: Correct git merge driver placeholders from %L/%R to %A/%B
Git merge drivers only support three placeholders: - %O (ancestor/base) - %A (current version) - %B (other branch's version) The code was incorrectly using %L and %R, which don't exist in git, causing them to be passed through literally and breaking JSONL merges. Changes: - Fixed merge driver config in init.go, merge.go, README.md, docs - Added detection in bd doctor with clear error messages - Added auto-fix in bd doctor --fix - Added proactive warning in bd sync before git pull - Added reactive error detection after merge failures - Updated all tests to use correct placeholders Now users get helpful guidance at every step: 1. bd doctor detects the issue 2. bd doctor --fix auto-corrects it 3. bd sync warns before pulling if misconfigured 4. Error messages suggest bd doctor --fix when merge fails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+114
-114
File diff suppressed because one or more lines are too long
@@ -156,7 +156,7 @@ echo "BEFORE ANYTHING ELSE: run 'bd onboard' and follow the instructions" >> AGE
|
|||||||
|
|
||||||
**Git merge driver:** During `bd init`, beads configures git to use `bd merge` for intelligent JSONL merging. This prevents conflicts when multiple branches modify issues. Skip with `--skip-merge-driver` if needed. To configure manually later:
|
**Git merge driver:** During `bd init`, beads configures git to use `bd merge` for intelligent JSONL merging. This prevents conflicts when multiple branches modify issues. Skip with `--skip-merge-driver` if needed. To configure manually later:
|
||||||
```bash
|
```bash
|
||||||
git config merge.beads.driver "bd merge %A %O %L %R"
|
git config merge.beads.driver "bd merge %A %O %A %B"
|
||||||
git config merge.beads.name "bd JSONL merge driver"
|
git config merge.beads.name "bd JSONL merge driver"
|
||||||
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
|
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -192,6 +193,8 @@ func applyFixes(result doctorResult) {
|
|||||||
err = fix.DatabaseVersion(result.Path)
|
err = fix.DatabaseVersion(result.Path)
|
||||||
case "Schema Compatibility":
|
case "Schema Compatibility":
|
||||||
err = fix.SchemaCompatibility(result.Path)
|
err = fix.SchemaCompatibility(result.Path)
|
||||||
|
case "Git Merge Driver":
|
||||||
|
err = fix.MergeDriver(result.Path)
|
||||||
default:
|
default:
|
||||||
fmt.Printf(" ⚠ No automatic fix available for %s\n", check.Name)
|
fmt.Printf(" ⚠ No automatic fix available for %s\n", check.Name)
|
||||||
fmt.Printf(" Manual fix: %s\n", check.Fix)
|
fmt.Printf(" Manual fix: %s\n", check.Fix)
|
||||||
@@ -327,6 +330,11 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, gitignoreCheck)
|
result.Checks = append(result.Checks, gitignoreCheck)
|
||||||
// Don't fail overall check for gitignore, just warn
|
// Don't fail overall check for gitignore, just warn
|
||||||
|
|
||||||
|
// Check 15: Git merge driver configuration
|
||||||
|
mergeDriverCheck := checkMergeDriver(path)
|
||||||
|
result.Checks = append(result.Checks, mergeDriverCheck)
|
||||||
|
// Don't fail overall check for merge driver, just warn
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1513,6 +1521,64 @@ func checkSchemaCompatibility(path string) doctorCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkMergeDriver(path string) doctorCheck {
|
||||||
|
// Check if we're in a git repository
|
||||||
|
gitDir := filepath.Join(path, ".git")
|
||||||
|
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Git Merge Driver",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "N/A (not a git repository)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current merge driver configuration
|
||||||
|
cmd := exec.Command("git", "config", "merge.beads.driver")
|
||||||
|
cmd.Dir = path
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Merge driver not configured
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Git Merge Driver",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "Git merge driver not configured",
|
||||||
|
Fix: "Run 'bd init' to configure the merge driver, or manually: git config merge.beads.driver \"bd merge %A %O %A %B\"",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConfig := strings.TrimSpace(string(output))
|
||||||
|
correctConfig := "bd merge %A %O %A %B"
|
||||||
|
|
||||||
|
// Check if using old incorrect placeholders
|
||||||
|
if strings.Contains(currentConfig, "%L") || strings.Contains(currentConfig, "%R") {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Git Merge Driver",
|
||||||
|
Status: statusError,
|
||||||
|
Message: fmt.Sprintf("Incorrect merge driver config: %q (uses invalid %%L/%%R placeholders)", currentConfig),
|
||||||
|
Detail: "Git only supports %O (base), %A (current), %B (other). Using %L/%R causes merge failures.",
|
||||||
|
Fix: "Run 'bd doctor --fix' to update to correct config, or manually: git config merge.beads.driver \"bd merge %A %O %A %B\"",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if config is correct
|
||||||
|
if currentConfig != correctConfig {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Git Merge Driver",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: fmt.Sprintf("Non-standard merge driver config: %q", currentConfig),
|
||||||
|
Detail: fmt.Sprintf("Expected: %q", correctConfig),
|
||||||
|
Fix: fmt.Sprintf("Run 'bd doctor --fix' to update config, or manually: git config merge.beads.driver \"%s\"", correctConfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Git Merge Driver",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "Correctly configured",
|
||||||
|
Detail: currentConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(doctorCmd)
|
rootCmd.AddCommand(doctorCmd)
|
||||||
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")
|
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package fix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MergeDriver fixes the git merge driver configuration to use correct placeholders.
|
||||||
|
// Git only supports %O (base), %A (current), %B (other) - not %L/%R.
|
||||||
|
func MergeDriver(path string) error {
|
||||||
|
if err := validateBeadsWorkspace(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update git config to use correct placeholders
|
||||||
|
// #nosec G204 -- path is validated by validateBeadsWorkspace
|
||||||
|
cmd := exec.Command("git", "config", "merge.beads.driver", "bd merge %A %O %A %B")
|
||||||
|
cmd.Dir = path
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update git merge driver config: %w\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+1
-1
@@ -790,7 +790,7 @@ func mergeDriverInstalled() bool {
|
|||||||
// installMergeDriver configures git to use bd merge for JSONL files
|
// installMergeDriver configures git to use bd merge for JSONL files
|
||||||
func installMergeDriver() error {
|
func installMergeDriver() error {
|
||||||
// Configure git merge driver
|
// Configure git merge driver
|
||||||
cmd := exec.Command("git", "config", "merge.beads.driver", "bd merge %A %O %L %R")
|
cmd := exec.Command("git", "config", "merge.beads.driver", "bd merge %A %O %A %B")
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("failed to configure git merge driver: %w\n%s", err, output)
|
return fmt.Errorf("failed to configure git merge driver: %w\n%s", err, output)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -627,7 +627,7 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pre-configure merge driver manually
|
// Pre-configure merge driver manually
|
||||||
if err := runCommandInDir(tmpDir, "git", "config", "merge.beads.driver", "bd merge %A %O %L %R"); err != nil {
|
if err := runCommandInDir(tmpDir, "git", "config", "merge.beads.driver", "bd merge %A %O %A %B"); err != nil {
|
||||||
t.Fatalf("Failed to set git config: %v", err)
|
t.Fatalf("Failed to set git config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +776,7 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
|
|||||||
t.Fatalf("Failed to get merge.beads.driver: %v", err)
|
t.Fatalf("Failed to get merge.beads.driver: %v", err)
|
||||||
}
|
}
|
||||||
driver = strings.TrimSpace(driver)
|
driver = strings.TrimSpace(driver)
|
||||||
expected := "bd merge %A %O %L %R"
|
expected := "bd merge %A %O %A %B"
|
||||||
if driver != expected {
|
if driver != expected {
|
||||||
t.Errorf("Expected merge.beads.driver to be %q, got %q", expected, driver)
|
t.Errorf("Expected merge.beads.driver to be %q, got %q", expected, driver)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ markers for unresolvable conflicts.
|
|||||||
|
|
||||||
Designed to work as a git merge driver. Configure with:
|
Designed to work as a git merge driver. Configure with:
|
||||||
|
|
||||||
git config merge.beads.driver "bd merge %A %O %L %R"
|
git config merge.beads.driver "bd merge %A %O %A %B"
|
||||||
git config merge.beads.name "bd JSONL merge driver"
|
git config merge.beads.name "bd JSONL merge driver"
|
||||||
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
|
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
|
||||||
|
|
||||||
|
|||||||
@@ -198,9 +198,22 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Println("→ [DRY RUN] Would pull from remote")
|
fmt.Println("→ [DRY RUN] Would pull from remote")
|
||||||
} else {
|
} else {
|
||||||
|
// Check merge driver configuration before pulling
|
||||||
|
checkMergeDriverConfig()
|
||||||
|
|
||||||
fmt.Println("→ Pulling from remote...")
|
fmt.Println("→ Pulling from remote...")
|
||||||
if err := gitPull(ctx); err != nil {
|
if err := gitPull(ctx); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error pulling: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error pulling: %v\n", err)
|
||||||
|
|
||||||
|
// Check if this looks like a merge driver failure
|
||||||
|
errStr := err.Error()
|
||||||
|
if strings.Contains(errStr, "merge driver") ||
|
||||||
|
strings.Contains(errStr, "no such file or directory") ||
|
||||||
|
strings.Contains(errStr, "MERGE DRIVER INVOKED") {
|
||||||
|
fmt.Fprintf(os.Stderr, "\nThis may be caused by an incorrect merge driver configuration.\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Fix: bd doctor --fix\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n")
|
fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -427,6 +440,28 @@ func hasGitRemote(ctx context.Context) bool {
|
|||||||
|
|
||||||
// gitPull pulls from the current branch's upstream
|
// gitPull pulls from the current branch's upstream
|
||||||
// Returns nil if no remote configured (local-only mode)
|
// Returns nil if no remote configured (local-only mode)
|
||||||
|
func checkMergeDriverConfig() {
|
||||||
|
// Get current merge driver configuration
|
||||||
|
cmd := exec.Command("git", "config", "merge.beads.driver")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// No merge driver configured - this is OK, user may not need it
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConfig := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
// Check if using old incorrect placeholders
|
||||||
|
if strings.Contains(currentConfig, "%L") || strings.Contains(currentConfig, "%R") {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n⚠️ WARNING: Git merge driver is misconfigured!\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Current: %s\n", currentConfig)
|
||||||
|
fmt.Fprintf(os.Stderr, " Problem: Git only supports %%O (base), %%A (current), %%B (other)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Using %%L/%%R will cause merge failures!\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "\n Fix now: bd doctor --fix\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Or manually: git config merge.beads.driver \"bd merge %%A %%O %%A %%B\"\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func gitPull(ctx context.Context) error {
|
func gitPull(ctx context.Context) error {
|
||||||
// Check if any remote exists (bd-biwp: support local-only repos)
|
// Check if any remote exists (bd-biwp: support local-only repos)
|
||||||
if !hasGitRemote(ctx) {
|
if !hasGitRemote(ctx) {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ git commit
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# These are configured automatically:
|
# These are configured automatically:
|
||||||
git config merge.beads.driver "bd merge %A %O %L %R"
|
git config merge.beads.driver "bd merge %A %O %A %B"
|
||||||
git config merge.beads.name "bd JSONL merge driver"
|
git config merge.beads.name "bd JSONL merge driver"
|
||||||
|
|
||||||
# .gitattributes entry added:
|
# .gitattributes entry added:
|
||||||
@@ -140,7 +140,7 @@ git config merge.beads.name "bd JSONL merge driver"
|
|||||||
**If you skipped merge driver with `--skip-merge-driver`:**
|
**If you skipped merge driver with `--skip-merge-driver`:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git config merge.beads.driver "bd merge %A %O %L %R"
|
git config merge.beads.driver "bd merge %A %O %A %B"
|
||||||
git config merge.beads.name "bd JSONL merge driver"
|
git config merge.beads.name "bd JSONL merge driver"
|
||||||
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
|
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user