fix: name a repository the forge renamed

GitHub, GitLab and Gitea redirect a renamed repository, and every call
follows the redirect: AloysHF/NicaiEmu answers as AloysHF/CoolbarEmu
and the profile read as current. profile_sync now reads the name the
forge answers under and counts a move as review work.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 06:09:31 +02:00
1 parent d6fc66274f
commit b784a73d5c
4 files changed
+99 -2

No files matched your search

+29 -2
View File
@@ -939,6 +939,10 @@ class ProfileReport:
# Repositories whose pin is no ancestor of the branch the profile
# tracks: history was rewritten, or the pin lies on another branch.
orphaned: list[str] = field(default_factory=list)
# Repositories the forge answers under another owner/name: renamed or
# transferred. The redirect is followed in silence, so the move is named
# and counted as review work, since a renamed project is re-profiled.
moved_repos: list[str] = field(default_factory=list)
# A profile off any forge names the page announcing its releases
# (version_source): the version read there, beside the one declared.
declared_version: str | None = None
@@ -976,6 +980,7 @@ class ProfileReport:
sum(counts.get(status, 0) for status in REVIEW_STATUSES)
+ len(self.orphaned)
+ len(self.stale_prose or ())
+ len(self.moved_repos)
)
@@ -1332,6 +1337,25 @@ def _path_roles(refs: list) -> dict[str, set[str]]:
return roles
def moved_repositories(
views: list[RepoView], cache_dir: str, offline: bool
) -> list[str]:
"""Declared repositories the forge now answers under another name."""
moved = []
seen: set[tuple[str, str]] = set()
for view in views:
key = (view.repo.host, view.repo.slug)
if key in seen:
continue
seen.add(key)
current = upstream.current_slug(view.repo, cache_dir, offline)
if current and current.lower() != view.repo.slug.lower():
moved.append(
f"{view.repo.slug} answers as {current} (renamed or transferred)"
)
return moved
def build_report(
name: str, profile: dict, cache_dir: str, offline: bool = False,
text: str | None = None,
@@ -1451,6 +1475,7 @@ def build_report(
report.upstream_pin = view.pin
report.upstream_head = view.head
report.upstream_origin = view.origin
report.moved_repos = moved_repositories(views, cache_dir, offline)
# A profile can hold its pin on purpose, documenting a build that is that
# revision. Comparing it to HEAD says nothing and recaling would repoint
# its refs at code the build never had, so it is judged like a frozen tag:
@@ -1954,6 +1979,8 @@ def format_report(report: ProfileReport, changed_only: bool = False) -> str:
lines.append(f" not read (unsupported host): {url}")
for line in report.orphaned:
lines.append(f" orphaned pin: {line}")
for line in report.moved_repos:
lines.append(f" moved repository: {line}")
if report.declared_version or report.page_version:
lines.append(
f" version page: declared {report.declared_version or '-'}, "
@@ -3509,9 +3536,9 @@ def _check_quota(count: int, offline: bool) -> None:
"""Refuse a run the anonymous quota cannot carry to completion."""
if offline or github_token():
return
if count * 4 > ANON_QUOTA:
if count * 5 > ANON_QUOTA:
print(
f"{count} profiles need roughly {count * 4} API calls, the anonymous "
f"{count} profiles need roughly {count * 5} API calls, the anonymous "
f"quota is {ANON_QUOTA} per hour. Log in with `gh auth login`, set "
"GITHUB_TOKEN, or use --offline.",
file=sys.stderr,
+19
View File
@@ -455,6 +455,25 @@ def _first_sha(payload: object) -> str | None:
return None
def current_slug(repo: Repo, cache_dir: str, offline: bool = False) -> str | None:
"""The owner/name the forge answers for a repository today.
A renamed or transferred repository keeps answering under its old name:
GitHub redirects (AloysHF/NicaiEmu answers as AloysHF/CoolbarEmu), GitLab
and Gitea do the same. Every later call follows the redirect, so only the
name in the payload tells the project moved.
"""
if repo.family == "gitlab":
url, key = f"{repo.api_base}/projects/{_project(repo)}", "path_with_namespace"
else:
url, key = f"{repo.api_base}/repos/{repo.slug}", "full_name"
payload = _api(url, cache_dir, offline, MOVING_TTL)
if not isinstance(payload, dict):
return None
value = payload.get(key)
return str(value) if value else None
def resolve_head(
repo: Repo, cache_dir: str, offline: bool = False, branch: str | None = None
) -> str | None:
+27
View File
@@ -1163,6 +1163,13 @@ class TestBuildReport(unittest.TestCase):
profile_sync.upstream._http_json,
profile_sync.upstream._http_text,
profile_sync.upstream.list_gitlinks,
profile_sync.upstream.current_slug,
)
self.current_slugs: dict[str, str] = {}
profile_sync.upstream.current_slug = (
lambda repo, cache_dir, offline=False: self.current_slugs.get(
repo.slug, repo.slug
)
)
self.tags: list[str] = []
self.gitlinks: dict[str, dict[str, str]] = {}
@@ -1218,6 +1225,7 @@ class TestBuildReport(unittest.TestCase):
profile_sync.upstream._http_json,
profile_sync.upstream._http_text,
profile_sync.upstream.list_gitlinks,
profile_sync.upstream.current_slug,
) = self._orig
self.tmp.cleanup()
@@ -1232,6 +1240,25 @@ class TestBuildReport(unittest.TestCase):
],
}
def test_a_renamed_repository_is_review_work(self):
"""AloysHF/NicaiEmu answers as AloysHF/CoolbarEmu: the redirect was
followed in silence and the profile read as up to date."""
self.files[("pinsha", "a.c")] = ["x", "y"]
self.files[("headsha", "a.c")] = ["x", "y"]
self.current_slugs["o/n"] = "o/renamed"
report = build_report("test", self._profile(["a.c:2"]), self.dir)
self.assertEqual(len(report.moved_repos), 1)
self.assertIn("o/renamed", report.moved_repos[0])
self.assertGreaterEqual(report.needs_review(), 1)
self.assertIn("moved repository", profile_sync.format_report(report))
def test_a_repository_under_its_own_name_is_not_moved(self):
self.files[("pinsha", "a.c")] = ["x", "y"]
self.files[("headsha", "a.c")] = ["x", "y"]
self.current_slugs["o/n"] = "O/N"
report = build_report("test", self._profile(["a.c:2"]), self.dir)
self.assertEqual(report.moved_repos, [])
def test_a_refusing_forge_reads_as_unread_not_gone(self):
"""git.eden-emu.dev answers 403 to scripts: the muted repository's
citations read "absent at HEAD" for files that exist."""
+24
View File
@@ -805,5 +805,29 @@ class TestGiteaCommitAt(unittest.TestCase):
)
class TestCurrentSlug(unittest.TestCase):
"""The name a forge answers under, read from the repository payload."""
def test_github_and_gitlab_payloads(self):
answers = {
"https://api.github.com/repos/AloysHF/NicaiEmu": {
"full_name": "AloysHF/CoolbarEmu"
},
"https://gitlab.com/api/v4/projects/g%2Fsub%2Fp": {
"path_with_namespace": "g/sub/p"
},
}
with tempfile.TemporaryDirectory() as tmp, \
mock.patch.object(upstream, "_http_json", side_effect=answers.get):
github = parse_repo("https://github.com/AloysHF/NicaiEmu")
gitlab = parse_repo("https://gitlab.com/g/sub/p")
self.assertEqual(upstream.current_slug(github, tmp), "AloysHF/CoolbarEmu")
self.assertEqual(upstream.current_slug(gitlab, tmp), "g/sub/p")
self.assertIsNone(
upstream.current_slug(parse_repo("https://github.com/a/b"), tmp)
)
if __name__ == "__main__":
unittest.main()