fix: respect sync.remote config in bd sync (GH#872, bd-ypvj)
The sync.remote config was being set via `bd config set sync.remote <name>` but `bd sync` was still using 'origin' for git pull/push operations. Changes: - Updated gitPull() and gitPush() in sync_git.go to accept a configuredRemote parameter that takes precedence over git's branch tracking config - Updated sync.go to read sync.remote config and pass it to gitPull/gitPush - Updated daemon_sync.go to read sync.remote config for all daemon sync ops - Added user-facing messages to show which remote is being used 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
This commit is contained in:
@@ -486,9 +486,10 @@ func performExport(ctx context.Context, store storage.Storage, autoCommit, autoP
|
|||||||
}
|
}
|
||||||
log.log("Committed changes")
|
log.log("Committed changes")
|
||||||
|
|
||||||
// Auto-push if enabled
|
// Auto-push if enabled (GH#872: use sync.remote config)
|
||||||
if autoPush {
|
if autoPush {
|
||||||
if err := gitPush(exportCtx); err != nil {
|
configuredRemote, _ := store.GetConfig(exportCtx, "sync.remote")
|
||||||
|
if err := gitPush(exportCtx, configuredRemote); err != nil {
|
||||||
log.log("Push failed: %v", err)
|
log.log("Push failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -597,9 +598,10 @@ func performAutoImport(ctx context.Context, store storage.Storage, skipGit bool,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If sync branch not configured, use regular pull
|
// If sync branch not configured, use regular pull (GH#872: use sync.remote config)
|
||||||
if !pulled {
|
if !pulled {
|
||||||
if err := gitPull(importCtx); err != nil {
|
configuredRemote, _ := store.GetConfig(importCtx, "sync.remote")
|
||||||
|
if err := gitPull(importCtx, configuredRemote); err != nil {
|
||||||
backoff := RecordSyncFailure(beadsDir, err.Error())
|
backoff := RecordSyncFailure(beadsDir, err.Error())
|
||||||
log.log("Pull failed: %v (backoff: %v)", err, backoff)
|
log.log("Pull failed: %v (backoff: %v)", err, backoff)
|
||||||
return
|
return
|
||||||
@@ -800,9 +802,10 @@ func performSync(ctx context.Context, store storage.Storage, autoCommit, autoPus
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If sync branch not configured, use regular pull
|
// If sync branch not configured, use regular pull (GH#872: use sync.remote config)
|
||||||
if !pulled {
|
if !pulled {
|
||||||
if err := gitPull(syncCtx); err != nil {
|
configuredRemote, _ := store.GetConfig(syncCtx, "sync.remote")
|
||||||
|
if err := gitPull(syncCtx, configuredRemote); err != nil {
|
||||||
log.log("Pull failed: %v", err)
|
log.log("Pull failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -889,8 +892,10 @@ func performSync(ctx context.Context, store storage.Storage, autoCommit, autoPus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GH#872: use sync.remote config
|
||||||
if autoPush && autoCommit {
|
if autoPush && autoCommit {
|
||||||
if err := gitPush(syncCtx); err != nil {
|
configuredRemote, _ := store.GetConfig(syncCtx, "sync.remote")
|
||||||
|
if err := gitPush(syncCtx, configuredRemote); err != nil {
|
||||||
log.log("Push failed: %v", err)
|
log.log("Push failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,7 +382,11 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
var repoRoot string
|
var repoRoot string
|
||||||
var useSyncBranch bool
|
var useSyncBranch bool
|
||||||
var onSyncBranch bool // GH#519: track if we're on the sync branch
|
var onSyncBranch bool // GH#519: track if we're on the sync branch
|
||||||
|
// GH#872: Get configured remote from sync.remote (for fork workflows, etc.)
|
||||||
|
var configuredRemote string
|
||||||
if err := ensureStoreActive(); err == nil && store != nil {
|
if err := ensureStoreActive(); err == nil && store != nil {
|
||||||
|
// Read sync.remote config (e.g., "upstream" for fork workflows)
|
||||||
|
configuredRemote, _ = store.GetConfig(ctx, "sync.remote")
|
||||||
syncBranchName, _ = syncbranch.Get(ctx, store)
|
syncBranchName, _ = syncbranch.Get(ctx, store)
|
||||||
if syncBranchName != "" && syncbranch.HasGitRemote(ctx) {
|
if syncBranchName != "" && syncbranch.HasGitRemote(ctx) {
|
||||||
// GH#829/bd-e2q9/bd-kvus: Get repo root from beads location, not cwd.
|
// GH#829/bd-e2q9/bd-kvus: Get repo root from beads location, not cwd.
|
||||||
@@ -629,12 +633,15 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
checkMergeDriverConfig()
|
checkMergeDriverConfig()
|
||||||
|
|
||||||
// GH#519: show appropriate message when on sync branch
|
// GH#519: show appropriate message when on sync branch
|
||||||
|
// GH#872: show configured remote if using sync.remote
|
||||||
if onSyncBranch {
|
if onSyncBranch {
|
||||||
fmt.Printf("→ Pulling from remote on sync branch '%s'...\n", syncBranchName)
|
fmt.Printf("→ Pulling from remote on sync branch '%s'...\n", syncBranchName)
|
||||||
|
} else if configuredRemote != "" {
|
||||||
|
fmt.Printf("→ Pulling from %s...\n", configuredRemote)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("→ Pulling from remote...")
|
fmt.Println("→ Pulling from remote...")
|
||||||
}
|
}
|
||||||
err := gitPull(ctx)
|
err := gitPull(ctx, configuredRemote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if it's a rebase conflict on beads.jsonl that we can auto-resolve
|
// Check if it's a rebase conflict on beads.jsonl that we can auto-resolve
|
||||||
if isInRebase() && hasJSONLConflict() {
|
if isInRebase() && hasJSONLConflict() {
|
||||||
@@ -789,12 +796,21 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
// Step 5: Push to remote (skip if using sync branch - all pushes go via worktree)
|
// Step 5: Push to remote (skip if using sync branch - all pushes go via worktree)
|
||||||
// When sync.branch is configured, we don't push the main branch at all.
|
// When sync.branch is configured, we don't push the main branch at all.
|
||||||
// The sync branch worktree handles all pushes.
|
// The sync branch worktree handles all pushes.
|
||||||
|
// GH#872: Use sync.remote config if set
|
||||||
if !noPush && hasChanges && !pushedViaSyncBranch && !useSyncBranch {
|
if !noPush && hasChanges && !pushedViaSyncBranch && !useSyncBranch {
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Println("→ [DRY RUN] Would push to remote")
|
if configuredRemote != "" {
|
||||||
|
fmt.Printf("→ [DRY RUN] Would push to %s\n", configuredRemote)
|
||||||
|
} else {
|
||||||
|
fmt.Println("→ [DRY RUN] Would push to remote")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("→ Pushing to remote...")
|
if configuredRemote != "" {
|
||||||
if err := gitPush(ctx); err != nil {
|
fmt.Printf("→ Pushing to %s...\n", configuredRemote)
|
||||||
|
} else {
|
||||||
|
fmt.Println("→ Pushing to remote...")
|
||||||
|
}
|
||||||
|
if err := gitPush(ctx, configuredRemote); err != nil {
|
||||||
FatalErrorWithHint(fmt.Sprintf("pushing: %v", err), "pull may have brought new changes, run 'bd sync' again")
|
FatalErrorWithHint(fmt.Sprintf("pushing: %v", err), "pull may have brought new changes, run 'bd sync' again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,85 @@ func runGitRebaseContinue(ctx context.Context) error {
|
|||||||
|
|
||||||
// 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)
|
||||||
|
// If configuredRemote is non-empty, uses that instead of the branch's configured remote.
|
||||||
|
// This allows respecting the sync.remote bd config.
|
||||||
|
func gitPull(ctx context.Context, configuredRemote string) error {
|
||||||
|
// Check if any remote exists (support local-only repos)
|
||||||
|
if !hasGitRemote(ctx) {
|
||||||
|
return nil // Gracefully skip - local-only mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current branch name
|
||||||
|
// Use symbolic-ref to work in fresh repos without commits
|
||||||
|
branchCmd := exec.CommandContext(ctx, "git", "symbolic-ref", "--short", "HEAD")
|
||||||
|
branchOutput, err := branchCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current branch: %w", err)
|
||||||
|
}
|
||||||
|
branch := strings.TrimSpace(string(branchOutput))
|
||||||
|
|
||||||
|
// Determine remote to use:
|
||||||
|
// 1. If configuredRemote (from sync.remote bd config) is set, use that
|
||||||
|
// 2. Otherwise, get from git branch tracking config
|
||||||
|
// 3. Fall back to "origin"
|
||||||
|
remote := configuredRemote
|
||||||
|
if remote == "" {
|
||||||
|
remoteCmd := exec.CommandContext(ctx, "git", "config", "--get", fmt.Sprintf("branch.%s.remote", branch)) //nolint:gosec // G204: branch from git symbolic-ref
|
||||||
|
remoteOutput, err := remoteCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// If no remote configured, default to "origin"
|
||||||
|
remote = "origin"
|
||||||
|
} else {
|
||||||
|
remote = strings.TrimSpace(string(remoteOutput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull with explicit remote and branch
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "pull", remote, branch) //nolint:gosec // G204: remote/branch from git config, not user input
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git pull failed: %w\n%s", err, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitPush pushes to the current branch's upstream
|
||||||
|
// Returns nil if no remote configured (local-only mode)
|
||||||
|
// If configuredRemote is non-empty, pushes to that remote explicitly.
|
||||||
|
// This allows respecting the sync.remote bd config.
|
||||||
|
func gitPush(ctx context.Context, configuredRemote string) error {
|
||||||
|
// Check if any remote exists (support local-only repos)
|
||||||
|
if !hasGitRemote(ctx) {
|
||||||
|
return nil // Gracefully skip - local-only mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// If configuredRemote is set, push explicitly to that remote with current branch
|
||||||
|
if configuredRemote != "" {
|
||||||
|
// Get current branch name
|
||||||
|
branchCmd := exec.CommandContext(ctx, "git", "symbolic-ref", "--short", "HEAD")
|
||||||
|
branchOutput, err := branchCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current branch: %w", err)
|
||||||
|
}
|
||||||
|
branch := strings.TrimSpace(string(branchOutput))
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "push", configuredRemote, branch) //nolint:gosec // G204: configuredRemote from bd config
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git push failed: %w\n%s", err, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: use git's default push behavior
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "push")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git push failed: %w\n%s", err, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkMergeDriverConfig() {
|
func checkMergeDriverConfig() {
|
||||||
// Get current merge driver configuration
|
// Get current merge driver configuration
|
||||||
cmd := exec.Command("git", "config", "merge.beads.driver")
|
cmd := exec.Command("git", "config", "merge.beads.driver")
|
||||||
@@ -349,55 +428,6 @@ func checkMergeDriverConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitPull(ctx context.Context) error {
|
|
||||||
// Check if any remote exists (support local-only repos)
|
|
||||||
if !hasGitRemote(ctx) {
|
|
||||||
return nil // Gracefully skip - local-only mode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current branch name
|
|
||||||
// Use symbolic-ref to work in fresh repos without commits
|
|
||||||
branchCmd := exec.CommandContext(ctx, "git", "symbolic-ref", "--short", "HEAD")
|
|
||||||
branchOutput, err := branchCmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get current branch: %w", err)
|
|
||||||
}
|
|
||||||
branch := strings.TrimSpace(string(branchOutput))
|
|
||||||
|
|
||||||
// Get remote name for current branch (usually "origin")
|
|
||||||
remoteCmd := exec.CommandContext(ctx, "git", "config", "--get", fmt.Sprintf("branch.%s.remote", branch)) //nolint:gosec // G204: branch from git symbolic-ref
|
|
||||||
remoteOutput, err := remoteCmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
// If no remote configured, default to "origin"
|
|
||||||
remoteOutput = []byte("origin\n")
|
|
||||||
}
|
|
||||||
remote := strings.TrimSpace(string(remoteOutput))
|
|
||||||
|
|
||||||
// Pull with explicit remote and branch
|
|
||||||
cmd := exec.CommandContext(ctx, "git", "pull", remote, branch) //nolint:gosec // G204: remote/branch from git config, not user input
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("git pull failed: %w\n%s", err, output)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// gitPush pushes to the current branch's upstream
|
|
||||||
// Returns nil if no remote configured (local-only mode)
|
|
||||||
func gitPush(ctx context.Context) error {
|
|
||||||
// Check if any remote exists (support local-only repos)
|
|
||||||
if !hasGitRemote(ctx) {
|
|
||||||
return nil // Gracefully skip - local-only mode
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "git", "push")
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("git push failed: %w\n%s", err, output)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// restoreBeadsDirFromBranch restores .beads/ directory from the current branch's committed state.
|
// restoreBeadsDirFromBranch restores .beads/ directory from the current branch's committed state.
|
||||||
// This is used after sync when sync.branch is configured to keep the working directory clean.
|
// This is used after sync when sync.branch is configured to keep the working directory clean.
|
||||||
// The actual beads data lives on the sync branch; the main branch's .beads/ is just a snapshot.
|
// The actual beads data lives on the sync branch; the main branch's .beads/ is just a snapshot.
|
||||||
|
|||||||
Reference in New Issue
Block a user