lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 1 | #!/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 | |
| 6 | import argparse |
Rupert Swarbrick | bf5ca97 | 2020-08-19 10:42:40 +0100 | [diff] [blame] | 7 | import re |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 8 | import sys |
| 9 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 10 | from git import Repo |
| 11 | |
| 12 | error_msg_prefix = 'ERROR: ' |
| 13 | warning_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 Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 18 | COMMIT_MSG_MAX_SUMMARY_LEN = 100 |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 19 | |
| 20 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 21 | def 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 | |
| 28 | def 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 | |
| 35 | def 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 Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 41 | 'set a valid email address, then update the commit ' |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 42 | '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 Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 50 | if ' ' not in commit.author.name: |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 51 | warning( |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 52 | 'The commit author name "%s" contains no space. ' |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 53 | '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 | |
| 62 | def 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 Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 68 | if summary_line_len > COMMIT_MSG_MAX_SUMMARY_LEN: |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 69 | error( |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 70 | "The summary line in the commit message is %d characters long; " |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 71 | "only %d characters are allowed." % |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 72 | (summary_line_len, COMMIT_MSG_MAX_SUMMARY_LEN), commit) |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 73 | 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 Bradbury | 6d091ec | 2019-11-04 14:24:31 +0000 | [diff] [blame] | 82 | |
Rupert Swarbrick | bf5ca97 | 2020-08-19 10:42:40 +0100 | [diff] [blame] | 83 | # 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 Bradbury | 6d091ec | 2019-11-04 14:24:31 +0000 | [diff] [blame] | 122 | success = False |
| 123 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 124 | return success |
| 125 | |
| 126 | |
| 127 | def 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 Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 134 | |
| 135 | |
| 136 | def main(): |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 137 | global error_msg_prefix |
| 138 | global warning_msg_prefix |
| 139 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 140 | parser = argparse.ArgumentParser( |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 141 | description='Check commit metadata for common mistakes') |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 142 | parser.add_argument('--error-msg-prefix', |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 143 | default=error_msg_prefix, |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 144 | required=False, |
| 145 | help='string to prepend to all error messages') |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 146 | 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 Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 155 | metavar='commit-range', |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 156 | help=('commit range to check ' |
| 157 | '(must be understood by git log)')) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 158 | args = parser.parse_args() |
| 159 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 160 | error_msg_prefix = args.error_msg_prefix |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 161 | warning_msg_prefix = args.warning_msg_prefix |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 162 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 163 | lint_successful = True |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 164 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 165 | 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 Wagner | dfd6ac6 | 2019-09-05 10:51:00 +0100 | [diff] [blame] | 172 | continue |
| 173 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 174 | if not lint_commit(commit): |
| 175 | lint_successful = False |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 176 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 177 | if not lint_successful: |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 178 | error('Commit lint failed.') |
| 179 | sys.exit(1) |
| 180 | |
| 181 | |
| 182 | if __name__ == '__main__': |
| 183 | main() |