ref:81b6f84d5c73fe2f46b44fb2bae5f28d96a22e3e

fix(upload-pack): don't taint reachability with gitlink (mode 160000) SHAs

A v2 fetch on fangorn/hephaestus failed with `did not receive expected object 36e9a223...`. Trace: 1. Tree d85a381c has a gitlink entry `{mode: "160000", name: "ecad-drc", sha: 36e9a223...}`. By coincidence (or because the submodule URL loops back at this repo), 36e9a223 is *also* a real commit in this repo's history, reachable from every wanted branch tip. 2. collect_single_tree_entry dispatched the gitlink to the same head as regular files (the function head matched on `%{sha: sha}` without checking mode). That head did `MapSet.put(vis, sha)` before calling read_blob_entry. The read returned %Commit{}, not %Blob{}, so the entry produced no pack output — but the SHA was now in `vis`. 3. Walking parent_chain from a tip eventually reached abc92a55, whose parent is 36e9a223. collect_reachable saw 36e9a223 in `visited` and short-circuited (`{[], visited}`), never reading it, never recursing into its parents, never emitting it into the pack. 4. Result: 1043 reachable objects omitted (121 commits + 562 trees + 360 blobs). Client's index-pack rejected the resulting pack. Fix: give mode 160000 its own function head that returns `{acc, vis}` unchanged. Submodule pointers never emit anything and never touch the visited set. Regression test builds a 3-commit chain where the middle commit's tree has a gitlink whose SHA equals the root commit, and asserts all three commits land in the pack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SHA: 81b6f84d5c73fe2f46b44fb2bae5f28d96a22e3e
Author: CI <ci@anvil.test>
Date: 2026-05-14 03:32
Parents: 44a1729
2 files changed +207 -0
Type
lib/ex_git_objectstore/protocol/upload_pack_v2.ex +12 −0
@@ -850,6 +850,18 @@
{objs ++ acc, vis}
end
# Mode 160000 = gitlink (submodule pointer). The SHA names a commit
# in a different repo, so it must not enter `vis` — if the same SHA
# also appears as a real commit in this repo's ancestry, the existing
# `MapSet.put(vis, sha)` in the blob-entry head below would mark it
# visited, and the commit-graph walk would later skip over it. That
# silently prunes everything reachable through that commit. (Seen
# against fangorn/hephaestus, where a gitlink and a real commit
# happened to share SHA 36e9a223...; 1043 objects went missing.)
defp collect_single_tree_entry(_repo, %{mode: "160000"}, acc, vis, _depth, _opts) do
{acc, vis}
end
defp collect_single_tree_entry(repo, %{sha: sha}, acc, vis, _depth, opts) do
cond do
MapSet.member?(vis, sha) ->