#!/usr/bin/env python3

# Check a Git commit message according to the seven rules of a good commit message:
# https://chris.beams.io/posts/git-commit/
import sys


class GitCommitMessage:
    "Represents a parsed Git commit message"

    rules = [
        "Separate subject from body with a blank line",
        "Limit the subject line to 50 characters",
        "Capitalize the subject line",
        "Do not end the subject line with a period",
        "Use the imperative mood in the subject line",
        "Wrap the body at 72 characters",
        "Use the body to explain what and why vs. how",
    ]

    valid_rules = [False, False, False, False, False, False, False]

    def __init__(self, filename=None):
        lines = []

        if filename is not None:
            with open(filename, "r", encoding="utf-8") as f:
                for line in f:
                    if line.startswith(
                        "# ------------------------ >8 ------------------------"
                    ):
                        break
                    if not line.startswith("#"):
                        lines.append(line)

        self.parse_lines(lines)

    def parse_lines(self, lines):
        self.body_lines = []
        self.subject = []

        if not lines or len(lines) == 0:
            return self

        self.subject = lines[0]
        self.subject_words = self.subject.split()
        self.has_subject_body_separator = False

        if len(lines) > 1:
            self.has_subject_body_separator = len(lines[1].strip()) == 0

            if self.has_subject_body_separator:
                self.body_lines = lines[2:]
            else:
                self.body_lines = lines[1:]

        return self

    def check_subject_body_separtor(self):
        "Rule 1: Separate subject from body with a blank line"

        if len(self.body_lines) > 0:
            return self.has_subject_body_separator
        return True

    def check_subject_limit(self):
        "Rule 2: Limit the subject line to 50 characters"
        return len(self.subject.rstrip("\n")) <= 50

    def check_subject_capitalized(self):
        "Rule 3: Capitalize the subject line"
        return len(self.subject) > 0 and self.subject[0].isupper()

    def check_subject_no_period(self):
        "Rule 4: Do not end the subject line with a period"
        return not self.subject.endswith(".")

    common_first_words = [
        "Add",
        "Adjust",
        "Support",
        "Change",
        "Remove",
        "Fix",
        "Print",
        "Track",
        "Refactor",
        "Combine",
        "Release",
        "Set",
        "Stop",
        "Make",
        "Mark",
        "Enable",
        "Check",
        "Exclude",
        "Format",
        "Correct",
    ]

    def check_subject_imperative(self):
        """Rule 5: Use the imperative mood in the subject line.

        We can only check for common mistakes here, like using
        the -ing form of a verb or non-imperative version of
        common verbs
        """

        firstword = self.subject_words[0]

        if firstword.endswith("ing"):
            return False

        for word in self.common_first_words:
            if firstword.startswith(word) and firstword != word:
                return False

        return True

    def check_body_limit(self):
        "Rule 6: Wrap the body at 72 characters"

        if len(self.body_lines) == 0:
            return True

        for line in self.body_lines:
            if len(line.rstrip("\n")) > 72:
                return False

        return True

    def check_body_uses_why(self):
        "Rule 7: Use the body to explain what and why vs. how"
        # Not enforceable
        return True

    rule_funcs = [
        check_subject_body_separtor,
        check_subject_limit,
        check_subject_capitalized,
        check_subject_no_period,
        check_subject_imperative,
        check_body_limit,
        check_body_uses_why,
    ]

    def check_the_seven_rules(self):
        "validates the commit message against the seven rules"

        num_violations = 0

        for i, func in enumerate(self.rule_funcs):
            res = func(self)
            self.valid_rules[i] = res

            if not res:
                num_violations += 1

        if num_violations > 0:
            print()
            print("**** WARNING ****")
            print()
            print(
                "The commit message does not seem to comply with the project's guidelines."
            )
            print('Please try to follow the "Seven rules of a great commit message":')
            print("https://chris.beams.io/posts/git-commit/")
            print()
            print("The following rules are violated:\n")

            for i in range(len(self.rule_funcs)):
                if not self.valid_rules[i]:
                    print(f'\t* Rule {i+1}: "{self.rules[i]}"')

        # Extra sanity checks beyond the seven rules
        if len(self.body_lines) == 0:
            print()
            print("NOTE: the commit message has no body.")
            print("It is recommended to add a body with a description of your")
            print(
                "changes, even if they are small. Explain what and why instead of how:"
            )
            print("https://chris.beams.io/posts/git-commit/#why-not-how")

        if len(self.subject_words) < 3:
            print()
            print("Warning: the subject line has less than three words.")
            print("Consider using a more explanatory subject line.")

        if num_violations > 0:
            print()
            print("Run 'git commit --amend' to change the commit message")

        print()

        return num_violations


def main():
    if len(sys.argv) != 2:
        print("Unexpected number of arguments")
        sys.exit(1)

    msg = GitCommitMessage(sys.argv[1])
    return msg.check_the_seven_rules()


if __name__ == "__main__":
    main()
    # Always exit with success. We could also fail the commit if with
    # a non-zero exit code, but that might be a bit excessive and we'd
    # have to save the failed commit message to a file so that it can
    # be recovered.
    sys.exit(0)
