commit-checker.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import subprocess
  2. import re
  3. import sys
  4. import argparse
  5. from typing import Match
  6. # Conventional commit pattern
  7. CONVENTIONAL_COMMIT_PATTERN: str = r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}"
  8. def get_commit_message(commit_hash: str) -> str:
  9. """Get the commit message for a given commit hash."""
  10. try:
  11. result: subprocess.CompletedProcess = subprocess.run(
  12. ["git", "show", "-s", "--format=%B", commit_hash],
  13. capture_output=True,
  14. text=True,
  15. check=True,
  16. )
  17. return result.stdout.strip()
  18. except subprocess.CalledProcessError as e:
  19. print(f"Error retrieving commit message: {e}")
  20. sys.exit(1)
  21. def check_commit_message(
  22. message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN
  23. ) -> 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(
  45. {"hash": commit_hash, "message": message.split("\n")[0]}
  46. )
  47. return non_compliant
  48. except subprocess.CalledProcessError as e:
  49. print(f"Error checking commit range: {e}")
  50. sys.exit(1)
  51. def main() -> None:
  52. parser: argparse.ArgumentParser = argparse.ArgumentParser(
  53. description="Check conventional commit compliance"
  54. )
  55. parser.add_argument(
  56. "--base", required=True, help="Base ref (starting commit, exclusive)"
  57. )
  58. parser.add_argument(
  59. "--head", required=True, help="Head ref (ending commit, inclusive)"
  60. )
  61. args: argparse.Namespace = parser.parse_args()
  62. non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)
  63. if non_compliant:
  64. print("The following commits do not follow the conventional commit format:")
  65. for commit in non_compliant:
  66. print(f"- {commit['hash'][:8]}: {commit['message']}")
  67. print("\nPlease ensure your commit messages follow the format:")
  68. print("type(scope): subject")
  69. print(
  70. "\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
  71. )
  72. sys.exit(1)
  73. else:
  74. print("All commits follow the conventional commit format!")
  75. sys.exit(0)
  76. if __name__ == "__main__":
  77. main()