1# frozen_string_literal: true
2module Gitlab
3  module MergeRequests
4    class CommitMessageGenerator
5      def initialize(merge_request:)
6        @merge_request = merge_request
7      end
8
9      def merge_message
10        return unless @merge_request.target_project.merge_commit_template.present?
11
12        replace_placeholders(@merge_request.target_project.merge_commit_template)
13      end
14
15      def squash_message
16        return unless @merge_request.target_project.squash_commit_template.present?
17
18        replace_placeholders(@merge_request.target_project.squash_commit_template)
19      end
20
21      private
22
23      attr_reader :merge_request
24
25      PLACEHOLDERS = {
26        'source_branch' => ->(merge_request) { merge_request.source_branch.to_s },
27        'target_branch' => ->(merge_request) { merge_request.target_branch.to_s },
28        'title' => ->(merge_request) { merge_request.title },
29        'issues' => ->(merge_request) do
30          return "" if merge_request.visible_closing_issues_for.blank?
31
32          closes_issues_references = merge_request.visible_closing_issues_for.map do |issue|
33            issue.to_reference(merge_request.target_project)
34          end
35          "Closes #{closes_issues_references.to_sentence}"
36        end,
37        'description' => ->(merge_request) { merge_request.description.presence || '' },
38        'reference' => ->(merge_request) { merge_request.to_reference(full: true) },
39        'first_commit' => -> (merge_request) { merge_request.first_commit&.safe_message&.strip.presence || '' },
40        'first_multiline_commit' => -> (merge_request) { merge_request.first_multiline_commit&.safe_message&.strip.presence || merge_request.title }
41      }.freeze
42
43      PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map do |key|
44        Regexp.new(Regexp.escape(key))
45      end).freeze
46
47      BLANK_PLACEHOLDERS_REGEXES = (PLACEHOLDERS.map do |key, value|
48        [key, Regexp.new("[\n\r]+%{#{Regexp.escape(key)}}$")]
49      end).to_h.freeze
50
51      def replace_placeholders(message)
52        # convert CRLF to LF
53        message = message.delete("\r")
54
55        # Remove placeholders that correspond to empty values and are the last word in the line
56        # along with all whitespace characters preceding them.
57        # This allows us to recreate previous default merge commit message behaviour - we skipped new line character
58        # before empty description and before closed issues when none were present.
59        PLACEHOLDERS.each do |key, value|
60          unless value.call(merge_request).present?
61            message = message.gsub(BLANK_PLACEHOLDERS_REGEXES[key], '')
62          end
63        end
64
65        Gitlab::StringPlaceholderReplacer
66          .replace_string_placeholders(message, PLACEHOLDERS_REGEX) do |key|
67          PLACEHOLDERS[key].call(merge_request)
68        end
69      end
70    end
71  end
72end
73