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( |
Philipp Wagner | 0949e6d | 2021-03-08 17:01:01 +0000 | [diff] [blame] | 39 | f'Commit author has no valid email address set: ' |
| 40 | '{commit.author.email!r}. ' |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 41 | 'Use "git config user.email user@example.com" to ' |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 42 | 'set a valid email address, then update the commit ' |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 43 | 'with "git rebase -i" and/or ' |
Philipp Wagner | 0949e6d | 2021-03-08 17:01:01 +0000 | [diff] [blame] | 44 | '"git commit --amend --signoff --reset-author". ' |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 45 | '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 Wagner | 0949e6d | 2021-03-08 17:01:01 +0000 | [diff] [blame] | 48 | '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 Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 52 | success = False |
| 53 | |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 54 | if ' ' not in commit.author.name: |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 55 | warning( |
Philipp Wagner | 0949e6d | 2021-03-08 17:01:01 +0000 | [diff] [blame] | 56 | f'The commit author name {commit.author.name!r} contains no space. ' |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 57 | 'Use "git config user.name \'Johnny English\'" to ' |
| 58 | 'set your real name, and update the commit with "git rebase -i " ' |
Philipp Wagner | 0949e6d | 2021-03-08 17:01:01 +0000 | [diff] [blame] | 59 | '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 Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 63 | # A warning doesn't fail lint. |
| 64 | |
| 65 | return success |
| 66 | |
| 67 | |
| 68 | def 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 Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 74 | if summary_line_len > COMMIT_MSG_MAX_SUMMARY_LEN: |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 75 | error( |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 76 | "The summary line in the commit message is %d characters long; " |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 77 | "only %d characters are allowed." % |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 78 | (summary_line_len, COMMIT_MSG_MAX_SUMMARY_LEN), commit) |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 79 | 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 Bradbury | 6d091ec | 2019-11-04 14:24:31 +0000 | [diff] [blame] | 88 | |
Rupert Swarbrick | bf5ca97 | 2020-08-19 10:42:40 +0100 | [diff] [blame] | 89 | # 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 Wagner | 0949e6d | 2021-03-08 17:01:01 +0000 | [diff] [blame] | 117 | 'You can use "git commit --signoff" to ask git to add ' |
| 118 | 'this line for you.') |
Rupert Swarbrick | bf5ca97 | 2020-08-19 10:42:40 +0100 | [diff] [blame] | 119 | |
| 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 Bradbury | 6d091ec | 2019-11-04 14:24:31 +0000 | [diff] [blame] | 128 | success = False |
| 129 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 130 | return success |
| 131 | |
| 132 | |
| 133 | def 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 Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 140 | |
| 141 | |
| 142 | def main(): |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 143 | global error_msg_prefix |
| 144 | global warning_msg_prefix |
| 145 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 146 | parser = argparse.ArgumentParser( |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 147 | description='Check commit metadata for common mistakes') |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 148 | parser.add_argument('--error-msg-prefix', |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 149 | default=error_msg_prefix, |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 150 | required=False, |
| 151 | help='string to prepend to all error messages') |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 152 | 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 Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 161 | metavar='commit-range', |
Rupert Swarbrick | 32fcf7c | 2020-08-19 10:23:12 +0100 | [diff] [blame] | 162 | help=('commit range to check ' |
| 163 | '(must be understood by git log)')) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 164 | args = parser.parse_args() |
| 165 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 166 | error_msg_prefix = args.error_msg_prefix |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 167 | warning_msg_prefix = args.warning_msg_prefix |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 168 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 169 | lint_successful = True |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 170 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 171 | 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 Wagner | dfd6ac6 | 2019-09-05 10:51:00 +0100 | [diff] [blame] | 178 | continue |
| 179 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 180 | if not lint_commit(commit): |
| 181 | lint_successful = False |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 182 | |
Philipp Wagner | 1e64355 | 2019-09-04 15:39:14 +0100 | [diff] [blame] | 183 | if not lint_successful: |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 184 | error('Commit lint failed.') |
| 185 | sys.exit(1) |
| 186 | |
| 187 | |
| 188 | if __name__ == '__main__': |
| 189 | main() |