fix: follow an upstream the port vendors as module

libretro/TIC-80 builds nesbox/TIC-80 through its core submodule
and tic80's upstream_commit is that submodule commit. Compared to
nesbox HEAD, profile_sync reported changes the shipped core never
had, and a bump would have moved the pin past it. When an upstream
pin is a submodule pointer of the source pin, its head is the
pointer at the source HEAD.
This commit is contained in:
Abdessamad Derraz committed 2026-10-18 02:45:27 +02:00
1 parent 99d9ac12c2
commit b91449590d
3 files changed
+114 -2

No files matched your search

+39 -2
View File
@@ -21,7 +21,7 @@ import subprocess
import sys
import tempfile
from collections.abc import Callable, Iterable, Iterator, Sequence
from dataclasses import asdict, dataclass, field
from dataclasses import asdict, dataclass, field, replace
from datetime import date
from pathlib import Path
from typing import Any
@@ -1293,7 +1293,44 @@ def select_views(
continue
seen.add((repo.host, repo.slug))
views.append(RepoView(repo, pin, head, origin, field))
return views
return _follow_submodule(views, cache_dir, offline)
def _follow_submodule(
views: list[RepoView], cache_dir: str, offline: bool
) -> list[RepoView]:
"""An upstream the port vendors as a submodule moves with the port.
libretro/TIC-80 builds nesbox/TIC-80 through its `core` submodule, and
upstream_commit was that submodule's commit. Compared to nesbox's own
HEAD, the view reported changes the shipped core never had, and a bump
would have moved upstream_commit past what it builds. The commit the
port's HEAD points the submodule at is the upstream HEAD that matters.
"""
source = next((v for v in views if v.field == "source"), None)
behind = [v for v in views if v.field == "upstream" and v.pin != v.head]
if source is None or not behind:
return views
pinned = {
sha: path
for path, sha in upstream.list_gitlinks(
source.repo, source.pin, cache_dir, offline
).items()
}
if not pinned:
return views
followed: list[RepoView] = []
for view in views:
path = pinned.get(view.pin) if view.field == "upstream" else None
head = (
upstream.list_gitlinks(source.repo, source.head, cache_dir, offline).get(path)
if path else None
)
followed.append(
replace(view, head=head, origin=f"{view.origin} via {path}")
if head else view
)
return followed
def declared_repositories(profile: dict) -> list[tuple[str, str, str]]:
+39
View File
@@ -673,6 +673,45 @@ def list_tree(
return paths, bool(payload.get("truncated"))
def list_gitlinks(
repo: Repo, sha: str, cache_dir: str, offline: bool = False
) -> dict[str, str]:
"""Submodule path -> the commit it points at, at one revision.
Read from the same tree listing as list_tree, so the call costs nothing
once the tree is cached.
"""
if repo.family == "gitlab":
links: dict[str, str] = {}
for page in range(1, MAX_TREE_PAGES + 1):
url = (
f"{repo.api_base}/projects/{_project(repo)}/repository/tree"
f"?ref={sha}&recursive=true&per_page={TREE_PAGE_SIZE}&page={page}"
)
payload = _api(url, cache_dir, offline)
if not isinstance(payload, list):
break
links.update(
(entry["path"], entry["id"])
for entry in payload
if isinstance(entry, dict) and entry.get("type") == "commit"
and entry.get("path") and entry.get("id")
)
if len(payload) < TREE_PAGE_SIZE:
break
return links
url = _tree_url(repo, sha)
payload = _api(url, cache_dir, offline) if url else None
if not isinstance(payload, dict):
return {}
return {
entry["path"]: entry["sha"]
for entry in payload.get("tree") or []
if isinstance(entry, dict) and entry.get("type") == "commit"
and entry.get("path") and entry.get("sha")
}
def commit_date(repo: Repo, sha: str, cache_dir: str, offline: bool = False) -> str | None:
"""When a revision was committed, as the forge reports it (ISO 8601)."""
if repo.family == "gitlab":
+36
View File
@@ -1155,8 +1155,13 @@ class TestBuildReport(unittest.TestCase):
profile_sync.upstream.tag_commit,
profile_sync.upstream._http_json,
profile_sync.upstream._http_text,
profile_sync.upstream.list_gitlinks,
)
self.tags: list[str] = []
self.gitlinks: dict[str, dict[str, str]] = {}
profile_sync.upstream.list_gitlinks = (
lambda repo, sha, cache_dir, offline=False: self.gitlinks.get(sha, {})
)
self.tag_commits: dict[str, str] = {}
# Any code path reaching the HTTP layer is a leak, not a slow test.
def _no_network(url):
@@ -1205,6 +1210,7 @@ class TestBuildReport(unittest.TestCase):
profile_sync.upstream.tag_commit,
profile_sync.upstream._http_json,
profile_sync.upstream._http_text,
profile_sync.upstream.list_gitlinks,
) = self._orig
self.tmp.cleanup()
@@ -3896,5 +3902,35 @@ class TestEmulatorsDirStatus(unittest.TestCase):
profile_sync.emulators_dir_is_dirty(root)
class UpstreamVendoredAsASubmodule(unittest.TestCase):
"""libretro/TIC-80 builds nesbox/TIC-80 through its `core` submodule, and
tic80's upstream_commit is that submodule's commit. Compared to nesbox's
own HEAD it reported changes the shipped core never had, and a bump would
have moved upstream_commit past what libretro builds."""
def _views(self, links: dict) -> list:
from unittest import mock # noqa: PLC0415
repo_s, repo_u = object(), object()
views = [
profile_sync.RepoView(repo_s, "src-pin", "src-head", "source_commit", "source"),
profile_sync.RepoView(repo_u, "sub-old", "nesbox-tip", "upstream_commit", "upstream"),
]
with mock.patch.object(
profile_sync.upstream, "list_gitlinks",
side_effect=lambda repo, sha, *a, **k: links.get(sha, {}),
):
return profile_sync._follow_submodule(views, "cache", True)
def test_the_upstream_head_is_what_the_port_head_builds(self):
views = self._views({"src-pin": {"core": "sub-old"}, "src-head": {"core": "sub-new"}})
self.assertEqual(views[1].head, "sub-new")
self.assertEqual(views[0].head, "src-head")
def test_an_upstream_the_port_does_not_vendor_keeps_its_tip(self):
views = self._views({"src-pin": {"deps/zlib": "other"}, "src-head": {}})
self.assertEqual(views[1].head, "nesbox-tip")
if __name__ == "__main__":
unittest.main()