fix(sync): support external BEADS_DIR in separate git repository
When BEADS_DIR environment variable points to a separate git repository, bd sync previously failed with "fatal: 'main' is already used by worktree" because it computed repoRoot from cwd instead of the beads directory. This fix detects when beads dir is in a different git repo than cwd and uses direct git operations (add/commit/push/pull) instead of worktree-based sync, bypassing the problematic worktree creation entirely. Cherry-picked from PR #533 (cleaned up unrelated changes). Co-Authored-By: dand-oss <dand-oss@users.noreply.github.com>
This commit is contained in:
170
cmd/bd/sync.go
170
cmd/bd/sync.go
@@ -304,6 +304,73 @@ Use --merge to merge the sync branch back to main branch.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if BEADS_DIR points to an external repository (dand-oss fix)
|
||||
// If so, use direct git operations instead of worktree-based sync
|
||||
beadsDir := filepath.Dir(jsonlPath)
|
||||
isExternal := isExternalBeadsDir(ctx, beadsDir)
|
||||
|
||||
if isExternal {
|
||||
// External BEADS_DIR: commit/pull directly to the beads repo
|
||||
fmt.Println("→ External BEADS_DIR detected, using direct commit...")
|
||||
|
||||
// Check for changes in the external beads repo
|
||||
externalRepoRoot, err := getRepoRootFromPath(ctx, beadsDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if there are changes to commit
|
||||
relBeadsDir, _ := filepath.Rel(externalRepoRoot, beadsDir)
|
||||
statusCmd := exec.CommandContext(ctx, "git", "-C", externalRepoRoot, "status", "--porcelain", relBeadsDir)
|
||||
statusOutput, _ := statusCmd.Output()
|
||||
externalHasChanges := len(strings.TrimSpace(string(statusOutput))) > 0
|
||||
|
||||
if externalHasChanges {
|
||||
if dryRun {
|
||||
fmt.Printf("→ [DRY RUN] Would commit changes to external beads repo at %s\n", externalRepoRoot)
|
||||
} else {
|
||||
committed, err := commitToExternalBeadsRepo(ctx, beadsDir, message, !noPush)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if committed {
|
||||
if !noPush {
|
||||
fmt.Println("✓ Committed and pushed to external beads repo")
|
||||
} else {
|
||||
fmt.Println("✓ Committed to external beads repo")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("→ No changes to commit in external beads repo")
|
||||
}
|
||||
|
||||
if !noPull {
|
||||
if dryRun {
|
||||
fmt.Printf("→ [DRY RUN] Would pull from external beads repo at %s\n", externalRepoRoot)
|
||||
} else {
|
||||
fmt.Println("→ Pulling from external beads repo...")
|
||||
if err := pullFromExternalBeadsRepo(ctx, beadsDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error pulling: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("✓ Pulled from external beads repo")
|
||||
|
||||
// Re-import after pull to update local database
|
||||
fmt.Println("→ Importing JSONL...")
|
||||
if err := importFromJSONL(ctx, jsonlPath, renameOnImport, noGitHistory); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error importing: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n✓ Sync complete")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if sync.branch is configured for worktree-based sync (bd-e3w)
|
||||
// This allows committing to a separate branch without changing the user's working directory
|
||||
var syncBranchName string
|
||||
@@ -1731,3 +1798,106 @@ func resolveNoGitHistoryForFromMain(fromMain, noGitHistory bool) bool {
|
||||
}
|
||||
return noGitHistory
|
||||
}
|
||||
|
||||
// isExternalBeadsDir checks if the beads directory is in a different git repo than cwd.
|
||||
// This is used to detect when BEADS_DIR points to a separate repository.
|
||||
// Contributed by dand-oss (https://github.com/steveyegge/beads/pull/533)
|
||||
func isExternalBeadsDir(ctx context.Context, beadsDir string) bool {
|
||||
// Get repo root of cwd
|
||||
cwdRepoRoot, err := syncbranch.GetRepoRoot(ctx)
|
||||
if err != nil {
|
||||
return false // Can't determine, assume local
|
||||
}
|
||||
|
||||
// Get repo root of beads dir
|
||||
beadsRepoRoot, err := getRepoRootFromPath(ctx, beadsDir)
|
||||
if err != nil {
|
||||
return false // Can't determine, assume local
|
||||
}
|
||||
|
||||
return cwdRepoRoot != beadsRepoRoot
|
||||
}
|
||||
|
||||
// getRepoRootFromPath returns the git repository root for a given path.
|
||||
// Unlike syncbranch.GetRepoRoot which uses cwd, this allows getting the repo root
|
||||
// for any path.
|
||||
// Contributed by dand-oss (https://github.com/steveyegge/beads/pull/533)
|
||||
func getRepoRootFromPath(ctx context.Context, path string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "git", "-C", path, "rev-parse", "--show-toplevel")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get git root for %s: %w", path, err)
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
// commitToExternalBeadsRepo commits changes directly to an external beads repo.
|
||||
// Used when BEADS_DIR points to a different git repository than cwd.
|
||||
// This bypasses the worktree-based sync which fails when beads dir is external.
|
||||
// Contributed by dand-oss (https://github.com/steveyegge/beads/pull/533)
|
||||
func commitToExternalBeadsRepo(ctx context.Context, beadsDir, message string, push bool) (bool, error) {
|
||||
repoRoot, err := getRepoRootFromPath(ctx, beadsDir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get repo root: %w", err)
|
||||
}
|
||||
|
||||
// Stage beads files (use relative path from repo root)
|
||||
relBeadsDir, err := filepath.Rel(repoRoot, beadsDir)
|
||||
if err != nil {
|
||||
relBeadsDir = beadsDir // Fallback to absolute path
|
||||
}
|
||||
|
||||
addCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "add", relBeadsDir)
|
||||
if output, err := addCmd.CombinedOutput(); err != nil {
|
||||
return false, fmt.Errorf("git add failed: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
// Check if there are staged changes
|
||||
diffCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "diff", "--cached", "--quiet")
|
||||
if diffCmd.Run() == nil {
|
||||
return false, nil // No changes to commit
|
||||
}
|
||||
|
||||
// Commit
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("bd sync: %s", time.Now().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
commitCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "commit", "-m", message)
|
||||
if output, err := commitCmd.CombinedOutput(); err != nil {
|
||||
return false, fmt.Errorf("git commit failed: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
// Push if requested
|
||||
if push {
|
||||
pushCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "push")
|
||||
if output, err := pushCmd.CombinedOutput(); err != nil {
|
||||
return true, fmt.Errorf("git push failed: %w\n%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// pullFromExternalBeadsRepo pulls changes in an external beads repo.
|
||||
// Used when BEADS_DIR points to a different git repository than cwd.
|
||||
// Contributed by dand-oss (https://github.com/steveyegge/beads/pull/533)
|
||||
func pullFromExternalBeadsRepo(ctx context.Context, beadsDir string) error {
|
||||
repoRoot, err := getRepoRootFromPath(ctx, beadsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get repo root: %w", err)
|
||||
}
|
||||
|
||||
// Check if remote exists
|
||||
remoteCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "remote")
|
||||
remoteOutput, err := remoteCmd.Output()
|
||||
if err != nil || len(strings.TrimSpace(string(remoteOutput))) == 0 {
|
||||
return nil // No remote, skip pull
|
||||
}
|
||||
|
||||
pullCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "pull")
|
||||
if output, err := pullCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git pull failed: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user