@@ -331,7 +331,18 @@
end
describe "partial clone (--filter)" do
# Each filter test asserts directly on the pack contents via
# `git cat-file --batch-check --batch-all-objects` in the cloned
# repo. `--no-checkout` is used so that post-clone lazy-fetches
# don't pollute the local store — after clone the only objects
# present are exactly what the server sent. That's what lets us
# prove the filter was honoured instead of relying on git's
# `remote.origin.promisor=true` config (which git sets whenever
# the server merely advertised `filter`, regardless of pack
# contents).
@tag :tmp_dir
test "--filter=blob:none produces a pack with zero blobs",
%{tmp_dir: tmp_dir} do
test "--filter=blob:none produces a blobless pack", %{tmp_dir: tmp_dir} do
repo = make_linear_repo("filter-blob", 3)
{port, stop} = GitDaemon.start_upload_pack(repo)
@@ -345,25 +356,28 @@
"protocol.version=2",
"clone",
"--filter=blob:none",
"--no-checkout",
"--no-local",
"git://127.0.0.1:#{port}/repo",
dest
])
assert code == 0, "partial clone failed:\n#{out}"
assert_promisor_configured(dest)
counts = count_local_objects_by_type(dest)
# Git considers a blobless clone "partial" when the repo-local
# config has `remote.origin.promisor=true`. With a real filter
# server the config gets set automatically; here we assert the
# clone claims partial state.
{cfg, _} = GitDaemon.git_at(dest, ["config", "--get", "remote.origin.promisor"])
assert String.trim(cfg) == "true"
assert counts.blob == 0, "blob:none must produce zero blobs, got #{counts.blob}"
assert counts.commit == 3, "expected 3 commits in pack, got #{counts.commit}"
assert counts.tree >= 3, "expected ≥ 3 trees in pack, got #{counts.tree}"
after
stop.()
end
end
@tag :tmp_dir
test "--filter=tree:0 produces a treeless pack", %{tmp_dir: tmp_dir} do
test "--filter=tree:0 produces a pack with no trees and no blobs",
%{tmp_dir: tmp_dir} do
repo = make_linear_repo("filter-tree", 3)
{port, stop} = GitDaemon.start_upload_pack(repo)
@@ -377,12 +391,26 @@
"protocol.version=2",
"clone",
"--filter=tree:0",
"--no-checkout",
"--no-local",
"git://127.0.0.1:#{port}/repo",
dest
])
assert code == 0, "tree:0 clone failed:\n#{out}"
assert_promisor_configured(dest)
counts = count_local_objects_by_type(dest)
# tree:0 = include objects with tree-depth < 0, which per git's
# "only the root tree per commit" semantics means no trees at
# all in our emission (our walker emits depth-0 root trees
# only when depth < limit, and limit 0 rules everything out).
# Every commit must still be present.
assert counts.tree == 0, "tree:0 must produce zero trees, got #{counts.tree}"
assert counts.blob == 0, "tree:0 implies zero blobs, got #{counts.blob}"
assert counts.commit == 3, "expected 3 commits, got #{counts.commit}"
after
stop.()
end
@@ -393,6 +421,7 @@
# pack that contains only commit objects; git considers this a
# partial clone and sets promisor=true.
@tag :tmp_dir
test "--filter=object:type=commit yields a commits-only pack", %{tmp_dir: tmp_dir} do
test "--filter=object:type=commit yields a pack with only commits",
%{tmp_dir: tmp_dir} do
repo = make_linear_repo("filter-obj-type", 3)
{port, stop} = GitDaemon.start_upload_pack(repo)
@@ -406,6 +435,7 @@
"protocol.version=2",
"clone",
"--filter=object:type=commit",
"--no-checkout",
"--no-local",
"git://127.0.0.1:#{port}/repo",
dest
@@ -413,17 +443,24 @@
assert code == 0, "object:type=commit clone failed:\n#{out}"
assert_promisor_configured(dest)
counts = count_local_objects_by_type(dest)
assert counts.commit == 3, "expected 3 commits, got #{counts.commit}"
{cfg, _} = GitDaemon.git_at(dest, ["config", "--get", "remote.origin.promisor"])
assert String.trim(cfg) == "true"
assert counts.tree == 0, "object:type=commit must exclude trees, got #{counts.tree}"
assert counts.blob == 0, "object:type=commit must exclude blobs, got #{counts.blob}"
assert counts.tag == 0, "object:type=commit must exclude tags, got #{counts.tag}"
after
stop.()
end
end
# `combine:blob:none+tree:1` applies both filters: `blob:none`
# excludes every blob; `tree:1` keeps only trees at depth < 1,
# i.e. only the root tree per commit (sub-trees and their entries
# are at depth ≥ 1).
# `combine:blob:none+tree:1` applies both filters: no blobs, and
# only root trees + their direct entries (which, with blobs
# already excluded, means just the root tree per commit).
@tag :tmp_dir
test "--filter=combine:… composes sub-filters", %{tmp_dir: tmp_dir} do
test "--filter=combine:… applies every sub-filter", %{tmp_dir: tmp_dir} do
repo = make_linear_repo("filter-combine", 3)
{port, stop} = GitDaemon.start_upload_pack(repo)
@@ -437,15 +474,31 @@
"protocol.version=2",
"clone",
"--filter=combine:blob:none+tree:1",
"--no-checkout",
"--no-local",
"git://127.0.0.1:#{port}/repo",
dest
])
assert code == 0, "combine filter clone failed:\n#{out}"
assert_promisor_configured(dest)
{cfg, _} = GitDaemon.git_at(dest, ["config", "--get", "remote.origin.promisor"])
assert String.trim(cfg) == "true"
counts = count_local_objects_by_type(dest)
assert counts.commit == 3, "expected 3 commits, got #{counts.commit}"
assert counts.blob == 0,
"combine:blob:none+tree:1 must exclude blobs, got #{counts.blob}"
# Trees: make_linear_repo builds flat commits with one root
# tree each (no sub-trees), so tree:1 keeps the 3 root trees
# and nothing below (there is nothing below). The distinct
# root-tree count is ≤ 3 (may be ≤ due to tree reuse, but
# for this fixture each commit has a different blob-sha so
# each tree is distinct).
assert counts.tree > 0 and counts.tree <= 3,
"expected 1..3 trees (only root level), got #{counts.tree}"
after
stop.()
end
@@ -507,6 +560,7 @@
"protocol.version=2",
"clone",
"--filter=sparse:oid=#{sparse_sha}",
"--no-checkout",
"--no-local",
"git://127.0.0.1:#{port}/repo",
dest
@@ -514,8 +568,18 @@
assert code == 0, "sparse filter clone failed:\n#{out}"
assert_promisor_configured(dest)
# The pack must contain app_sha (under src/) but must NOT
# contain test_sha (under test/). Trees and commits are
# always included regardless of sparse:oid.
shas = local_object_shas(dest)
assert app_sha in shas,
"sparse:oid clone missing the matching blob src/app.ex (#{app_sha})"
{cfg, _} = GitDaemon.git_at(dest, ["config", "--get", "remote.origin.promisor"])
assert String.trim(cfg) == "true"
refute test_sha in shas,
"sparse:oid clone included the excluded blob test/app_test.ex (#{test_sha})"
after
stop.()
end
@@ -689,5 +753,42 @@
defp walk_back(repo, sha, steps) do
{:ok, %Commit{parents: [parent | _]}} = ExGitObjectstore.ObjectResolver.read(repo, sha)
walk_back(repo, parent, steps - 1)
end
# Enumerate every object in the clone's local store and count by
# git object type. Requires `--no-checkout` at clone time so
# post-clone lazy-fetches don't pollute the count.
defp count_local_objects_by_type(clone_dir) do
{out, 0} =
GitDaemon.git_at(clone_dir, ["cat-file", "--batch-check", "--batch-all-objects"])
out
|> String.split("\n", trim: true)
|> Enum.reduce(%{blob: 0, tree: 0, commit: 0, tag: 0}, fn line, acc ->
case String.split(line, " ", parts: 3) do
[_sha, "blob", _size] -> Map.update!(acc, :blob, &(&1 + 1))
[_sha, "tree", _size] -> Map.update!(acc, :tree, &(&1 + 1))
[_sha, "commit", _size] -> Map.update!(acc, :commit, &(&1 + 1))
[_sha, "tag", _size] -> Map.update!(acc, :tag, &(&1 + 1))
_ -> acc
end
end)
end
defp local_object_shas(clone_dir) do
{out, 0} =
GitDaemon.git_at(clone_dir, ["cat-file", "--batch-check", "--batch-all-objects"])
out
|> String.split("\n", trim: true)
|> Enum.map(fn line -> line |> String.split(" ", parts: 2) |> List.first() end)
|> MapSet.new()
end
defp assert_promisor_configured(clone_dir) do
{cfg, _} = GitDaemon.git_at(clone_dir, ["config", "--get", "remote.origin.promisor"])
assert String.trim(cfg) == "true",
"expected remote.origin.promisor=true in clone (indicates filter was honoured)"
end
end