| #!/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( | 
 |             'Commit author has no valid email address set: %s. ' | 
 |             '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 --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.' % (commit.author.email, ), commit) | 
 |         success = False | 
 |  | 
 |     if ' ' not in commit.author.name: | 
 |         warning( | 
 |             'The commit author name "%s" 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 --reset-author".' % | 
 |             (commit.author.name, ), 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 -s" 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() |