diff --git a/cmd/bd/daemon_sync.go b/cmd/bd/daemon_sync.go index 17682bb6..aacae268 100644 --- a/cmd/bd/daemon_sync.go +++ b/cmd/bd/daemon_sync.go @@ -486,9 +486,10 @@ func performExport(ctx context.Context, store storage.Storage, autoCommit, autoP } log.log("Committed changes") - // Auto-push if enabled + // Auto-push if enabled (GH#872: use sync.remote config) 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) return } @@ -597,9 +598,10 @@ func performAutoImport(ctx context.Context, store storage.Storage, skipGit bool, 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 err := gitPull(importCtx); err != nil { + configuredRemote, _ := store.GetConfig(importCtx, "sync.remote") + if err := gitPull(importCtx, configuredRemote); err != nil { backoff := RecordSyncFailure(beadsDir, err.Error()) log.log("Pull failed: %v (backoff: %v)", err, backoff) return @@ -800,9 +802,10 @@ func performSync(ctx context.Context, store storage.Storage, autoCommit, autoPus 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 err := gitPull(syncCtx); err != nil { + configuredRemote, _ := store.GetConfig(syncCtx, "sync.remote") + if err := gitPull(syncCtx, configuredRemote); err != nil { log.log("Pull failed: %v", err) 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 err := gitPush(syncCtx); err != nil { + configuredRemote, _ := store.GetConfig(syncCtx, "sync.remote") + if err := gitPush(syncCtx, configuredRemote); err != nil { log.log("Push failed: %v", err) return } diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index b38be498..a94ed2bf 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -382,7 +382,11 @@ Use --merge to merge the sync branch back to main branch.`, var repoRoot string var useSyncBranch bool 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 { + // Read sync.remote config (e.g., "upstream" for fork workflows) + configuredRemote, _ = store.GetConfig(ctx, "sync.remote") syncBranchName, _ = syncbranch.Get(ctx, store) if syncBranchName != "" && syncbranch.HasGitRemote(ctx) { // 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() // GH#519: show appropriate message when on sync branch + // GH#872: show configured remote if using sync.remote if onSyncBranch { fmt.Printf("→ Pulling from remote on sync branch '%s'...\n", syncBranchName) + } else if configuredRemote != "" { + fmt.Printf("→ Pulling from %s...\n", configuredRemote) } else { fmt.Println("→ Pulling from remote...") } - err := gitPull(ctx) + err := gitPull(ctx, configuredRemote) if err != nil { // Check if it's a rebase conflict on beads.jsonl that we can auto-resolve 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) // When sync.branch is configured, we don't push the main branch at all. // The sync branch worktree handles all pushes. + // GH#872: Use sync.remote config if set if !noPush && hasChanges && !pushedViaSyncBranch && !useSyncBranch { 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 { - fmt.Println("→ Pushing to remote...") - if err := gitPush(ctx); err != nil { + if configuredRemote != "" { + 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") } } diff --git a/cmd/bd/sync_git.go b/cmd/bd/sync_git.go index 30ea2b1e..26a481da 100644 --- a/cmd/bd/sync_git.go +++ b/cmd/bd/sync_git.go @@ -327,6 +327,85 @@ func runGitRebaseContinue(ctx context.Context) error { // gitPull pulls from the current branch's upstream // 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() { // Get current merge driver configuration 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. // 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.