Skip to content

Commit 6ca8bc1

Browse files
committed
fix(restack): handle GC'd fork points gracefully with a warning
When a stored fork point commit has been garbage collected (e.g. after a history rewrite followed by aggressive `git gc`), the previous code silently fell through to a plain rebase. Now it emits a warning so the user knows why `--onto` wasn't used, then falls back to a simple rebase as before.
1 parent c28c409 commit 6ca8bc1

File tree

1 file changed

+37
-21
lines changed

1 file changed

+37
-21
lines changed

cmd/cascade.go

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)