blob: 437f13bab28819473298cf60bbc2fe86a472c1c0 [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(
39 'Commit author has no valid email address set: %s. '
40 'Use "git config user.email user@example.com" to '
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010041 'set a valid email address, then update the commit '
Philipp Wagner1e643552019-09-04 15:39:14 +010042 'with "git rebase -i" and/or '
43 '"git commit --amend --reset-author". '
44 'Also check your GitHub settings at '
45 'https://github.com/settings/emails: your email address '
46 'must be verified, and the option "Keep my email address '
47 'private" must be disabled.' % (commit.author.email, ), commit)
48 success = False
49
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010050 if ' ' not in commit.author.name:
Philipp Wagner1e643552019-09-04 15:39:14 +010051 warning(
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010052 'The commit author name "%s" contains no space. '
Philipp Wagner1e643552019-09-04 15:39:14 +010053 'Use "git config user.name \'Johnny English\'" to '
54 'set your real name, and update the commit with "git rebase -i " '
55 'and/or "git commit --amend --reset-author".' %
56 (commit.author.name, ), commit)
57 # A warning doesn't fail lint.
58
59 return success
60
61
62def lint_commit_message(commit):
63 success = True
64 lines = commit.message.splitlines()
65
66 # Check length of summary line.
67 summary_line_len = len(lines[0])
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010068 if summary_line_len > COMMIT_MSG_MAX_SUMMARY_LEN:
Philipp Wagner1e643552019-09-04 15:39:14 +010069 error(
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010070 "The summary line in the commit message is %d characters long; "
Philipp Wagner1e643552019-09-04 15:39:14 +010071 "only %d characters are allowed." %
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +010072 (summary_line_len, COMMIT_MSG_MAX_SUMMARY_LEN), commit)
Philipp Wagner1e643552019-09-04 15:39:14 +010073 success = False
74
75 # Check for an empty line separating the summary line from the long
76 # description.
77 if len(lines) > 1 and lines[1] != "":
78 error(
79 "The second line of a commit message must be empty, as it "
80 "separates the summary from the long description.", commit)
81 success = False
Alex Bradbury6d091ec2019-11-04 14:24:31 +000082
Rupert Swarbrickbf5ca972020-08-19 10:42:40 +010083 # Check that the commit message contains at least one Signed-off-by line
84 # that matches the author name and email. There might be other signoffs (if
85 # there are multiple authors). We don't have any requirements about those
86 # at the moment and just pass them through.
87 signoff_lines = []
88 signoff_pfx = 'Signed-off-by: '
89 for idx, line in enumerate(lines):
90 if not line.startswith(signoff_pfx):
91 continue
92
93 signoff_body = line[len(signoff_pfx):]
94 match = re.match(r'[^<]+ <[^>]*>$', signoff_body)
95 if match is None:
96 error('Commit has Signed-off-by line {!r}, but the second part '
97 'is not of the required form. It should be of the form '
98 '"Signed-off-by: NAME <EMAIL>".'
99 .format(line))
100 success = False
101
102 signoff_lines.append(line)
103
104 expected_signoff_line = ("Signed-off-by: {} <{}>"
105 .format(commit.author.name,
106 commit.author.email))
107 signoff_req_msg = ('The commit message must contain a Signed-off-by line '
108 'that matches the commit author name and email, '
109 'indicating agreement to the Contributor License '
110 'Agreement. See CONTRIBUTING.md for more details. '
111 'You can use "git commit -s" to ask git to add this '
112 'line for you.')
113
114 if not signoff_lines:
115 error('Commit has no Signed-off-by line. ' + signoff_req_msg)
116 success = False
117 elif expected_signoff_line not in signoff_lines:
118 error(('Commit has one or more Signed-off-by lines, but not the one '
119 'we expect. We expected to find "{}". '
120 .format(expected_signoff_line)) +
121 signoff_req_msg)
Alex Bradbury6d091ec2019-11-04 14:24:31 +0000122 success = False
123
Philipp Wagner1e643552019-09-04 15:39:14 +0100124 return success
125
126
127def lint_commit(commit):
128 success = True
129 if not lint_commit_author(commit):
130 success = False
131 if not lint_commit_message(commit):
132 success = False
133 return success
lowRISC Contributors802543a2019-08-31 12:12:56 +0100134
135
136def main():
Philipp Wagner1e643552019-09-04 15:39:14 +0100137 global error_msg_prefix
138 global warning_msg_prefix
139
lowRISC Contributors802543a2019-08-31 12:12:56 +0100140 parser = argparse.ArgumentParser(
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +0100141 description='Check commit metadata for common mistakes')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100142 parser.add_argument('--error-msg-prefix',
Philipp Wagner1e643552019-09-04 15:39:14 +0100143 default=error_msg_prefix,
lowRISC Contributors802543a2019-08-31 12:12:56 +0100144 required=False,
145 help='string to prepend to all error messages')
Philipp Wagner1e643552019-09-04 15:39:14 +0100146 parser.add_argument('--warning-msg-prefix',
147 default=warning_msg_prefix,
148 required=False,
149 help='string to prepend to all warning messages')
150 parser.add_argument('--no-merges',
151 required=False,
152 action="store_true",
153 help='do not check commits with more than one parent')
154 parser.add_argument('commit_range',
lowRISC Contributors802543a2019-08-31 12:12:56 +0100155 metavar='commit-range',
Rupert Swarbrick32fcf7c2020-08-19 10:23:12 +0100156 help=('commit range to check '
157 '(must be understood by git log)'))
lowRISC Contributors802543a2019-08-31 12:12:56 +0100158 args = parser.parse_args()
159
lowRISC Contributors802543a2019-08-31 12:12:56 +0100160 error_msg_prefix = args.error_msg_prefix
Philipp Wagner1e643552019-09-04 15:39:14 +0100161 warning_msg_prefix = args.warning_msg_prefix
lowRISC Contributors802543a2019-08-31 12:12:56 +0100162
Philipp Wagner1e643552019-09-04 15:39:14 +0100163 lint_successful = True
lowRISC Contributors802543a2019-08-31 12:12:56 +0100164
Philipp Wagner1e643552019-09-04 15:39:14 +0100165 repo = Repo()
166 commits = repo.iter_commits(args.commit_range)
167 for commit in commits:
168 print("Checking commit %s" % commit.hexsha)
169 is_merge = len(commit.parents) > 1
170 if is_merge and args.no_merges:
171 print("Skipping merge commit.")
Philipp Wagnerdfd6ac62019-09-05 10:51:00 +0100172 continue
173
Philipp Wagner1e643552019-09-04 15:39:14 +0100174 if not lint_commit(commit):
175 lint_successful = False
lowRISC Contributors802543a2019-08-31 12:12:56 +0100176
Philipp Wagner1e643552019-09-04 15:39:14 +0100177 if not lint_successful:
lowRISC Contributors802543a2019-08-31 12:12:56 +0100178 error('Commit lint failed.')
179 sys.exit(1)
180
181
182if __name__ == '__main__':
183 main()