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:
- Squash-merge
A into main.
- 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):
a20414b fix stale onto-boundary for merged branches
16e4b73 avoid the recorded base cache in resolveRebaseOldBase
Changes:
internal/github/github.go:
- (commit 1) Add
HeadRefOid to PullRequest. This is GitHub's record of the exact commit that was merged.
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.
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.
Problem
gh stack rebaseuses 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:Ahas commitsa1anda2Bhas commitsb1andb2Then do the following:
Aintomain.gh stack rebase.As expected,
gh stack rebasesees thatAwas merged, and rebasesBontomain.To avoid replaying
A's old commits,gh stackmust knowA's last commit (a2, in this case) so that it selects the correct onto-boundary and only rebasesb1andb2. However, instead of looking upA's last commit on GitHub,gh stacklooks it up in the local repository.The local copy can be stale. Say
Awas rebased and force-pushed from another clone, and GitHub mergeda1'anda2'instead ofa1anda2. Meanwhile, the localAstill points ata2. Thengh stack rebasecuts at the wrong commit and replays some ofA's commits along withb1andb2. They conflict with the squashed commit onmain, producing conflicts in filesBnever 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 (
maincommitd4ab7a):a20414bfix stale onto-boundary for merged branches16e4b73avoid the recorded base cache inresolveRebaseOldBaseChanges:
internal/github/github.go:HeadRefOidtoPullRequest. This is GitHub's record of the exact commit that was merged.cmd/utils.go: - (commit 1) When a PR is detected as merged, save itsHeadRefOid.HeadRefOidinstead of the possibly stale local SHA fromgit.Base, which can be just as stale.cmd/rebase_test.go:TestRebase_MergedBranch_PreferRemoteRef(local has SHAX, GitHub has the real merge pointY, and the tool picksY)TestRebase_MergedBranch_PreferForkPoint.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.