Show a clear error when automatic rebase before merge conflicts
When automatic rebase before merge fails because of a conflict, the widget currently shows only the generic "An error occurred while merging" - the customer in the issue retried the merge repeatedly with no way to tell they had to resolve a conflict. This change shows an explicit message for that case: "Automatic rebase before merge failed because the source branch conflicts with the target branch. Rebase the source branch manually and resolve the conflicts." All other failures keep today's generic message - raw Gitaly text is never shown to users, only logged.
Detailed context for AI agents
Issue: #616915 (closed)
A customer retried a merge repeatedly with no indication the real cause was a rebase conflict, and was only unblocked by disabling automatic rebase (see #524048 (comment 3687401486)).
Root cause: MergeRequests::MergeService#try_merge has a blanket rescue StandardError that logs the real message server-side and re-raises the generic GENERIC_ERROR_MESSAGE. This swallowed MergeRequests::MergeStrategies::StrategyError. The outer rescue in #execute already handles StrategyError with save_message_on_model: true (i.e. strategy messages were intended to be user-facing), but it only ever saw StrategyErrors raised from validate!, which runs outside try_merge. Git-merge-time failures (the rebase performed via MergeRequests::CreateRefService) never reached that outer rescue.
Fix (3 small changes):
try_mergere-raisesMergeRequests::MergeStrategies::StrategyErrorbefore therescue StandardErrorclause, so the outer rescue persists the message onmerge_request.merge_error(surfaced in the MR widget). This is safe because everyStrategyErrormessage is app-curated text, never raw Gitaly output (see change 3).MergeRequests::CreateRefServiceclassifies conflicts by returningreason: REBASE_CONFLICTon the errorServiceResponse. Gitaly exposes no structured error here:UserRebaseToRefcatches its internal typedRebaseConflictErrorbut flattens it to a plainFailedPreconditionreadingfailed to rebase X on Y while preparing Z due to conflict(noWithDetail, unlikeUserRebase/UserMergeBranch/UserSquash). Classification matches that wording only. The merge step (UserMergeToRef) is deliberately not classified: in this flow it merges the already-rebased head onto its base so it cannot conflict, and it reports one genericFailed to create merge commit ...message for all failures anyway. If Gitaly ever rephrases, production fails open to the generic message - never to showing raw text.ServiceResponsemessages are unchanged, so other consumers (merge trains) are unaffected.MergeStrategies::FromSourceBranch#execute_git_merge!maps the reason:REBASE_CONFLICTraises aStrategyErrorwith the curated conflict message; any other create-ref failure raises a plainRuntimeErrorwith the raw message, whichtry_mergelogs and converts to the generic message - byte-for-byte today's behaviour for non-conflict failures.
Side benefit: the "Fast-forward merge did not advance the target branch" message (rebase-collapse guard, https://gitlab.com/gitlab-org/gitlab/-/work_items/598820) and squash failure messages - already app-curated - were also being swallowed to the generic message and now surface via change 1.
How this was found: while production-testing the retain_auto_merge_with_automatic_rebase rollout (#611435), on a test MR whose source branch was 3-way-merge-clean but rebase-conflicting (one commit changes a line, a second commit reverts it, target branch changes the same line). mergeable? passes because it does a 3-way merge test, the merge fires, the commit-by-commit rebase conflicts, and the MR ends up with detailed_merge_status=mergeable and only the generic merge_error. With auto-merge armed this failure is effectively invisible - the MR never becomes "unmergeable", so no to-do or email fires. The retain flag makes this path more frequent since auto-merges now survive target-branch pushes.
Verification: new spec in spec/services/merge_requests/merge_service_spec.rb asserting the StrategyError message lands on merge_error; specs in spec/services/merge_requests/merge_strategies/from_source_branch_spec.rb covering both the conflict (curated message) and non-conflict (plain re-raise) paths; specs in spec/services/merge_requests/create_ref_service_spec.rb drive the real Gitaly RPCs with genuinely conflicting branches (no stubbing) - the ff case asserts the conflict is classified and the merge-commit case asserts it is not - so the message match fails loudly in CI if Gitaly's wording ever drifts. All three files pass locally. RuboCop clean.
Deliberately out of scope:
- Surfacing any detail for non-conflict rebase failures - kept generic on purpose so raw Gitaly text never reaches users.
- A structured conflict error from Gitaly's
UserRebaseToRefRPC (addingWithDetail(MergeConflictError)like its sibling RPCs already do), which would replace the message match (possible follow-up). - Aborting/notifying the auto-merge when a merge attempt fails (possible follow-up for #611435).
- i18n of the new message: neighbouring messages in these classes are plain strings, and
merge_erroris persisted per-MR, so render-time translation would not apply anyway.
Resolves #616915 (closed)