Skip to content

Re-parent with the vendored git-merge-onto instead of the merge dance#56

Merged
Phlogistique merged 6 commits into
mainfrom
use-git-merge-onto
Jun 26, 2026
Merged

Re-parent with the vendored git-merge-onto instead of the merge dance#56
Phlogistique merged 6 commits into
mainfrom
use-git-merge-onto

Conversation

@Phlogistique

@Phlogistique Phlogistique commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Re-parents each child PR with a single git-merge-onto call (the merge equivalent of git rebase --onto) instead of the five-command dance update_direct_target used: merge the merged branch, merge the pre-squash target, merge -s ours the squash, commit-tree a three-parent commit, reset. That sequence only ever reconstructed one operation: a merge of the squash commit with the base forced to merge-base(HEAD, merged branch). git-merge-onto is vendored as a zero-dependency file run with python3, so the action needs no network download.

Conflict: the action commits and pushes nothing on a conflict, leaving the head at its pre-conflict commit (still mergeable, so the resuming synchronize event still fires). The pre-push of the clean half is gone, and the comment asks for a single uvx git-merge-onto origin/<trunk> origin/<merged> (re-home onto the live trunk, since any newer-trunk conflicts must be resolved before the PR can merge anyway).

Resume: the continuation no longer re-runs the merge. Re-parenting an already-resolved head against the old-parent base would re-open the hunk the user just fixed, because that forced base predates their resolution. It now checks the pushed head contains the squash, then retargets and drops the label; if it does not, the run fails so the user sees they need to look again. update_direct_target is used only by the squash-merge path now.

Removes the conflict-matrix e2e scenario: with one merge there is no base-vs-trunk distinction, and the "follow-up trunk conflict on continuation" case it covered cannot happen, since the single merge brings the trunk content in the first pass.

Phlogistique and others added 6 commits June 26, 2026 16:06
update_direct_target re-homed each child with five git commands (merge the
merged branch, merge the pre-squash target, merge -s ours the squash,
commit-tree a three-parent commit, reset). That is one re-parent: a merge of
the squash commit with the base forced to merge-base(HEAD, merged branch).
Replace it with a single git-merge-onto call, vendored as a zero-dependency
file run with python3 so the action needs no download.

The two-merge conflict path collapses: on a conflict the action now commits
and pushes nothing (the pre-push of the clean half is gone), and the comment
asks for one `uvx git-merge-onto`. Drops the conflict-matrix e2e scenario,
whose base-vs-trunk distinctions and follow-up-trunk-conflict case only
existed because there were two merges.

git-merge-onto: https://github.com/scortexio/git-merge-onto

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRBBuBCbXvdmtRQiSMixVL
astral-sh/setup-uv publishes no moving `v8` major tag, so `@v8` fails to
resolve. Pin the exact v8.2.0 commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRBBuBCbXvdmtRQiSMixVL
"Merge updates from <target> and squash commit" was vague; the squash
commit lives on the target, and the branch this commit exists because of is
the merged one. Say "<target> and <merged branch>".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRBBuBCbXvdmtRQiSMixVL
The human-facing conflict comment now says
`uvx git-merge-onto origin/<trunk> origin/<merged>` instead of pinning the
squash SHA. If trunk advanced since the parent landed, the user resolves
those conflicts now (they would have to before merging anyway) and gets
readable branch names. The action's own call keeps SQUASH_COMMIT, the stable
pin that keeps the resume a no-op.

Also fix the e2e's get_conflict_comment: it still grepped the old plural
"merge conflicts" heading after the single-merge rewrite made it "a merge
conflict", so it found the (correctly posted) comment as 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRBBuBCbXvdmtRQiSMixVL
continue_after_resolution re-ran the re-parent through update_direct_target to
"confirm" it. But git-merge-onto's forced base is the old parent, where the
lines the user just resolved still differ from the trunk, so the re-merge
re-raised the very conflict they had fixed; the no-op skip never fired because
the conflict (rc=1) precedes it. (Not a merge-onto bug: "theirs is an ancestor"
cannot mean "no-op", or a down-move that must drop the old parent's content
would wrongly skip.)

The resume only needs to confirm the user's pushed head contains the squash,
then retarget and drop the label. Replace the re-merge with that ancestry
check. update_direct_target is now used only by the squash-merge path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRBBuBCbXvdmtRQiSMixVL
If the user pushes without finishing the re-parent, the head lacks the squash;
make the run fail with an actionable message instead of retrying silently, so
they notice and look again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRBBuBCbXvdmtRQiSMixVL
@Phlogistique Phlogistique marked this pull request as ready for review June 26, 2026 15:18
@Phlogistique Phlogistique merged commit ab4f3bc into main Jun 26, 2026
3 checks passed
@github-actions github-actions Bot deleted the use-git-merge-onto branch June 26, 2026 15:19
Phlogistique added a commit that referenced this pull request Jul 3, 2026
When the simulated squash cherry-pick in test_update_pr_stack.sh lands in the same second as the original feature1 commit, it reproduces its sha exactly; the script then sees the child as already up to date and the test passes without exercising git-merge-onto at all. When the timing crosses a second boundary, the run reaches git-merge-onto, which refuses to start because the test's own tee output sits untracked inside the test repo. So since #56 the test either tested nothing or flaked.

Cherry-pick with -x so the sha always differs, and write the run log outside the repo. Found while rebasing #52.
Phlogistique added a commit that referenced this pull request Jul 6, 2026
…59)

In scortexio/gh-stack-mv#36 a conflict resolution followed the posted
instructions but the action never resumed and the conflict label stayed
stuck. The resume rides on a `synchronize` event, and GitHub creates no
`pull_request` runs for a PR that conflicts with its base. The PR did
conflict with its base (the merged branch, kept until the resume
retargets it): autorestack itself had advanced that branch when a
grandparent PR merged, so the head no longer descended from its tip, and
the resolution rewrote the same lines the squash reshaped, so GitHub's
textual mergeability check failed too. The old merge dance guaranteed
the descent structurally; the switch to git-merge-onto (#56) dropped
that guarantee.

This re-vendors git-merge-onto 0.2.0 with its new `--absorbed` flag
(scortexio/git-merge-onto#3) and passes it both in the action's own
re-parent and in the posted resolution command. The merged branch's tip
is recorded as an extra parent of the merge (on a conflict it rides
along in `MERGE_HEAD`, so the user's plain `git commit` records it), so
the pushed resolution descends from its base again and the resume event
is guaranteed to fire.

New unit test replays the incident: parent advanced after the child
forked, squash-merged, conflicting re-parent, resolution via the posted
command; asserts the resolution descends from the old base's tip. The
e2e gets the same assertion on its real conflict scenario.

Do not merge before git-merge-onto 0.2.0 is on PyPI: the posted command,
which the e2e replays verbatim, runs `uvx 'git-merge-onto>=0.2'`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01PHUVU3Ek3U9j3LrdjPnyAy)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Phlogistique added a commit that referenced this pull request Jul 6, 2026
Stacked on #59.

A rebase merge lands the branch's content as new commits without merging
its history, same as a squash spread over several commits. The old
refusal reasons were specific to the merge dance, which merged
`SQUASH_COMMIT~` and so could conflict against intermediate copies; the
single-merge re-parent (#56) only reads the landed tip's tree, and with
`--absorbed` (#59) the recorded parent is the merged branch's tip in
both cases. So the squash path handles rebase merges as-is.

This deletes the squash-vs-rebase detection (`is_rebase_merge` and its
commit-PR association polling, the flakiest part of the action: the
association is computed asynchronously and had to be waited for). The
only case left to detect is a merge-commit merge, where the children
already contain their parent's history and the action only retargets.
`tests/test_rebase_merge_skip.sh`, which asserted the old refusal,
becomes `tests/test_rebase_merge.sh` and asserts the update.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01PHUVU3Ek3U9j3LrdjPnyAy)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant