Skip to content

gh stack rebase --upstack doesn't detect squash-merged branches below the current branch #31

@benschoel

Description

@benschoel

Summary

When gh stack rebase --upstack is 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

  1. Create a stack main ← A ← B ← C with PRs for each.
  2. Squash-merge A's PR into main.
  3. From B or C, run gh stack rebase --upstack.
  4. Observe: B and C are rebased but still contain A's pre-squash commits.
  5. The PRs for B and C now show a diff that includes A's changes in addition to their own.

Plain gh stack rebase (no flag) handles the same scenario correctly — it detects A as merged, sets needsOnto, and rebases B and C with --onto to skip A's pre-squash commits.

Root cause

In cmd/rebase.go:

if opts.upstack {
    startIdx = currentIdx
}
branchesToRebase := s.Branches[startIdx:endIdx]

The squash-merge detection runs inside the loop that iterates over branchesToRebase:

if br.IsMerged() {
    ontoOldBase = originalRefs[br.Branch]
    needsOnto = true
    cfg.Successf("Skipping %s (PR %s merged)", br.Branch, ...)
    continue
}

--upstack sets startIdx = currentIdx, so any merged branch below currentIdx is excluded from the slice. IsMerged() never fires on it, needsOnto stays false, and the subsequent rebases use the old (pre-squash) parent instead of --onto to the squashed base.

Suggested fixes

Two options:

  1. Seed merge state before the upstack loop. Scan s.Branches[:startIdx] for merged branches before starting the loop and, if any is found, pre-populate ontoOldBase / needsOnto with the appropriate old base so that the first branch in the upstack slice rebases with --onto.

  2. Refuse --upstack when a merged branch exists below. Detect the condition up front and error out with a message directing the user to run plain gh stack rebase instead. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions