ref:8701785765d01e1fd639fcd1349d685be1d45113

fix: serve promisor lazy-fetch without re-filtering wanted objects

Partial-clone clients fill in missing blobs on demand by sending a new fetch with the specific object SHA as a `want`. Our server was applying the same filter that made the object missing in the first place, so the lazy fetch would return an empty pack and the post-clone checkout would fail with git://…/repo did not send all necessary objects `collect_objects_maybe_shallow` now inspects each want's object type. When any want is a blob, tree, or tag (i.e. not a commit), the request is treated as a lazy fetch: the session-level filter is bypassed and the early-blob-skip walker option is disabled. Normal commit-ranged fetches continue to honour the filter as before. With this fix the existing `--filter=blob:none` and `--filter=tree:0` integration tests no longer need `--no-checkout` — the implicit post-clone checkout now succeeds because missing blobs get lazily fetched on request.
SHA: 8701785765d01e1fd639fcd1349d685be1d45113
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-04-19 01:04
Parents: d84adbf
2 files changed +28 -4
Type
lib/ex_git_objectstore/protocol/upload_pack_v2.ex +28 −2
@@ -639,8 +639,19 @@
# Route through the shallow walker when shallow args are present,
# otherwise fall through to the standard full-reachability walk.
# Objects are then passed through the filter (if any).
#
# Promisor lazy-fetch exception: partial-clone clients fill in
# missing blobs/trees on demand by sending a fetch with the
# specific object as a `want`. The server MUST return that exact
# object even if the session-level filter would normally exclude
# it — the client already knows the SHA and is explicitly asking
# for it. `lazy_fetch?/2` detects this by peeking at each want's
# object type: if any want is a non-commit, we treat the request
# as a lazy fetch and bypass the filter + blob-skip optimization.
defp collect_objects_maybe_shallow(repo, wants, haves, shallow_opts, filter_spec) do
lazy? = lazy_fetch?(repo, wants)
effective_filter = if lazy?, do: nil, else: filter_spec
walk_opts = %{skip_blobs: not lazy? and early_skip_blobs?(filter_spec)}
walk_opts = %{skip_blobs: early_skip_blobs?(filter_spec)}
result =
case shallow_opts do
@@ -649,8 +660,23 @@
end
with {:ok, walk} <- result do
{:ok, %{walk | objects: apply_filter(walk.objects, effective_filter, repo)}}
{:ok, %{walk | objects: apply_filter(walk.objects, filter_spec, repo)}}
end
end
# Lazy-fetch heuristic: a fetch whose want-set includes any blob,
# tree, or tag is the client asking for specific objects by SHA
# (typically a promisor remote filling in a missing object after a
# partial clone). Commits-only `wants` are normal reachability
# fetches and the filter applies as usual.
defp lazy_fetch?(repo, wants) do
Enum.any?(wants, fn sha ->
case ObjectResolver.read(repo, sha) do
{:ok, %Commit{}} -> false
{:ok, _other} -> true
_ -> false
end
end)
end
# Some filter specs exclude every blob outright. When that's the