Repository navigation
gh stack rebase --upstack doesn't detect squash-merged branches below the current branch #31
Description
Activity
Hit a related manifestation of the same root cause today, this time with plain
gh stack rebase(no--upstack). Worth flagging since it shows the bug is broader than just the--upstackflag.Trigger
A cascade of merged branches at the bottom of the stack, where
ontoOldBaseends up stale.main ← A (merged) ← B (merged) ← C (merged) ← D (merged) ← E (merged) ← F (active) ← G ← H ...gh stack rebasecorrectly skips A–E via squash-merge detection. But because the loop body does:if br.IsMerged() { ontoOldBase = originalRefs[br.Branch] // overwrites each iteration needsOnto = true continue }
ontoOldBaseis overwritten on each merged branch. After the cascade,ontoOldBase = originalRefs[E]— E's local pre-squash tip.What goes wrong
For F's rebase:
git rebase --onto main <originalRefs[E]> FBut
<originalRefs[E]>(E's local pre-squash tip) is NOT an ancestor of F if F was independently advanced past it (e.g. by a previous gh-stack rebase that brought F up to recent main, or a manualgit rebase mainon F). git falls back tomerge-base(<originalRefs[E]>, F), which can be much older than F's actual divergence from main.Result: the rebase tries to apply many commits that are already in main but as different SHAs (post-squash). Most get patch-id-skipped, but any that touch the same files as the squash commits hit a 3-way merge conflict on totally unrelated code.
Concrete example
In our case, A–E were 5 squash-merged PRs.
ontoOldBaseended up as the local pre-squash tip of E (chore/remove-nexus-benchmarks). F's actualmerge-base(main, F)was a much later main commit. The rebase tried to apply ~10 unrelated PRs from main on top of main, and PR #19419 conflicted ondelete_account/*files because the squashed E branch had also touched those files. The conflict markers all referenced9129510f7c (delete account opportunities activity (#19419))— a commit nobody on the stack had ever interacted with.Doing
git rebase mainon F manually first didn't help either —gh stack rebaseignores F's current state and re-runsgit rebase --onto main <stale ontoOldBase> F, hitting the same conflict.Suggested fix
Compute the rebase base as
merge-base(parent, branch)rather than using the parent's local tip. That auto-handles both the cascade case and the case where the upstack branch was independently rebased forward.Or as a narrower fix: when the parent (or last-skipped merged branch) is squash-merged, use the squash commit on
--ontotarget (main) instead of the local pre-squash tip —git merge-base main branchwould give the right divergence point.Thanks for the detailed bug report! Will look into this and get a fix out in the next release.
- linked a pull request that will close this issuefix --onto rebase for merged branches #43
on Apr 22, 2026
Summary
When
gh stack rebase --upstackis run from a branch that has a squash-merged PR somewhere below it in the stack, the merged branch is never detected and skipped. Pre-squash commits from the merged branch stay in the history of every branch above, which (after push) shows up as bloated PR diffs containing unrelated code from the already-merged PR.Reproducer
main ← A ← B ← Cwith PRs for each.gh stack rebase --upstack.Plain
gh stack rebase(no flag) handles the same scenario correctly — it detects A as merged, setsneedsOnto, and rebases B and C with--ontoto skip A's pre-squash commits.Root cause
In
cmd/rebase.go:The squash-merge detection runs inside the loop that iterates over
branchesToRebase:--upstacksetsstartIdx = currentIdx, so any merged branch belowcurrentIdxis excluded from the slice.IsMerged()never fires on it,needsOntostaysfalse, and the subsequent rebases use the old (pre-squash) parent instead of--ontoto the squashed base.Suggested fixes
Two options:
Seed merge state before the upstack loop. Scan
s.Branches[:startIdx]for merged branches before starting the loop and, if any is found, pre-populateontoOldBase/needsOntowith the appropriate old base so that the first branch in the upstack slice rebases with--onto.Refuse
--upstackwhen a merged branch exists below. Detect the condition up front and error out with a message directing the user to run plaingh stack rebaseinstead. Simpler and surfaces the problem loudly rather than silently producing a bad rebase.Option 2 is probably cheapest and prevents silent data corruption. Option 1 is more user-friendly but needs more care around multiple merged branches in sequence.
Impact
Repeatedly observed in the wild — the bad rebase is silent, the push succeeds, and the only signal is that PRs suddenly show hundreds of lines of unrelated diff. Easy to miss if you don't self-check every push.