Add bd doctor check and fix for missing sync.branch config (bd-rsua)
Problem: Existing beads repositories initialized before commit a4c38d5 don't have sync.branch configured. This causes 'bd sync --status' to fail with a confusing error.
Solution: Added new check in 'bd doctor' that detects when sync.branch is not configured and provides automatic fix via 'bd doctor --fix'. The fix automatically sets sync.branch to the current branch using 'git symbolic-ref --short HEAD'.
Changes:
- Added checkSyncBranchConfig() function in doctor.go
- Created fix/sync_branch.go with SyncBranchConfig() fix handler
- Added comprehensive test coverage in doctor_test.go
- Integrated check into applyFixes() switch statement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
104
cmd/bd/doctor.go
104
cmd/bd/doctor.go
@@ -196,6 +196,8 @@ func applyFixes(result doctorResult) {
|
|||||||
err = fix.SchemaCompatibility(result.Path)
|
err = fix.SchemaCompatibility(result.Path)
|
||||||
case "Git Merge Driver":
|
case "Git Merge Driver":
|
||||||
err = fix.MergeDriver(result.Path)
|
err = fix.MergeDriver(result.Path)
|
||||||
|
case "Sync Branch Config":
|
||||||
|
err = fix.SyncBranchConfig(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)
|
||||||
@@ -348,6 +350,11 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, metadataCheck)
|
result.Checks = append(result.Checks, metadataCheck)
|
||||||
// Don't fail overall check for metadata, just warn
|
// Don't fail overall check for metadata, just warn
|
||||||
|
|
||||||
|
// Check 17: Sync branch configuration (bd-rsua)
|
||||||
|
syncBranchCheck := checkSyncBranchConfig(path)
|
||||||
|
result.Checks = append(result.Checks, syncBranchCheck)
|
||||||
|
// Don't fail overall check for missing sync.branch, just warn
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1724,6 +1731,103 @@ func parseVersionParts(version string) []int {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkSyncBranchConfig(path string) doctorCheck {
|
||||||
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
|
// Skip if .beads doesn't exist
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "N/A (no .beads directory)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're in a git repository
|
||||||
|
gitDir := filepath.Join(path, ".git")
|
||||||
|
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "N/A (not a git repository)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check metadata.json first for custom database name
|
||||||
|
var dbPath string
|
||||||
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
|
dbPath = cfg.DatabasePath(beadsDir)
|
||||||
|
} else {
|
||||||
|
// Fall back to canonical database name
|
||||||
|
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no database (JSONL-only mode)
|
||||||
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "N/A (JSONL-only mode)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database to check config
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "Unable to check sync.branch config",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check if sync.branch is configured
|
||||||
|
var syncBranch string
|
||||||
|
err = db.QueryRow("SELECT value FROM config WHERE key = ?", "sync.branch").Scan(&syncBranch)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "Unable to read sync.branch config",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sync.branch is already configured, we're good
|
||||||
|
if syncBranch != "" {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: fmt.Sprintf("Configured (%s)", syncBranch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync.branch is not configured - get current branch for the fix message
|
||||||
|
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
|
||||||
|
cmd.Dir = path
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "sync.branch not configured",
|
||||||
|
Detail: "Unable to detect current branch",
|
||||||
|
Fix: "Run 'bd config set sync.branch <branch-name>' or 'bd doctor --fix' to auto-configure",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBranch := strings.TrimSpace(string(output))
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Sync Branch Config",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "sync.branch not configured",
|
||||||
|
Detail: fmt.Sprintf("Current branch: %s", currentBranch),
|
||||||
|
Fix: fmt.Sprintf("Run 'bd doctor --fix' to auto-configure to '%s', or manually: bd config set sync.branch <branch-name>", currentBranch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
43
cmd/bd/doctor/fix/sync_branch.go
Normal file
43
cmd/bd/doctor/fix/sync_branch.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package fix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncBranchConfig fixes missing sync.branch configuration by auto-setting it to the current branch
|
||||||
|
func SyncBranchConfig(path string) error {
|
||||||
|
if err := validateBeadsWorkspace(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current branch
|
||||||
|
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
|
||||||
|
cmd.Dir = path
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current branch: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBranch := strings.TrimSpace(string(output))
|
||||||
|
if currentBranch == "" {
|
||||||
|
return fmt.Errorf("current branch is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bd binary
|
||||||
|
bdBinary, err := getBdBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sync.branch using bd config set
|
||||||
|
setCmd := exec.Command(bdBinary, "config", "set", "sync.branch", currentBranch)
|
||||||
|
setCmd.Dir = path
|
||||||
|
if output, err := setCmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to set sync.branch: %w\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" Set sync.branch = %s\n", currentBranch)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -848,3 +849,124 @@ func TestParseVersionParts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckSyncBranchConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupFunc func(t *testing.T, tmpDir string)
|
||||||
|
expectedStatus string
|
||||||
|
expectWarning bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no beads directory",
|
||||||
|
setupFunc: func(t *testing.T, tmpDir string) {
|
||||||
|
// No .beads directory
|
||||||
|
},
|
||||||
|
expectedStatus: statusOK,
|
||||||
|
expectWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not a git repo",
|
||||||
|
setupFunc: func(t *testing.T, tmpDir string) {
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedStatus: statusOK,
|
||||||
|
expectWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sync.branch configured",
|
||||||
|
setupFunc: func(t *testing.T, tmpDir string) {
|
||||||
|
// Initialize git repo
|
||||||
|
cmd := exec.Command("git", "init")
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("git", "config", "user.email", "test@example.com")
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
_ = cmd.Run()
|
||||||
|
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
_ = cmd.Run()
|
||||||
|
|
||||||
|
// Create .beads directory and database
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create config table and set sync.branch
|
||||||
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`INSERT INTO config (key, value) VALUES ('sync.branch', 'main')`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedStatus: statusOK,
|
||||||
|
expectWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sync.branch not configured",
|
||||||
|
setupFunc: func(t *testing.T, tmpDir string) {
|
||||||
|
// Initialize git repo
|
||||||
|
cmd := exec.Command("git", "init")
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("git", "config", "user.email", "test@example.com")
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
_ = cmd.Run()
|
||||||
|
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
_ = cmd.Run()
|
||||||
|
|
||||||
|
// Create .beads directory and database
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create config table but don't set sync.branch
|
||||||
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedStatus: statusWarning,
|
||||||
|
expectWarning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
tc.setupFunc(t, tmpDir)
|
||||||
|
|
||||||
|
result := checkSyncBranchConfig(tmpDir)
|
||||||
|
|
||||||
|
if result.Status != tc.expectedStatus {
|
||||||
|
t.Errorf("Expected status %q, got %q", tc.expectedStatus, result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectWarning && result.Fix == "" {
|
||||||
|
t.Error("Expected Fix field to be set for warning status")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user