Skip to content

Merge only the commit you checked

Use this when a tool decides to merge a pull request because of facts about one commit (its pipeline, its review, its diff), and must not merge if someone pushed after it looked.

PullRequestMerger.Merge merges whatever the head is when the call arrives. PinnedMerger.MergeAtHead merges only if the head is still the commit you name, and the forge itself refuses otherwise, so nothing can land in the gap between your check and your merge.

Which backends implement this

PinnedMerger is defined in forge from v0.33.0, and implemented from forge-gitlab v0.26.0, forge-github v0.27.0 and forge-gitea v0.27.0. Against an older adapter forge.As finds nothing. Bitbucket never will: its merge endpoint takes no expected commit, so a pin there could not be enforced. Check the capability matrix.

Merge at the head you checked

var pinned forge.PinnedMerger
if !forge.As(provider, &pinned) {
    return errCannotPin // do not fall back to Merge: that drops the guarantee
}

err := pinned.MergeAtHead(ctx, owner, repo, number, checkedSHA, forge.PinnedMergeOptions{})

checkedSHA is the full commit ID, 40 or 64 hex characters. An abbreviated one is refused as a plain error before any request is made.

Act on what comes back

Error What happened What to do
nil merged at checkedSHA, now or on an earlier call nothing
wraps forge.ErrConflict the head is not checkedSHA; nothing merged. The message names the head the provider saw re-read, re-check the new head, call again with it if it passes
wraps forge.ErrNotMergeable the forge or the squash guard refused; nothing merged stop, and leave it for a person
wraps forge.ErrMergedDifferently GitLab only: it merged, but squashed differently from what you asked, because the project's settings changed during the call do not retry; a retry answers nil. Report it
a plain error a malformed checkedSHA, or a transport failure fix the call, or retry later

A pull request that has already merged answers from the head it merged at, so a tool re-running after an interrupted run can call MergeAtHead again.

Choose whether it squashes

PinnedMergeOptions.Squash is false by default, which means do not squash. Set it to true to squash. Either way, if the project's policy or the pull request's own squash flag would land it the other way, the call returns ErrNotMergeable and merges nothing:

Forge Squash: true is refused when Squash: false is refused when
GitLab the project's squash option is "do not allow" the merge request will squash: the project requires it, or its author ticked the box
GitHub the repository does not allow squash merges the repository does not allow merge commits
Gitea the repository does not allow squash merges squash is the only style left to it

GitLab needs these checks most, because it answers a contradicting merge with a 200 and does the other thing. The provider never clears an author's squash flag to get the merge through, because the flag belongs to the person who set it.

What a pin does not promise

  • It pins which head merged, not which commit lands. A squash or a merge commit makes a new commit from the pinned head. Only a fast-forward without squash lands the pinned commit itself, and GitHub has no fast-forward.
  • There is no "merge when checks pass" form. GitHub and Gitea check a pin on a scheduled merge once, when it is scheduled, and then merge whatever head passes, so a scheduled pin would break its promise there. Check the pipeline yourself, then call MergeAtHead.

Spec 0028 records each forge's measured behaviour.