f3b10a5e72
Phase 0 of the implementer-parity plan: rename the formerly review-only
pre-clone + diff-fetch helpers so the implementer dispatcher can share
them in upcoming phases without forking the substrate or hauling
review-specific naming into a different work-group registry.
Renames (git rename-detected):
- tools/_review_clone.py -> tools/_pr_clone.py
- tools/_review_diff.py -> tools/_pr_diff.py
Extractions (new shared modules):
- tools/_pr_prompt.py UNTRUSTED-CONTENT marker helpers
(fence_markers, redact_marker,
wrap_untrusted_section) for Phase 2
pre-fetch consumers.
- tools/_commit_lint.py lint_commit_message + CONVENTIONAL_TYPES
+ _bot_committer_email so both
self-validation CLIs reuse one lint.
- tools/_validate_cli_common.py DiffResult / DiffErrorKind /
diff_from_worktree /
commit_from_worktree / _excerpt_*
/ resolve_base_ref / emit_error so
implementer_validate (Phase 1) does
not duplicate ~280 lines of CLI
plumbing.
Slim:
- tools/_review_validate_helpers.py shrinks 499 -> 223 lines and now
owns only review-specific validate_position_in_diff +
draft_strict_checks. Re-exports the moved helpers so existing test
monkeypatches (helpers.diff_from_worktree, helpers.subprocess) keep
working without churn.
API surface change:
- prepare_pr_worktree(cfg, n, sha, *, kind="review") -- back-compat
default; implementer dispatcher passes kind="implementer" in Phase 3.
- _worktree_base / _is_preclone_disabled now consult per-kind env
vars (REVIEW_DISPATCHER_WORKTREE_BASE vs.
IMPLEMENTER_DISPATCHER_WORKTREE_BASE; matching DISABLE_PRECLONE
toggles). Mirror is shared per repo regardless of kind.
- WorktreeHandle gains a `kind` field (defaulted) so cleanup paths
can discriminate.
Bug fix discovered along the way:
- _validate_cli_common.emit_error froze stream=sys.stdout at
function-definition time, which made pytest's capsys invisible to
the JSON error output. Now resolves sys.stdout at call time.
Tests:
- New tests/auto_agents/test_shared_substrate.py (14 tests) covers:
every public symbol the legacy modules exposed, helper-re-export
identity preservation, per-kind worktree-base + disable-toggle
semantics, the kind back-compat default, and the new _pr_prompt
fence/redact helpers. Also guards against the deleted
_review_clone.py / _review_diff.py reappearing on disk.
- All 24 importing call-sites updated; full reviewer test suite
remains green (637 passed, 3 skipped).
- `git grep '_review_clone\|_review_diff' tools/` returns zero
matches, the plan's Phase 0 exit criterion.
Sets up Phase 1 (implementer-helpers skill), Phase 2 (pre-fetch
parity), and Phase 3 (pre-cloned implementer worktrees) with no
shared reviewer-only code paths.
ISSUES CLOSED: #N/A
Co-authored-by: Cursor <cursoragent@cursor.com>
145 lines
5.1 KiB
Python
145 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",
|
|
)
|