Skip to content

Commit e874e26

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

11 files changed

Lines changed: 296 additions & 149 deletions

File tree

cmd/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
213213
return nil
214214
}
215215

216-
base, _ := git.HeadSHA(currentBranch)
216+
base, _ := git.RevParse(currentBranch)
217217
s.Branches = append(s.Branches, stack.BranchRef{Branch: branchName, Base: base})
218218

219219
// Stage and commit on the NEW branch if -m is provided

cmd/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
213213
}
214214

215215
// Build stack
216-
trunkSHA, _ := git.HeadSHA(trunk)
216+
trunkSHA, _ := git.RevParse(trunk)
217217
branchRefs := make([]stack.BranchRef, len(branches))
218218
for i, b := range branches {
219219
parent := trunk

cmd/push.go

Lines changed: 49 additions & 32 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,40 @@ 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 := activeBranchNames(s)
88+
cfg.Printf("Pushing %d %s to %s...", len(activeBranches), plural(len(activeBranches), "branch", "branches"), remote)
89+
if err := git.Push(remote, activeBranches, true, true); err != nil {
90+
cfg.Errorf("failed to push: %s", err)
91+
return nil
9192
}
9293

9394
if opts.skipPRs {
@@ -174,18 +175,7 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
174175
fmt.Fprintf(cfg.Err, " grouped into a Stack.\n")
175176

176177
// Update base commit hashes and sync PR state
177-
for i := range s.Branches {
178-
if s.Branches[i].IsMerged() {
179-
continue
180-
}
181-
parent := s.ActiveBaseBranch(s.Branches[i].Branch)
182-
if base, err := git.HeadSHA(parent); err == nil {
183-
s.Branches[i].Base = base
184-
}
185-
if head, err := git.HeadSHA(s.Branches[i].Branch); err == nil {
186-
s.Branches[i].Head = head
187-
}
188-
}
178+
updateBaseSHAs(s)
189179
syncStackPRs(cfg, s)
190180

191181
if err := stack.Save(gitDir, sf); err != nil {
@@ -235,3 +225,30 @@ func humanize(s string) string {
235225
return r
236226
}, s)
237227
}
228+
229+
// pickRemote determines which remote to push to. It delegates to
230+
// git.ResolveRemote for config-based resolution and remote listing.
231+
// If multiple remotes exist with no configured default, the user is
232+
// prompted to select one interactively.
233+
func pickRemote(cfg *config.Config, branch string) (string, error) {
234+
remote, err := git.ResolveRemote(branch)
235+
if err == nil {
236+
return remote, nil
237+
}
238+
239+
var multi *git.ErrMultipleRemotes
240+
if !errors.As(err, &multi) {
241+
return "", err
242+
}
243+
244+
if !cfg.IsInteractive() {
245+
return "", fmt.Errorf("multiple remotes configured; set remote.pushDefault or use an interactive terminal")
246+
}
247+
248+
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
249+
selected, promptErr := p.Select("Multiple remotes found. Which remote should be used?", "", multi.Remotes)
250+
if promptErr != nil {
251+
return "", fmt.Errorf("remote selection: %w", promptErr)
252+
}
253+
return multi.Remotes[selected], nil
254+
}

cmd/rebase.go

Lines changed: 28 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -114,25 +114,35 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
114114
// Enable git rerere so conflict resolutions are remembered.
115115
_ = git.EnableRerere()
116116

117-
if err := git.Fetch("origin"); err != nil {
118-
cfg.Warningf("Failed to fetch origin: %v", err)
117+
// Resolve remote for fetch and trunk comparison
118+
remote, err := pickRemote(cfg, currentBranch)
119+
if err != nil {
120+
cfg.Errorf("%s", err)
121+
return nil
122+
}
123+
124+
if err := git.Fetch(remote); err != nil {
125+
cfg.Warningf("Failed to fetch %s: %v", remote, err)
119126
} else {
120-
cfg.Successf("Fetched origin")
127+
cfg.Successf("Fetched %s", remote)
121128
}
122129

123130
// Fast-forward trunk so the cascade rebase targets the latest upstream.
124131
trunk := s.Trunk.Branch
125-
localSHA, localErr := git.HeadSHA(trunk)
126-
remoteSHA, remoteErr := git.HeadSHA("origin/" + trunk)
132+
localSHA, remoteSHA := "", ""
133+
trunkRefs, trunkErr := git.RevParseMulti([]string{trunk, remote + "/" + trunk})
134+
if trunkErr == nil {
135+
localSHA, remoteSHA = trunkRefs[0], trunkRefs[1]
136+
}
127137

128-
if localErr == nil && remoteErr == nil && localSHA != remoteSHA {
138+
if trunkErr == nil && localSHA != remoteSHA {
129139
isAncestor, err := git.IsAncestor(localSHA, remoteSHA)
130140
if err != nil {
131141
cfg.Warningf("Could not determine fast-forward status for %s: %v", trunk, err)
132142
} else if !isAncestor {
133-
cfg.Warningf("Trunk %s has diverged from origin — skipping trunk update", trunk)
143+
cfg.Warningf("Trunk %s has diverged from %s — skipping trunk update", trunk, remote)
134144
} else if currentBranch == trunk {
135-
if err := ffMerge(trunk); err != nil {
145+
if err := git.MergeFF(remote + "/" + trunk); err != nil {
136146
cfg.Warningf("Failed to fast-forward %s: %v", trunk, err)
137147
} else {
138148
cfg.Successf("Trunk %s fast-forwarded to %s", trunk, short(remoteSHA))
@@ -184,14 +194,14 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
184194
// Sync PR state before rebase so we can detect merged PRs.
185195
syncStackPRs(cfg, s)
186196

187-
originalRefs := make(map[string]string)
188-
for _, b := range s.Branches {
189-
sha, err := git.HeadSHA(b.Branch)
190-
if err != nil {
191-
cfg.Errorf("failed to resolve HEAD SHA for %s: %s", b.Branch, err)
192-
return nil
193-
}
194-
originalRefs[b.Branch] = sha
197+
branchNames := make([]string, len(s.Branches))
198+
for i, b := range s.Branches {
199+
branchNames[i] = b.Branch
200+
}
201+
originalRefs, err := git.RevParseMap(branchNames)
202+
if err != nil {
203+
cfg.Errorf("failed to resolve branch SHAs: %s", err)
204+
return nil
195205
}
196206

197207
// Track --onto rebase state for squash-merged branches.
@@ -312,25 +322,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
312322

313323
_ = git.CheckoutBranch(currentBranch)
314324

315-
for i := range s.Branches {
316-
// Skip merged branches when updating base SHAs.
317-
if s.Branches[i].IsMerged() {
318-
continue
319-
}
320-
// Find the first non-merged ancestor, or trunk.
321-
parent := s.Trunk.Branch
322-
for j := i - 1; j >= 0; j-- {
323-
if !s.Branches[j].IsMerged() {
324-
parent = s.Branches[j].Branch
325-
break
326-
}
327-
}
328-
base, _ := git.HeadSHA(parent)
329-
s.Branches[i].Base = base
330-
if head, err := git.HeadSHA(s.Branches[i].Branch); err == nil {
331-
s.Branches[i].Head = head
332-
}
333-
}
325+
updateBaseSHAs(s)
334326

335327
syncStackPRs(cfg, s)
336328

@@ -521,25 +513,7 @@ func continueRebase(cfg *config.Config, gitDir string) error {
521513
clearRebaseState(gitDir)
522514
_ = git.CheckoutBranch(state.OriginalBranch)
523515

524-
for i := range s.Branches {
525-
// Skip merged branches when updating base SHAs.
526-
if s.Branches[i].IsMerged() {
527-
continue
528-
}
529-
// Find the first non-merged ancestor, or trunk.
530-
parent := s.Trunk.Branch
531-
for j := i - 1; j >= 0; j-- {
532-
if !s.Branches[j].IsMerged() {
533-
parent = s.Branches[j].Branch
534-
break
535-
}
536-
}
537-
base, _ := git.HeadSHA(parent)
538-
s.Branches[i].Base = base
539-
if head, err := git.HeadSHA(s.Branches[i].Branch); err == nil {
540-
s.Branches[i].Head = head
541-
}
542-
}
516+
updateBaseSHAs(s)
543517

544518
syncStackPRs(cfg, s)
545519

cmd/rebase_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func newRebaseMock(tmpDir string, currentBranch string) *git.MockOps {
3434
return &git.MockOps{
3535
GitDirFn: func() (string, error) { return tmpDir, nil },
3636
CurrentBranchFn: func() (string, error) { return currentBranch, nil },
37-
HeadSHAFn: func(ref string) (string, error) { return "sha-" + ref, nil },
37+
RevParseFn: func(ref string) (string, error) { return "sha-" + ref, nil },
3838
IsAncestorFn: func(a, d string) (bool, error) { return true, nil },
3939
FetchFn: func(string) error { return nil },
4040
EnableRerereFn: func() error { return nil },

0 commit comments

Comments
 (0)