Files
hermes-agent/scripts/releases/authors.py
ethernet e78e3a77be refactor(release): move the author map out of release.py
release.py was 2,804 lines, 2,076 of them the frozen legacy author dict.
The map and its resolver now live in scripts/releases/: authors.py holds
the directory loader, the merged AUTHOR_MAP and resolve_author, and
authors_legacy.py holds only the frozen LEGACY_AUTHOR_MAP literal (same
1,895 entries, same order). release.py drops to 707 lines.

The contributor-check job and audit_pr_attribution.py grep the legacy
file for quoted emails, so both now read authors_legacy.py. The
importers (contributor_audit.py, add_contributor.py), contributors/README
and the tests read the defining modules. No behaviour change.
2026-09-24 14:08:22 -04:00

70 lines
2.6 KiB
Python

"""Map commit authors to GitHub logins for release notes and attribution checks.
``AUTHOR_MAP`` merges the frozen ``LEGACY_AUTHOR_MAP`` (authors_legacy.py) with
the one-file-per-email entries under ``contributors/emails/``; the directory
wins on duplicates.
"""
from __future__ import annotations
import re
from pathlib import Path
from scripts.releases.authors_legacy import LEGACY_AUTHOR_MAP
REPO_ROOT = Path(__file__).resolve().parents[2]
# ──────────────────────────────────────────────────────────────────────
# Directory-based mappings: contributors/emails/<email> → login
# ──────────────────────────────────────────────────────────────────────
CONTRIBUTORS_EMAILS_DIR = REPO_ROOT / "contributors" / "emails"
def _load_contributor_dir(directory: "Path | None" = None) -> dict:
"""Load one-file-per-email mappings from contributors/emails/.
Filename = commit-author email, first non-comment line = GitHub login.
Additions never merge-conflict (each mapping is a distinct file), which
is why new entries go here instead of the frozen LEGACY_AUTHOR_MAP.
"""
directory = directory or CONTRIBUTORS_EMAILS_DIR
mapping = {}
if not directory.is_dir():
return mapping
for path in sorted(directory.iterdir()):
if not path.is_file() or path.name.startswith("."):
continue
try:
for line in path.read_text(encoding="utf-8-sig").splitlines():
line = line.strip()
if line and not line.startswith("#"):
mapping[path.name] = line.lstrip("@")
break
except OSError:
continue
return mapping
# Effective map: frozen legacy dict + directory entries (directory wins).
AUTHOR_MAP = {**LEGACY_AUTHOR_MAP, **_load_contributor_dir()}
def resolve_author(name: str, email: str) -> str:
"""Resolve a git author to a GitHub @mention."""
# Try email lookup first
gh_user = AUTHOR_MAP.get(email)
if gh_user:
return f"@{gh_user}"
# Try noreply pattern
noreply_match = re.match(r"(\d+)\+(.+)@users\.noreply\.github\.com", email)
if noreply_match:
return f"@{noreply_match.group(2)}"
# Try username@users.noreply.github.com
noreply_match2 = re.match(r"(.+)@users\.noreply\.github\.com", email)
if noreply_match2:
return f"@{noreply_match2.group(1)}"
# Fallback to git name
return name