summaryrefslogtreecommitdiff
path: root/danger/commit_messages/Dangerfile
blob: d6eb050930cf3422ccec125177c85bde69b607e7 (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
# frozen_string_literal: true

require_relative File.expand_path('../../lib/gitlab/danger/commit_linter', __dir__)

URL_GIT_COMMIT = "https://chris.beams.io/posts/git-commit/"
MAX_COMMITS_COUNT = 10

def gitlab_danger
  @gitlab_danger ||= GitlabDanger.new(helper.gitlab_helper)
end

def fail_commit(commit, message)
  self.fail("#{commit.sha}: #{message}")
end

def warn_commit(commit, message)
  self.warn("#{commit.sha}: #{message}")
end

def squash_mr?
  gitlab_danger.ci? ? gitlab.mr_json['squash'] : false
end

def wip_mr?
  gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false
end

# Perform various checks against commits. We're not using
# https://github.com/jonallured/danger-commit_lint because its output is not
# very helpful, and it doesn't offer the means of ignoring merge commits.
def lint_commit(commit)
  linter = Gitlab::Danger::CommitLinter.new(commit)

  # For now we'll ignore merge commits, as getting rid of those is a problem
  # separate from enforcing good commit messages.
  return linter if linter.merge?

  # We ignore revert commits as they are well structured by Git already
  return linter if linter.revert?

  # If MR is set to squash, we ignore fixup commits
  return linter if linter.fixup? && squash_mr?

  if linter.fixup?
    msg = 'Squash or fixup commits must be squashed before merge, or enable squash merge option'
    if wip_mr? || squash_mr?
      warn_commit(commit, msg)
    else
      fail_commit(commit, msg)
    end

    # Makes no sense to process other rules for fixup commits, they trigger just more noise
    return linter
  end

  # Fail if a suggestion commit is used and squash is not enabled
  if linter.suggestion?
    unless squash_mr?
      fail_commit(commit, "If you are applying suggestions, enable squash in the merge request and re-run the `danger-review` job")
    end

    return linter
  end

  linter.lint
end

def lint_mr_title(mr_title)
  commit = Struct.new(:message, :sha).new(mr_title)

  Gitlab::Danger::CommitLinter.new(commit).lint_subject("merge request title")
end

def count_non_fixup_commits(commit_linters)
  commit_linters.count { |commit_linter| !commit_linter.fixup? }
end

def lint_commits(commits)
  commit_linters = commits.map { |commit| lint_commit(commit) }
  failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? }
  warn_or_fail_commits(failed_commit_linters, default_to_fail: !squash_mr?)

  if count_non_fixup_commits(commit_linters) > MAX_COMMITS_COUNT
    level = squash_mr? ? :warn : :fail
    self.__send__(level, # rubocop:disable GitlabSecurity/PublicSend
      "This merge request includes more than #{MAX_COMMITS_COUNT} commits. " \
        'Please rebase these commits into a smaller number of commits or split ' \
        'this merge request into multiple smaller merge requests.')
  end

  if squash_mr?
    multi_line_commit_linter = commit_linters.detect { |commit_linter| commit_linter.multi_line? }

    if multi_line_commit_linter && multi_line_commit_linter.lint.failed?
      warn_or_fail_commits(multi_line_commit_linter)
      fail_message('The commit message that will be used in the squash commit does not meet our Git commit message standards.')
    else
      title_linter = lint_mr_title(gitlab.mr_json['title'])
      if title_linter.failed?
        warn_or_fail_commits(title_linter)
        fail_message('The merge request title that will be used in the squash commit does not meet our Git commit message standards.')
      end
    end
  else
    if failed_commit_linters.any?
      fail_message('One or more commit messages do not meet our Git commit message standards.')
    end
  end
end

def warn_or_fail_commits(failed_linters, default_to_fail: true)
  level = default_to_fail ? :fail : :warn

  Array(failed_linters).each do |linter|
    linter.problems.each do |problem_key, problem_desc|
      case problem_key
      when :subject_above_warning
        warn_commit(linter.commit, problem_desc)
      else
        self.__send__("#{level}_commit", linter.commit, problem_desc) # rubocop:disable GitlabSecurity/PublicSend
      end
    end
  end
end

def fail_message(intro)
  markdown(<<~MARKDOWN)
    ## Commit message standards

    #{intro}

    For more information on how to write a good commit message, take a look at
    [How to Write a Git Commit Message](#{URL_GIT_COMMIT}).

    Here is an example of a good commit message:

        Reject ruby interpolation in externalized strings

        When using ruby interpolation in externalized strings, they can't be
        detected. Which means they will never be presented to be translated.

        To mix variables into translations we need to use `sprintf`
        instead.

        Instead of:

            _("Hello \#{subject}")

        Use:

            _("Hello %{subject}") % { subject: 'world' }

    This is an example of a bad commit message:

        updated README.md

    This commit message is bad because although it tells us that README.md is
    updated, it doesn't tell us why or how it was updated.
  MARKDOWN
end

lint_commits(git.commits)