Skip to content

Commit 75fac77

Browse files
committed
push all branches atomically and resolve remotes
1 parent 627832d commit 75fac77

4 files changed

Lines changed: 113 additions & 20 deletions

File tree

cmd/push.go

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"errors"
45
"fmt"
56
"strings"
67

@@ -12,8 +13,8 @@ import (
1213
)
1314

1415
type pushOptions struct {
15-
auto bool
16-
draft bool
16+
auto bool
17+
draft bool
1718
skipPRs bool
1819
}
1920

@@ -54,40 +55,44 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
5455
return nil
5556
}
5657

57-
s, err := resolveStack(sf, currentBranch, cfg)
58-
if err != nil {
59-
cfg.Errorf("%s", err)
60-
return nil
61-
}
62-
if s == nil {
58+
// Find the stack for the current branch without switching branches.
59+
// Push should never change the user's checked-out branch.
60+
stacks := sf.FindAllStacksForBranch(currentBranch)
61+
if len(stacks) == 0 {
6362
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
6463
return nil
6564
}
66-
67-
// Re-read current branch in case disambiguation caused a checkout
68-
currentBranch, err = git.CurrentBranch()
69-
if err != nil {
70-
cfg.Errorf("failed to get current branch: %s", err)
65+
if len(stacks) > 1 {
66+
cfg.Errorf("branch %q belongs to multiple stacks; checkout a non-trunk branch first", currentBranch)
7167
return nil
7268
}
69+
s := stacks[0]
7370

7471
client, err := cfg.GitHubClient()
7572
if err != nil {
7673
cfg.Errorf("failed to create GitHub client: %s", err)
7774
return nil
7875
}
7976

80-
// Push all branches
77+
// Push all active branches atomically
78+
remote, err := pickRemote(cfg, currentBranch)
79+
if err != nil {
80+
cfg.Errorf("%s", err)
81+
return nil
82+
}
8183
merged := s.MergedBranches()
8284
if len(merged) > 0 {
8385
cfg.Printf("Skipping %d merged %s", len(merged), plural(len(merged), "branch", "branches"))
8486
}
85-
for _, b := range s.ActiveBranches() {
86-
cfg.Printf("Pushing %s...", b.Branch)
87-
if err := git.Push("origin", []string{b.Branch}, true, false); err != nil {
88-
cfg.Errorf("failed to push %s: %s", b.Branch, err)
89-
return nil
90-
}
87+
activeBranches := s.ActiveBranches()
88+
branchNames := make([]string, len(activeBranches))
89+
for i, b := range activeBranches {
90+
branchNames[i] = b.Branch
91+
}
92+
cfg.Printf("Pushing %d %s to %s...", len(branchNames), plural(len(branchNames), "branch", "branches"), remote)
93+
if err := git.Push(remote, branchNames, true, true); err != nil {
94+
cfg.Errorf("failed to push: %s", err)
95+
return nil
9196
}
9297

9398
if opts.skipPRs {
@@ -235,3 +240,30 @@ func humanize(s string) string {
235240
return r
236241
}, s)
237242
}
243+
244+
// pickRemote determines which remote to push to. It delegates to
245+
// git.ResolveRemote for config-based resolution and remote listing.
246+
// If multiple remotes exist with no configured default, the user is
247+
// prompted to select one interactively.
248+
func pickRemote(cfg *config.Config, branch string) (string, error) {
249+
remote, err := git.ResolveRemote(branch)
250+
if err == nil {
251+
return remote, nil
252+
}
253+
254+
var multi *git.ErrMultipleRemotes
255+
if !errors.As(err, &multi) {
256+
return "", err
257+
}
258+
259+
if !cfg.IsInteractive() {
260+
return "", fmt.Errorf("multiple remotes configured; set remote.pushDefault or use an interactive terminal")
261+
}
262+
263+
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
264+
selected, promptErr := p.Select("Multiple remotes found. Which remote should be used?", "", multi.Remotes)
265+
if promptErr != nil {
266+
return "", fmt.Errorf("remote selection: %w", promptErr)
267+
}
268+
return multi.Remotes[selected], nil
269+
}

internal/git/git.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package git
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"os/exec"
78
"strings"
@@ -13,6 +14,16 @@ import (
1314
// client is a shared git client used by all package-level functions.
1415
var client = &cligit.Client{}
1516

17+
// ErrMultipleRemotes is returned by ResolveRemote when multiple remotes
18+
// are configured and none is designated as the push target.
19+
type ErrMultipleRemotes struct {
20+
Remotes []string
21+
}
22+
23+
func (e *ErrMultipleRemotes) Error() string {
24+
return fmt.Sprintf("multiple remotes configured: %s", strings.Join(e.Remotes, ", "))
25+
}
26+
1627
// CommitInfo holds metadata about a single commit.
1728
type CommitInfo struct {
1829
SHA string
@@ -118,6 +129,14 @@ func Push(remote string, branches []string, force, atomic bool) error {
118129
return ops.Push(remote, branches, force, atomic)
119130
}
120131

132+
// ResolveRemote determines the remote for pushing a branch. Checks git
133+
// config in priority order, falls back to listing remotes. Returns
134+
// *ErrMultipleRemotes if multiple remotes exist with no configured default.
135+
func ResolveRemote(branch string) (string, error) {
136+
return ops.ResolveRemote(branch)
137+
}
138+
139+
121140
// Rebase rebases the current branch onto the given base.
122141
// If rerere resolves all conflicts automatically, the rebase continues
123142
// without user intervention.

internal/git/gitops.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"os"
78
"os/exec"
89
"path/filepath"
@@ -23,6 +24,7 @@ type Ops interface {
2324
DefaultBranch() (string, error)
2425
CreateBranch(name, base string) error
2526
Push(remote string, branches []string, force, atomic bool) error
27+
ResolveRemote(branch string) (string, error)
2628
Rebase(base string) error
2729
EnableRerere() error
2830
RebaseOnto(newBase, oldBase, branch string) error
@@ -122,6 +124,38 @@ func (d *defaultOps) Push(remote string, branches []string, force, atomic bool)
122124
return runSilent(args...)
123125
}
124126

127+
// ResolveRemote determines the remote for pushing a branch. It checks git
128+
// config keys in priority order (branch.<name>.pushRemote, remote.pushDefault,
129+
// branch.<name>.remote), then falls back to listing all remotes. If exactly
130+
// one remote exists it is returned. If multiple exist, ErrMultipleRemotes is
131+
// returned with the list attached. If none exist, a plain error is returned.
132+
func (d *defaultOps) ResolveRemote(branch string) (string, error) {
133+
candidates := []string{
134+
"branch." + branch + ".pushRemote",
135+
"remote.pushDefault",
136+
"branch." + branch + ".remote",
137+
}
138+
for _, key := range candidates {
139+
out, err := run("config", "--get", key)
140+
if err == nil && out != "" {
141+
return out, nil
142+
}
143+
}
144+
145+
out, err := run("remote")
146+
if err != nil {
147+
return "", fmt.Errorf("could not list remotes: %w", err)
148+
}
149+
remotes := strings.Fields(strings.TrimSpace(out))
150+
if len(remotes) == 1 {
151+
return remotes[0], nil
152+
}
153+
if len(remotes) > 1 {
154+
return "", &ErrMultipleRemotes{Remotes: remotes}
155+
}
156+
return "", fmt.Errorf("no remotes configured")
157+
}
158+
125159
func (d *defaultOps) Rebase(base string) error {
126160
err := runSilent("rebase", base)
127161
if err == nil {

internal/git/mock_ops.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type MockOps struct {
1212
DefaultBranchFn func() (string, error)
1313
CreateBranchFn func(string, string) error
1414
PushFn func(string, []string, bool, bool) error
15+
ResolveRemoteFn func(string) (string, error)
1516
RebaseFn func(string) error
1617
EnableRerereFn func() error
1718
RebaseOntoFn func(string, string, string) error
@@ -98,6 +99,13 @@ func (m *MockOps) Push(remote string, branches []string, force, atomic bool) err
9899
return nil
99100
}
100101

102+
func (m *MockOps) ResolveRemote(branch string) (string, error) {
103+
if m.ResolveRemoteFn != nil {
104+
return m.ResolveRemoteFn(branch)
105+
}
106+
return "origin", nil
107+
}
108+
101109
func (m *MockOps) Rebase(base string) error {
102110
if m.RebaseFn != nil {
103111
return m.RebaseFn(base)

0 commit comments

Comments
 (0)