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:
Steve Yegge
2025-12-13 10:44:26 -08:00
parent e5068df3aa
commit 947e7188aa

View File

@@ -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
}