#!/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.

ignore_prefixes = []


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


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


class ErrorCollection:
    # Track errors and warnings as the program runs
    def __init__(self):
        self.has_errors = False
        self.has_warnings = False
        self.prefix = ""

    def error(self, text):
        print("error: {}{}".format(self.prefix, text))
        self.has_errors = True

    def warning(self, text):
        print("warning: {}{}".format(self.prefix, text))
        self.has_warnings = True


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 diagnose_subject_line(subject_line, subject_line_format, err):
    err.error("Subject line: " + subject_line)
    if not subject_line.endswith("."):
        err.error('* must end with "."')
    if not re.match(r"^[^!]+: ", subject_line):
        err.error('* must start with "path: "')
    if re.match(r"^[^!]+: *$", subject_line):
        err.error("* must contain a subject after the path.")
    m = re.match(r"^[^!]+: ([a-z][^ ]*)", subject_line)
    if m:
        err.error('* first word of subject ("{}") must be capitalised.'.format(m.group(1)))
    if re.match(r"^[^!]+: [^ ]+$", subject_line):
        err.error("* subject must contain more than one word.")
    err.error("* must match: " + repr(subject_line_format))
    err.error('* Example: "py/runtime: Add support for foo to bar."')


def verify(sha, err):
    verbose("verify", sha)
    err.prefix = "commit " + sha + ": "

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

    # Message body.
    raw_body = list(git_log("%B", sha, "-n1"))
    verify_message_body(raw_body, err)


def verify_message_body(raw_body, err):
    if not raw_body:
        err.error("Message is empty")
        return

    # Subject line.
    subject_line = raw_body[0]
    for prefix in ignore_prefixes:
        if subject_line.startswith(prefix):
            verbose("Skipping ignored commit message")
            return
    very_verbose("subject_line", subject_line)
    subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
    if not re.match(subject_line_format, subject_line):
        diagnose_subject_line(subject_line, subject_line_format, err)
    if len(subject_line) >= 73:
        err.error("Subject line must be 72 or fewer characters: " + subject_line)

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

    # Message body lines.
    for line in raw_body[2:]:
        # Long lines with URLs are exempt from the line length rule.
        if len(line) >= 76 and "://" not in line:
            err.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]:
        err.error('Message must be signed-off. Use "git commit -s".')


def run(args):
    verbose("run", *args)

    err = ErrorCollection()

    if "--check-file" in args:
        filename = args[-1]
        verbose("checking commit message from", filename)
        with open(args[-1]) as f:
            # Remove comment lines as well as any empty lines at the end.
            lines = [line.rstrip("\r\n") for line in f if not line.startswith("#")]
            while not lines[-1]:
                lines.pop()
            verify_message_body(lines, err)
    else:  # Normal operation, pass arguments to git log
        for sha in git_log("%h", *args):
            verify(sha, err)

    if err.has_errors or err.has_warnings:
        if suggestions:
            print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
    else:
        print("ok")
    if err.has_errors:
        sys.exit(1)


def show_help():
    print("usage: verifygitlog.py [-v -n -h --check-file] ...")
    print("-v  : increase verbosity, can be specified multiple times")
    print("-n  : do not print multi-line suggestions")
    print("-h  : print this help message and exit")
    print(
        "--check-file : Pass a single argument which is a file containing a candidate commit message"
    )
    print(
        "--ignore-rebase : Skip checking commits with git rebase autosquash prefixes or WIP as a prefix"
    )
    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 "--ignore-rebase" in args:
        args.remove("--ignore-rebase")
        ignore_prefixes = ["squash!", "fixup!", "amend!", "WIP"]

    if "-h" in args:
        show_help()
    else:
        args = [arg for arg in args if arg not in ["-v", "-n", "-h"]]
        run(args)
