File: commit-checker.py

package info (click to toggle)
miniflux 2.2.16-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,188 kB
  • sloc: xml: 4,853; javascript: 1,158; sh: 257; makefile: 161
file content (83 lines) | stat: -rw-r--r-- 3,108 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import subprocess
import re
import sys
import argparse
from typing import Match

# Conventional commit pattern (including Git revert messages)
CONVENTIONAL_COMMIT_PATTERN: str = (
    r"^((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}|Revert .+)"
)


def get_commit_message(commit_hash: str) -> str:
    """Get the commit message for a given commit hash."""
    try:
        result: subprocess.CompletedProcess = subprocess.run(
            ["git", "show", "-s", "--format=%B", commit_hash],
            capture_output=True,
            text=True,
            check=True,
        )
        return result.stdout.strip()
    except subprocess.CalledProcessError as e:
        print(f"Error retrieving commit message: {e}")
        sys.exit(1)


def check_commit_message(message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN) -> bool:
    """Check if commit message follows conventional commit format."""
    first_line: str = message.split("\n")[0]
    match: Match[str] | None = re.match(pattern, first_line)
    return bool(match)


def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]:
    """Check all commits in a range for compliance."""
    try:
        result: subprocess.CompletedProcess = subprocess.run(
            ["git", "log", "--format=%H", f"{base_ref}..{head_ref}"],
            capture_output=True,
            text=True,
            check=True,
        )
        commit_hashes: list[str] = result.stdout.strip().split("\n")

        # Filter out empty lines
        commit_hashes = [hash for hash in commit_hashes if hash]

        non_compliant: list[dict[str, str]] = []
        for commit_hash in commit_hashes:
            message: str = get_commit_message(commit_hash)
            if not check_commit_message(message):
                non_compliant.append({"hash": commit_hash, "message": message.split("\n")[0]})

        return non_compliant
    except subprocess.CalledProcessError as e:
        print(f"Error checking commit range: {e}")
        sys.exit(1)


def main() -> None:
    parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Check conventional commit compliance")
    parser.add_argument("--base", required=True, help="Base ref (starting commit, exclusive)")
    parser.add_argument("--head", required=True, help="Head ref (ending commit, inclusive)")
    args: argparse.Namespace = parser.parse_args()

    non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)

    if non_compliant:
        print("The following commits do not follow the conventional commit format:")
        for commit in non_compliant:
            print(f"- {commit['hash'][:8]}: {commit['message']}")
        print("\nPlease ensure your commit messages follow the format:")
        print("type(scope): subject")
        print("\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test")
        sys.exit(1)
    else:
        print("All commits follow the conventional commit format!")
        sys.exit(0)


if __name__ == "__main__":
    main()