mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
feat(meta): derive author identity and profile from .mailmap
The README generator read its author URL out of the ## Credits sections it
writes itself. .mailmap now folds every address a person commits under into
their @infinito.nexus one, whose local part names their social profile, and
that profile becomes the single point of truth for an author.
utils/meta/identity.py parses .mailmap rather than shelling out to
git check-mailmap, which refuses the worktree the test container mounts.
Cross-checked against git over all 14 identities in the history: no
disagreement.
A new env handler carries the committing identity into the container, which
mounts the checkout but not the contributor's git configuration. Without it
both new tests would skip exactly where they are meant to fire:
* tests/integration/meta/test_commit_identity.py fails when the configured
address does not fold, and prints the .mailmap line to add.
* tests/external/repository/test_author_profile.py probes that profile
live, behind a canary so an unreachable instance is not reported as a
missing profile.
Both look at the current address only. Probing every declared author would be
red for people who have not created a profile yet, and a suite that is red for
someone else's omission stops being read.
Marko Pjevac is declared as a role author but has no address here, so his
Credits line stays unlinked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
caee86e168
commit
fa015ab432
14 files changed
+481
-46
No files matched your search
@@ -1 +1,7 @@
|
||||
Kevin Veen-Birkenbach <kevin@veen.world> Environment Test <environment-test@example.invalid>
|
||||
Alejandro Roman Ibanez <alexromanibanez@infinito.nexus> <romanibanez.alex@gmail.com>
|
||||
Evangelos Tsakoudis <evangelostsakoudis@infinito.nexus> <baggtsak55@gmail.com>
|
||||
Evangelos Tsakoudis <evangelostsakoudis@infinito.nexus> <tsakoudis.evangelos@hotmail.com>
|
||||
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> <kevin@veen.world>
|
||||
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> <mail@Kevin-Frantz.de>
|
||||
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> Environment Test <environment-test@example.invalid>
|
||||
Prageeth Panicker <prageethpanicker@infinito.nexus> <prage.pani@gmail.com>
|
||||
@@ -28,7 +28,7 @@ from cli.build.docs.readme.cosmos import derive_cosmos_mermaid
|
||||
from cli.build.docs.readme.sections import Readme, parse_readme
|
||||
from utils.cache.files import PROJECT_ROOT, read_text
|
||||
from utils.cache.yaml import load_yaml
|
||||
from utils.roles.credits import author_urls
|
||||
from utils.meta.identity import by_name, profile_url
|
||||
from utils.roles.deploy import role_has_stack
|
||||
from utils.roles.entity.name import get_entity_name
|
||||
from utils.roles.mapping import ROLE_FILE_META_MAIN, ROLE_FILE_README
|
||||
@@ -97,7 +97,7 @@ def _base_context(role_dir, role_name: str, app_name: str, *, invokable: bool) -
|
||||
{"name": "Feature", "description": "Describe a capability."}
|
||||
],
|
||||
"application_author": _role_author(role_dir),
|
||||
"application_author_url": author_urls().get(_role_author(role_dir)),
|
||||
"application_author_url": profile_url(by_name().get(_role_author(role_dir), "")),
|
||||
"application_is_host": not role_has_stack(role_dir),
|
||||
"application_needs_clearnet_resolver": _needs_clearnet_resolver(role_dir),
|
||||
"application_deploy_ids": ",".join(
|
||||
|
||||
@@ -35,8 +35,8 @@ _REQUIRED_CTX: dict = {
|
||||
|
||||
_OPTIONAL_CTX: dict = {
|
||||
"application_invokable": True,
|
||||
"application_author_url": "https://social.infinito.nexus/profile/example/profile",
|
||||
"cosmos_intro": "Example cosmos intro.",
|
||||
"application_author_url": "https://www.veen.world",
|
||||
"application_use_cases": "Example use case.",
|
||||
"application_developer_notes": [{"file": "Administration.md", "description": "notes."}],
|
||||
"application_further_resources": [{"label": "Example", "url": "https://example.com/"}],
|
||||
|
||||
@@ -80,6 +80,9 @@ services:
|
||||
INFINITO_DNS_IP: ${INFINITO_DNS_IP}
|
||||
INFINITO_DOMAIN: ${INFINITO_DOMAIN}
|
||||
INFINITO_GHCR_MIRROR_PREFIX: ${INFINITO_GHCR_MIRROR_PREFIX}
|
||||
INFINITO_GIT_AUTHOR_EMAIL: ${INFINITO_GIT_AUTHOR_EMAIL}
|
||||
INFINITO_GIT_AUTHOR_NAME: ${INFINITO_GIT_AUTHOR_NAME}
|
||||
INFINITO_GPU_COUNT: ${INFINITO_GPU_COUNT}
|
||||
INFINITO_IMAGE_TAG: ${INFINITO_IMAGE_TAG}
|
||||
INFINITO_IP4: ${INFINITO_IP4}
|
||||
INFINITO_IP6: ${INFINITO_IP6}
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
Optional context:
|
||||
application_invokable When true, emit the Quick Setup section
|
||||
(invokable roles only; forbidden otherwise).
|
||||
application_author_url The author's social profile, derived from the
|
||||
address .mailmap folds their commits into.
|
||||
cosmos_intro Sentence above the Cosmos diagram.
|
||||
application_author_url Wraps the Credits author in a Markdown link.
|
||||
application_use_cases Paragraph for the optional Use Cases section.
|
||||
application_developer_notes List of {file, description} local-doc links.
|
||||
application_further_resources List of {label, url} external links.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""The profile this contributor's address names is public and answers.
|
||||
|
||||
``.mailmap`` folds a commit address into an ``@infinito.nexus`` one, whose
|
||||
local part is the slug of a profile on the social instance. That profile is
|
||||
the single point of truth for an author's details, so it has to exist and be
|
||||
readable without logging in.
|
||||
|
||||
Only the address this checkout commits under is probed. Probing every declared
|
||||
author would fail for people who have not created their profile yet, and a
|
||||
suite that is red for someone else's omission stops being read.
|
||||
|
||||
This is an external test because it performs a live HTTP request. A request
|
||||
that never gets an answer is reported as unverified rather than as a missing
|
||||
profile: the run could not reach the instance, so it learned nothing. The
|
||||
canary below separates the two.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from utils.annotations.message import warning
|
||||
from utils.meta.identity import DOMAIN, canonical, committing_as, profile_url
|
||||
|
||||
CANARY = f"https://social.{DOMAIN}/"
|
||||
TIMEOUT = 20
|
||||
READABLE = (200, 301, 302)
|
||||
|
||||
|
||||
def _probe(url: str) -> tuple[int | None, str]:
|
||||
try:
|
||||
response = requests.get(url, timeout=TIMEOUT, allow_redirects=True)
|
||||
except requests.RequestException as exc:
|
||||
return None, str(exc)
|
||||
return response.status_code, response.reason or ""
|
||||
|
||||
|
||||
class TestAuthorProfile(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
name, email = committing_as()
|
||||
if not email:
|
||||
raise unittest.SkipTest("git config user.email is unset here")
|
||||
_name, self.email = canonical(name, email)
|
||||
self.url = profile_url(self.email)
|
||||
if not self.url:
|
||||
raise unittest.SkipTest(
|
||||
f"{email!r} is not mapped to an @{DOMAIN} address yet; "
|
||||
"tests/integration/meta/test_commit_identity.py reports that"
|
||||
)
|
||||
|
||||
def test_the_profile_is_publicly_readable(self) -> None:
|
||||
status, detail = _probe(CANARY)
|
||||
if status is None:
|
||||
raise unittest.SkipTest(f"social.{DOMAIN} is unreachable here: {detail}")
|
||||
|
||||
status, detail = _probe(self.url)
|
||||
if status is None:
|
||||
warning(f"{self.url} was not answered ({detail}); profile unverified")
|
||||
raise unittest.SkipTest("the instance answered the canary but not the probe")
|
||||
|
||||
self.assertIn(
|
||||
status,
|
||||
READABLE,
|
||||
f"{self.url} answered {status} {detail}. Your author details are read "
|
||||
f"from that profile, so create it and make it public. It is derived "
|
||||
f"from {self.email!r}, the address .mailmap folds your commits into.",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""The identity this checkout commits under resolves to an Infinito.Nexus one.
|
||||
|
||||
A contributor may commit under any address, but ``.mailmap`` has to fold it
|
||||
into their ``@infinito.nexus`` one, because that address is what names their
|
||||
public profile and the profile is where an author's details live.
|
||||
|
||||
Only the address configured here is checked. A historic address nobody commits
|
||||
under any more is not this contributor's problem to fix, and failing on it
|
||||
would make the suite red for everyone.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from utils.meta.identity import DOMAIN, MAILMAP, canonical, committing_as, profile_url
|
||||
|
||||
|
||||
class TestCommitIdentity(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.name, self.email = committing_as()
|
||||
if not self.email:
|
||||
raise unittest.SkipTest("git config user.email is unset here")
|
||||
|
||||
def test_the_configured_address_maps_to_an_infinito_nexus_one(self) -> None:
|
||||
name, email = canonical(self.name, self.email)
|
||||
|
||||
self.assertTrue(
|
||||
email.endswith(f"@{DOMAIN}"),
|
||||
f"commits from this checkout carry {self.email!r}, which {MAILMAP} "
|
||||
f"does not fold into an @{DOMAIN} address. Add a line to {MAILMAP}:\n\n"
|
||||
f" {name} <yourname@{DOMAIN}> <{self.email}>\n\n"
|
||||
f"That address names your profile at "
|
||||
f"{profile_url(f'yourname@{DOMAIN}')}, which is where your author "
|
||||
f"details are read from.",
|
||||
)
|
||||
|
||||
def test_the_mapped_address_names_a_profile(self) -> None:
|
||||
_name, email = canonical(self.name, self.email)
|
||||
if not email.endswith(f"@{DOMAIN}"):
|
||||
raise unittest.SkipTest("address is not mapped yet")
|
||||
|
||||
self.assertTrue(
|
||||
profile_url(email),
|
||||
f"{email!r} yields no profile slug; the local part must not be empty",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from utils.meta.authors import author_names
|
||||
from utils.roles.credits import author_urls
|
||||
from utils.cache.files import PROJECT_ROOT
|
||||
from utils.meta.authors import author_names, role_authors
|
||||
from utils.software import SOFTWARE_AUTHOR
|
||||
|
||||
|
||||
@@ -17,14 +17,30 @@ class TestAuthorNames(unittest.TestCase):
|
||||
self.assertEqual(list(names), sorted(names))
|
||||
self.assertEqual(len(names), len(set(names)))
|
||||
|
||||
def test_it_carries_exactly_the_keys_of_the_url_map(self) -> None:
|
||||
def test_every_name_is_declared_by_at_least_one_role(self) -> None:
|
||||
declared = set()
|
||||
for role_dir in sorted((PROJECT_ROOT / "roles").iterdir()):
|
||||
if role_dir.is_dir():
|
||||
declared.update(role_authors(role_dir))
|
||||
|
||||
self.assertEqual(
|
||||
set(author_names()),
|
||||
set(author_urls()),
|
||||
"the names are the map's keys, not a second list that could drift "
|
||||
"from the Credits sections they are derived from",
|
||||
declared,
|
||||
"the names come from galaxy_info.author, not from prose that could "
|
||||
"drift from the role metadata",
|
||||
)
|
||||
|
||||
def test_a_role_crediting_several_people_yields_each_of_them(self) -> None:
|
||||
multi = [
|
||||
names
|
||||
for role_dir in sorted((PROJECT_ROOT / "roles").iterdir())
|
||||
if role_dir.is_dir() and len(names := role_authors(role_dir)) > 1
|
||||
]
|
||||
|
||||
self.assertTrue(multi, "no role credits more than one author any more")
|
||||
for names in multi:
|
||||
self.assertTrue(all(name and not name.startswith(" ") for name in names))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from utils.meta.identity import (
|
||||
DOMAIN,
|
||||
MAILMAP,
|
||||
PROFILE_BASE,
|
||||
by_name,
|
||||
canonical,
|
||||
committing_as,
|
||||
profile_url,
|
||||
)
|
||||
|
||||
MAILMAP_BODY = """\
|
||||
# a comment line
|
||||
Alejandro Roman Ibanez <alexromanibanez@infinito.nexus> <romanibanez.alex@gmail.com>
|
||||
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> <kevin@veen.world>
|
||||
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> Old Name <old@example.com>
|
||||
<prageethpanicker@infinito.nexus> <prage.pani@gmail.com>
|
||||
"""
|
||||
|
||||
|
||||
class TestProfileUrl(unittest.TestCase):
|
||||
def test_the_local_part_names_the_profile(self) -> None:
|
||||
self.assertEqual(
|
||||
profile_url(f"kevinveenbirkenbach@{DOMAIN}"),
|
||||
f"{PROFILE_BASE}/kevinveenbirkenbach/profile",
|
||||
)
|
||||
|
||||
def test_a_foreign_domain_names_no_profile(self) -> None:
|
||||
self.assertEqual(profile_url("someone@example.com"), "")
|
||||
|
||||
def test_the_domain_is_matched_case_insensitively(self) -> None:
|
||||
self.assertTrue(profile_url(f"someone@{DOMAIN.upper()}"))
|
||||
|
||||
def test_an_empty_local_part_names_no_profile(self) -> None:
|
||||
self.assertEqual(profile_url(f"@{DOMAIN}"), "")
|
||||
|
||||
|
||||
class TestMailmap(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="infinito-identity-")
|
||||
self.root = Path(self._tmp.name)
|
||||
(self.root / MAILMAP).write_text(MAILMAP_BODY, encoding="utf-8")
|
||||
self.addCleanup(self._tmp.cleanup)
|
||||
by_name.cache_clear()
|
||||
self.addCleanup(by_name.cache_clear)
|
||||
|
||||
def test_an_alias_folds_into_the_canonical_address(self) -> None:
|
||||
self.assertEqual(
|
||||
canonical("Alejandro Roman", "romanibanez.alex@gmail.com", self.root),
|
||||
("Alejandro Roman Ibanez", "alexromanibanez@infinito.nexus"),
|
||||
)
|
||||
|
||||
def test_an_alias_written_with_a_name_folds_too(self) -> None:
|
||||
self.assertEqual(
|
||||
canonical("Old Name", "old@example.com", self.root)[1],
|
||||
"kevinveenbirkenbach@infinito.nexus",
|
||||
)
|
||||
|
||||
def test_an_entry_without_a_canonical_name_keeps_the_commit_name(self) -> None:
|
||||
self.assertEqual(
|
||||
canonical("Prageeth Panicker", "prage.pani@gmail.com", self.root),
|
||||
("Prageeth Panicker", "prageethpanicker@infinito.nexus"),
|
||||
)
|
||||
|
||||
def test_an_unknown_address_is_returned_unchanged(self) -> None:
|
||||
self.assertEqual(
|
||||
canonical("Nobody", "nobody@example.com", self.root),
|
||||
("Nobody", "nobody@example.com"),
|
||||
)
|
||||
|
||||
def test_the_address_is_matched_case_insensitively(self) -> None:
|
||||
self.assertEqual(
|
||||
canonical("Kevin Veen-Birkenbach", "Kevin@Veen.World", self.root)[1],
|
||||
"kevinveenbirkenbach@infinito.nexus",
|
||||
)
|
||||
|
||||
def test_by_name_reads_the_canonical_column(self) -> None:
|
||||
self.assertEqual(
|
||||
by_name(str(self.root))["Kevin Veen-Birkenbach"],
|
||||
"kevinveenbirkenbach@infinito.nexus",
|
||||
)
|
||||
|
||||
def test_a_missing_mailmap_folds_nothing(self) -> None:
|
||||
empty = Path(self._tmp.name) / "empty"
|
||||
empty.mkdir()
|
||||
|
||||
self.assertEqual(
|
||||
canonical("Nobody", "nobody@example.com", empty),
|
||||
("Nobody", "nobody@example.com"),
|
||||
)
|
||||
|
||||
|
||||
class TestCommittingAs(unittest.TestCase):
|
||||
def test_the_carried_environment_wins_over_git(self) -> None:
|
||||
carried = {
|
||||
"INFINITO_GIT_AUTHOR_NAME": "Carried Name",
|
||||
"INFINITO_GIT_AUTHOR_EMAIL": "carried@example.com",
|
||||
}
|
||||
with mock.patch.dict("os.environ", carried):
|
||||
self.assertEqual(committing_as(), ("Carried Name", "carried@example.com"))
|
||||
|
||||
def test_an_empty_carried_address_falls_through_to_git(self) -> None:
|
||||
with mock.patch.dict("os.environ", {"INFINITO_GIT_AUTHOR_EMAIL": " "}):
|
||||
with mock.patch("subprocess.run") as run:
|
||||
run.return_value = mock.Mock(stdout="from-git\n")
|
||||
self.assertEqual(committing_as(), ("from-git", "from-git"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Vendored
+2
@@ -20,6 +20,7 @@ from .infinito import (
|
||||
distros,
|
||||
docker_volume,
|
||||
domain,
|
||||
git_identity,
|
||||
gpu_count,
|
||||
image,
|
||||
image_repository,
|
||||
@@ -98,6 +99,7 @@ ORDERED_HANDLERS = [
|
||||
fork_repository_url,
|
||||
nix_config,
|
||||
registry_cache_max_size,
|
||||
git_identity,
|
||||
gpu_count,
|
||||
i18n_libretranslate,
|
||||
i18n_tuning,
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"""INFINITO_GIT_AUTHOR_NAME / INFINITO_GIT_AUTHOR_EMAIL: the identity this
|
||||
checkout commits under. The test container mounts the checkout but not the
|
||||
contributor's git configuration, so the suite cannot ask git who is about to
|
||||
commit; the ``.env`` carries the answer across."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from utils.env.builder import BuildContext, EnvBuilder
|
||||
|
||||
NAME_KEY = "INFINITO_GIT_AUTHOR_NAME"
|
||||
EMAIL_KEY = "INFINITO_GIT_AUTHOR_EMAIL"
|
||||
NAME_COMMENT = "Name this checkout commits under; empty when git has none."
|
||||
EMAIL_COMMENT = "Address this checkout commits under; empty when git has none."
|
||||
|
||||
|
||||
def _configured(repo_root, key: str) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repo_root), "config", "--get", key],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError:
|
||||
return ""
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
|
||||
eb.set(NAME_KEY, _configured(ctx.repo_root, "user.name"), comment=NAME_COMMENT)
|
||||
eb.set(EMAIL_KEY, _configured(ctx.repo_root, "user.email"), comment=EMAIL_COMMENT)
|
||||
+35
-9
@@ -1,19 +1,45 @@
|
||||
"""The people the repository credits, as one source for every reader.
|
||||
"""The people the roles name as their authors.
|
||||
|
||||
The names are derived from the ``## Credits`` sections themselves rather than
|
||||
kept as a list, so crediting a contributor needs no code change. The derivation
|
||||
lives in :mod:`utils.roles.credits`; this module is the entry point for readers
|
||||
that want the names alone.
|
||||
``galaxy_info.author`` is where a role records who implemented it, so that is
|
||||
where the names are read. A role may credit more than one person, comma
|
||||
separated.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from utils.roles.credits import author_urls
|
||||
from utils.cache.files import PROJECT_ROOT
|
||||
from utils.cache.yaml import load_yaml
|
||||
from utils.meta.role.names import ROLES_DIR
|
||||
from utils.roles.mapping import ROLE_FILE_META_MAIN
|
||||
|
||||
|
||||
def role_authors(role_dir: Path) -> tuple[str, ...]:
|
||||
"""Return the people ``role_dir`` names as its authors.
|
||||
|
||||
Args:
|
||||
role_dir: the role's directory.
|
||||
"""
|
||||
meta = role_dir / ROLE_FILE_META_MAIN
|
||||
if not meta.is_file():
|
||||
return ()
|
||||
declared = ((load_yaml(str(meta)) or {}).get("galaxy_info") or {}).get("author")
|
||||
if not declared:
|
||||
return ()
|
||||
return tuple(part.strip() for part in str(declared).split(",") if part.strip())
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def author_names() -> tuple[str, ...]:
|
||||
"""Return every credited person's name, sorted."""
|
||||
return tuple(sorted(author_urls()))
|
||||
def author_names(root: str = str(PROJECT_ROOT)) -> tuple[str, ...]:
|
||||
"""Return every declared role author, sorted and without duplicates.
|
||||
|
||||
Args:
|
||||
root: repository root, as a string so the cache key stays hashable.
|
||||
"""
|
||||
found = set()
|
||||
for path in sorted(Path(root, ROLES_DIR).iterdir()):
|
||||
if path.is_dir():
|
||||
found.update(role_authors(path))
|
||||
return tuple(sorted(found))
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Who an author is, and where their public profile lives.
|
||||
|
||||
``.mailmap`` maps every address a person has committed under to their
|
||||
``@infinito.nexus`` address, and that address names their profile: the local
|
||||
part is the profile slug. One social profile is therefore the single point of
|
||||
truth for an author, and nothing here keeps a second copy of their name, URL
|
||||
or avatar.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from utils.cache.files import PROJECT_ROOT, read_text
|
||||
|
||||
DOMAIN = "infinito.nexus"
|
||||
PROFILE_BASE = f"https://social.{DOMAIN}/profile"
|
||||
MAILMAP = ".mailmap"
|
||||
|
||||
_CANONICAL = re.compile(r"^(?P<name>[^<#]+?)\s*<(?P<email>[^>]+)>")
|
||||
_ENTRY = re.compile(
|
||||
r"^(?P<name>[^<#]+?)?\s*<(?P<email>[^>]+)>"
|
||||
r"(?:\s*(?P<alias_name>[^<#]+?)?\s*<(?P<alias_email>[^>]+)>)?\s*$"
|
||||
)
|
||||
|
||||
|
||||
def profile_url(email: str) -> str:
|
||||
"""Return the public profile the address names, empty for a foreign one.
|
||||
|
||||
Args:
|
||||
email: an author's address.
|
||||
"""
|
||||
local, _, domain = email.strip().partition("@")
|
||||
if domain.lower() != DOMAIN or not local:
|
||||
return ""
|
||||
return f"{PROFILE_BASE}/{local}/profile"
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _aliases(root: str = str(PROJECT_ROOT)) -> dict[str, tuple[str, str]]:
|
||||
"""Return ``{commit address: canonical (name, address)}``.
|
||||
|
||||
Exception: ``.mailmap`` is parsed here rather than handed to
|
||||
``git check-mailmap``, because the test container mounts the checkout
|
||||
without the worktree's git directory and git then refuses to read it.
|
||||
|
||||
Args:
|
||||
root: repository root, as a string so the cache key stays hashable.
|
||||
"""
|
||||
path = Path(root, MAILMAP)
|
||||
if not path.is_file():
|
||||
return {}
|
||||
found: dict[str, tuple[str, str]] = {}
|
||||
for line in read_text(str(path)).splitlines():
|
||||
match = _ENTRY.match(line.strip())
|
||||
if not match:
|
||||
continue
|
||||
name = (match.group("name") or "").strip()
|
||||
email = match.group("email")
|
||||
alias = match.group("alias_email") or email
|
||||
found[alias.lower()] = (name, email)
|
||||
return found
|
||||
|
||||
|
||||
def canonical(name: str, email: str, root: Path | None = None) -> tuple[str, str]:
|
||||
"""Return the identity ``.mailmap`` folds ``name``/``email`` into.
|
||||
|
||||
Args:
|
||||
name: the name the commit carries.
|
||||
email: the address the commit carries.
|
||||
root: repository root; defaults to this checkout.
|
||||
|
||||
Returns:
|
||||
``(name, email)``, unchanged when the mailmap knows the address not.
|
||||
"""
|
||||
mapped = _aliases(str(root or PROJECT_ROOT)).get(email.strip().lower())
|
||||
if not mapped:
|
||||
return name, email
|
||||
return mapped[0] or name, mapped[1]
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def by_name(root: str = str(PROJECT_ROOT)) -> dict[str, str]:
|
||||
"""Return ``{author name: canonical address}`` as ``.mailmap`` declares it.
|
||||
|
||||
Args:
|
||||
root: repository root, as a string so the cache key stays hashable.
|
||||
"""
|
||||
path = Path(root, MAILMAP)
|
||||
if not path.is_file():
|
||||
return {}
|
||||
found: dict[str, str] = {}
|
||||
for line in read_text(str(path)).splitlines():
|
||||
match = _CANONICAL.match(line.strip())
|
||||
if match:
|
||||
found[match.group("name")] = match.group("email")
|
||||
return found
|
||||
|
||||
|
||||
def committing_as(root: Path | None = None) -> tuple[str, str]:
|
||||
"""Return the identity a commit made here right now would carry.
|
||||
|
||||
Exception: the env pair wins over git. The test container mounts the
|
||||
checkout but not the contributor's git configuration, so asking git there
|
||||
yields nothing; ``utils/env/handlers/infinito/git_identity.py`` records the
|
||||
host's answer in the ``.env`` the container reads.
|
||||
|
||||
Args:
|
||||
root: repository root; defaults to this checkout.
|
||||
|
||||
Returns:
|
||||
``(name, email)``; either is empty when neither source knows it.
|
||||
"""
|
||||
carried = (
|
||||
os.environ.get("INFINITO_GIT_AUTHOR_NAME", "").strip(),
|
||||
os.environ.get("INFINITO_GIT_AUTHOR_EMAIL", "").strip(),
|
||||
)
|
||||
if carried[1]:
|
||||
return carried
|
||||
base = ["git", "-C", str(root or PROJECT_ROOT), "config", "--get"]
|
||||
values = [
|
||||
subprocess.run([*base, key], capture_output=True, text=True, check=False)
|
||||
for key in ("user.name", "user.email")
|
||||
]
|
||||
return tuple(value.stdout.strip() for value in values) # type: ignore[return-value]
|
||||
+4
-26
@@ -1,15 +1,9 @@
|
||||
"""Attribution recorded in markdown ``## Credits`` sections.
|
||||
|
||||
The link that belongs to a credited person lives nowhere but the Credits
|
||||
lines themselves: ``galaxy_info.author`` carries the name without a URL, and
|
||||
``galaxy_info.company`` names the project owner rather than the role's
|
||||
author, so neither can supply it. This module derives the name -> URL map
|
||||
from the linked mentions already present in the repository, which keeps the
|
||||
README generator and the ``credits-attribution`` lint reading the same
|
||||
source instead of a hand-maintained list.
|
||||
|
||||
Adding a contributor therefore needs no code change: one linked credit
|
||||
anywhere makes the generator emit that link for every role they author.
|
||||
A role's author is declared in ``galaxy_info.author`` and rendered without a
|
||||
link, so nothing here feeds the README generator any more. What remains
|
||||
backs the ``credits-attribution`` lint: it reads the linked mentions already
|
||||
present in prose and holds them to one link per person.
|
||||
|
||||
Patterns exported here:
|
||||
|
||||
@@ -28,7 +22,6 @@ Patterns exported here:
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from utils.cache.files import iter_non_ignored_files, read_text
|
||||
@@ -101,18 +94,3 @@ def iter_markdown_documents() -> Iterator[tuple[str, list[str]]]:
|
||||
yield path, read_text(path).splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def author_urls() -> dict[str, str]:
|
||||
"""Name -> URL for every person credited with exactly one URL repo-wide.
|
||||
|
||||
Names linked to conflicting URLs are omitted, so an ambiguous mapping
|
||||
never silently picks a winner.
|
||||
"""
|
||||
documents = [lines for _path, lines in iter_markdown_documents()]
|
||||
return {
|
||||
name: next(iter(seen))
|
||||
for name, seen in collect_author_urls(documents).items()
|
||||
if len(seen) == 1
|
||||
}
|
||||
Reference in new issue
Block a user