Return a ServiceResponse from MergeRequests::Conflicts::ResolveService
MergeRequests::Conflicts::ResolveService#execute is currently three lines: it builds a Gitlab::Conflict::FileCollection and calls resolve, raising on failure and returning nothing useful. All the checks that make resolving conflicts safe (push access, resolvable-in-UI, already-resolved, error handling) live in Projects::MergeRequests::ConflictsController#resolve_conflicts instead, which only works from a browser. This issue moves those checks into the service with no behaviour change, so later work (API access) can build on a clean, testable interface.
Scope
- Move into the service, in master's exact order: the push-access check (
authorize_can_resolve_conflicts!), thecan_be_resolved_in_ui?check, theMergeRequestActivityUniqueCounter.track_resolve_conflict_actioncall, thecan_be_merged?("already resolved") check, and the rescue ofGitlab::Git::Conflict::Resolver::ResolutionErrorandGitlab::Git::PreReceiveError. - Service returns
ServiceResponse.success(payload: { merge_request: })on success,ServiceResponse.error(message:, reason:)on failure. - Reason constants are owned and defined by the service.
- Controller maps each reason back to the exact status code and body master returns today (404 / 400 / 422).
- Controller keeps its
before_action :authorize_can_resolve_conflicts!(defense in depth, and it also covers theshowandconflict_for_pathactions, not onlyresolve_conflicts). - Gitaly errors keep propagating to
HandlesGitalyErrorsunchanged for now (a later issue moves that into the service).
Done when
- The existing controller spec for
resolve_conflictspasses unchanged, proving nothing observable changed for the web endpoint. - New service specs cover each reason and each response directly.
- No status code, message, or response body changes.
Detailed context for AI agents
Current controller flow in Projects::MergeRequests::ConflictsController#resolve_conflicts on master, in order:
before_action :authorize_can_resolve_conflicts!builds@conflicts_list = ListService.new(@merge_request)and renders 404 unless@conflicts_list.can_be_resolved_by?(current_user)(push access to the source branch).can_be_resolved_in_ui?is checked early (before the main action body) soHandlesGitalyErrorscan catch any Gitaly outage during that check.- In the action itself: 404 unless
can_be_resolved_in_ui?. MergeRequestActivityUniqueCounter.track_resolve_conflict_action(user: current_user).- 400 with "The merge conflicts for this merge request have already been resolved." if
@merge_request.can_be_merged?. - Calls
MergeRequests::Conflicts::ResolveService. - Rescues
Gitlab::Git::Conflict::Resolver::ResolutionError-> 400 with the exception's message. - Rescues
Gitlab::Git::PreReceiveError-> 422 with the exception's message.
Gitaly errors (HandlesGitalyErrors::GITALY_ERRORS) are handled separately by the HandlesGitalyErrors concern's rescue_from, which tracks the exception and renders 503 { error: gitaly_unavailable_message } for JSON requests. That is out of scope for this issue (see the Gitaly-unavailability issue).
Permitted params: commit_message, files: [:old_path, :new_path, :content, { sections: {} }].
ListService#can_be_resolved_in_ui? returns false unless the merge request cannot_be_merged?, has complete diff refs, and has no missing branch; otherwise it delegates to Gitlab::Conflict::FileCollection#can_be_resolved_in_ui?, which is cached via Redis cache_method and swallows parser, encoding, and side-missing errors (and non-Gitaly CommandError), re-raising only when Gitaly is genuinely unavailable.
Note the ordering bug this creates on master: because can_be_resolved_in_ui? is false for a mergeable (already resolved) merge request, the "already been resolved" 400 is currently unreachable - an already-resolved MR gets a 404 instead. Do not fix that here; it is intentionally deferred to a follow-up issue so this move stays behaviour-neutral and provable via the unchanged spec. Preserve the exact current (buggy) order when moving the code.
This is the foundation for the rest of the epic: input validation, the already-resolved/still-checking fix, Gitaly-unavailability reporting, and the eventual GraphQL mutation all build on this service's ServiceResponse shape and reason constants.
Prior art: this move was previously attempted as !256973 (closed), closed unmerged on 2026-09-25 because it mixed this pure move with behaviour changes in the same MR, which made review difficult. Re-check its review thread for edge cases before implementing.