0bc734c020
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>
146 lines
5.1 KiB
Python
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",
|
|
)
|