commit-checker.py 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import subprocess
  2. import re
  3. import sys
  4. import argparse
  5. from typing import Match
  6. # Conventional commit pattern (including Git revert messages)
  7. CONVENTIONAL_COMMIT_PATTERN: str = (
  8. r"^((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}|Revert .+)"
  9. )
  10. def get_commit_message(commit_hash: str) -> str:
  11. """Get the commit message for a given commit hash."""
  12. try:
  13. result: subprocess.CompletedProcess = subprocess.run(
  14. ["git", "show", "-s", "--format=%B", commit_hash],
  15. capture_output=True,
  16. text=True,
  17. check=True,
  18. )
  19. return result.stdout.strip()
  20. except subprocess.CalledProcessError as e:
  21. print(f"Error retrieving commit message: {e}")
  22. sys.exit(1)
  23. def check_commit_message(message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN) -> bool:
  24. """Check if commit message follows conventional commit format."""
  25. first_line: str = message.split("\n")[0]
  26. match: Match[str] | None = re.match(pattern, first_line)
  27. return bool(match)
  28. def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]:
  29. """Check all commits in a range for compliance."""
  30. try:
  31. result: subprocess.CompletedProcess = subprocess.run(
  32. ["git", "log", "--format=%H", f"{base_ref}..{head_ref}"],
  33. capture_output=True,
  34. text=True,
  35. check=True,
  36. )
  37. commit_hashes: list[str] = result.stdout.strip().split("\n")
  38. # Filter out empty lines
  39. commit_hashes = [hash for hash in commit_hashes if hash]
  40. non_compliant: list[dict[str, str]] = []
  41. for commit_hash in commit_hashes:
  42. message: str = get_commit_message(commit_hash)
  43. if not check_commit_message(message):
  44. non_compliant.append({"hash": commit_hash, "message": message.split("\n")[0]})
  45. return non_compliant
  46. except subprocess.CalledProcessError as e:
  47. print(f"Error checking commit range: {e}")
  48. sys.exit(1)
  49. def main() -> None:
  50. parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Check conventional commit compliance")
  51. parser.add_argument("--base", required=True, help="Base ref (starting commit, exclusive)")
  52. parser.add_argument("--head", required=True, help="Head ref (ending commit, inclusive)")
  53. args: argparse.Namespace = parser.parse_args()
  54. non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)
  55. if non_compliant:
  56. print("The following commits do not follow the conventional commit format:")
  57. for commit in non_compliant:
  58. print(f"- {commit['hash'][:8]}: {commit['message']}")
  59. print("\nPlease ensure your commit messages follow the format:")
  60. print("type(scope): subject")
  61. print("\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test")
  62. sys.exit(1)
  63. else:
  64. print("All commits follow the conventional commit format!")
  65. sys.exit(0)
  66. if __name__ == "__main__":
  67. main()