@@ -164,30 +164,46 @@ func doCascadeWithState(g *git.Git, cfg *config.Config, branches []*tree.Node, d
164164 continue
165165 }
166166
167- // Check if we should use --onto rebase
168- // This is needed when parent has been rebased/amended since child was created
167+ // Check if we should use --onto rebase.
168+ // This is needed when the parent has been rebased/amended since the child was created.
169169 storedForkPoint , fpErr := cfg .GetForkPoint (b .Name )
170170 useOnto := false
171171
172- if fpErr == nil && g .CommitExists (storedForkPoint ) {
173- currentMergeBase , mbErr := g .GetMergeBase (b .Name , parent )
174- if mbErr == nil && currentMergeBase != storedForkPoint {
175- // Fork point differs from merge-base. Determine why:
176- //
177- // If the stored fork point is an ancestor of the merge-base,
178- // it's just stale (e.g. branch was rebased outside gh-stack,
179- // or fork point wasn't updated after a conflict resolution).
180- // A simple rebase using the merge-base is correct; refresh the
181- // fork point so it stays current.
182- //
183- // If the stored fork point is NOT an ancestor of the merge-base,
184- // the parent's history was rewritten (squash merge, force push).
185- // We need --onto with the stored fork point to identify the
186- // correct commit range.
187- if g .IsAncestor (storedForkPoint , currentMergeBase ) {
188- _ = cfg .SetForkPoint (b .Name , currentMergeBase ) //nolint:errcheck // best effort
189- } else {
190- useOnto = true
172+ if fpErr == nil {
173+ if ! g .CommitExists (storedForkPoint ) {
174+ // The stored fork point no longer exists — it may have been
175+ // garbage collected after a history rewrite or a sufficiently
176+ // aggressive `git gc`. Without it we cannot use --onto, so we
177+ // fall back to a plain rebase against the parent tip. If the
178+ // parent's history was genuinely rewritten this fallback may
179+ // produce spurious conflicts or silently re-apply commits that
180+ // were already in the parent; the user should resolve conflicts
181+ // as normal or re-run `gh stack restack` after history settles.
182+ fmt .Printf (" %s\n " , s .Muted (fmt .Sprintf (
183+ "warning: stored fork point %s is no longer available (garbage collected?); falling back to simple rebase" ,
184+ git .AbbrevSHA (storedForkPoint ),
185+ )))
186+ } else {
187+ // Fork point is reachable — determine whether --onto is appropriate.
188+ currentMergeBase , mbErr := g .GetMergeBase (b .Name , parent )
189+ if mbErr == nil && currentMergeBase != storedForkPoint {
190+ // Fork point differs from merge-base. Determine why:
191+ //
192+ // If the stored fork point is an ancestor of the merge-base,
193+ // it's just stale (e.g. branch was rebased outside gh-stack,
194+ // or fork point wasn't updated after a conflict resolution).
195+ // A simple rebase using the merge-base is correct; refresh the
196+ // fork point so it stays current.
197+ //
198+ // If the stored fork point is NOT an ancestor of the merge-base,
199+ // the parent's history was rewritten (squash merge, force push).
200+ // We need --onto with the stored fork point to identify the
201+ // correct commit range.
202+ if g .IsAncestor (storedForkPoint , currentMergeBase ) {
203+ _ = cfg .SetForkPoint (b .Name , currentMergeBase ) //nolint:errcheck // best effort
204+ } else {
205+ useOnto = true
206+ }
191207 }
192208 }
193209 }
0 commit comments