crates/storage-git-gix/parked-gix-2/gunnar-wire-gix/tests/real_git_surface.rs
//! **The rest of the git command surface, against the real `git` binary.**
//!
//! `real_git_interop` proves clone, fetch, negotiation, deepening and push.
//! This binary drives the same [`git_harness`] pipe over the commands and
//! options that suite never issues: the whole-namespace clones (`--bare`,
//! `--mirror`, `--reference`), pruning in both directions, the refspec forms
//! (`+`, globs, `HEAD:`), `refs/notes/*` and unusual ref namespaces, the two
//! object filters that had no end-to-end test, and `git bundle`.
//!
//! # The judge is stock git's own inventory
//!
//! Where a case claims "the repository came across", it is not enough that the
//! clone succeeded. [`assert_same_graph`] compares **every object id** from
//! `cat-file --batch-all-objects` and **every ref** from `for-each-ref`,
//! oid by oid, between the origin and the clone.
//!
//! Both halves carry a floor. A comparison of two empty repositories is equal,
//! and would let a server that shipped nothing pass every case in this file;
//! `min_objects` and `min_refs` are what make an empty fixture FAIL rather than
//! sail through. The floors are asserted against the **origin**, before the
//! comparison, so a fixture that quietly stopped building its own history is
//! reported as a broken fixture and not as a passing test.
//!
//! # Refusals are asserted against an acceptance
//!
//! A server that refuses everything passes any test that only checks a refusal.
//! Each case that asserts git was turned away also runs the **legitimate form
//! of the same request against the same server** and asserts it succeeds:
//! `push_non_fast_forward_needs_the_plus` rejects the bare refspec and then
//! accepts `+`, and the pruning cases assert the un-pruned fetch *leaves* the
//! ref that `--prune` then removes. Without the second half, "the ref is gone"
//! is equally true of a server that never sent it.
#[path = "git_harness/mod.rs"]
mod git_harness;
use std::path::{Path, PathBuf};
use git_harness::*;
use gix_hash::Kind;
use gunnar_wire::ProtocolVersion;
fn main() {
serve_if_asked();
let cases: &[(&str, fn())] = &[
// whole-namespace clones
("clone_bare_holds_the_whole_graph", clone_bare),
("clone_mirror_holds_every_ref", clone_mirror),
("clone_reference_dissociate_is_complete", clone_reference),
// pruning, in both directions
("fetch_prune_removes_a_deleted_branch", fetch_prune),
("fetch_prune_tags_removes_a_deleted_tag", fetch_prune_tags),
("push_mirror_makes_the_origin_match", push_mirror),
("push_prune_deletes_what_has_no_local_ref", push_prune),
// refspec forms
(
"push_non_fast_forward_needs_the_plus",
push_non_fast_forward,
),
("push_glob_refspec_moves_every_branch", push_glob_refspec),
("push_head_colon_ref_creates_the_branch", push_head_colon),
("fetch_glob_refspec_into_a_private_namespace", fetch_glob),
// ref namespaces
("notes_round_trip_through_the_server", notes_round_trip),
(
"unusual_ref_namespaces_survive_a_round_trip",
odd_namespaces,
),
("ls_remote_refs_hides_the_peeled_tag", ls_remote_refs),
// the two filters that had no end-to-end test
("filter_tree_0_sends_no_trees_and_no_blobs", filter_tree_0),
("filter_tree_1_sends_the_root_tree_only", filter_tree_1),
(
"filter_blob_limit_sends_only_small_blobs",
filter_blob_limit,
),
(
"RED_unsupported_filter_is_refused_not_ignored",
red_unsupported_filter,
),
// bundles
("bundle_from_a_gunnar_clone_rebuilds_the_graph", bundle),
// empty repositories
(
"clone_of_an_empty_repository_succeeds_and_is_empty",
clone_of_an_empty_repository_succeeds_and_is_empty,
),
(
"first_push_into_an_empty_repository",
first_push_into_an_empty_repository,
),
// content the server must not touch
(
"a_signed_tag_survives_verbatim",
a_signed_tag_survives_verbatim,
),
// not implemented, said out loud
(
"archive_over_the_protocol_is_refused_not_hung",
archive_over_the_protocol_is_refused_not_hung,
),
("combined_filters_are_refused", combined_filters_are_refused),
];
run_cases("real_git_surface", cases, 24);
}
// ── the judge ───────────────────────────────────────────────────────────────
/// Every object id in a repository, sorted: stock git's own inventory of what
/// it holds, not gunnar re-reading its own bytes.
fn all_objects(dir: &Path) -> Vec<String> {
let out = git_in(dir)
.args(["cat-file", "--batch-check", "--batch-all-objects"])
.ok();
let mut ids: Vec<String> = out
.lines()
// `<oid> <type> <size>`; the size is dropped because a repacked copy
// may store the same object at a different on-disk size, while the id
// and the type are what "the same graph" means.
.filter_map(|line| {
let mut parts = line.split_whitespace();
let id = parts.next()?;
let kind = parts.next()?;
Some(format!("{id} {kind}"))
})
.collect();
ids.sort();
ids
}
/// Every reference and the object it resolves to, sorted.
///
/// `%(objectname)` rather than the peeled id, so an annotated tag compares as
/// the tag object it is. A comparison that peeled would call a repository that
/// lost its tag objects equal to one that kept them.
fn all_refs(dir: &Path) -> Vec<String> {
let out = git_in(dir)
.args(["for-each-ref", "--format=%(refname) %(objectname)"])
.ok();
let mut refs: Vec<String> = out.lines().map(str::to_owned).collect();
refs.sort();
refs
}
/// The two repositories hold the same objects and the same references.
///
/// The floors are the whole point: `assert_eq!(vec![], vec![])` passes, so
/// without them every case here would be green against a server that sent
/// nothing at all. They are checked against `origin` first, so a fixture that
/// stopped building its history fails as a broken fixture rather than passing
/// as a clean comparison.
fn assert_same_graph(
label: &str,
origin: &Path,
clone: &Path,
min_objects: usize,
min_refs: usize,
) {
let origin_objects = all_objects(origin);
let origin_refs = all_refs(origin);
assert!(
origin_objects.len() >= min_objects,
"{label}: the ORIGIN holds {} objects, fewer than the floor of {min_objects} — \
the fixture is broken, and an empty fixture would make this comparison vacuous",
origin_objects.len()
);
assert!(
origin_refs.len() >= min_refs,
"{label}: the ORIGIN holds {} refs, fewer than the floor of {min_refs} — \
the fixture is broken, and a repository with no refs compares equal to anything",
origin_refs.len()
);
let clone_objects = all_objects(clone);
let clone_refs = all_refs(clone);
if origin_objects != clone_objects {
let missing: Vec<_> = origin_objects
.iter()
.filter(|id| !clone_objects.contains(id))
.take(10)
.collect();
let extra: Vec<_> = clone_objects
.iter()
.filter(|id| !origin_objects.contains(id))
.take(10)
.collect();
panic!(
"{label}: the object graphs differ — origin has {} objects, the clone {}.\n\
missing from the clone: {missing:#?}\nextra in the clone: {extra:#?}",
origin_objects.len(),
clone_objects.len()
);
}
assert_eq!(
origin_refs, clone_refs,
"{label}: the reference sets differ"
);
}
/// The refs a repository holds, as bare names.
fn ref_names(dir: &Path) -> Vec<String> {
git_in(dir)
.args(["for-each-ref", "--format=%(refname)"])
.ok()
.lines()
.map(str::to_owned)
.collect()
}
fn has_ref(dir: &Path, name: &str) -> bool {
ref_names(dir).iter().any(|line| line == name)
}
/// Every object of one type, by id.
fn objects_of_kind(dir: &Path, kind: &str) -> Vec<String> {
all_objects(dir)
.into_iter()
.filter(|line| line.ends_with(&format!(" {kind}")))
.collect()
}
// ── whole-namespace clones ──────────────────────────────────────────────────
fn clone_bare() {
let scene = Scene::new(Kind::Sha1);
let target = scene.root.join("bare.git");
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--bare"])
.arg(scene.url())
.arg(target.to_str().unwrap())
.ok();
// A bare clone maps `refs/heads/*` to `refs/heads/*` and keeps tags, so
// its ref set is the origin's. Its object graph must be the origin's too.
assert_same_graph("clone --bare", &scene.origin, &target, 12, 3);
git_in(&target).args(["fsck", "--no-progress"]).ok();
}
fn clone_mirror() {
let scene = Scene::new(Kind::Sha1);
// A ref stock git would never put in `refs/heads` or `refs/tags`. `--bare`
// would not carry it; `--mirror` must.
seed_extra_ref(&scene, "refs/custom/thing");
let target = scene.root.join("mirror.git");
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--mirror"])
.arg(scene.url())
.arg(target.to_str().unwrap())
.ok();
assert_same_graph("clone --mirror", &scene.origin, &target, 12, 4);
assert!(
has_ref(&target, "refs/custom/thing"),
"a mirror clone dropped the ref outside refs/heads and refs/tags: {:#?}",
ref_names(&target)
);
}
/// A clone whose objects the client **already has**, taken through the
/// negotiation path and then cut loose from its reference.
///
/// `--dissociate` is what makes this a real assertion: without it the second
/// clone borrows the first one's objects through `alternates`, and a server
/// that sent nothing at all would still produce a repository that passes
/// `fsck`. With it, git must end up holding every object itself.
fn clone_reference() {
let scene = Scene::new(Kind::Sha1);
let first = scene.root.join("first.git");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--bare"])
.arg(scene.url())
.arg(first.to_str().unwrap())
.ok();
let second = scene.root.join("second.git");
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--bare", "--dissociate", "--reference"])
.arg(first.to_str().unwrap())
.arg(scene.url())
.arg(second.to_str().unwrap())
.ok();
assert!(
!second.join("objects/info/alternates").exists(),
"--dissociate left the alternates file in place, so this proves nothing"
);
assert_same_graph(
"clone --reference --dissociate",
&scene.origin,
&second,
12,
3,
);
}
// ── pruning ─────────────────────────────────────────────────────────────────
/// `--prune` removes a remote-tracking branch whose upstream is gone.
///
/// Discriminating: the plain fetch immediately before it is asserted to
/// **leave** the ref. Without that half, "the ref is absent" would also be true
/// of a server that never advertised it.
fn fetch_prune() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
assert!(
has_ref(&target, "refs/remotes/origin/side"),
"the clone never tracked refs/remotes/origin/side, so there is nothing to prune"
);
// Delete the branch on the origin directly, not through gunnar: what is
// under test is the fetch, not the delete.
scene
.origin_git()
.args(["update-ref", "-d", "refs/heads/side"])
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.cwd(&target)
.args(["fetch", "-q", "origin"])
.ok();
assert!(
has_ref(&target, "refs/remotes/origin/side"),
"a fetch WITHOUT --prune already removed refs/remotes/origin/side, so the \
--prune assertion below would be vacuous"
);
scene
.client(ProtocolVersion::V2)
.cwd(&target)
.args(["fetch", "-q", "--prune", "origin"])
.ok();
assert!(
!has_ref(&target, "refs/remotes/origin/side"),
"--prune left refs/remotes/origin/side behind: {:#?}",
ref_names(&target)
);
// And it pruned only what was gone.
assert!(
has_ref(&target, "refs/remotes/origin/main"),
"--prune took refs/remotes/origin/main with it"
);
}
/// `--prune-tags` is a different question from `--prune`, and the case asserts
/// the difference rather than assuming it: a plain `--prune` is required to
/// leave the tag, because tags do not live under the remote's refspec.
fn fetch_prune_tags() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
assert!(
has_ref(&target, "refs/tags/v1"),
"the clone has no tag to prune"
);
scene
.origin_git()
.args(["update-ref", "-d", "refs/tags/v1"])
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.cwd(&target)
.args(["fetch", "-q", "--prune", "origin"])
.ok();
assert!(
has_ref(&target, "refs/tags/v1"),
"--prune alone removed refs/tags/v1; then --prune-tags below proves nothing"
);
scene
.client(ProtocolVersion::V2)
.cwd(&target)
.args(["fetch", "-q", "--prune", "--prune-tags", "origin"])
.ok();
assert!(
!has_ref(&target, "refs/tags/v1"),
"--prune-tags left refs/tags/v1 behind: {:#?}",
ref_names(&target)
);
}
/// `push --mirror` makes the origin's ref namespace **exactly** the local one,
/// which means it has to carry deletions as well as creations.
fn push_mirror() {
let scene = Scene::new(Kind::Sha1);
let mirror = scene.root.join("mirror.git");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--mirror"])
.arg(scene.url())
.arg(mirror.to_str().unwrap())
.ok();
// One ref appears, one disappears. A push that only creates would pass a
// test that only looked for the new branch.
let main = scene.origin_ref("refs/heads/main");
git_in(&mirror)
.args(["update-ref", "refs/heads/added", &main])
.ok();
git_in(&mirror)
.args(["update-ref", "-d", "refs/heads/side"])
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&mirror)
.args(["push", "-q", "--mirror", "origin"])
.ok();
assert_eq!(
all_refs(&scene.origin),
all_refs(&mirror),
"after push --mirror the origin's refs are not the mirror's"
);
assert!(
has_ref(&scene.origin, "refs/heads/added"),
"push --mirror did not create refs/heads/added"
);
assert!(
!has_ref(&scene.origin, "refs/heads/side"),
"push --mirror did not delete refs/heads/side"
);
scene.origin_git().args(["fsck", "--no-progress"]).ok();
}
/// `push --prune` with an explicit refspec deletes the remote refs the refspec
/// covers and the local side no longer has.
fn push_prune() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
// A branch that exists only on the origin, under the refspec's reach.
let main = scene.origin_ref("refs/heads/main");
scene
.origin_git()
.args(["update-ref", "refs/heads/doomed", &main])
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args([
"push",
"-q",
"--prune",
"origin",
"refs/heads/*:refs/heads/*",
])
.ok();
assert!(
!has_ref(&scene.origin, "refs/heads/doomed"),
"push --prune left refs/heads/doomed behind: {:#?}",
ref_names(&scene.origin)
);
// Discriminating: it deleted the unmatched ref and NOT the matched ones.
assert!(
has_ref(&scene.origin, "refs/heads/main"),
"push --prune deleted refs/heads/main, which the local side still has"
);
}
// ── refspec forms ───────────────────────────────────────────────────────────
/// The `+` is what makes a non-fast-forward legal, and this asserts both
/// halves: the same push is **refused** without it and **accepted** with it,
/// against the same server, one after the other.
fn push_non_fast_forward() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
let before = scene.origin_ref("refs/heads/main");
// Rewind to a commit that is not a descendant, so the push is genuinely a
// non-fast-forward rather than a no-op.
git_in(&target)
.args(["reset", "-q", "--hard", "HEAD~2"])
.ok();
let rewound = git_in(&target).args(["rev-parse", "HEAD"]).ok();
assert_ne!(
rewound, before,
"the reset did not move HEAD, so nothing is being tested"
);
scene.clear_log();
let (ok, _, stderr) = scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args(["push", "origin", "refs/heads/main:refs/heads/main"])
.run();
assert!(
!ok,
"a non-fast-forward push without + was accepted:\n{stderr}"
);
assert_eq!(
scene.origin_ref("refs/heads/main"),
before,
"the refused push moved the ref anyway"
);
// The acceptance half. Without it, the refusal above is equally true of a
// server that refuses every push.
scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args(["push", "-q", "origin", "+refs/heads/main:refs/heads/main"])
.ok();
assert_eq!(
scene.origin_ref("refs/heads/main"),
rewound,
"the forced push did not move the ref"
);
scene.origin_git().args(["fsck", "--no-progress"]).ok();
}
fn push_glob_refspec() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
// Three local branches the glob must carry in one push.
for name in ["alpha", "beta", "gamma"] {
git_in(&target)
.args(["branch", name, "refs/remotes/origin/main"])
.ok();
}
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args(["push", "-q", "origin", "refs/heads/*:refs/heads/mirrored/*"])
.ok();
for name in ["alpha", "beta", "gamma"] {
assert!(
has_ref(&scene.origin, &format!("refs/heads/mirrored/{name}")),
"the glob refspec did not create refs/heads/mirrored/{name}: {:#?}",
ref_names(&scene.origin)
);
}
scene.origin_git().args(["fsck", "--no-progress"]).ok();
}
fn push_head_colon() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
commit(
&target,
"head-colon.txt",
"content",
"a commit to push by HEAD",
);
let local = git_in(&target).args(["rev-parse", "HEAD"]).ok();
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args(["push", "-q", "origin", "HEAD:refs/heads/from-head"])
.ok();
assert_eq!(
scene.origin_ref("refs/heads/from-head"),
local,
"HEAD:refs/heads/from-head did not land on the commit HEAD names"
);
scene.origin_git().args(["fsck", "--no-progress"]).ok();
}
/// A fetch refspec into a namespace no default clone would use, which is the
/// case that catches a server that only ever answers the refs it advertised
/// under their advertised names.
fn fetch_glob() {
let scene = Scene::new(Kind::Sha1);
let target = scene.root.join("private");
Git::new()
.cwd(&scene.root)
.args(["init", "-q", "--bare"])
.arg(target.to_str().unwrap())
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.cwd(&target)
.args(["fetch", "-q"])
.arg(scene.url())
.arg("refs/heads/*:refs/private/upstream/*")
.ok();
for name in ["main", "side"] {
assert!(
has_ref(&target, &format!("refs/private/upstream/{name}")),
"the fetch refspec did not create refs/private/upstream/{name}: {:#?}",
ref_names(&target)
);
}
// And it created nothing under the names the server advertised.
assert!(
!has_ref(&target, "refs/heads/main"),
"the fetch wrote refs/heads/main, which the refspec did not ask for"
);
git_in(&target).args(["fsck", "--no-progress"]).ok();
}
// ── ref namespaces ──────────────────────────────────────────────────────────
/// `refs/notes/*` is an ordinary ref namespace carrying an ordinary commit, and
/// that is exactly why it is worth a case: a server that special-cases
/// `refs/heads` and `refs/tags` serves it wrong.
fn notes_round_trip() {
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
git_in(&target)
.args(["notes", "add", "-m", "a note on the tip", "HEAD"])
.ok();
let note_ref = git_in(&target)
.args(["rev-parse", "refs/notes/commits"])
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args(["push", "-q", "origin", "refs/notes/*:refs/notes/*"])
.ok();
assert_eq!(
scene.origin_ref("refs/notes/commits"),
note_ref,
"the notes ref did not arrive at the origin"
);
// The server must *advertise* it, named rather than inferred from the
// fetch below succeeding: a server that hides the namespace fails the
// fetch with "couldn't find remote ref", which is a true failure reported
// as the wrong thing.
let advertised = scene
.client(ProtocolVersion::V2)
.args(["ls-remote"])
.arg(scene.url())
.ok();
assert!(
advertised.contains("refs/notes/commits"),
"the server does not advertise refs/notes/commits:\n{advertised}"
);
// And back out again, into a fresh repository, with the note readable.
let readback = scene.root.join("readback");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q"])
.arg(scene.url())
.arg(readback.to_str().unwrap())
.ok();
scene
.client(ProtocolVersion::V2)
.cwd(&readback)
.args(["fetch", "-q", "origin", "refs/notes/*:refs/notes/*"])
.ok();
let text = git_in(&readback).args(["notes", "show", "HEAD"]).ok();
assert_eq!(
text.trim(),
"a note on the tip",
"the note did not survive the round trip"
);
}
/// Legal reference names that no default clone produces: deep nesting, a
/// namespace outside `refs/heads` and `refs/tags`, and the `refs/pull/N/head`
/// shape forges use.
fn odd_namespaces() {
const ODD: &[&str] = &[
"refs/heads/feature/deep/nested/branch",
"refs/pull/42/head",
"refs/custom/thing",
"refs/heads/dash-and.dots_and+plus",
];
let scene = Scene::new(Kind::Sha1);
let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
let tip = git_in(&target).args(["rev-parse", "HEAD"]).ok();
scene.clear_log();
let mut push = scene
.client(ProtocolVersion::V0)
.cwd(&target)
.args(["push", "-q", "origin"]);
for name in ODD {
push = push.arg(format!("HEAD:{name}"));
}
push.ok();
for name in ODD {
assert_eq!(
&scene.origin_ref(name),
&tip,
"the origin does not hold {name} at the pushed tip: {:#?}",
ref_names(&scene.origin)
);
}
// A mirror clone must bring every one of them back.
let back = scene.root.join("odd-mirror.git");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--mirror"])
.arg(scene.url())
.arg(back.to_str().unwrap())
.ok();
for name in ODD {
assert!(
has_ref(&back, name),
"a mirror clone lost {name}: {:#?}",
ref_names(&back)
);
}
assert_same_graph("odd namespaces mirror", &scene.origin, &back, 12, 7);
}
/// `--refs` is the filter that hides both `HEAD` and the peeled `^{}` lines.
/// The interop suite asserts the peeled line is *present* without it; this
/// asserts it is *absent* with it, which is the half that catches an
/// advertisement the server builds the same way every time.
fn ls_remote_refs() {
let scene = Scene::new(Kind::Sha1);
scene.clear_log();
let full = scene
.client(ProtocolVersion::V2)
.args(["ls-remote", "--symref"])
.arg(scene.url())
.ok();
assert!(
full.contains("refs/tags/v1^{}"),
"the unfiltered ls-remote has no peeled tag, so --refs hiding one proves nothing:\n{full}"
);
assert!(
full.lines().any(|line| line.ends_with("\tHEAD")),
"the unfiltered ls-remote has no HEAD line:\n{full}"
);
let refs_only = scene
.client(ProtocolVersion::V2)
.args(["ls-remote", "--refs"])
.arg(scene.url())
.ok();
assert!(
!refs_only.contains("^{}"),
"ls-remote --refs still reported a peeled tag:\n{refs_only}"
);
assert!(
!refs_only.lines().any(|line| line.ends_with("\tHEAD")),
"ls-remote --refs still reported HEAD:\n{refs_only}"
);
assert!(
refs_only.contains("refs/tags/v1"),
"ls-remote --refs dropped the tag itself, not just its peeled line:\n{refs_only}"
);
}
// ── object filters ──────────────────────────────────────────────────────────
/// A fixture with **nested trees and two blob sizes**, which is what the tree
/// and size filters need and [`Scene::new`] is not.
///
/// `Scene::new` puts one small file per commit at the root: against it,
/// `tree:0`, `tree:1` and `blob:limit=` all produce the same answer as
/// `blob:none`, and a server that implemented one and aliased the rest would
/// pass every case. This shape makes the three disagree.
///
/// ```text
/// small.txt 16 bytes at depth 1 (the root tree)
/// large.bin 40960 bytes at depth 1
/// deep/inner.txt 16 bytes at depth 2
/// deep/sub/leaf.txt 16 bytes at depth 3
/// ```
fn layered(scene_root: &Path) -> (PathBuf, PathBuf) {
let work = scene_root.join("layered-seed");
Git::new()
.cwd(scene_root)
.args(["init", "-q", "-b", "main"])
.arg(work.to_str().unwrap())
.ok();
std::fs::create_dir_all(work.join("deep/sub")).unwrap();
std::fs::write(work.join("small.txt"), "small contents\n").unwrap();
// Incompressible-ish and comfortably over any limit the case picks, so
// "large" is large by the size git reports and not by luck.
let large: String = (0..LARGE_LINES)
.map(|i| format!("large blob line {i:06} padding padding padding\n"))
.collect();
std::fs::write(work.join("large.bin"), &large).unwrap();
std::fs::write(work.join("deep/inner.txt"), "inner contents\n").unwrap();
std::fs::write(work.join("deep/sub/leaf.txt"), "leaf contents\n").unwrap();
git_in(&work).args(["add", "-A"]).ok();
git_in(&work)
.args(["commit", "-q", "-m", "the layered tree"])
.ok();
let origin = scene_root.join("layered.git");
Git::new()
.cwd(scene_root)
.args(["clone", "-q", "--bare"])
.arg(work.to_str().unwrap())
.arg(origin.to_str().unwrap())
.ok();
(work, origin)
}
/// Lines in `large.bin`. ~2 KiB per 40 lines; 800 lines is ~40 KiB, far above
/// the 1 KiB limit the size case uses and far below anything that would make
/// the fixture expensive.
const LARGE_LINES: usize = 800;
/// A [`Scene`] rebuilt around the layered fixture, so [`Scene::url`] serves it.
fn layered_scene() -> Scene {
let mut scene = Scene::new(Kind::Sha1);
let (_work, origin) = layered(&scene.root);
scene.origin = origin;
scene
}
fn filter_tree_0() {
let scene = layered_scene();
let target = scene.root.join("tree0");
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--no-checkout", "--filter=tree:0"])
.arg(scene.url())
.arg(target.to_str().unwrap())
.ok();
// The fixture must actually contain what the filter is supposed to remove,
// or "no trees arrived" is true of a repository that never had any.
assert!(
objects_of_kind(&scene.origin, "tree").len() >= 3,
"the layered fixture has fewer than three trees, so tree:0 proves nothing"
);
let trees = objects_of_kind(&target, "tree");
let blobs = objects_of_kind(&target, "blob");
assert!(
trees.is_empty(),
"a tree:0 clone received {} trees, so the filter did nothing: {trees:#?}",
trees.len()
);
assert!(
blobs.is_empty(),
"a tree:0 clone received {} blobs; no blob is reachable without a tree",
blobs.len()
);
assert!(
!objects_of_kind(&target, "commit").is_empty(),
"a tree:0 clone received no commits either, so it received nothing at all"
);
}
/// `tree:1` is the case that separates a real depth filter from `tree:0`
/// wearing a different name: the root tree arrives, the two below it do not.
fn filter_tree_1() {
let scene = layered_scene();
let target = scene.root.join("tree1");
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--no-checkout", "--filter=tree:1"])
.arg(scene.url())
.arg(target.to_str().unwrap())
.ok();
let origin_trees = objects_of_kind(&scene.origin, "tree").len();
assert!(
origin_trees >= 3,
"the layered fixture has {origin_trees} trees; tree:1 needs at least three to be \
distinguishable from tree:0"
);
let trees = objects_of_kind(&target, "tree");
assert_eq!(
trees.len(),
1,
"a tree:1 clone should hold exactly the root tree, and holds {}: {trees:#?}",
trees.len()
);
}
fn filter_blob_limit() {
let scene = layered_scene();
let target = scene.root.join("bloblimit");
scene.clear_log();
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--no-checkout", "--filter=blob:limit=1k"])
.arg(scene.url())
.arg(target.to_str().unwrap())
.ok();
// The fixture straddles the limit, asserted rather than assumed.
let sizes = blob_sizes(&scene.origin);
assert!(
sizes.iter().any(|size| *size <= 1024),
"the fixture has no blob at or under 1 KiB, so the limit would drop everything"
);
assert!(
sizes.iter().any(|size| *size > 1024),
"the fixture has no blob over 1 KiB, so the limit would drop nothing"
);
let kept = blob_sizes(&target);
assert!(
!kept.is_empty(),
"a blob:limit=1k clone received no blobs at all; the small ones should have come"
);
let oversize: Vec<_> = kept.iter().filter(|size| **size > 1024).collect();
assert!(
oversize.is_empty(),
"a blob:limit=1k clone received blobs of {oversize:?} bytes, over the 1024 limit"
);
// Trees are not blobs: the limit must not have taken them too.
assert!(
!objects_of_kind(&target, "tree").is_empty(),
"a blob:limit clone received no trees, which no blob filter should affect"
);
}
/// Every blob's size, from git's own `--batch-check`.
fn blob_sizes(dir: &Path) -> Vec<u64> {
git_in(dir)
.args(["cat-file", "--batch-check", "--batch-all-objects"])
.ok()
.lines()
.filter_map(|line| {
let mut parts = line.split_whitespace();
let _id = parts.next()?;
if parts.next()? != "blob" {
return None;
}
parts.next()?.parse().ok()
})
.collect()
}
/// A filter gunnar does not implement must be **refused**, not accepted and
/// ignored — the failure `filter.rs` names in its own module docs.
///
/// Discriminating twice over: a supported filter is run against the same server
/// first and asserted to succeed, so this is not a server that refuses every
/// filter, and the refused clone is asserted to have left no repository behind.
fn red_unsupported_filter() {
let scene = layered_scene();
// The acceptance half, first.
let good = scene.root.join("supported");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--no-checkout", "--filter=blob:none"])
.arg(scene.url())
.arg(good.to_str().unwrap())
.ok();
assert!(
!objects_of_kind(&good, "commit").is_empty(),
"the supported filter produced an empty clone, so the refusal below proves nothing"
);
// `sparse:oid` is a real git filter gunnar does not implement.
let bad = scene.root.join("unsupported");
scene.clear_log();
let (ok, _, stderr) = scene
.client(ProtocolVersion::V2)
.args([
"clone",
"-q",
"--no-checkout",
"--filter=sparse:oid=HEAD:sparse",
])
.arg(scene.url())
.arg(bad.to_str().unwrap())
.run();
assert!(
!ok,
"a clone with an unimplemented filter SUCCEEDED — the filter was accepted and \
ignored, which is the exact failure filter.rs warns about:\n{stderr}"
);
}
// ── bundles ─────────────────────────────────────────────────────────────────
/// The graph gunnar served has to be **self-contained**, and a bundle is the
/// sharpest way to say so: `git bundle create --all` walks every ref, `verify`
/// checks the prerequisites, and a clone from the bundle must rebuild the
/// origin's object graph without the server in the loop at all.
fn bundle() {
let scene = Scene::new(Kind::Sha1);
let source = scene.root.join("bundle-source.git");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--mirror"])
.arg(scene.url())
.arg(source.to_str().unwrap())
.ok();
let file = scene.root.join("all.bundle");
git_in(&source)
.args(["bundle", "create", "-q"])
.arg(file.to_str().unwrap())
.arg("--all")
.ok();
assert!(
std::fs::metadata(&file).map(|m| m.len()).unwrap_or(0) > 0,
"git bundle create produced an empty file"
);
let verify = git_in(&source)
.args(["bundle", "verify"])
.arg(file.to_str().unwrap())
.ok();
assert!(
verify.contains("is okay") || verify.contains("The bundle"),
"git bundle verify did not report the bundle as usable:\n{verify}"
);
let rebuilt = scene.root.join("from-bundle.git");
Git::new()
.cwd(&scene.root)
.args(["clone", "-q", "--mirror"])
.arg(file.to_str().unwrap())
.arg(rebuilt.to_str().unwrap())
.ok();
assert_same_graph("bundle round trip", &scene.origin, &rebuilt, 12, 3);
}
// ── fixture helpers ─────────────────────────────────────────────────────────
/// Put a reference outside `refs/heads` and `refs/tags` on the origin.
fn seed_extra_ref(scene: &Scene, name: &str) {
let main = scene.origin_ref("refs/heads/main");
scene.origin_git().args(["update-ref", name, &main]).ok();
}
// ── empty repositories ──────────────────────────────────────────────────────
/// A [`Scene`] whose origin has **no references and no objects**.
///
/// The state every repository starts in, and the one most likely to divide by
/// zero: the advertisement has nothing to advertise and the first push has no
/// old value to compare against.
fn empty_scene() -> Scene {
let mut scene = Scene::new(Kind::Sha1);
let origin = scene.root.join("empty.git");
Git::new()
.cwd(&scene.root)
.args(["init", "-q", "--bare", "-b", "main"])
.arg(origin.to_str().unwrap())
.ok();
scene.origin = origin;
scene
}
fn clone_of_an_empty_repository_succeeds_and_is_empty() {
let scene = empty_scene();
assert!(
ref_names(&scene.origin).is_empty(),
"the fixture origin is not empty: {:#?}",
ref_names(&scene.origin)
);
let target = scene.root.join("empty-clone");
scene.clear_log();
let (ok, _, stderr) = scene
.client(ProtocolVersion::V2)
.args(["clone", "-q"])
.arg(scene.url())
.arg(target.to_str().unwrap())
.run();
assert!(ok, "cloning an empty repository failed:\n{stderr}");
assert!(
all_objects(&target).is_empty(),
"an empty repository produced a clone holding {:?}",
all_objects(&target)
);
assert!(ref_names(&target).is_empty());
// It must be a usable repository, not a directory that happens to exist.
git_in(&target).args(["fsck", "--no-progress"]).ok();
}
/// The first push a repository ever receives: every command has a null old
/// value, and there is no history to make the pack thin against.
fn first_push_into_an_empty_repository() {
let scene = empty_scene();
let work = scene.root.join("first-work");
Git::new()
.cwd(&scene.root)
.args(["init", "-q", "-b", "main"])
.arg(work.to_str().unwrap())
.ok();
commit(&work, "first.txt", "hello", "the very first commit");
let tip = git_in(&work).args(["rev-parse", "HEAD"]).ok();
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&work)
.args(["push", "-q"])
.arg(scene.url())
.arg("refs/heads/main:refs/heads/main")
.ok();
assert_eq!(
scene.origin_ref("refs/heads/main"),
tip,
"the first push did not land"
);
scene.origin_git().args(["fsck", "--no-progress"]).ok();
// And it is servable back out, which is the half that catches a push
// that wrote refs without writing objects.
// --mirror, not a plain clone: a working clone also holds
// refs/remotes/origin/*, which the origin does not, and comparing the two
// ref sets would fail on the difference between a clone and its origin
// rather than on anything the server did.
let back = scene.root.join("first-back.git");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--mirror"])
.arg(scene.url())
.arg(back.to_str().unwrap())
.ok();
assert_same_graph("first push", &scene.origin, &back, 3, 1);
}
// ── object content the server must not touch ────────────────────────────────
/// **A signed tag's bytes must survive verbatim.**
///
/// A signature is only worth anything if the object it covers comes back
/// byte-for-byte, and a tag object is the one place a server is tempted to
/// re-serialise: it parses tags to peel them for the advertisement. This pushes
/// a tag carrying a PGP signature block through gunnar and asserts the object
/// id is unchanged on the far side — which, the id being the hash of the
/// content, is the strongest available statement that not one byte moved.
///
/// The signature is crafted rather than made with `git tag -s`, so the case
/// needs no key on the machine. gunnar cannot tell the difference: nothing in
/// the server verifies signatures, and that is exactly the property under test.
fn a_signed_tag_survives_verbatim() {
let scene = Scene::new(Kind::Sha1);
let work = scene.root.join("seed");
let target = scene.origin_ref("refs/heads/main");
let tag_object = format!(
"object {target}\n\
type commit\n\
tag signed-release\n\
tagger Gunnar Test <gunnar@example.invalid> 1700000100 +0000\n\
\n\
a release that carries a signature\n\
-----BEGIN PGP SIGNATURE-----\n\
\n\
iQEzBAABCAAdFiEE0000000000000000000000000000BQJhAAAACgkQ0000000000\n\
AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\
=AAAA\n\
-----END PGP SIGNATURE-----\n"
);
let tag_path = scene.root.join("tag-object.txt");
std::fs::write(&tag_path, &tag_object).unwrap();
let tag_id = git_in(&work)
.args(["hash-object", "-t", "tag", "-w"])
.arg(tag_path.to_str().unwrap())
.ok();
let tag_id = tag_id.trim().to_owned();
assert_eq!(
tag_id.len(),
40,
"the tag object was not written: {tag_id:?}"
);
git_in(&work)
.args(["update-ref", "refs/tags/signed-release", &tag_id])
.ok();
scene.clear_log();
scene
.client(ProtocolVersion::V0)
.cwd(&work)
.args(["push", "-q"])
.arg(scene.url())
.arg("refs/tags/signed-release:refs/tags/signed-release")
.ok();
// Same id on the server: the content is identical, signature included.
assert_eq!(
scene.origin_ref("refs/tags/signed-release"),
tag_id,
"the tag object's id changed in transit, so its bytes did not survive"
);
// And the signature is still readable after a round trip back out.
let back = scene.root.join("signed-back");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--mirror"])
.arg(scene.url())
.arg(back.to_str().unwrap())
.ok();
let body = git_in(&back)
.args(["cat-file", "tag", "refs/tags/signed-release"])
.ok();
assert!(
body.contains("-----BEGIN PGP SIGNATURE-----")
&& body.contains("-----END PGP SIGNATURE-----"),
"the signature block did not survive the round trip:\n{body}"
);
assert_eq!(
git_in(&back)
.args(["rev-parse", "refs/tags/signed-release"])
.ok(),
tag_id,
"the cloned tag has a different id"
);
}
// ── what gunnar does not implement, said out loud ───────────────────────────
/// `git archive --remote` asks for the v2 `git-upload-archive` service, which
/// gunnar does not implement.
///
/// Recorded as a case rather than left unstated: the requirement is that an
/// unimplemented service is **refused**, because the alternative — a server
/// that accepts the request and answers nothing — is a client that hangs. The
/// acceptance half proves the same server is reachable and serving.
fn archive_over_the_protocol_is_refused_not_hung() {
let scene = Scene::new(Kind::Sha1);
// Reachable and serving, first.
scene
.client(ProtocolVersion::V2)
.args(["ls-remote"])
.arg(scene.url())
.ok();
let (ok, stdout, stderr) = scene
.client(ProtocolVersion::V2)
.args(["archive", "--format=tar", "--remote"])
.arg(scene.url())
.arg("HEAD")
.run();
assert!(
!ok,
"git archive --remote SUCCEEDED against a server that implements no \
upload-archive service:\nstdout {} bytes\n{stderr}",
stdout.len()
);
}
/// `combine:` is a real git filter gunnar refuses by name, and the refusal has
/// to reach the client rather than be ignored.
fn combined_filters_are_refused() {
let scene = Scene::new(Kind::Sha1);
// A single filter works against this server.
let good = scene.root.join("single-filter");
scene
.client(ProtocolVersion::V2)
.args(["clone", "-q", "--no-checkout", "--filter=blob:none"])
.arg(scene.url())
.arg(good.to_str().unwrap())
.ok();
assert!(!all_objects(&good).is_empty(), "the control clone is empty");
let bad = scene.root.join("combined-filter");
let (ok, _, stderr) = scene
.client(ProtocolVersion::V2)
.args([
"clone",
"-q",
"--no-checkout",
"--filter=blob:none",
"--filter=tree:0",
])
.arg(scene.url())
.arg(bad.to_str().unwrap())
.run();
assert!(
!ok,
"a combined filter was accepted by a server that implements one at a \
time, so the client believes it made a partial clone it did not:\n{stderr}"
);
}