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 moreDesign notes:
- Both callers (
MergeRequests::CreateRefService#safe_gitaly_operationandMergeRequests::MergeToRefService#extracted_merge_to_ref) rescue by class and forward onlyerror.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_filesisrepeated bytes, so each path goes throughGitlab::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
Notedeclarescache_markdown_field :note, so each path is wrapped in backticks. Without that, a file named__init__.pyrenders 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
#exceptionrather than rebuilt from a string. That keeps the Gitaly metadataGitlab::ExceptionLogFormatterreads, and it avoidsGitlab::Git::BaseErrorre-truncating the message atdebug_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
- #622305
- gitaly!9118 (merged)
- gitaly#7330 (closed)
- #394901
- !251122 (merged) reframes the same system note and strips the leading gRPC status code. It touches the same doc section, but neither MR depends on the other.
How to set up and validate locally
- Run
bundle exec rspec spec/lib/gitlab/gitaly_client/operation_service_spec.rb -e '#user_merge_to_ref'. - Check the
when the RPC failscontext: 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. - Confirm the last three cases raise exactly what they raise on
mastertoday.