feat: check refs into a submodule

idtech4a++ cites xash3d-fwgs filesystem/filesystem.c, starship and
ghostship Torch src/Companion.cpp: files of submodules of the declared
repository. No blob of the declared tree answers, so the parts stayed
external and a bump moved the submodule commit with nothing checked.
A project word now also resolves to the one submodule named after it,
and a file under a submodule is read in the repository .gitmodules
names, at the commit the declared revision pins it to.
This commit is contained in:
Abdessamad Derraz committed 2026-11-05 11:58:22 +01:00
1 parent d5c4cd3575
commit 739f651a16
3 files changed
+140 -6

No files matched your search

+44 -6
View File
@@ -1463,14 +1463,40 @@ def tag_pinned_views(
return tagged
def resolve_through_gitlinks(path: str, links: Iterable[Iterable[str]]) -> str | None:
"""Path of `project file` inside the one submodule named after the project.
idtech4a++ cites `xash3d-fwgs filesystem/filesystem.c` and starship
`Torch src/Companion.cpp`: the declared tree holds a gitlink there, not
the files, so no blob path answers and the part stayed external.
"""
word, _, rest = path.partition(" ")
rest = rest.strip().lstrip("/")
if not rest:
return None
word = word.lower()
for paths in links:
matches = [
link for link in paths or ()
if word in link.rsplit("/", 1)[-1].lower()
]
if len(matches) == 1:
return f"{matches[0]}/{rest}"
if matches:
return None
return None
def _resolve_project_words(
refs: list, tree_order: Callable[[], Iterable[list[str] | None]]
refs: list, tree_order: Callable[[], Iterable[list[str] | None]],
link_order: Callable[[], Iterable[Iterable[str]]] = lambda: (),
) -> list:
"""Point `project file` parts at the declared tree that vendors them.
tree_order() yields the trees to search, pin before HEAD. It is only
called for a part that needs it, since reading a HEAD tree costs a
request. Parts no tree carries stay external.
tree_order() yields the trees to search, pin before HEAD, and
link_order() the submodule paths of the declared trees at their pins.
Each is only called for a part that needs it, since reading a tree costs
a request. Parts no tree carries stay external.
"""
resolved: dict[str, str | None] = {}
out = []
@@ -1479,7 +1505,9 @@ def _resolve_project_words(
for part in parts:
if is_external_citation(part.path):
if part.path not in resolved:
resolved[part.path] = resolve_project_word(part.path, tree_order())
resolved[part.path] = resolve_project_word(
part.path, tree_order()
) or resolve_through_gitlinks(part.path, link_order())
if resolved[part.path]:
part = replace(part, path=resolved[part.path])
rewritten.append(part)
@@ -1860,7 +1888,14 @@ def build_report(
if key in mute:
return None
try:
return upstream.fetch_file(repo, sha, wanted, cache_dir, offline)
found = upstream.fetch_file(repo, sha, wanted, cache_dir, offline)
if found is None and "/" in wanted:
# A file of a submodule is read at the commit its gitlink
# pins in this revision, so a bump moves it with the port.
inner = upstream.through_gitlink(repo, sha, wanted, cache_dir, offline)
if inner is not None:
found = upstream.fetch_file(*inner, cache_dir, offline)
return found
except (upstream.RateLimitError, upstream.NotCachedError):
raise
except upstream.UpstreamError as exc:
@@ -1917,6 +1952,9 @@ def build_report(
lambda: itertools.chain(
map(_pin_tree_for, views), map(lambda v: _context_for(v)[1], views)
),
lambda: (
upstream.list_gitlinks(v.repo, v.pin, cache_dir, offline) for v in views
),
)
lines_cache: dict[tuple[str, str, int | None], list[str] | None] = {}
+52
View File
@@ -1115,6 +1115,58 @@ def list_gitlinks(
}
def submodule_urls(text: str, base: Repo) -> dict[str, Repo]:
"""Submodule path -> repository, read from a .gitmodules text.
A relative url (`../xash3d-fwgs.git`) names a sibling of the repository
that declares it, on the same forge.
"""
repos: dict[str, Repo] = {}
path = url = None
for line in [*text.splitlines(), "[end]"]:
stripped = line.strip()
if stripped.startswith("["):
if path and url:
if url.startswith(("../", "./")):
segments = [base.owner, base.name]
for part in url.split("/"):
if part == "..":
segments.pop()
elif part not in (".", ""):
segments.append(part)
url = f"https://{base.host}/{'/'.join(segments)}"
repo = parse_repo(url)
if repo is not None:
repos[path] = repo
path = url = None
continue
key, _, value = stripped.partition("=")
if key.strip() == "path":
path = value.strip()
elif key.strip() == "url":
url = value.strip()
return repos
def through_gitlink(
repo: Repo, sha: str, path: str, cache_dir: str, offline: bool = False
) -> tuple[Repo, str, str] | None:
"""The submodule repository, commit and inner path a path falls under.
starship cites `Torch src/Companion.cpp`, a file of the Torch submodule:
the declared tree holds only the gitlink, which pins the revision.
"""
links = list_gitlinks(repo, sha, cache_dir, offline)
link = next((p for p in links if path.startswith(p + "/")), None)
if link is None:
return None
modules = fetch_file(repo, sha, ".gitmodules", cache_dir, offline)
target = submodule_urls("\n".join(modules or ()), repo).get(link)
if target is None:
return None
return target, links[link], path[len(link) + 1:]
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":
+44
View File
@@ -3950,6 +3950,50 @@ class TestCollectCitations(unittest.TestCase):
self.assertIsNone(resolve("munt ROMInfo.cpp", [tree]))
self.assertIsNone(resolve("mupen64plus Config.cpp", [tree]))
def test_project_word_resolves_inside_a_submodule(self):
"""idtech4a++ and starship cite files of a submodule. The declared
tree holds only the gitlink, so the parts stayed external and moved
unchecked with every bump."""
links = ["Q3E/src/main/jni/xash3d/xash3d-fwgs", "tools/Torch", "libs/zlib"]
resolve = profile_sync.resolve_through_gitlinks
self.assertEqual(
resolve("xash3d-fwgs filesystem/filesystem.c", [links]),
"Q3E/src/main/jni/xash3d/xash3d-fwgs/filesystem/filesystem.c",
)
self.assertEqual(
resolve("Torch src/Companion.cpp", [links]), "tools/Torch/src/Companion.cpp"
)
self.assertIsNone(resolve("munt ROMInfo.cpp", [links]))
self.assertIsNone(resolve("lib x.c", [["a/lib1", "b/lib2"]]))
def test_a_submodule_file_is_read_at_its_pinned_commit(self):
port = upstream.make_repo("github.com", "glKarin", "com.n0n3m4.diii4a")
files = {
(port.slug, "p1", ".gitmodules"): [
'[submodule "Q3E/src/main/jni/xash3d/xash3d-fwgs"]',
"\tpath = Q3E/src/main/jni/xash3d/xash3d-fwgs",
"\turl = ../xash3d-fwgs.git",
],
("glKarin/xash3d-fwgs", "s1", "filesystem/filesystem.c"): ["getenv(x)"],
}
def fetch(repo, sha, path, *_):
return files.get((repo.slug, sha, path))
with mock.patch.object(upstream, "fetch_file", side_effect=fetch), \
mock.patch.object(
upstream, "list_gitlinks",
return_value={"Q3E/src/main/jni/xash3d/xash3d-fwgs": "s1"},
):
target = upstream.through_gitlink(
port, "p1",
"Q3E/src/main/jni/xash3d/xash3d-fwgs/filesystem/filesystem.c", "c",
)
self.assertEqual(
(target[0].slug, target[1], target[2]),
("glKarin/xash3d-fwgs", "s1", "filesystem/filesystem.c"),
)
def test_run_is_rendered_with_its_written_path(self):
parts = [RefPart("mupen64plus-video-rice/upstream/src/Config.cpp", 38, 38, "38")]
self.assertEqual(