Surface conflicting file names in merge train drop reasons

What does this MR do and why?

When a merge train drops a merge request because the train ref could not be built, the system note names the failure but not which files conflict. Finding the cause means reading Gitaly logs for information Gitaly already returns.

Gitaly now attaches the conflicting paths to a merge_conflict detailed error on UserMergeToRef (gitaly!9118 (merged)). This MR decodes that detail in Gitlab::GitalyClient::OperationService#user_merge_to_ref and appends the file names to the existing Gitlab::Git::CommandError message:

9:merging commits: merge: there are conflicting files. Conflicts in: `a.txt`, `b.txt`, and 2 more

Design notes:

  • Both callers (MergeRequests::CreateRefService#safe_gitaly_operation and MergeRequests::MergeToRefService#extracted_merge_to_ref) rescue by class and forward only error.message, so the exception class and the existing message are unchanged and the merge train layer needs no change.
  • The list is capped at 10 paths, with a count for the rest.
  • conflicting_files is repeated bytes, so each path goes through Gitlab::EncodingHelper#encode_utf8_safe_path. Invalid UTF-8 becomes U+FFFD, so the message is safe to persist.
  • The message ends up in a system note, and Note declares cache_markdown_field :note, so each path is wrapped in backticks. Without that, a file named __init__.py renders as bold and a path containing ](...) injects a link into a note attributed to the merge user. A path containing a backtick can still break out of the code span; that seems acceptable for a system note, but say so if you disagree.
  • The error is cloned with #exception rather than rebuilt from a string. That keeps the Gitaly metadata Gitlab::ExceptionLogFormatter reads, and it avoids Gitlab::Git::BaseError re-truncating the message at debug_error_string:, which would silently drop the whole appended list in production while specs still passed.
  • Any other detailed error, and an error with no detail at all, is re-raised unchanged.

Not included: the gitaly gem bump

The work item asks for a gitaly gem bump to a release containing Gitaly::UserMergeToRefError. No such release exists yet. The newest published gem is 19.3.0 (2026-08-20), cut five days before the Gitaly change merged, and there is no 19.4 release or prerelease. GITALY_SERVER_VERSION is also pinned to a commit before the Gitaly change.

Until both move, Gitlab::GitalyClient.decode_detailed_error cannot resolve the message type (Gitaly.const_get raises NameError, which it rescues to nil), so the new branch is unreachable and behaviour is identical to today. The Gemfile constraint is already ~> 19.2, so only Gemfile.lock will need to change.

Because of that, the positive spec examples stub the decoded wrapper. Gitaly::MergeConflictError itself is real and comes from the pinned gem, so the encoding and formatting behaviour is genuinely exercised; only the outer UserMergeToRefError is stubbed. The stub is marked with the work item URL so it can be swapped for new_detailed_error after the bump.

Known scope limit

This covers merge trains that build the train ref with a merge commit. For projects with Fast-forward merge or Merge commit with semi-linear history, CreateRefService rebases first, and UserRebaseToRef attaches no detailed error at all (there is no UserRebaseToRefError message in the Gitaly proto), so a conflict there still produces the old message. That needs a Gitaly change first.

UserMergeBranch has the same gap and already carries conflicting_files in the current gem, so the fork-sync conflict message could be improved without any bump. Left out of this MR to keep it focused.

References

How to set up and validate locally

  1. Run bundle exec rspec spec/lib/gitlab/gitaly_client/operation_service_spec.rb -e '#user_merge_to_ref'.
  2. Check the when the RPC fails context: it covers the message format, the 10-file cap, invalid UTF-8 paths, markdown-significant paths, an empty file list, an error with no detail, and a detail whose type the gem cannot resolve.
  3. Confirm the last three cases raise exactly what they raise on master today.

Merge request reports

Loading
Loading