blob: 7c2e47f6ba6707c69e47b3b1b69fa516c1bd58be [file] [log] [blame]
#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
import argparse
import re
import sys
from git import Repo
error_msg_prefix = 'ERROR: '
warning_msg_prefix = 'WARNING: '
# Maximum length of the summary line in the commit message (the first line)
# There is no hard limit, but a typical convention is to keep this line at or
# below 50 characters, with occasional outliers.
COMMIT_MSG_MAX_SUMMARY_LEN = 100
def error(msg, commit=None):
full_msg = msg
if commit:
full_msg = "Commit %s: %s" % (commit.hexsha, msg)
print(error_msg_prefix + full_msg, file=sys.stderr)
def warning(msg, commit=None):
full_msg = msg
if commit:
full_msg = "Commit %s: %s" % (commit.hexsha, msg)
print(warning_msg_prefix + full_msg, file=sys.stderr)
def lint_commit_author(commit):
success = True
if commit.author.email.endswith('users.noreply.github.com'):
error(
f'Commit author has no valid email address set: '
'{commit.author.email!r}. '
'Use "git config user.email user@example.com" to '
'set a valid email address, then update the commit '
'with "git rebase -i" and/or '
'"git commit --amend --signoff --reset-author". '
'Also check your GitHub settings at '
'https://github.com/settings/emails: your email address '
'must be verified, and the option "Keep my email address '
'private" must be disabled. '
'This command will also sign off your commit indicating agreement '
'to the Contributor License Agreement. See CONTRIBUTING.md for '
'more details.', commit)
success = False
if ' ' not in commit.author.name:
warning(
f'The commit author name {commit.author.name!r} contains no space. '
'Use "git config user.name \'Johnny English\'" to '
'set your real name, and update the commit with "git rebase -i " '
'and/or "git commit --amend --signoff --reset-author". '
'This command will also sign off your commit indicating agreement '
'to the Contributor License Agreement. See CONTRIBUTING.md for '
'more details.', commit)
# A warning doesn't fail lint.
return success
def lint_commit_message(commit):
success = True
lines = commit.message.splitlines()
# Check length of summary line.
summary_line_len = len(lines[0])
if summary_line_len > COMMIT_MSG_MAX_SUMMARY_LEN:
error(
"The summary line in the commit message is %d characters long; "
"only %d characters are allowed." %
(summary_line_len, COMMIT_MSG_MAX_SUMMARY_LEN), commit)
success = False
# Check for an empty line separating the summary line from the long
# description.
if len(lines) > 1 and lines[1] != "":
error(
"The second line of a commit message must be empty, as it "
"separates the summary from the long description.", commit)
success = False
# Check that the commit message contains at least one Signed-off-by line
# that matches the author name and email. There might be other signoffs (if
# there are multiple authors). We don't have any requirements about those
# at the moment and just pass them through.
signoff_lines = []
signoff_pfx = 'Signed-off-by: '
for idx, line in enumerate(lines):
if not line.startswith(signoff_pfx):
continue
signoff_body = line[len(signoff_pfx):]
match = re.match(r'[^<]+ <[^>]*>$', signoff_body)
if match is None:
error('Commit has Signed-off-by line {!r}, but the second part '
'is not of the required form. It should be of the form '
'"Signed-off-by: NAME <EMAIL>".'
.format(line))
success = False
signoff_lines.append(line)
expected_signoff_line = ("Signed-off-by: {} <{}>"
.format(commit.author.name,
commit.author.email))
signoff_req_msg = ('The commit message must contain a Signed-off-by line '
'that matches the commit author name and email, '
'indicating agreement to the Contributor License '
'Agreement. See CONTRIBUTING.md for more details. '
'You can use "git commit --signoff" to ask git to add '
'this line for you.')
if not signoff_lines:
error('Commit has no Signed-off-by line. ' + signoff_req_msg)
success = False
elif expected_signoff_line not in signoff_lines:
error(('Commit has one or more Signed-off-by lines, but not the one '
'we expect. We expected to find "{}". '
.format(expected_signoff_line)) +
signoff_req_msg)
success = False
return success
def lint_commit(commit):
success = True
if not lint_commit_author(commit):
success = False
if not lint_commit_message(commit):
success = False
return success
def main():
global error_msg_prefix
global warning_msg_prefix
parser = argparse.ArgumentParser(
description='Check commit metadata for common mistakes')
parser.add_argument('--error-msg-prefix',
default=error_msg_prefix,
required=False,
help='string to prepend to all error messages')
parser.add_argument('--warning-msg-prefix',
default=warning_msg_prefix,
required=False,
help='string to prepend to all warning messages')
parser.add_argument('--no-merges',
required=False,
action="store_true",
help='do not check commits with more than one parent')
parser.add_argument('commit_range',
metavar='commit-range',
help=('commit range to check '
'(must be understood by git log)'))
args = parser.parse_args()
error_msg_prefix = args.error_msg_prefix
warning_msg_prefix = args.warning_msg_prefix
lint_successful = True
repo = Repo()
commits = repo.iter_commits(args.commit_range)
for commit in commits:
print("Checking commit %s" % commit.hexsha)
is_merge = len(commit.parents) > 1
if is_merge and args.no_merges:
print("Skipping merge commit.")
continue
if not lint_commit(commit):
lint_successful = False
if not lint_successful:
error('Commit lint failed.')
sys.exit(1)
if __name__ == '__main__':
main()