#!/usr/bin/env python3 """Validate commit messages for GRM. Rules: - On feature branches: conventional commits ONLY, must NOT include GRM-N prefix. - On master branch: must follow ': ' pattern, e.g. 'GRM-24: fix: resolve timeout'. """ import re import subprocess # nosec B404 import click from gitea_runner_manager.config import CONVENTIONAL_RE from gitea_runner_manager.i18n import _ MASTER_TASK_ID_RE = re.compile(r"^GRM-\d+:") def first_line(text: str) -> str: return text.split("\n")[0] def get_branch() -> str: try: result = subprocess.run( # nosec ["git", "symbolic-ref", "--short", "HEAD"], capture_output=True, text=True, check=True, ) return result.stdout.strip() except subprocess.CalledProcessError: return "" @click.command() @click.argument("commit_msg_file") @click.option("--branch", default=None, help="Override branch detection (for CI use).") def main(commit_msg_file: str, branch: str | None) -> None: with open(commit_msg_file) as f: msg = f.read().strip() if branch is None: branch = get_branch() subject = first_line(msg) if branch == "master": if not MASTER_TASK_ID_RE.match(subject): raise click.ClickException( _( "Oops! Master branch commits must start with a task ID.\n" " Expected: GRM-N: \n" " Got: {subject}", subject=subject, ) ) remainder = MASTER_TASK_ID_RE.sub("", subject).strip() if not CONVENTIONAL_RE.match(remainder): raise click.ClickException( _( "Oops! Master branch commit must follow conventional format after task ID.\n" " Expected: GRM-N: : \n" " Got: {subject}", subject=subject, ) ) return if MASTER_TASK_ID_RE.match(subject): raise click.ClickException( _( "Oops! Do not include task ID (GRM-N) in feature branch commits.\n" " The task ID will be added automatically on merge via CI." ) ) if not CONVENTIONAL_RE.match(subject): raise click.ClickException( _( "Oops! Commit message must follow conventional commit format.\n" " Expected: : \n" " Got: {subject}\n" " Allowed types: feat, fix, chore, docs, style, refactor,\n" " perf, test, ci, build, revert, BREAKING CHANGE", subject=subject, ) ) if __name__ == "__main__": # pragma: no cover main()