1#!/usr/bin/env python3
2
3import re
4import subprocess
5import sys
6
7verbosity = 0  # Show what's going on, 0 1 or 2.
8suggestions = 1  # Set to 0 to not include lengthy suggestions in error messages.
9
10
11def verbose(*args):
12    if verbosity:
13        print(*args)
14
15
16def very_verbose(*args):
17    if verbosity > 1:
18        print(*args)
19
20
21def git_log(pretty_format, *args):
22    # Delete pretty argument from user args so it doesn't interfere with what we do.
23    args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
24    args.append("--pretty=format:" + pretty_format)
25    very_verbose("git_log", *args)
26    # Generator yielding each output line.
27    for line in subprocess.Popen(args, stdout=subprocess.PIPE).stdout:
28        yield line.decode().rstrip("\r\n")
29
30
31def verify(sha):
32    verbose("verify", sha)
33    errors = []
34    warnings = []
35
36    def error_text(err):
37        return "commit " + sha + ": " + err
38
39    def error(err):
40        errors.append(error_text(err))
41
42    def warning(err):
43        warnings.append(error_text(err))
44
45    # Author and committer email.
46    for line in git_log("%ae%n%ce", sha, "-n1"):
47        very_verbose("email", line)
48        if "noreply" in line:
49            error("Unwanted email address: " + line)
50
51    # Message body.
52    raw_body = list(git_log("%B", sha, "-n1"))
53    if not raw_body:
54        error("Message is empty")
55        return errors, warnings
56
57    # Subject line.
58    subject_line = raw_body[0]
59    very_verbose("subject_line", subject_line)
60    subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
61    if not re.match(subject_line_format, subject_line):
62        error("Subject line should match " + repr(subject_line_format) + ": " + subject_line)
63    if len(subject_line) >= 73:
64        error("Subject line should be 72 or less characters: " + subject_line)
65
66    # Second one divides subject and body.
67    if len(raw_body) > 1 and raw_body[1]:
68        error("Second message line should be empty: " + raw_body[1])
69
70    # Message body lines.
71    for line in raw_body[2:]:
72        if len(line) >= 76:
73            error("Message lines should be 75 or less characters: " + line)
74
75    if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
76        warning("Message should be signed-off")
77
78    return errors, warnings
79
80
81def run(args):
82    verbose("run", *args)
83    has_errors = False
84    has_warnings = False
85    for sha in git_log("%h", *args):
86        errors, warnings = verify(sha)
87        has_errors |= any(errors)
88        has_warnings |= any(warnings)
89        for err in errors:
90            print("error:", err)
91        for err in warnings:
92            print("warning:", err)
93    if has_errors or has_warnings:
94        if suggestions:
95            print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
96    else:
97        print("ok")
98    if has_errors:
99        sys.exit(1)
100
101
102def show_help():
103    print("usage: verifygitlog.py [-v -n -h] ...")
104    print("-v  : increase verbosity, can be speficied multiple times")
105    print("-n  : do not print multi-line suggestions")
106    print("-h  : print this help message and exit")
107    print("... : arguments passed to git log to retrieve commits to verify")
108    print("      see https://www.git-scm.com/docs/git-log")
109    print("      passing no arguments at all will verify all commits")
110    print("examples:")
111    print("verifygitlog.py -n10  # Check last 10 commits")
112    print("verifygitlog.py -v master..HEAD  # Check commits since master")
113
114
115if __name__ == "__main__":
116    args = sys.argv[1:]
117    verbosity = args.count("-v")
118    suggestions = args.count("-n") == 0
119    if "-h" in args:
120        show_help()
121    else:
122        args = [arg for arg in args if arg not in ["-v", "-n", "-h"]]
123        run(args)
124