crates/nornir-git/tests/checkout_parity.rs
//! **Every transport must leave the same bytes on disk.**
//!
//! WHY THIS FILE EXISTS — measured on oden 2026-09-12. This crate had **two**
//! worktree writers and they disagreed about content filters:
//!
//! | writer | reached by | smudge |
//! |---|---|---|
//! | gix `PrepareCheckout::main_worktree` | `clone_or_fetch` over a local / `file://` / `https` remote | **applied** |
//! | `materialize_worktree` (a hand-rolled `std::fs::write` of each index blob) | `ssh_sync`, `set_head_and_checkout`, `detach_head_and_checkout` | **skipped** |
//!
//! So the same commit checked out through one transport and through the other
//! produced *different files*, and which one you got depended on the URL scheme
//! in a descriptor. For an LFS-tracked path that is the difference between the
//! asset and its 131-byte pointer, which is why skade, holger and Skidbladnir
//! failed checkout: `gunnar-client::unfiltered_paths` REFUSES a pointer when
//! `filter.lfs.required=true`, which is git's own contract.
//!
//! # The fixture is a filter, not git-lfs
//!
//! The subject is "does this writer run the smudge driver the repository
//! declares", not "is git-lfs installed". So the fixture declares its own driver,
//! whose effect is unmistakable and whose input is committed verbatim:
//!
//! * the blob in the odb is the literal line `POINTER`;
//! * `.gitattributes` routes `*.bin` through `filter=probe`;
//! * `filter.probe.smudge` rewrites that line to `SMUDGED`, and `clean` rewrites
//! it back, so the pair round-trips exactly and a checkout can also be asked
//! to read CLEAN afterwards.
//!
//! A writer that runs the smudge produces `SMUDGED`; one that does not produces
//! `POINTER`. There is no third answer, and unlike a git-lfs fixture this one is
//! present on every box, so the file can never decay into a skip that proves
//! nothing.
//!
//! # Why one of the two halves is a source scan
//!
//! [`a_materialised_checkout_runs_the_configured_smudge_driver`] is the
//! behaviour. [`there_is_exactly_one_worktree_writer`] is the *reason* the two
//! transports now agree: there is one writer, so a future transport cannot pick
//! the wrong one because there is no wrong one to pick. That property is "no
//! other code path exists", and no execution of any one path can witness it — a
//! runtime assertion here would be the hollow guard, not the scan.
use std::path::{Path, PathBuf};
use nornir_git::gitio;
/// The committed bytes of the filtered path — what an unfiltered writer emits.
const POINTER: &[u8] = b"POINTER\n";
/// What the smudge driver turns [`POINTER`] into.
const SMUDGED: &[u8] = b"SMUDGED\n";
/// The `[filter "probe"]` block, as a repository would carry it.
///
/// Two `sed` substitutions and no shell quoting: git-config strips quotes and
/// gix then hands the value to `sh -c`, so a driver that quotes its own argument
/// arrives truncated ("Unterminated quoted string") and the test would be
/// measuring the fixture instead of the writer.
fn probe_driver() -> String {
"[filter \"probe\"]\n\
\tsmudge = sed s/POINTER/SMUDGED/\n\
\tclean = sed s/SMUDGED/POINTER/\n\
\trequired = true\n"
.to_string()
}
fn append_driver(repo_root: &Path) {
let cfg = repo_root.join(".git").join("config");
let mut text = std::fs::read_to_string(&cfg).unwrap_or_default();
text.push_str(&probe_driver());
std::fs::write(&cfg, text).expect("write .git/config");
}
/// A repo with one filtered path whose committed blob is `POINTER`.
///
/// `gitio::commit_all` writes blobs verbatim (it runs no `clean`), which is what
/// makes the odb content predictable: the blob IS `POINTER`.
fn origin_with_a_filtered_path() -> (tempfile::TempDir, PathBuf, String) {
let td = tempfile::tempdir().expect("tempdir");
let root = td.path().join("origin");
gitio::init(&root).expect("git init");
std::fs::write(root.join(".gitattributes"), b"*.bin filter=probe -text\n").expect("attrs");
std::fs::write(root.join("asset.bin"), POINTER).expect("asset");
std::fs::write(root.join("README.md"), b"hello\n").expect("readme");
let sha = gitio::commit_all(&root, "initial").expect("commit");
(td, root, sha)
}
fn asset_of(dir: &Path) -> Vec<u8> {
std::fs::read(dir.join("asset.bin")).expect("read asset.bin")
}
/// THE RED PROOF. A checkout written by this crate must run the smudge driver
/// the repository declares.
///
/// The clone comes first and carries no driver, so the file lands as the raw
/// blob; the driver is then declared exactly as `/etc/gitconfig` declares
/// `filter.lfs.*` on a real box, and the SAME commit is checked out again.
/// Before the fix the second checkout still wrote `POINTER` — `materialize_worktree`
/// hand-wrote `blob.data` and never asked the filter pipeline anything.
///
/// The assertion is on **applied output** — the bytes on disk — and not on a
/// config round-trip, because "the driver is configured" is exactly what was
/// true all along while the file was still wrong.
#[test]
fn a_materialised_checkout_runs_the_configured_smudge_driver() {
let (_td, origin, sha) = origin_with_a_filtered_path();
let dst_td = tempfile::tempdir().expect("tempdir");
let dst = dst_td.path().join("clone");
gitio::clone_or_fetch(origin.to_str().expect("utf-8"), &dst, None).expect("clone");
// Non-vacuity, in the negative direction: with NO driver declared, the same
// writer must leave the raw blob. If this already read `SMUDGED` the test
// below would pass against a writer that fabricates content rather than one
// that filters it.
assert_eq!(
asset_of(&dst),
POINTER,
"a clone of a repo with no `filter.probe` declared must leave the blob verbatim"
);
append_driver(&dst);
// The branch is read off the clone rather than assumed — `gix::init`'s
// default name is not this test's business, and guessing it would make a
// rename look like a filter bug.
let branch = gitio::head_branch(&dst).expect("head branch");
gitio::set_head_and_checkout(&dst, &branch, &sha).expect("re-materialise");
let after = asset_of(&dst);
assert_eq!(
after,
SMUDGED,
"the worktree writer skipped the configured smudge driver: `asset.bin` reads {:?} \
where the driver produces {:?}. An LFS-tracked path checked out this way is the \
pointer, which `gunnar-client::unfiltered_paths` refuses under \
`filter.lfs.required=true`.",
String::from_utf8_lossy(&after),
String::from_utf8_lossy(SMUDGED)
);
}
/// A smudged checkout must not read back as dirty.
///
/// This is the other half of the contract and the one a blunter fix breaks: a
/// writer that expands the content but leaves the index describing the raw blob
/// makes every filtered path modified forever, which is worse than the pointer
/// it cured — `worktree_freshness` feeds the release freshness gate.
#[test]
fn a_smudged_checkout_reads_clean() {
let (_td, origin, sha) = origin_with_a_filtered_path();
let dst_td = tempfile::tempdir().expect("tempdir");
let dst = dst_td.path().join("clone");
gitio::clone_or_fetch(origin.to_str().expect("utf-8"), &dst, None).expect("clone");
append_driver(&dst);
let branch = gitio::head_branch(&dst).expect("head branch");
gitio::set_head_and_checkout(&dst, &branch, &sha).expect("re-materialise");
assert_eq!(asset_of(&dst), SMUDGED, "precondition: the smudge ran");
let fresh = gitio::worktree_freshness(&dst).expect("status walk");
assert!(
!fresh.dirty,
"a checkout whose smudged content cleans back to the committed blob must read clean; \
reporting dirty here would make every filtered path look modified forever"
);
}
/// NON-VACUITY PROBE for the fixture: the blob in the object database is the
/// SHORT form. If this ever reads 4096 the fixture has started committing the
/// smudged bytes and the tests above became tautologies.
#[test]
fn the_committed_blob_is_the_unsmudged_form() {
let (_td, origin, _sha) = origin_with_a_filtered_path();
let repo = gix::open(&origin).expect("open");
let tree = repo.head_commit().expect("head").tree().expect("tree");
let entry = tree
.lookup_entry_by_path("asset.bin")
.expect("lookup")
.expect("asset.bin is in the tree");
let blob = entry.object().expect("blob");
assert_eq!(
blob.data, POINTER,
"the fixture must commit the UNSMUDGED bytes, or the parity tests compare nothing"
);
}
/// THE CONSTRUCTION GUARD — one writer, so a future transport cannot skip the
/// smudge by reaching for the other one.
///
/// The bug this file exists for was not a wrong line; it was a *choice* offered
/// at every call site. `gix_worktree_state::checkout` is now called from exactly
/// one place in this crate, and gix's `main_worktree` — the second writer, whose
/// mere existence was the defect — from none.
#[test]
fn there_is_exactly_one_worktree_writer() {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut checkout_calls: Vec<String> = Vec::new();
let mut main_worktree_calls: Vec<String> = Vec::new();
let mut files: Vec<PathBuf> = std::fs::read_dir(&src)
.expect("read src/")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
.collect();
files.sort();
assert!(files.len() > 3, "the scan found {} source files — that is a broken scan", files.len());
for path in &files {
let text = std::fs::read_to_string(path).expect("read source");
for (i, line) in text.lines().enumerate() {
// Comments talk ABOUT both writers on purpose; only code counts.
let code = line.split("//").next().unwrap_or("");
let at = format!("{}:{}", path.file_name().unwrap().to_string_lossy(), i + 1);
if code.contains("state::checkout(") || code.contains("gix_worktree_state::checkout(") {
checkout_calls.push(at.clone());
}
if code.contains(".main_worktree(") {
main_worktree_calls.push(at);
}
}
}
assert!(
main_worktree_calls.is_empty(),
"gix's `main_worktree` is a SECOND worktree writer and it is back at \
{main_worktree_calls:?} — every checkout in this crate goes through \
`materialize_worktree` so that the smudge decision has exactly one author"
);
assert_eq!(
checkout_calls.len(),
1,
"expected exactly ONE `gix::worktree::state::checkout` call site (inside \
`materialize_worktree`), found {checkout_calls:?}"
);
}