Merge request widget hides the reason a rebase was rejected
Summary
The rebase endpoint sends the reason it refused a rebase. The merge request widget throws that reason away and shows "Failed to rebase. Please try again." instead, which is wrong advice, because retrying cannot succeed. On a project that requires a fast-forward or semi-linear merge, the same rejection shows nothing at all.
POST /:namespace/:project/-/merge_requests/:iid/rebase answers 403 with
{"merge_error": "<reason>"} when the rebase is refused before it is queued, and 409 with the
same key on a lock timeout. The reason has been on the wire since 14.6
(!75496 (merged)), and 15.8 made the UI and the API
agree that a user who cannot force push gets a 403 carrying it
(#386537 (closed)).
Steps to reproduce
- Fork a project.
- In the fork, protect the branch you will use as the source branch and leave Allowed to force push off.
- Add a commit to the target branch in the upstream project, so the merge request ends up behind its target.
- Open a merge request from the protected branch in the fork to the upstream project.
- As a user who is allowed to push to that branch, open the merge request, expand Merge details and select Rebase source branch.
For the second variant, set the upstream project's merge method to Fast-forward merge, then use the Rebase button that the merge checks list offers.
Example Project
No public example project. The setup is two projects and one merge request:
| Setting | |
|---|---|
| Upstream project | default branch protected, Allowed to force push off |
| Fork | same branch protected, Allowed to force push off |
| Merge request | fork's protected branch into the upstream project's branch, one commit behind and one commit ahead of the target |
The merge request has to be behind its target, otherwise the Rebase button does not render.
What is the current bug behavior?
The request answers:
403 {"merge_error":"Source branch is protected from force push"}The widget then shows:
- In the merge widget, an alert reading "Failed to rebase. Please try again."
- In the merge checks list, no alert at all.
The same happens for the endpoint's other reasons, Source branch does not exist and
Cannot push to source branch, and for the 409 lock-timeout message.
What is the expected correct behavior?
The alert names the reason the backend sent, for example:
Failed to rebase: Source branch is protected from force push.
Both surfaces behave the same way, and the generic sentence appears only when no reason was sent.
Relevant logs and/or screenshots
Expand for the endpoint responses and the code that produces them
The three reasons raised before the rebase is queued, from
rebase_service.rb:
return error_response(_('Source branch does not exist')) unless
merge_request.source_branch_exists?
return error_response(_('Cannot push to source branch')) unless
user_access.can_push_to_branch?(merge_request.source_branch)
return error_response(_('Source branch is protected from force push')) unless
merge_request.permits_force_push?Rendered by
merge_requests_controller.rb:
render json: { merge_error: result.message }, status: :forbiddenWorth noting: the button and the request disagree by design. The button asks
MergeRequestPresenter#can_push_to_source_branch?, which a protected branch can satisfy. The
request asks MergeRequest#permits_force_push?, which the same protected branch fails when force
push is off. So the button is offered and then refused, and the refusal is the only place the user
could learn why.
Output of checks
Reproduced on a current master GDK, 19.4.0-pre EE. Not verified on GitLab.com.
Results of GitLab environment info
Expand for output related to GitLab environment info
GitLab 19.4.0-pre EE, GDK, master at 031c10901d819ce7d112b0e5a32384da590ecdf2 Affected files are Community Edition, under app/assets/javascripts/
Possible fixes
Two independent places drop the reason:
| Component | Line | What happens |
|---|---|---|
states/ready_to_merge.vue |
586 | Reads error.response.data.message. The payload key is merge_error, so the read never matches and the generic sentence always wins. Introduced in 17.9 by !179906 (merged), which added the Rebase button to the widget |
checks/rebase.vue |
128 | Raises its alert only when merge_error is absent, so a reason produces silence. Present since the component was created in October 2023 |
checks/rebase.vue appears to assume some other surface renders the reason. None does. The widget
does have a merge-error banner,
mr_widget_options.vue,
but it is fed by the persisted merge_error column and by an event that also moves the widget into
its failedToMerge state. A rebase refused before it is queued never writes that column, and the
merge request is still open and mergeable, so that banner is the wrong surface here.
Suggested direction: both call sites read merge_error from the response and fall back to the
generic sentence only when it is absent. A shared helper keeps the two from drifting apart again.
The reason may or may not end in a period, i.e. the three refusal reasons do not and the
lock-timeout message does, so the formatting has to handle both, the way
mr_widget_options.vue already does for merge errors.
Neither component has a test on the rejection path today, which is why this went unnoticed for several releases. Any fix should add one per call site.
Out of scope: the wording of the reasons themselves. Cannot push to source branch still does not
name the protected-branch rule that denied it. That belongs in a separate change.
Related
#616915 (closed) covers the same class of problem for the automatic rebase inside a merge. Different endpoint and different message, so it is not a duplicate.