Welcome to mirror list, hosted at ThFree Co, Russian Federation.

mergeability_check_service.rb « merge_requests « services « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 3e294aeaa077054f52fa7b1aed1f7d813db08e9a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# frozen_string_literal: true

module MergeRequests
  class MergeabilityCheckService < ::BaseService
    include Gitlab::Utils::StrongMemoize
    include Gitlab::ExclusiveLeaseHelpers

    delegate :project, to: :@merge_request
    delegate :repository, to: :project

    def initialize(merge_request)
      @merge_request = merge_request
    end

    def async_execute
      return service_error if service_error
      return unless merge_request.mark_as_checking

      MergeRequestMergeabilityCheckWorker.perform_async(merge_request.id)
    end

    # Updates the MR merge_status. Whenever it switches to a can_be_merged state,
    # the merge-ref is refreshed.
    #
    # recheck - When given, it'll enforce a merge-ref refresh if the current merge_status is
    # can_be_merged or cannot_be_merged and merge-ref is outdated.
    # Given MergeRequests::RefreshService is called async, it might happen that the target
    # branch gets updated, but the MergeRequest#merge_status lags behind. So in scenarios
    # where we need the current state of the merge ref in repository, the `recheck`
    # argument is required.
    #
    # retry_lease - Concurrent calls wait for at least 10 seconds until the
    # lease is granted (other process finishes running). Returns an error
    # ServiceResponse if the lease is not granted during this time.
    #
    # Returns a ServiceResponse indicating merge_status is/became can_be_merged
    # and the merge-ref is synced. Success in case of being/becoming mergeable,
    # error otherwise.
    def execute(recheck: false, retry_lease: true)
      return service_error if service_error

      in_write_lock(retry_lease: retry_lease) do |retried|
        # When multiple calls are waiting for the same lock (retry_lease),
        # it's possible that when granted, the MR status was already updated for
        # that object, therefore we reset if there was a lease retry.
        merge_request.reset if retried

        check_mergeability(recheck)
      end
    rescue FailedToObtainLockError => error
      ServiceResponse.error(message: error.message)
    end

    private

    attr_reader :merge_request

    def check_mergeability(recheck)
      recheck! if recheck
      update_merge_status

      unless merge_request.can_be_merged?
        return ServiceResponse.error(message: 'Merge request is not mergeable')
      end

      unless payload.fetch(:merge_ref_head)
        return ServiceResponse.error(message: 'Merge ref cannot be updated')
      end

      ServiceResponse.success(payload: payload)
    end

    # It's possible for this service to send concurrent requests to Gitaly in order
    # to "git update-ref" the same ref. Therefore we handle a light exclusive
    # lease here.
    #
    def in_write_lock(retry_lease:, &block)
      lease_key = "mergeability_check:#{merge_request.id}"

      lease_opts = {
        ttl:       1.minute,
        retries:   retry_lease ? 10 : 0,
        sleep_sec: retry_lease ? 1.second : 0
      }

      in_lock(lease_key, **lease_opts, &block)
    end

    def payload
      strong_memoize(:payload) do
        {
          merge_ref_head: merge_ref_head_payload
        }
      end
    end

    def merge_ref_head_payload
      commit = merge_request.merge_ref_head

      return unless commit

      target_id, source_id = commit.parent_ids

      {
        commit_id: commit.id,
        source_id: source_id,
        target_id: target_id
      }
    end

    def update_merge_status
      return unless merge_request.recheck_merge_status?
      return merge_request.mark_as_unmergeable if merge_request.broken?

      merge_to_ref_success = merge_to_ref

      reload_merge_head_diff
      update_diff_discussion_positions! if merge_to_ref_success

      if merge_to_ref_success && can_git_merge?
        merge_request.mark_as_mergeable
      else
        merge_request.mark_as_unmergeable
      end
    end

    def reload_merge_head_diff
      MergeRequests::ReloadMergeHeadDiffService.new(merge_request).execute
    end

    def update_diff_discussion_positions!
      Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
    end

    def recheck!
      if !merge_request.recheck_merge_status? && outdated_merge_ref?
        merge_request.mark_as_unchecked
      end
    end

    # Checks if the existing merge-ref is synced with the target branch.
    #
    # Returns true if the merge-ref does not exists or is out of sync.
    def outdated_merge_ref?
      return false unless merge_request.open?

      return true unless ref_head = merge_request.merge_ref_head
      return true unless target_sha = merge_request.target_branch_sha
      return true unless source_sha = merge_request.source_branch_sha

      ref_head.parent_ids != [target_sha, source_sha]
    end

    def can_git_merge?
      repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
    end

    def merge_to_ref
      params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) }
      result = MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author, params: params).execute(merge_request)

      result[:status] == :success
    end

    def service_error
      strong_memoize(:service_error) do
        if !merge_request
          ServiceResponse.error(message: 'Invalid argument')
        elsif Gitlab::Database.read_only?
          ServiceResponse.error(message: 'Unsupported operation')
        end
      end
    end
  end
end