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.