summaryrefslogtreecommitdiff
path: root/lib/gitlab/email/handler/create_merge_request_handler.rb
blob: bb62d76a091a51b9cdccba13076fb4026452224c (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
# frozen_string_literal: true

require 'gitlab/email/handler/base_handler'
require 'gitlab/email/handler/reply_processing'

# handles merge request creation emails with these forms:
#   incoming+gitlab-org-gitlab-ce-20-Author_Token12345678-merge-request@incoming.gitlab.com
#   incoming+gitlab-org/gitlab-ce+merge-request+Author_Token12345678@incoming.gitlab.com (legacy)
module Gitlab
  module Email
    module Handler
      class CreateMergeRequestHandler < BaseHandler
        include ReplyProcessing

        HANDLER_REGEX        = /\A.+-(?<project_id>.+)-(?<incoming_email_token>.+)-merge-request\z/.freeze
        HANDLER_REGEX_LEGACY = /\A(?<project_path>[^\+]*)\+merge-request\+(?<incoming_email_token>.*)/.freeze

        def initialize(mail, mail_key)
          super(mail, mail_key)

          if matched = HANDLER_REGEX.match(mail_key.to_s)
            @project_id, @incoming_email_token = matched.captures
          elsif matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s)
            @project_path, @incoming_email_token = matched.captures
          end
        end

        def can_handle?
          incoming_email_token && (project_id || project_path)
        end

        def execute
          raise ProjectNotFound unless project

          validate_permission!(:create_merge_request_in)
          validate_permission!(:create_merge_request_from)

          verify_record!(
            record: create_merge_request,
            invalid_exception: InvalidMergeRequestError,
            record_name: 'merge_request')
        end

        # rubocop: disable CodeReuse/ActiveRecord
        def author
          @author ||= User.find_by(incoming_email_token: incoming_email_token)
        end
        # rubocop: enable CodeReuse/ActiveRecord

        def project
          @project ||= if project_id
                         Project.find_by_id(project_id)
                       else
                         Project.find_by_full_path(project_path)
                       end
        end

        def metrics_params
          super.merge(includes_patches: patch_attachments.any?)
        end

        private

        attr_reader :project_id, :project_path, :incoming_email_token

        def build_merge_request
          MergeRequests::BuildService.new(project, author, merge_request_params).execute
        end

        def create_merge_request
          merge_request = build_merge_request

          if patch_attachments.any?
            apply_patches_to_source_branch(start_branch: merge_request.target_branch)
            remove_patch_attachments
            # Rebuild the merge request as the source branch might just have
            # been created, so we should re-validate.
            merge_request = build_merge_request
          end

          if merge_request.errors.any?
            merge_request
          else
            MergeRequests::CreateService.new(project, author).create(merge_request)
          end
        end

        def merge_request_params
          params = {
            source_project_id: project.id,
            source_branch: source_branch,
            target_project_id: project.id
          }
          params[:description] = message if message.present?
          params
        end

        def apply_patches_to_source_branch(start_branch:)
          patches = patch_attachments.map { |patch| patch.body.decoded }

          result = Commits::CommitPatchService
                     .new(project, author, branch_name: source_branch, patches: patches, start_branch: start_branch)
                     .execute

          if result[:status] != :success
            message = "Could not apply patches to #{source_branch}:\n#{result[:message]}"
            raise InvalidAttachment, message
          end
        end

        def remove_patch_attachments
          patch_attachments.each { |patch| mail.parts.delete(patch) }
          # reset the message, so it needs to be reprocessed when the attachments
          # have been modified
          @message = nil
        end

        def patch_attachments
          @patches ||= mail.attachments
                         .select { |attachment| attachment.filename.ends_with?('.patch') }
                         .sort_by(&:filename)
        end

        def source_branch
          @source_branch ||= mail.subject
        end
      end
    end
  end
end