blob: 7c2e47f6ba6707c69e47b3b1b69fa516c1bd58be [file] [log] [blame]
lowRISC Contributors802543a2019-08-31 12:12:56 +01001#!/usr/bin/env python3
2# Copyright lowRISC contributors.
3# Licensed under the Apache License, Version 2.0, see LICENSE for details.
4# SPDX-License-Identifier: Apache-2.0
5
6import argparse
Rupert Swarbrickbf5ca972020-08-19 10:42:40 +01007import re
lowRISC Contributors802543a2019-08-31 12:12:56 +01008import sys
9
Philipp Wagner1e643552019-09-04 15:39:14 +010010from git import Repo
11
12error_msg_prefix = 'ERROR: '
13warning_msg_prefix = 'WARNING: '
14
15# Maximum length of the summary line in the commit message (the first line)
16# There is no hard limit, but a typical convention is to keep this line at or
17# below 50 characters, with occasional outliers.
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010018COMMIT_MSG_MAX_SUMMARY_LEN = 100
lowRISC Contributors802543a2019-08-31 12:12:56 +010019
20
Philipp Wagner1e643552019-09-04 15:39:14 +010021def error(msg, commit=None):
22 full_msg = msg
23 if commit:
24 full_msg = "Commit %s: %s" % (commit.hexsha, msg)
25 print(error_msg_prefix + full_msg, file=sys.stderr)
26
27
28def warning(msg, commit=None):
29 full_msg = msg
30 if commit:
31 full_msg = "Commit %s: %s" % (commit.hexsha, msg)
32 print(warning_msg_prefix + full_msg, file=sys.stderr)
33
34
35def lint_commit_author(commit):
36 success = True
37 if commit.author.email.endswith('users.noreply.github.com'):
38 error(
Philipp Wagner0949e6d2021-03-08 17:01:01 +000039 f'Commit author has no valid email address set: '
40 '{commit.author.email!r}. '
Philipp Wagner1e643552019-09-04 15:39:14 +010041 'Use "git config user.email user@example.com" to '
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010042 'set a valid email address, then update the commit '
Philipp Wagner1e643552019-09-04 15:39:14 +010043 'with "git rebase -i" and/or '
Philipp Wagner0949e6d2021-03-08 17:01:01 +000044 '"git commit --amend --signoff --reset-author". '
Philipp Wagner1e643552019-09-04 15:39:14 +010045 'Also check your GitHub settings at '
46 'https://github.com/settings/emails: your email address '
47 'must be verified, and the option "Keep my email address '
Philipp Wagner0949e6d2021-03-08 17:01:01 +000048 'private" must be disabled. '
49 'This command will also sign off your commit indicating agreement '
50 'to the Contributor License Agreement. See CONTRIBUTING.md for '
51 'more details.', commit)
Philipp Wagner1e643552019-09-04 15:39:14 +010052 success = False
53
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010054 if ' ' not in commit.author.name:
Philipp Wagner1e643552019-09-04 15:39:14 +010055 warning(
Philipp Wagner0949e6d2021-03-08 17:01:01 +000056 f'The commit author name {commit.author.name!r} contains no space. '
Philipp Wagner1e643552019-09-04 15:39:14 +010057 'Use "git config user.name \'Johnny English\'" to '
58 'set your real name, and update the commit with "git rebase -i " '
Philipp Wagner0949e6d2021-03-08 17:01:01 +000059 'and/or "git commit --amend --signoff --reset-author". '
60 'This command will also sign off your commit indicating agreement '
61 'to the Contributor License Agreement. See CONTRIBUTING.md for '
62 'more details.', commit)
Philipp Wagner1e643552019-09-04 15:39:14 +010063 # A warning doesn't fail lint.
64
65 return success
66
67
68def lint_commit_message(commit):
69 success = True
70 lines = commit.message.splitlines()
71
72 # Check length of summary line.
73 summary_line_len = len(lines[0])
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010074 if summary_line_len > COMMIT_MSG_MAX_SUMMARY_LEN:
Philipp Wagner1e643552019-09-04 15:39:14 +010075 error(
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010076 "The summary line in the commit message is %d characters long; "
Philipp Wagner1e643552019-09-04 15:39:14 +010077 "only %d characters are allowed." %
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010078 (summary_line_len, COMMIT_MSG_MAX_SUMMARY_LEN), commit)
Philipp Wagner1e643552019-09-04 15:39:14 +010079 success = False
80
81 # Check for an empty line separating the summary line from the long
82 # description.
83 if len(lines) > 1 and lines[1] != "":
84 error(
85 "The second line of a commit message must be empty, as it "
86 "separates the summary from the long description.", commit)
87 success = False
Alex Bradbury6d091ec2019-11-04 14:24:31 +000088
Rupert Swarbrickbf5ca972020-08-19 10:42:40 +010089 # Check that the commit message contains at least one Signed-off-by line
90 # that matches the author name and email. There might be other signoffs (if
91 # there are multiple authors). We don't have any requirements about those
92 # at the moment and just pass them through.
93 signoff_lines = []
94 signoff_pfx = 'Signed-off-by: '
95 for idx, line in enumerate(lines):
96 if not line.startswith(signoff_pfx):
97 continue
98
99 signoff_body = line[len(signoff_pfx):]
100 match = re.match(r'[^<]+ <[^>]*>$', signoff_body)
101 if match is None:
102 error('Commit has Signed-off-by line {!r}, but the second part '
103 'is not of the required form. It should be of the form '
104 '"Signed-off-by: NAME <EMAIL>".'
105 .format(line))
106 success = False
107
108 signoff_lines.append(line)
109
110 expected_signoff_line = ("Signed-off-by: {} <{}>"
111 .format(commit.author.name,
112 commit.author.email))
113 signoff_req_msg = ('The commit message must contain a Signed-off-by line '
114 'that matches the commit author name and email, '
115 'indicating agreement to the Contributor License '
116 'Agreement. See CONTRIBUTING.md for more details. '
Philipp Wagner0949e6d2021-03-08 17:01:01 +0000117 'You can use "git commit --signoff" to ask git to add '
118 'this line for you.')
Rupert Swarbrickbf5ca972020-08-19 10:42:40 +0100119
120 if not signoff_lines:
121 error('Commit has no Signed-off-by line. ' + signoff_req_msg)
122 success = False
123 elif expected_signoff_line not in signoff_lines:
124 error(('Commit has one or more Signed-off-by lines, but not the one '
125 'we expect. We expected to find "{}". '
126 .format(expected_signoff_line)) +
127 signoff_req_msg)
Alex Bradbury6d091ec2019-11-04 14:24:31 +0000128 success = False
129
Philipp Wagner1e643552019-09-04 15:39:14 +0100130 return success
131
132
133def lint_commit(commit):
134 success = True
135 if not lint_commit_author(commit):
136 success = False
137 if not lint_commit_message(commit):
138 success = False
139 return success
lowRISC Contributors802543a2019-08-31 12:12:56 +0100140
141
142def main():
Philipp Wagner1e643552019-09-04 15:39:14 +0100143 global error_msg_prefix
144 global warning_msg_prefix
145
lowRISC Contributors802543a2019-08-31 12:12:56 +0100146 parser = argparse.ArgumentParser(
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +0100147 description='Check commit metadata for common mistakes')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100148 parser.add_argument('--error-msg-prefix',
Philipp Wagner1e643552019-09-04 15:39:14 +0100149 default=error_msg_prefix,
lowRISC Contributors802543a2019-08-31 12:12:56 +0100150 required=False,
151 help='string to prepend to all error messages')
Philipp Wagner1e643552019-09-04 15:39:14 +0100152 parser.add_argument('--warning-msg-prefix',
153 default=warning_msg_prefix,
154 required=False,
155 help='string to prepend to all warning messages')
156 parser.add_argument('--no-merges',
157 required=False,
158 action="store_true",
159 help='do not check commits with more than one parent')
160 parser.add_argument('commit_range',
lowRISC Contributors802543a2019-08-31 12:12:56 +0100161 metavar='commit-range',
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +0100162 help=('commit range to check '
163 '(must be understood by git log)'))
lowRISC Contributors802543a2019-08-31 12:12:56 +0100164 args = parser.parse_args()
165
lowRISC Contributors802543a2019-08-31 12:12:56 +0100166 error_msg_prefix = args.error_msg_prefix
Philipp Wagner1e643552019-09-04 15:39:14 +0100167 warning_msg_prefix = args.warning_msg_prefix
lowRISC Contributors802543a2019-08-31 12:12:56 +0100168
Philipp Wagner1e643552019-09-04 15:39:14 +0100169 lint_successful = True
lowRISC Contributors802543a2019-08-31 12:12:56 +0100170
Philipp Wagner1e643552019-09-04 15:39:14 +0100171 repo = Repo()
172 commits = repo.iter_commits(args.commit_range)
173 for commit in commits:
174 print("Checking commit %s" % commit.hexsha)
175 is_merge = len(commit.parents) > 1
176 if is_merge and args.no_merges:
177 print("Skipping merge commit.")
Philipp Wagnerdfd6ac62019-09-05 10:51:00 +0100178 continue
179
Philipp Wagner1e643552019-09-04 15:39:14 +0100180 if not lint_commit(commit):
181 lint_successful = False
lowRISC Contributors802543a2019-08-31 12:12:56 +0100182
Philipp Wagner1e643552019-09-04 15:39:14 +0100183 if not lint_successful:
lowRISC Contributors802543a2019-08-31 12:12:56 +0100184 error('Commit lint failed.')
185 sys.exit(1)
186
187
188if __name__ == '__main__':
189 main()