1# frozen_string_literal: true 2 3module MergeRequests 4 class MergeabilityCheckService < ::BaseService 5 include Gitlab::Utils::StrongMemoize 6 include Gitlab::ExclusiveLeaseHelpers 7 8 delegate :project, to: :@merge_request 9 delegate :repository, to: :project 10 11 def initialize(merge_request) 12 @merge_request = merge_request 13 end 14 15 def async_execute 16 return service_error if service_error 17 return unless merge_request.mark_as_checking 18 19 MergeRequestMergeabilityCheckWorker.perform_async(merge_request.id) 20 end 21 22 # Updates the MR merge_status. Whenever it switches to a can_be_merged state, 23 # the merge-ref is refreshed. 24 # 25 # recheck - When given, it'll enforce a merge-ref refresh if the current merge_status is 26 # can_be_merged or cannot_be_merged and merge-ref is outdated. 27 # Given MergeRequests::RefreshService is called async, it might happen that the target 28 # branch gets updated, but the MergeRequest#merge_status lags behind. So in scenarios 29 # where we need the current state of the merge ref in repository, the `recheck` 30 # argument is required. 31 # 32 # retry_lease - Concurrent calls wait for at least 10 seconds until the 33 # lease is granted (other process finishes running). Returns an error 34 # ServiceResponse if the lease is not granted during this time. 35 # 36 # Returns a ServiceResponse indicating merge_status is/became can_be_merged 37 # and the merge-ref is synced. Success in case of being/becoming mergeable, 38 # error otherwise. 39 def execute(recheck: false, retry_lease: true) 40 return service_error if service_error 41 42 in_write_lock(retry_lease: retry_lease) do |retried| 43 # When multiple calls are waiting for the same lock (retry_lease), 44 # it's possible that when granted, the MR status was already updated for 45 # that object, therefore we reset if there was a lease retry. 46 merge_request.reset if retried 47 48 check_mergeability(recheck) 49 end 50 rescue FailedToObtainLockError => error 51 ServiceResponse.error(message: error.message) 52 end 53 54 private 55 56 attr_reader :merge_request 57 58 def check_mergeability(recheck) 59 recheck! if recheck 60 update_merge_status 61 62 unless merge_request.can_be_merged? 63 return ServiceResponse.error(message: 'Merge request is not mergeable') 64 end 65 66 unless payload.fetch(:merge_ref_head) 67 return ServiceResponse.error(message: 'Merge ref cannot be updated') 68 end 69 70 ServiceResponse.success(payload: payload) 71 end 72 73 # It's possible for this service to send concurrent requests to Gitaly in order 74 # to "git update-ref" the same ref. Therefore we handle a light exclusive 75 # lease here. 76 # 77 def in_write_lock(retry_lease:, &block) 78 lease_key = "mergeability_check:#{merge_request.id}" 79 80 lease_opts = { 81 ttl: 1.minute, 82 retries: retry_lease ? 10 : 0, 83 sleep_sec: retry_lease ? 1.second : 0 84 } 85 86 in_lock(lease_key, **lease_opts, &block) 87 end 88 89 def payload 90 strong_memoize(:payload) do 91 { 92 merge_ref_head: merge_ref_head_payload 93 } 94 end 95 end 96 97 def merge_ref_head_payload 98 commit = merge_request.merge_ref_head 99 100 return unless commit 101 102 target_id, source_id = commit.parent_ids 103 104 { 105 commit_id: commit.id, 106 source_id: source_id, 107 target_id: target_id 108 } 109 end 110 111 def update_merge_status 112 return unless merge_request.recheck_merge_status? 113 return merge_request.mark_as_unmergeable if merge_request.broken? 114 115 merge_to_ref_success = merge_to_ref 116 117 reload_merge_head_diff 118 update_diff_discussion_positions! if merge_to_ref_success 119 120 if merge_to_ref_success && can_git_merge? 121 merge_request.mark_as_mergeable 122 else 123 merge_request.mark_as_unmergeable 124 end 125 end 126 127 def reload_merge_head_diff 128 MergeRequests::ReloadMergeHeadDiffService.new(merge_request).execute 129 end 130 131 def update_diff_discussion_positions! 132 Discussions::CaptureDiffNotePositionsService.new(merge_request).execute 133 end 134 135 def recheck! 136 if !merge_request.recheck_merge_status? && outdated_merge_ref? 137 merge_request.mark_as_unchecked 138 end 139 end 140 141 # Checks if the existing merge-ref is synced with the target branch. 142 # 143 # Returns true if the merge-ref does not exists or is out of sync. 144 def outdated_merge_ref? 145 return false unless merge_request.open? 146 147 return true unless ref_head = merge_request.merge_ref_head 148 return true unless target_sha = merge_request.target_branch_sha 149 return true unless source_sha = merge_request.source_branch_sha 150 151 ref_head.parent_ids != [target_sha, source_sha] 152 end 153 154 def can_git_merge? 155 repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch) 156 end 157 158 def merge_to_ref 159 params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) } 160 result = MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author, params: params).execute(merge_request) 161 162 result[:status] == :success 163 end 164 165 def service_error 166 strong_memoize(:service_error) do 167 if !merge_request 168 ServiceResponse.error(message: 'Invalid argument') 169 elsif Gitlab::Database.read_only? 170 ServiceResponse.error(message: 'Unsupported operation') 171 end 172 end 173 end 174 end 175end 176