To realign a stacked branch after its base branch is squash-merged, record the base branch's tip hash, verify tree equivalence against origin/main, and replay only the child branch's commits using git rebase --onto. A squash merge collapses history into a single new commit without referencing the original commit hashes. Rebasing onto origin/main directly replays duplicate commits and creates merge conflicts; targeting the boundary commit bypasses the squashed commits entirely and preserves a clean tree.
The workflow at a glance
flowchart LR
A[Record old base tip SHA] --> B[Fetch origin main]
B --> C[Verify tree diff is empty]
C --> D[Rebase onto main from old tip]
D --> E[Run tests and typecheck]
E --> F[Force push with lease]
When to use it
- Working with stacked branches where an AI agent builds a downstream pull request on top of an unmerged feature branch.
- Repositories configured to squash-merge pull requests into
main. - Automating agentic pull request lifecycle tasks without manual conflict resolution.
Skip it when merging branches with merge commits or standard rebases, where Git tracks commit ancestry across branches automatically.
Step 1: Record the base branch tip
- Do: Before branch A is deleted or merged, record its HEAD commit hash. The squash commit created on
origin/mainwill not record original commit hashes. - Output: The commit hash of the base branch tip, stored in an environment variable or scratchpad such as
OLD_BASE_TIP.
Step 2: Fetch main and verify tree equivalence
- Do: Fetch the squash commit from
origin/main. Rungit diff --stat $OLD_BASE_TIP origin/mainto confirm that the squash commit contains the exact tree of branch A without drift. - Output: An empty diff output, proving the squash commit on
mainmatches the base branch.
Step 3: Transplant commits onto main
- Do: Check out the stacked branch and run
git rebase --onto origin/main $OLD_BASE_TIP branch-B. This instructs Git to take only commits on branch B that follow the old base tip and replay them ontoorigin/main. - Output: A rebased branch containing only the stacked branch's unique commits directly on top of
origin/main.
Step 4: Validate and update remote
- Do: Run test suites, type checks, and builds to confirm the transplant introduced no regressions, applying the verification mindset from Spec, Plan, Build, Review: A Workflow for Agent Coding. Push the branch with
git push --force-with-lease. - Output: A passing verification suite and an updated pull request ready for review.
Aligned stacked branches after a squash merge
An agent built two pull requests as a stack: branch B on top of branch A. After squash-merging branch A, main gained one new commit while branch B still retained branch A's original three commits.
Running git diff --stat <last commit of A> <squash commit> printed nothing, confirming the squash commit held the exact tree of branch A. Running git rebase --onto origin/main <last commit of A> B moved branch B's three commits onto origin/main without conflicts. Tests, type check, and build passed unchanged.
Variations
- Automated agent oversight: Wrap this sequence into an agent hook or script to prevent agent stalls on conflict resolution, as examined in AI Agent Oversight: Why Passive Approval Fails Systems.
- Lost tip hash: If branch A was deleted without saving the hash, inspect
git reflogor retrieve the commit SHA from the closed pull request timeline on GitHub. - Upstream divergence: If
origin/mainincludes other commits merged alongside the squash, compare against the squash commit hash directly (git diff --stat <last commit of A> <squash commit SHA>) before rebasing.
Frequently asked questions
Why does a plain git rebase fail after a squash merge?
A squash merge condenses the base branch's commits into a brand new commit on main with a different hash. When running git rebase origin/main, Git does not recognize that the squashed commit already contains the base changes. It attempts to re-apply the original base commits against the squash commit, causing duplicate change conflicts.
What if git diff between the old tip and squash commit is not empty?
If the diff shows changes, the squash merge modified code or resolved a conflict during merge. Rebasing onto origin/main directly might discard or clash with those modifications. Inspect the diff to see what changed, then rebase onto origin/main and manually reconcile the differences before running test checks.
How do I find the old tip if the branch was already deleted?
Check git reflog on the machine where the branch existed, or open the closed pull request on GitHub and copy the last commit hash shown in the pull request history. Git retains the unreachable commit object locally until garbage collection runs.