Files
cleveragents-core/tools/_commit_lint.py
T
drew 0bc734c020 style: ruff format the controller-state-machine branch (288 files)
Applies `ruff format` to the accumulated formatting debt on this branch.
Formatting-only — no behavioral changes. Required for CI/lint's format
gate (`nox -s format -- --check`), which the branch was failing on 288
tracked files that drifted from ruff's canonical style.

In-progress WIP files are intentionally excluded so this commit stays a
clean formatting-only diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:09:17 -04:00

146 lines
5.1 KiB
Python

"""Conventional-commit + ISSUES CLOSED footer linter.
Extracted from :mod:`_review_validate_helpers` in Phase 0 so the
implementer pipeline can reuse the same lint without depending on
reviewer-specific helpers. The implementer's own self-validation
CLI (:mod:`implementer_validate`) and the reviewer's CLI
(:mod:`review_validate`) both call :func:`lint_commit_message` —
keeping the lint here gives them a single source of truth and
matches the project's CONTRIBUTING.md / worker-prompt contract.
What's here:
- :data:`CONVENTIONAL_TYPES` -- whitelist of conventional-commit
type prefixes (``feat`` / ``fix`` / ``refactor`` / ...).
- :func:`lint_commit_message` -- subject-prefix + ISSUES CLOSED
footer + bot-author exemption check. Returns a structured dict
matching the CLI output contract; never raises.
- :func:`_bot_committer_email` -- resolves the bot identity from
``REVIEW_BOT_COMMITTER_EMAIL`` (default
``forgejo@cleverthis.com``). Bot-authored merge commits bypass
every check.
The lint is INTENTIONALLY identical between dispatchers: a draft
that passes the reviewer's lint must also pass the implementer's
lint by construction, so a worker can self-validate against either
CLI and trust the verdict.
"""
from __future__ import annotations
import os
import re
from typing import Any
CONVENTIONAL_TYPES: tuple[str, ...] = (
"feat",
"fix",
"refactor",
"perf",
"docs",
"test",
"build",
"ci",
"chore",
"style",
"revert",
)
_CONVENTIONAL_SUBJECT_RE = re.compile(
r"^(?P<type>" + "|".join(CONVENTIONAL_TYPES) + r")"
r"(?:\([^)]+\))?" # optional scope
r"(?P<bang>!?)" # optional breaking-change marker
r": (?P<rest>.+)$"
)
_ISSUES_CLOSED_RE = re.compile(
r"(?im)^ISSUES CLOSED:\s*#(?P<n>\d+)(?:\s*,\s*#\d+)*\s*$"
)
def _bot_committer_email() -> str:
"""Resolve the bot committer email used for the lint-commit
exemption. Defaults to ``forgejo@cleverthis.com``, overridable
via ``REVIEW_BOT_COMMITTER_EMAIL`` so forks / test deployments
don't have to monkey-patch a private constant. Always
lowercased so the callers' comparison is case-insensitive."""
return os.environ.get(
"REVIEW_BOT_COMMITTER_EMAIL", "forgejo@cleverthis.com"
).lower()
# Module-level constant for back-compat with older tests / callers
# that patch this directly. New code should call
# :func:`_bot_committer_email` at use site so a per-test
# ``monkeypatch.setenv`` flows through without re-importing.
BOT_COMMITTER_EMAIL = _bot_committer_email()
def lint_commit_message(
message: str, committer_email: str, *, is_head: bool
) -> dict[str, Any]:
"""Lint one commit's message + committer.
Returns the same shape the CLIs ultimately serialise to stdout::
{"ok": bool, "exempt_bot": bool, "violations": [{"rule": ..., "details": ...}, ...]}
Bot-authored merge commits (committer email matches
:func:`_bot_committer_email` verbatim) bypass every check —
they are mechanical merge artifacts (umbrella PR titles,
train-merge sub-merges) and were never produced by an
implementer. Per CONTRIBUTING.md / the worker prompt's
Review Checklist category 10 exemption. The bot identity is
overridable via ``REVIEW_BOT_COMMITTER_EMAIL`` for forks /
test deployments.
The ``is_head`` flag activates the ``ISSUES CLOSED: #N`` footer
check; only the head commit is required to carry it (the merge
driver checks the merge commit, the linter must run on HEAD
here because the dispatcher does not produce a merge commit
until the merge supervisor runs much later).
"""
# Resolve at use site so a test's ``monkeypatch.setenv`` (set
# AFTER module import) is honoured without requiring a fresh
# re-import. Both sides are lowercased to make the comparison
# case-insensitive even if a future operator overrides the env
# var to mixed case.
exempt_bot = committer_email.lower() == _bot_committer_email()
violations: list[dict[str, str]] = []
if not exempt_bot:
subject = (message.splitlines() or [""])[0].strip()
if not _CONVENTIONAL_SUBJECT_RE.match(subject):
violations.append(
{
"rule": "subject-prefix",
"details": (
f"subject {subject!r} does not match conventional-commit "
f"format <type>[(scope)][!]: <description>; allowed "
f"types: {', '.join(CONVENTIONAL_TYPES)}"
),
}
)
if is_head and not _ISSUES_CLOSED_RE.search(message):
violations.append(
{
"rule": "issues-closed-footer",
"details": (
"head commit body is missing the required "
"'ISSUES CLOSED: #N' footer (case-insensitive, "
"must be on its own line)"
),
}
)
return {
"ok": not violations,
"exempt_bot": exempt_bot,
"violations": violations,
}
__all__ = (
"BOT_COMMITTER_EMAIL",
"CONVENTIONAL_TYPES",
"_bot_committer_email",
"lint_commit_message",
)