| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283 |
- 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|security|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()
|