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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions