Skip to content

Commit f1421ad

Browse files
committed
rebase onto for handling squashed prs
1 parent 2661ee7 commit f1421ad

4 files changed

Lines changed: 288 additions & 80 deletions

File tree

cmd/push.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
159159
if base, err := git.HeadSHA(parent); err == nil {
160160
s.Branches[i].Base = base
161161
}
162+
if head, err := git.HeadSHA(s.Branches[i].Branch); err == nil {
163+
s.Branches[i].Head = head
164+
}
162165
}
163166
syncStackPRs(cfg, s)
164167

cmd/rebase.go

Lines changed: 193 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type rebaseState struct {
2626
RemainingBranches []string `json:"remainingBranches"`
2727
OriginalBranch string `json:"originalBranch"`
2828
OriginalRefs map[string]string `json:"originalRefs"`
29+
UseOnto bool `json:"useOnto,omitempty"`
30+
OntoOldBase string `json:"ontoOldBase,omitempty"`
2931
}
3032

3133
const rebaseStateFile = "gh-stack-rebase-state"
@@ -146,12 +148,19 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
146148
cfg.Printf("Rebasing branches in order, starting from %s to %s",
147149
branchesToRebase[0].Branch, branchesToRebase[len(branchesToRebase)-1].Branch)
148150

151+
// Sync PR state before rebase so we can detect merged PRs.
152+
syncStackPRs(cfg, s)
153+
149154
originalRefs := make(map[string]string)
150155
for _, b := range s.Branches {
151156
sha, _ := git.HeadSHA(b.Branch)
152157
originalRefs[b.Branch] = sha
153158
}
154159

160+
// Track --onto rebase state for squash-merged branches.
161+
needsOnto := false
162+
var ontoOldBase string
163+
155164
for i, br := range branchesToRebase {
156165
var base string
157166
absIdx := startIdx + i
@@ -161,51 +170,118 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
161170
base = s.Branches[absIdx-1].Branch
162171
}
163172

164-
cfg.Printf("Rebasing %s onto %s ...", br.Branch, base)
165-
166-
if err := git.CheckoutBranch(br.Branch); err != nil {
167-
return fmt.Errorf("checking out %s: %w", br.Branch, err)
173+
// Skip branches whose PRs have already been merged (e.g. via squash).
174+
// Record state so subsequent branches can use --onto rebase.
175+
if br.PullRequest != nil && br.PullRequest.Merged {
176+
ontoOldBase = originalRefs[br.Branch]
177+
needsOnto = true
178+
cfg.Successf("Skipping %s (PR #%d merged)", br.Branch, br.PullRequest.Number)
179+
continue
168180
}
169181

170-
if err := git.Rebase(base); err != nil {
171-
cfg.Warningf("Rebasing %s onto %s ... conflict", br.Branch, base)
182+
if needsOnto {
183+
// Find the proper --onto target: the first non-merged ancestor, or trunk.
184+
newBase := s.Trunk.Branch
185+
for j := absIdx - 1; j >= 0; j-- {
186+
b := s.Branches[j]
187+
if b.PullRequest == nil || !b.PullRequest.Merged {
188+
newBase = b.Branch
189+
break
190+
}
191+
}
192+
193+
cfg.Printf("Rebasing %s onto %s (squash-merge detected) ...", br.Branch, newBase)
194+
195+
if err := git.RebaseOnto(newBase, ontoOldBase, br.Branch); err != nil {
196+
cfg.Warningf("Rebasing %s onto %s ... conflict", br.Branch, newBase)
197+
198+
remaining := make([]string, 0)
199+
for j := i + 1; j < len(branchesToRebase); j++ {
200+
remaining = append(remaining, branchesToRebase[j].Branch)
201+
}
202+
203+
state := &rebaseState{
204+
CurrentBranchIndex: absIdx,
205+
ConflictBranch: br.Branch,
206+
RemainingBranches: remaining,
207+
OriginalBranch: currentBranch,
208+
OriginalRefs: originalRefs,
209+
UseOnto: true,
210+
OntoOldBase: originalRefs[br.Branch],
211+
}
212+
saveRebaseState(gitDir, state)
213+
214+
printConflictDetails(cfg, newBase)
215+
cfg.Printf("")
172216

173-
remaining := make([]string, 0)
174-
for j := i + 1; j < len(branchesToRebase); j++ {
175-
remaining = append(remaining, branchesToRebase[j].Branch)
217+
cfg.Printf("Resolve conflicts on %s, then run %s",
218+
br.Branch, cfg.ColorCyan("gh stack rebase --continue"))
219+
cfg.Printf("Or abort this operation with %s",
220+
cfg.ColorCyan("gh stack rebase --abort"))
221+
return fmt.Errorf("rebase conflict on %s", br.Branch)
176222
}
177223

178-
state := &rebaseState{
179-
CurrentBranchIndex: absIdx,
180-
ConflictBranch: br.Branch,
181-
RemainingBranches: remaining,
182-
OriginalBranch: currentBranch,
183-
OriginalRefs: originalRefs,
224+
cfg.Successf("Rebasing %s onto %s", br.Branch, newBase)
225+
// Keep --onto mode; update old base for the next branch.
226+
ontoOldBase = originalRefs[br.Branch]
227+
} else {
228+
cfg.Printf("Rebasing %s onto %s ...", br.Branch, base)
229+
230+
if err := git.CheckoutBranch(br.Branch); err != nil {
231+
return fmt.Errorf("checking out %s: %w", br.Branch, err)
184232
}
185-
saveRebaseState(gitDir, state)
186233

187-
printConflictDetails(cfg, base)
188-
cfg.Printf("")
234+
if err := git.Rebase(base); err != nil {
235+
cfg.Warningf("Rebasing %s onto %s ... conflict", br.Branch, base)
189236

190-
cfg.Printf("Resolve conflicts on %s, then run %s",
191-
br.Branch, cfg.ColorCyan("gh stack rebase --continue"))
192-
cfg.Printf("Or abort this operation with %s",
193-
cfg.ColorCyan("gh stack rebase --abort"))
194-
return fmt.Errorf("rebase conflict on %s", br.Branch)
195-
}
237+
remaining := make([]string, 0)
238+
for j := i + 1; j < len(branchesToRebase); j++ {
239+
remaining = append(remaining, branchesToRebase[j].Branch)
240+
}
196241

197-
cfg.Successf("Rebasing %s onto %s", br.Branch, base)
242+
state := &rebaseState{
243+
CurrentBranchIndex: absIdx,
244+
ConflictBranch: br.Branch,
245+
RemainingBranches: remaining,
246+
OriginalBranch: currentBranch,
247+
OriginalRefs: originalRefs,
248+
}
249+
saveRebaseState(gitDir, state)
250+
251+
printConflictDetails(cfg, base)
252+
cfg.Printf("")
253+
254+
cfg.Printf("Resolve conflicts on %s, then run %s",
255+
br.Branch, cfg.ColorCyan("gh stack rebase --continue"))
256+
cfg.Printf("Or abort this operation with %s",
257+
cfg.ColorCyan("gh stack rebase --abort"))
258+
return fmt.Errorf("rebase conflict on %s", br.Branch)
259+
}
260+
261+
cfg.Successf("Rebasing %s onto %s", br.Branch, base)
262+
}
198263
}
199264

200265
_ = git.CheckoutBranch(currentBranch)
201266

202267
for i := range s.Branches {
268+
// Skip merged branches when updating base SHAs.
269+
if s.Branches[i].PullRequest != nil && s.Branches[i].PullRequest.Merged {
270+
continue
271+
}
272+
// Find the first non-merged ancestor, or trunk.
203273
parent := s.Trunk.Branch
204-
if i > 0 {
205-
parent = s.Branches[i-1].Branch
274+
for j := i - 1; j >= 0; j-- {
275+
if s.Branches[j].PullRequest == nil || !s.Branches[j].PullRequest.Merged {
276+
parent = s.Branches[j].Branch
277+
break
278+
}
206279
}
207280
base, _ := git.HeadSHA(parent)
208281
s.Branches[i].Base = base
282+
if head, err := git.HeadSHA(s.Branches[i].Branch); err == nil {
283+
s.Branches[i].Head = head
284+
}
209285
}
210286

211287
syncStackPRs(cfg, s)
@@ -275,56 +351,118 @@ func continueRebase(cfg *config.Config, gitDir string) error {
275351

276352
for _, branchName := range state.RemainingBranches {
277353
idx := s.IndexOf(branchName)
354+
355+
// Skip branches whose PRs have already been merged.
356+
br := s.Branches[idx]
357+
if br.PullRequest != nil && br.PullRequest.Merged {
358+
state.OntoOldBase = state.OriginalRefs[branchName]
359+
state.UseOnto = true
360+
cfg.Successf("Skipping %s (PR #%d merged)", branchName, br.PullRequest.Number)
361+
continue
362+
}
363+
278364
var base string
279365
if idx == 0 {
280366
base = s.Trunk.Branch
281367
} else {
282368
base = s.Branches[idx-1].Branch
283369
}
284370

285-
cfg.Printf("Rebasing %s onto %s ...", branchName, base)
371+
if state.UseOnto {
372+
// Find the proper --onto target: first non-merged ancestor, or trunk.
373+
newBase := s.Trunk.Branch
374+
for j := idx - 1; j >= 0; j-- {
375+
b := s.Branches[j]
376+
if b.PullRequest == nil || !b.PullRequest.Merged {
377+
newBase = b.Branch
378+
break
379+
}
380+
}
286381

287-
if err := git.CheckoutBranch(branchName); err != nil {
288-
cfg.Errorf("checking out %s: %s", branchName, err)
289-
return nil
290-
}
382+
cfg.Printf("Rebasing %s onto %s (squash-merge detected) ...", branchName, newBase)
291383

292-
if err := git.Rebase(base); err != nil {
293-
remainIdx := -1
294-
for ri, rb := range state.RemainingBranches {
295-
if rb == branchName {
296-
remainIdx = ri
297-
break
384+
if err := git.RebaseOnto(newBase, state.OntoOldBase, branchName); err != nil {
385+
remainIdx := -1
386+
for ri, rb := range state.RemainingBranches {
387+
if rb == branchName {
388+
remainIdx = ri
389+
break
390+
}
298391
}
392+
state.RemainingBranches = state.RemainingBranches[remainIdx+1:]
393+
state.CurrentBranchIndex = idx
394+
state.ConflictBranch = branchName
395+
state.OntoOldBase = state.OriginalRefs[branchName]
396+
saveRebaseState(gitDir, state)
397+
398+
cfg.Warningf("Rebasing %s onto %s ... conflict", branchName, newBase)
399+
printConflictDetails(cfg, newBase)
400+
cfg.Printf("")
401+
cfg.Printf("Resolve conflicts on %s, then run %s",
402+
branchName, cfg.ColorCyan("gh stack rebase --continue"))
403+
cfg.Printf("Or abort this operation with %s",
404+
cfg.ColorCyan("gh stack rebase --abort"))
405+
return fmt.Errorf("rebase conflict on %s", branchName)
299406
}
300-
state.RemainingBranches = state.RemainingBranches[remainIdx+1:]
301-
state.CurrentBranchIndex = idx
302-
state.ConflictBranch = branchName
303-
saveRebaseState(gitDir, state)
304-
305-
cfg.Warningf("Rebasing %s onto %s ... conflict", branchName, base)
306-
printConflictDetails(cfg, base)
307-
cfg.Printf("")
308-
cfg.Printf("Resolve conflicts on %s, then run %s",
309-
branchName, cfg.ColorCyan("gh stack rebase --continue"))
310-
cfg.Printf("Or abort this operation with %s",
311-
cfg.ColorCyan("gh stack rebase --abort"))
312-
return fmt.Errorf("rebase conflict on %s", branchName)
313-
}
314407

315-
cfg.Successf("Rebasing %s onto %s", branchName, base)
408+
cfg.Successf("Rebasing %s onto %s", branchName, newBase)
409+
state.OntoOldBase = state.OriginalRefs[branchName]
410+
} else {
411+
cfg.Printf("Rebasing %s onto %s ...", branchName, base)
412+
413+
if err := git.CheckoutBranch(branchName); err != nil {
414+
cfg.Errorf("checking out %s: %s", branchName, err)
415+
return nil
416+
}
417+
418+
if err := git.Rebase(base); err != nil {
419+
remainIdx := -1
420+
for ri, rb := range state.RemainingBranches {
421+
if rb == branchName {
422+
remainIdx = ri
423+
break
424+
}
425+
}
426+
state.RemainingBranches = state.RemainingBranches[remainIdx+1:]
427+
state.CurrentBranchIndex = idx
428+
state.ConflictBranch = branchName
429+
saveRebaseState(gitDir, state)
430+
431+
cfg.Warningf("Rebasing %s onto %s ... conflict", branchName, base)
432+
printConflictDetails(cfg, base)
433+
cfg.Printf("")
434+
cfg.Printf("Resolve conflicts on %s, then run %s",
435+
branchName, cfg.ColorCyan("gh stack rebase --continue"))
436+
cfg.Printf("Or abort this operation with %s",
437+
cfg.ColorCyan("gh stack rebase --abort"))
438+
return fmt.Errorf("rebase conflict on %s", branchName)
439+
}
440+
441+
cfg.Successf("Rebasing %s onto %s", branchName, base)
442+
}
316443
}
317444

318445
clearRebaseState(gitDir)
319446
_ = git.CheckoutBranch(state.OriginalBranch)
320447

321448
for i := range s.Branches {
449+
// Skip merged branches when updating base SHAs.
450+
if s.Branches[i].PullRequest != nil && s.Branches[i].PullRequest.Merged {
451+
continue
452+
}
453+
// Find the first non-merged ancestor, or trunk.
322454
parent := s.Trunk.Branch
323-
if i > 0 {
324-
parent = s.Branches[i-1].Branch
455+
for j := i - 1; j >= 0; j-- {
456+
if s.Branches[j].PullRequest == nil || !s.Branches[j].PullRequest.Merged {
457+
parent = s.Branches[j].Branch
458+
break
459+
}
325460
}
326461
base, _ := git.HeadSHA(parent)
327462
s.Branches[i].Base = base
463+
if head, err := git.HeadSHA(s.Branches[i].Branch); err == nil {
464+
s.Branches[i].Head = head
465+
}
328466
}
329467

330468
syncStackPRs(cfg, s)

0 commit comments

Comments
 (0)