#!/usr/bin/env python3

import re
import subprocess
import sys

verbosity = 0  # Show what's going on, 0 1 or 2.
suggestions = 1  # Set to 0 to not include lengthy suggestions in error messages.


def verbose(*args):
    if verbosity:
        print(*args)


def very_verbose(*args):
    if verbosity > 1:
        print(*args)


def git_log(pretty_format, *args):
    # Delete pretty argument from user args so it doesn't interfere with what we do.
    args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
    args.append("--pretty=format:" + pretty_format)
    very_verbose("git_log", *args)
    # Generator yielding each output line.
    for line in subprocess.Popen(args, stdout=subprocess.PIPE).stdout:
        yield line.decode().rstrip("\r\n")


def verify(sha):
    verbose("verify", sha)
    errors = []
    warnings = []

    def error_text(err):
        return "commit " + sha + ": " + err

    def error(err):
        errors.append(error_text(err))

    def warning(err):
        warnings.append(error_text(err))

    # Author and committer email.
    for line in git_log("%ae%n%ce", sha, "-n1"):
        very_verbose("email", line)
        if "noreply" in line:
            error("Unwanted email address: " + line)

    # Message body.
    raw_body = list(git_log("%B", sha, "-n1"))
    if not raw_body:
        error("Message is empty")
        return errors, warnings

    # Subject line.
    subject_line = raw_body[0]
    very_verbose("subject_line", subject_line)
    if not re.match(r"^[^!]+: [A-Z]+.+ .+\.$", subject_line):
        error("Subject line should contain ': ' and end in '.': " + subject_line)
    if len(subject_line) >= 73:
        error("Subject line should be 72 or less characters: " + subject_line)

    # Second one divides subject and body.
    if len(raw_body) > 1 and raw_body[1]:
        error("Second message line should be empty: " + raw_body[1])

    # Message body lines.
    for line in raw_body[2:]:
        if len(line) >= 76:
            error("Message lines should be 75 or less characters: " + line)

    if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
        warning("Message should be signed-off")

    return errors, warnings


def run(args):
    verbose("run", *args)
    has_errors = False
    has_warnings = False
    for sha in git_log("%h", *args):
        errors, warnings = verify(sha)
        has_errors |= any(errors)
        has_warnings |= any(warnings)
        for err in errors:
            print("error:", err)
        for err in warnings:
            print("warning:", err)
    if has_errors or has_warnings:
        if suggestions:
            print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
    else:
        print("ok")
    if has_errors:
        sys.exit(1)


def show_help():
    print("usage: verifygitlog.py [-v -n -h] ...")
    print("-v  : increase verbosity, can be speficied multiple times")
    print("-n  : do not print multi-line suggestions")
    print("-h  : print this help message and exit")
    print("... : arguments passed to git log to retrieve commits to verify")
    print("      see https://www.git-scm.com/docs/git-log")
    print("      passing no arguments at all will verify all commits")
    print("examples:")
    print("verifygitlog.py -n10  # Check last 10 commits")
    print("verifygitlog.py -v master..HEAD  # Check commits since master")


if __name__ == "__main__":
    args = sys.argv[1:]
    verbosity = args.count("-v")
    suggestions = args.count("-n") == 0
    if "-h" in args:
        show_help()
    else:
        args = [arg for arg in args if arg not in ["-v", "-n", "-h"]]
        run(args)

Add script for verifying commit message format.