Loading
Fix fast-forward merge when a tag shares the target branch name
What does this MR do and why?
Fast-forward merges fail when a tag has the same name as the target branch and points at a different commit. The fast-forward now reads the target branch tip from the fully qualified refs/heads/<branch> ref, so a same-named tag is ignored.
Detailed context for AI agents
Change
- File:
app/services/merge_requests/merge_strategies/from_source_branch.rb, methodfast_forward!. prior_target_sha = repository.commit(merge_request.target_branch)&.shabecomesrepository.commit(merge_request.target_branch_ref)&.sha.target_branch_refisrefs/heads/<branch>. The same file already passesmerge_request.target_branch_reftoCreateRefService, andMergeRequest#target_branch_headuses it too.
Root cause and data flow
prior_target_shais sent to GitalyUserFFBranchasexpected_old_oid. It is an optimistic lock on the branch.- A bare name is resolved as
refs/tags/<name>beforerefs/heads/<name>. - If a tag has the same name as the target branch and points at a different commit, the lock value is the tag's commit.
- Gitaly refuses the update with
FailedPrecondition("reference does not point to expected object"). Expected is the tag commit, actual is the branch tip. - The client maps that refusal to nil. The merge then fails with
Fast-forward merge did not advance the target branch. - On 19.2 the user only sees "An error occurred while merging". Retrying never helps. The MR and source branch are left intact.
Regression history
- In 19.1 this read, and the
target_shaargument, were behind the default-off flagverify_ff_merge_advancement. - The flag was removed in 19.2 (commit 13d415ea). Self-managed instances got the behaviour in 19.2.0 with no way to turn it off.
- Reported by a self-managed customer on 19.2.6. A reproduction also exists on GitLab.com (by Support).
- The check itself was added for the no-op / source-branch-deletion bug in https://gitlab.com/gitlab-org/gitlab/-/work_items/598820. Flag rollout: #602293 (closed).
Workaround until this ships
- Switch the project to the "Merge commit" merge method. That path sends no expected old value.
- Or rename the tag.
Why no feature flag
- It switches one read to the qualified ref the rest of the file already uses. Behaviour only changes when another ref shadows the branch name, which is exactly the broken case.
Verification
- New example in
spec/services/merge_requests/merge_strategies/from_source_branch_spec.rb, against a real Gitaly repository. A tag with the target branch's name points behind the branch tip. The spec asserts the fast-forward lands on the branch. - Without the fix it fails with exactly
Fast-forward merge did not advance the target branch. With the fix it passes. from_source_branch_spec.rb(28 examples) andspec/services/merge_requests/merge_service_spec.rb(59 examples) pass.- Reproduced end to end on GDK through Praefect. On master the merge fails and Gitaly logs the expected/actual pattern above. The same setup merges with the 19.1 flag-off code and with this change.
Out of scope (same bare-name read elsewhere, left for follow-ups)
Projects::Forks::SyncService#head_sha. It is also used as a lock.Repository#commit_files. It passestarget_shato UserCommitFiles. Pre-existing since 2024, Source Code group.MergeRequest#squash_commit_reachable_from_target_branch?andMergeTrains::RefreshMergeRequestService#merge_commit_landed_on_target?. Both are read-only checks.- The
:reference_updateto nil mapping inGitlab::GitalyClient::OperationService#user_ff_branch(Source Code group). It hides why the update was refused, but it is not the cause here.
References
- Request for help: https://gitlab.com/gitlab-com/request-for-help/-/work_items/5507
- Flag rollout issue: #602293 (closed)
- Original bug that added the check: https://gitlab.com/gitlab-org/gitlab/-/work_items/598820
Screenshots or screen recordings
Backend only, no UI change.
How to set up and validate locally
- In a project, set Settings > Merge requests > Merge method to "Fast-forward merge".
- Create branch
ff-targetfrom the default branch. - Create branch
ff-sourcefrom the default branch, add one commit to it, and open a merge request fromff-sourceintoff-target. - Create a tag named
ff-targetfrom the parent of the default branch's tip commit (any commit behindff-target). - Merge the merge request. On master it fails with "Fast-forward merge did not advance the target branch". On this branch it merges and
ff-targetmoves to theff-sourcecommit.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.