Add 'Sync Branch Health' check that detects: 1. Local sync branch diverged from remote (after force-push reset) 2. Sync branch significantly behind main on source files (20+ commits, 50+ files) Add --fix support that: - Handles worktree case (resets within worktree) - Handles regular branch case (deletes and recreates from main) - Pushes the reset branch to remote This helps contributors whose local beads-sync becomes orphaned after someone else resets the branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
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
|
|
// #nosec G204 - bdBinary is controlled by getBdBinary() which returns os.Executable()
|
|
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
|
|
}
|
|
|
|
// SyncBranchHealth fixes a stale or diverged sync branch by resetting it to main.
|
|
// This handles two cases:
|
|
// 1. Local sync branch diverged from remote (after force-push)
|
|
// 2. Sync branch far behind main on source files
|
|
//
|
|
// bd-6rf: Detect and fix stale beads-sync branch
|
|
func SyncBranchHealth(path, syncBranch string) error {
|
|
if err := validateBeadsWorkspace(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine main branch
|
|
mainBranch := "main"
|
|
cmd := exec.Command("git", "rev-parse", "--verify", "main")
|
|
cmd.Dir = path
|
|
if err := cmd.Run(); err != nil {
|
|
cmd = exec.Command("git", "rev-parse", "--verify", "master")
|
|
cmd.Dir = path
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("cannot determine main branch (neither main nor master exists)")
|
|
}
|
|
mainBranch = "master"
|
|
}
|
|
|
|
// Check if there's a worktree for this branch
|
|
worktreePath := ""
|
|
cmd = exec.Command("git", "worktree", "list", "--porcelain")
|
|
cmd.Dir = path
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
lines := strings.Split(string(output), "\n")
|
|
for i, line := range lines {
|
|
if strings.HasPrefix(line, "worktree ") {
|
|
wt := strings.TrimPrefix(line, "worktree ")
|
|
// Check if next line has the branch
|
|
if i+2 < len(lines) && strings.Contains(lines[i+2], syncBranch) {
|
|
worktreePath = wt
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If worktree exists, reset within it
|
|
if worktreePath != "" {
|
|
fmt.Printf(" Resetting sync branch in worktree: %s\n", worktreePath)
|
|
cmd = exec.Command("git", "fetch", "origin", mainBranch)
|
|
cmd.Dir = worktreePath
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to fetch: %w\n%s", err, out)
|
|
}
|
|
|
|
cmd = exec.Command("git", "reset", "--hard", fmt.Sprintf("origin/%s", mainBranch))
|
|
cmd.Dir = worktreePath
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to reset worktree: %w\n%s", err, out)
|
|
}
|
|
|
|
// Push the reset branch
|
|
cmd = exec.Command("git", "push", "--force-with-lease", "origin", syncBranch)
|
|
cmd.Dir = worktreePath
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to push: %w\n%s", err, out)
|
|
}
|
|
|
|
fmt.Printf(" ✓ Reset %s to %s and pushed\n", syncBranch, mainBranch)
|
|
return nil
|
|
}
|
|
|
|
// No worktree - reset the branch directly
|
|
// First, make sure we're not on the sync branch
|
|
cmd = exec.Command("git", "symbolic-ref", "--short", "HEAD")
|
|
cmd.Dir = path
|
|
currentBranchOutput, err := cmd.Output()
|
|
if err == nil && strings.TrimSpace(string(currentBranchOutput)) == syncBranch {
|
|
return fmt.Errorf("currently on %s branch - checkout a different branch first", syncBranch)
|
|
}
|
|
|
|
// Delete and recreate the branch from main
|
|
fmt.Printf(" Deleting local %s branch...\n", syncBranch)
|
|
cmd = exec.Command("git", "branch", "-D", syncBranch)
|
|
cmd.Dir = path
|
|
_ = cmd.Run() // Ignore error if branch doesn't exist
|
|
|
|
// Fetch latest and recreate
|
|
cmd = exec.Command("git", "fetch", "origin", mainBranch)
|
|
cmd.Dir = path
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to fetch: %w\n%s", err, out)
|
|
}
|
|
|
|
cmd = exec.Command("git", "branch", syncBranch, fmt.Sprintf("origin/%s", mainBranch))
|
|
cmd.Dir = path
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create branch: %w\n%s", err, out)
|
|
}
|
|
|
|
// Push the new branch
|
|
cmd = exec.Command("git", "push", "--force-with-lease", "origin", syncBranch)
|
|
cmd.Dir = path
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to push: %w\n%s", err, out)
|
|
}
|
|
|
|
fmt.Printf(" ✓ Recreated %s from %s and pushed\n", syncBranch, mainBranch)
|
|
return nil
|
|
}
|