summaryrefslogtreecommitdiff
path: root/app/services/merge_requests/merge_service.rb
blob: 70a67baa01c2b3ac0f65ab37138d62cd84375157 (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
# 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::BaseService
    MergeError = Class.new(StandardError)

    attr_reader :merge_request, :source

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

    def execute(merge_request)
      if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
        FfMergeService.new(project, current_user, params).execute(merge_request)
        return
      end

      @merge_request = merge_request

      error_check!

      merge_request.in_locked_state do
        if commit
          after_merge
          clean_merge_jid
          success
        end
      end
      log_info("Merge process finished on JID #{merge_jid} with state #{state}")
    rescue MergeError => e
      handle_merge_error(log_message: e.message, save_message_on_model: true)
    end

    def source
      return merge_request.diff_head_sha unless merge_request.squash

      squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request)

      case squash_result[:status]
      when :success
        squash_result[:squash_sha]
      when :error
        raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
      end
    end

    # Overridden in EE.
    def hooks_validation_pass?(_merge_request)
      true
    end

    private

    def error_check!
      error =
        if @merge_request.should_be_rebased?
          'Only fast-forward merge is allowed for your project. Please update your source branch'
        elsif !@merge_request.mergeable?
          'Merge request is not mergeable'
        elsif !source
          'No source for merge'
        end

      raise MergeError, error if error
    end

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

      if commit_id
        log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
      else
        raise MergeError, 'Conflicts detected during merge'
      end

      merge_request.update!(merge_commit_sha: commit_id)
    end

    def try_merge
      message = params[:commit_message] || merge_request.merge_commit_message

      repository.merge(current_user, source, merge_request, message)
    rescue Gitlab::Git::PreReceiveError => e
      handle_merge_error(log_message: e.message)
      raise MergeError, 'Something went wrong during merge pre-receive hook'
    rescue => e
      handle_merge_error(log_message: e.message)
      raise MergeError, 'Something went wrong during merge'
    ensure
      merge_request.update!(in_progress_merge_commit_sha: nil)
    end

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

      if delete_source_branch?
        DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
          .execute(merge_request.source_branch)
      end
    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 handle_merge_error(log_message:, save_message_on_model: false)
      Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
      @merge_request.update(merge_error: log_message) if save_message_on_model
    end

    def log_info(message)
      @logger ||= Rails.logger
      @logger.info("#{merge_request_info} - #{message}")
    end

    def merge_request_info
      merge_request.to_reference(full: true)
    end
  end
end