Skip to content

Fix rebase treating queued PRs as merged#173

Open
skarim wants to merge 2 commits into
skarim/link-failing-queued-prsfrom
skarim/rebase-queued-bug
Open

Fix rebase treating queued PRs as merged#173
skarim wants to merge 2 commits into
skarim/link-failing-queued-prsfrom
skarim/rebase-queued-bug

Conversation

@skarim

@skarim skarim commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

gh stack rebase (and gh stack sync, which shares the same cascade-rebase code) treated a queued PR as if it had already merged. When a branch's PR was in the merge queue, the rebase skipped that branch and then rebased the branches above it --onto trunk, dropping the queued branch's commits from every downstream branch:

✓ Skipping aerendel/mixed-content-types1 (PR #9333 queued)
✓ Rebased aerendel/mixed-content-types2 onto main (adjusted for merged PR)
✓ Rebased aerendel/mixed-content-types3 onto aerendel/mixed-content-types2 (adjusted for merged PR)

A queued PR is not merged into trunk yet — its commits only exist on its own branch, which is frozen while it sits in the merge queue. Rebasing the branches above it onto trunk therefore discarded work that those downstream branches depend on.

Root cause

cascadeRebase (cmd/utils.go) branched on BranchRef.IsSkipped(), which is IsMerged() || IsQueued(). Both merged and queued branches set needsOnto = true, activating the git rebase --onto path that drops the skipped branch's commits from downstream branches. That is correct for a merged PR — its commits really are in trunk — but wrong for a queued PR. The --onto seed in runRebase (cmd/rebase.go) shared the same conflation for --upstack.

Behavior

Merged and queued branches are now handled differently during a cascade rebase:

  • Merged branch — unchanged: its commits are in trunk, so downstream branches rebase --onto the first non-merged ancestor to drop them.
  • Queued branch — the branch itself is still skipped (it is frozen in the merge queue and is not rebased or pushed), but downstream branches now rebase normally onto the queued branch, keeping its commits underneath. They are no longer moved onto trunk.
  • A queued branch sitting between a merged branch and an active branch keeps the active branch stacked on it; the merged branch's commits cannot be dropped while the frozen queued branch still carries them.

gh stack sync gets the same fix, since it calls the shared cascadeRebase helper.

Changes

cmd/utils.go:

  • cascadeRebase splits the skip handling — a merged branch sets needsOnto / ontoOldBase as before, while a queued branch resets needsOnto so downstream branches rebase onto it instead of trunk. The --onto target search now skips only merged ancestors (queued ancestors keep their commits, so they are valid targets).

cmd/rebase.go:

  • runRebase only seeds --onto mode when the branch immediately below the range IsMerged() (was IsSkipped()), so a queued predecessor no longer forces the first in-range branch onto trunk.
  • continueRebase resolves its display base against IsMerged() for consistency.

Testing

cmd/rebase_test.go — a queuedPRClient helper (drives the transient queued state through syncStackPRs via a merge-queue entry) plus:

  • TestRebase_QueuedBranch_DownstreamStaysStacked: downstream branches rebase onto the queued branch, not --onto trunk, with no "adjusted for merged PR".
  • TestRebase_MergedBelowQueued_KeepsStackedOnQueued: a branch above a queued branch stays stacked on it even when a merged branch sits below.
  • TestRebase_UpstackAboveQueuedBranch: --upstack above a queued branch rebases onto the queued predecessor.

cmd/sync_test.go:

  • TestSync_QueuedBranch_DownstreamStaysStacked: the same behavior through sync, and the queued branch is excluded from the push.

go vet ./... is clean and the full go test -race -count=1 ./... suite passes.


Stack created with GitHub Stacks CLIGive Feedback 💬

gh stack rebase and gh stack sync share cascadeRebase, which skipped
branches via IsSkipped() (merged or queued) and then switched to a
`git rebase --onto` that drops the skipped branch's commits from every
downstream branch. That is right for a merged PR — its commits are
already in trunk — but wrong for a queued PR: its commits only exist on
its own branch, which is frozen in the merge queue, so the branches
above it were rebased onto trunk and lost work they depend on.

Handle the two cases separately. A merged branch still activates --onto
so its commits are dropped. A queued branch is still skipped (its branch
is frozen and is not rebased or pushed), but onto mode is reset so
downstream branches rebase normally onto the queued branch, keeping its
commits underneath. The --onto target search, the runRebase --onto seed,
and the continueRebase display base now key on IsMerged() instead of
IsSkipped(), so a queued predecessor no longer forces downstream
branches onto trunk. gh stack sync is fixed through the same shared
helper.

Add rebase coverage for a queued branch mid-stack, a merged branch below
a queued branch, and --upstack above a queued branch, plus a sync test
that also asserts the queued branch is excluded from the push. The
transient queued state is injected through the GitHub mock's merge-queue
entry.
Copilot AI review requested due to automatic review settings July 11, 2026 11:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes queued PR handling during cascade rebases so downstream branches retain queued commits.

Changes:

  • Distinguishes merged and queued branches in rebase logic.
  • Corrects --upstack and continuation base selection.
  • Adds rebase and sync regression tests.
Show a summary per file
File Description
cmd/utils.go Preserves queued branches as downstream rebase bases.
cmd/rebase.go Updates --onto selection logic.
cmd/rebase_test.go Adds queued-branch rebase coverage.
cmd/sync_test.go Adds queued-branch sync coverage.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread cmd/rebase.go
continueRebase reloads the stack from disk, where the Queued flag is
transient (json:"-") and therefore lost, and it only called syncStackPRs
after the cascade. So if the initial rebase conflicted on a branch below
a queued branch, `gh stack rebase --continue` resumed with that branch
seen as active: it rebased the frozen merge-queue branch and rebuilt the
downstream branches on a local history that differs from the queued
branch.

Call syncStackPRs right after resolving the stack — before selecting the
base and cascading the remaining branches — mirroring the refresh
runRebase already does before its cascade. The queued flag is
repopulated, so queued branches stay skipped and downstream branches
stay stacked on them.

Add TestRebase_Continue_QueuedBranchBelowConflict, which conflicts below
a queued branch and asserts the frozen branch is not rebased and the
branch above stays stacked on it. Verified to fail without the refresh.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh stack rebase: queued PR treated as merged, causing incorrect rebases for downstream branches

2 participants