@@ -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
3133const 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