镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

gh stack rebase uses the wrong onto boundary for merged branches #512

Description

@cmolder

Problem

gh stack rebase uses the local copy of the repository to decide where a merged branch's commits end. If that local copy is stale, it replays commits that were already merged and produces unexpected conflicts. Instead, it should use the head commit which GitHub recorded when merging the PR.

Example

Consider the following stack of branches: main <- A <- B, where:

  • Branch A has commits a1 and a2
  • Branch B has commits b1 and b2

Then do the following:

  1. Squash-merge A into main.
  2. Run gh stack rebase.

As expected, gh stack rebase sees that A was merged, and rebases B onto main.

To avoid replaying A's old commits, gh stack must know A's last commit (a2, in this case) so that it selects the correct onto-boundary and only rebases b1 and b2. However, instead of looking up A's last commit on GitHub, gh stack looks it up in the local repository.

The local copy can be stale. Say A was rebased and force-pushed from another clone, and GitHub merged a1' and a2' instead of a1 and a2. Meanwhile, the local A still points at a2. Then gh stack rebase cuts at the wrong commit and replays some of A's commits along with b1 and b2. They conflict with the squashed commit on main, producing conflicts in files B never touched.

This is especially a problem for squash merges, since Git can't tell that the squashed commit already contains A's changes. I've been hitting it repeatedly, and it has forced me to rebase branches manually or edit the gh stack state to restore normal behavior.

Fix

I wrote a fix for this in two commits, based on v0.2.0 (main commit d4ab7a):

  1. a20414b fix stale onto-boundary for merged branches
  2. 16e4b73 avoid the recorded base cache in resolveRebaseOldBase

Changes:

  1. internal/github/github.go:
    • (commit 1) Add HeadRefOid to PullRequest. This is GitHub's record of the exact commit that was merged.
  2. cmd/utils.go: - (commit 1) When a PR is detected as merged, save its HeadRefOid.
    • (commit 1) When computing the rebase boundary, use its HeadRefOid instead of the possibly stale local SHA from git.
    • (commit 2) When the stored head is no longer in the upper branch's history, ask git for the fork point before falling back to the recorded Base, which can be just as stale.
  3. cmd/rebase_test.go:
    • (commit 1) Add TestRebase_MergedBranch_PreferRemoteRef (local has SHA X, GitHub has the real merge point Y, and the tool picks Y)
    • (commit 2) Add TestRebase_MergedBranch_PreferForkPoint.
    • Both tests fail on v0.2.0 and pass with these commits.

I would open a PR for this but it's locked to contributors only. Full disclosure: I used Claude to identify and write the fix, but I manually reviewed and edited it afterwards.

Result

gh stack rebase's onto-boundary now comes from GitHub (can't go stale) instead of the local clone, eliminating confusing rebase conflicts due to a misconfigured rebase.

Activity

  1. cwaldren-pushpress commented on Sep 29, 2026

    @cwaldren-pushpress

    I am running into this repeatedly on a stack of 8 PRs. The effect is that I'm essentially re-resolving the same conflicts multiple times per day, which is making me want to dissolve the stack (it'd be amazing to have a command for that like graphite did!).

  2. MichaelDoyle commented on Oct 1, 2026

    @MichaelDoyle

    We hit a related root cause for this exact symptom when working inside a git worktree after pruning merged branches (gh stack sync --prune):

    Reproduction / Root Cause in git worktree

    1. Suppose main is checked out in the primary repository worktree (at an older commit A), while a stack of PRs is worked on inside a linked worktree.
    2. The bottom 4 PRs in the stack are squash-merged into origin/main (advancing origin/main to B), and we run gh stack sync --prune in the stack worktree.
    3. Because main is checked out in the primary worktree, gh stack cannot fast-forward local main and falls back to origin/main for the rebase target:
      ✓ Fetched latest main from origin
      ⚠ Could not update local main: failed to run git: fatal: cannot force update the branch 'main' used by worktree at '...'
        Rebasing the stack onto origin/main instead; local main is unchanged.
      
    4. The bug: Even though gh stack rebases the first unmerged branch onto origin/main (B), when it saves the updated state to .git/worktrees/<name>/gh-stack (and after --prune has deleted the local refs for the 4 merged branches), it records local main's stale SHA (A) as both trunk.head and the first unmerged branch's "base":
      "trunk": {
        "branch": "main",
        "head": "<stale-local-main-SHA-A>"
      },
      ...
      {
        "branch": "first-unmerged-branch",
        "head": "<rebased-head>",
        "base": "<stale-local-main-SHA-A>"
      }
    5. On the next gh stack rebase (after another commit C lands on origin/main), gh stack uses "base": "<stale-local-main-SHA-A>" as the upstream boundary (git rebase --onto origin/main <A> first-unmerged-branch), which causes Git to replay all 4 already-squash-merged PR commits (A..B) on top of origin/main (C), producing immediate conflicts on every merged file.

    Suggested Fix

    When gh stack falls back to rebasing onto origin/<trunk> because local <trunk> cannot be updated (e.g., checked out in another worktree), it should record origin/<trunk>'s SHA (not the stale local <trunk> ref's SHA) as trunk.head and as the new base for the first unmerged branch in .git/gh-stack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions