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

merge_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: 29aba3c86795ebd421975154403341b4e932b1b7 (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
176
177
178
179
180
181
182
183
# frozen_string_literal: true

module MergeRequests
  # MergeService class
  #
  # Do git merge and in case of success
  # mark merge request as merged and execute all hooks and notifications
  # Executed when you do merge via GitLab UI
  #
  class MergeService < MergeRequests::MergeBaseService
    include Gitlab::Utils::StrongMemoize

    GENERIC_ERROR_MESSAGE = 'An error occurred while merging'
    LEASE_TIMEOUT = 15.minutes.to_i

    delegate :merge_jid, :state, to: :@merge_request

    def execute(merge_request, options = {})
      return if merge_request.merged?
      return unless exclusive_lease(merge_request.id).try_obtain

      merge_strategy_class = options[:merge_strategy] || MergeRequests::MergeStrategies::FromSourceBranch
      @merge_strategy = merge_strategy_class.new(merge_request, current_user, merge_params: params, options: options)

      @merge_request = merge_request
      @options = options
      jid = merge_jid

      validate!

      merge_request.in_locked_state do
        if commit
          after_merge
          clean_merge_jid
          success
        end
      end

      log_info("Merge process finished on JID #{jid} with state #{state}")
    rescue MergeError, MergeRequests::MergeStrategies::StrategyError => e
      handle_merge_error(log_message: e.message, save_message_on_model: true)
    ensure
      exclusive_lease(merge_request.id).cancel
    end

    private

    def validate!
      authorization_check!
      error_check!
      validate_strategy!
      updated_check!
    end

    def authorization_check!
      unless @merge_request.can_be_merged_by?(current_user)
        raise_error('You are not allowed to merge this merge request')
      end
    end

    def validate_strategy!
      @merge_strategy.validate!
    end

    def updated_check!
      unless source_matches?
        raise_error('Branch has been updated since the merge was requested. '\
                    'Please review the changes.')
      end
    end

    def commit
      log_info("Git merge started on JID #{merge_jid}")

      merge_result = try_merge { @merge_strategy.execute_git_merge! }

      commit_sha = merge_result[:commit_sha]
      raise_error(GENERIC_ERROR_MESSAGE) unless commit_sha

      log_info("Git merge finished on JID #{merge_jid} commit #{commit_sha}")

      new_merge_request_attributes = {
        merged_commit_sha: commit_sha,
        merge_commit_sha: merge_result[:merge_commit_sha],
        squash_commit_sha: merge_result[:squash_commit_sha]
      }.compact
      merge_request.update!(new_merge_request_attributes) if new_merge_request_attributes.present?

      commit_sha
    ensure
      merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
      log_info("Merge request marked in progress")
    end

    def try_merge
      yield
    rescue Gitlab::Git::PreReceiveError => e
      raise MergeError, "Something went wrong during merge pre-receive hook. #{e.message}".strip
    rescue StandardError => e
      handle_merge_error(log_message: e.message)
      raise_error(GENERIC_ERROR_MESSAGE)
    end

    def after_merge
      log_info("Post merge started on JID #{merge_jid} with state #{state}")
      MergeRequests::PostMergeService.new(project: project, current_user: current_user).execute(merge_request)
      log_info("Post merge finished on JID #{merge_jid} with state #{state}")

      if delete_source_branch?
        MergeRequests::DeleteSourceBranchWorker.perform_async(@merge_request.id, @merge_request.source_branch_sha, branch_deletion_user.id)
      end

      merge_request_merge_param
    end

    def clean_merge_jid
      merge_request.update_column(:merge_jid, nil)
    end

    def branch_deletion_user
      @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
    end

    # Verify again that the source branch can be removed, since branch may be protected,
    # or the source branch may have been updated, or the user may not have permission
    #
    def delete_source_branch?
      params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) &&
        @merge_request.can_remove_source_branch?(branch_deletion_user)
    end

    def merge_request_merge_param
      if @merge_request.can_remove_source_branch?(branch_deletion_user) && !params.fetch('should_remove_source_branch', nil).nil?
        @merge_request.update(merge_params: @merge_request.merge_params.merge('should_remove_source_branch' => params['should_remove_source_branch']))
      end
    end

    def handle_merge_error(log_message:, save_message_on_model: false)
      log_error("MergeService ERROR: #{merge_request_info}:#{merge_status} - #{log_message}")
      @merge_request.update(merge_error: log_message) if save_message_on_model
    end

    def log_info(message)
      payload = log_payload("#{merge_request_info} - #{message}")
      logger.info(**payload)
    end

    def log_error(message)
      payload = log_payload(message)
      logger.error(**payload)
    end

    def logger
      @logger ||= Gitlab::AppLogger
    end

    def log_payload(message)
      Gitlab::ApplicationContext.current.merge(merge_request_info: merge_request_info, message: message)
    end

    def merge_request_info
      @merge_request_info ||= merge_request.to_reference(full: true)
    end

    def merge_status
      @merge_status ||= @merge_request.merge_status
    end

    def source_matches?
      # params-keys are symbols coming from the controller, but when they get
      # loaded from the database they're strings
      params.with_indifferent_access[:sha] == merge_request.diff_head_sha
    end

    def exclusive_lease(merge_request_id)
      strong_memoize(:"exclusive_lease_#{merge_request_id}") do
        lease_key = ['merge_requests_merge_service', merge_request_id].join(':')

        Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
      end
    end
  end
end