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.
70 lines
2.6 KiB
Python
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
|