use std::path::Path;
use std::process::Command;
use grit_lib::config::environment::Environment;
use grit_lib::config::ConfigSet;
use grit_lib::objects::odb::Odb;
use grit_lib::objects::{ObjectId, ObjectKind};
use grit_lib::refs::resolve_ref;
use grit_lib::remote::transfer::{
build_pack, fetch_local, FetchOptions, PackBuildOptions, TagMode, UpdateMode,
};
use grit_test_support::{git, git_cmd};
fn rev_parse(dir: &Path, rev: &str) -> ObjectId {
ObjectId::from_hex(git(dir, &["rev-parse", rev]).trim()).expect("valid oid")
}
fn open_odb(git_dir: &Path) -> Odb {
Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf())
}
fn build_remote(dir: &Path) {
git(dir, &["init", "-q", "-b", "main", "."]);
std::fs::write(dir.join("a.txt"), "one\n").unwrap();
git(dir, &["add", "a.txt"]);
git(dir, &["commit", "-q", "-m", "c1"]);
std::fs::write(dir.join("b.txt"), "two\n").unwrap();
git(dir, &["add", "b.txt"]);
git(dir, &["commit", "-q", "-m", "c2"]);
git(dir, &["tag", "-a", "v1", "-m", "release one"]);
}
#[test]
fn fetch_local_copies_refs_and_objects_then_fast_forwards() {
let tmp = tempfile::tempdir().expect("tempdir");
let remote = tmp.path().join("remote");
let local = tmp.path().join("local");
std::fs::create_dir_all(&remote).unwrap();
std::fs::create_dir_all(&local).unwrap();
build_remote(&remote);
let remote_git = remote.join(".git");
git(&local, &["init", "-q", "-b", "main", "."]);
let local_git = local.join(".git");
let remote_main = rev_parse(&remote, "refs/heads/main");
let remote_c1 = rev_parse(&remote, "HEAD~1");
let remote_v1 = rev_parse(&remote, "refs/tags/v1");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::All,
..Default::default()
};
let outcome = fetch_local(&local_git, &remote_git, &opts).expect("fetch_local");
assert_eq!(outcome.default_branch.as_deref(), Some("main"));
let tracked = resolve_ref(&local_git, "refs/remotes/origin/main").expect("tracking ref");
assert_eq!(tracked, remote_main);
let local_odb = open_odb(&local_git);
let c2 = local_odb.read(&remote_main).expect("commit c2 present");
assert_eq!(c2.kind, ObjectKind::Commit);
assert!(local_odb.exists(&remote_c1), "parent commit c1 present");
let b_blob = rev_parse(&remote, "HEAD:b.txt");
let blob = local_odb.read(&b_blob).expect("b.txt blob present");
assert_eq!(blob.kind, ObjectKind::Blob);
assert_eq!(blob.data, b"two\n");
assert!(local_odb.exists(&remote_v1), "tag object present");
assert_eq!(
resolve_ref(&local_git, "refs/tags/v1").expect("tag ref"),
remote_v1
);
let head_update = outcome
.updates
.iter()
.find(|u| u.remote_ref == "refs/heads/main")
.expect("main update present");
assert_eq!(head_update.mode, UpdateMode::New);
assert_eq!(head_update.new_oid, Some(remote_main));
assert!(head_update.old_oid.is_none());
for u in &outcome.updates {
if u.mode == UpdateMode::DeletedMissing {
continue;
}
assert_eq!(u.mode, UpdateMode::New, "ref {} not New", u.remote_ref);
}
assert_eq!(
outcome.transfer.objects, 7,
"first fetch installs seven objects"
);
let kinds = outcome
.transfer
.kinds
.expect("first fetch reports object types");
assert_eq!(kinds.total(), outcome.transfer.objects);
std::fs::write(remote.join("c.txt"), "three\n").unwrap();
git(&remote, &["add", "c.txt"]);
git(&remote, &["commit", "-q", "-m", "c3"]);
let remote_main2 = rev_parse(&remote, "refs/heads/main");
let c_blob = rev_parse(&remote, "HEAD:c.txt");
assert_ne!(remote_main2, remote_main);
let outcome2 = fetch_local(&local_git, &remote_git, &opts).expect("second fetch");
let head_update2 = outcome2
.updates
.iter()
.find(|u| u.remote_ref == "refs/heads/main")
.expect("main update present (2)");
assert_eq!(head_update2.mode, UpdateMode::FastForward);
assert_eq!(head_update2.old_oid, Some(remote_main));
assert_eq!(head_update2.new_oid, Some(remote_main2));
assert_eq!(
resolve_ref(&local_git, "refs/remotes/origin/main").unwrap(),
remote_main2
);
let local_odb = open_odb(&local_git);
assert!(local_odb.exists(&remote_main2), "new commit present");
let c_blob_obj = local_odb.read(&c_blob).expect("c.txt blob present");
assert_eq!(c_blob_obj.data, b"three\n");
let tag_update2 = outcome2
.updates
.iter()
.find(|u| u.remote_ref == "refs/tags/v1");
if let Some(u) = tag_update2 {
assert_eq!(u.mode, UpdateMode::UpToDate);
}
assert_eq!(
outcome2.transfer.objects, 3,
"second fetch installs only the new commit, tree, and blob"
);
let kinds2 = outcome2
.transfer
.kinds
.expect("incremental fetch reports object types");
assert_eq!(kinds2.total(), outcome2.transfer.objects);
}
#[test]
fn pack_build_honors_explicit_zero_window_and_depth() {
let tmp = tempfile::tempdir().expect("tempdir");
let remote = tmp.path().join("remote");
std::fs::create_dir_all(&remote).unwrap();
git(&remote, &["init", "-q", "-b", "main", "."]);
for i in 0..12 {
let mut body = String::from("shared prefix\n");
body.push_str(&"z".repeat(4096));
body.push_str(&format!("tail-{i}\n"));
std::fs::write(remote.join(format!("b{i}.txt")), body).unwrap();
}
git(&remote, &["add", "."]);
git(&remote, &["commit", "-qm", "similar blobs"]);
git(
&remote,
&[
"repack",
"-q",
"-a",
"-d",
"-f",
"--window=10",
"--depth=50",
],
);
git(&remote, &["config", "pack.window", "0"]);
git(&remote, &["config", "pack.depth", "0"]);
let remote_git = remote.join(".git");
let cfg = ConfigSet::load(&Environment::empty(), Some(&remote_git), true).expect("config");
assert_eq!(cfg.pack_object_window(), 0);
assert_eq!(cfg.pack_object_depth(), 0);
let odb = open_odb(&remote_git);
let tip = rev_parse(&remote, "HEAD");
let pack = build_pack(
&odb,
&[tip],
&[],
&PackBuildOptions::for_local_copy(Some(&cfg)),
)
.expect("build");
let scratch = tempfile::tempdir().expect("scratch");
let pack_path = scratch.path().join("z.pack");
std::fs::write(&pack_path, &pack).unwrap();
let idx_out = Command::new("git")
.current_dir(scratch.path())
.args(["index-pack", "z.pack"])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("index-pack");
assert!(
idx_out.status.success(),
"index-pack failed: {}",
String::from_utf8_lossy(&idx_out.stderr)
);
let idx_path = std::fs::read_dir(scratch.path())
.expect("read dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().is_some_and(|x| x == "idx"))
.expect("idx beside pack");
let out = Command::new("git")
.args(["verify-pack", "-v"])
.arg(&idx_path)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("verify-pack");
assert!(
out.status.success(),
"verify-pack failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let new_deltas = String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|line| line.split_whitespace().count() >= 7)
.count();
assert_eq!(
new_deltas, 0,
"window=0 depth=0 must not emit newly computed deltas"
);
}
fn pack_files(git_dir: &Path) -> Vec<std::path::PathBuf> {
std::fs::read_dir(git_dir.join("objects/pack"))
.expect("pack dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "pack"))
.collect()
}
#[test]
fn fetch_local_from_delta_packed_remote_produces_compact_pack() {
let tmp = tempfile::tempdir().expect("tempdir");
let remote = tmp.path().join("remote");
let local = tmp.path().join("local");
std::fs::create_dir_all(&remote).unwrap();
std::fs::create_dir_all(&local).unwrap();
git(&remote, &["init", "-q", "-b", "main", "."]);
for i in 0..24 {
let mut body = String::from("shared blob prefix for delta compression\n");
body.push_str(&"q".repeat(48_000));
body.push_str(&format!("\nunique suffix {i}\n"));
std::fs::write(remote.join(format!("blob{i}.txt")), body).unwrap();
}
git(&remote, &["add", "."]);
git(&remote, &["commit", "-q", "-m", "many similar blobs"]);
let remote_git = remote.join(".git");
git(&local, &["init", "-q", "-b", "main", "."]);
let local_git = local.join(".git");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
..Default::default()
};
fetch_local(&local_git, &remote_git, &opts).expect("fetch_local");
let packs = pack_files(&local_git);
assert_eq!(packs.len(), 1, "expected one installed pack");
let grit_pack_len = std::fs::metadata(&packs[0]).expect("pack metadata").len();
let remote_odb = open_odb(&remote_git);
let tip = rev_parse(&remote, "HEAD");
let undeltified = build_pack(&remote_odb, &[tip], &[], &PackBuildOptions::default())
.expect("undeltified pack for comparison");
assert!(
grit_pack_len < undeltified.len() as u64,
"fetch_local must delta-compress blobs (grit={grit_pack_len}, undeltified={})",
undeltified.len()
);
let count_out = git(&local_git, &["count-objects", "-v"]);
let mut loose = u64::MAX;
for line in count_out.lines() {
if let Some(v) = line.strip_prefix("count: ") {
loose = v.trim().parse().expect("count");
}
}
assert_eq!(
loose, 0,
"fetch_local should install a pack, not loose objects"
);
let fsck = git_cmd(&["fsck", "--strict"]).in_dir(&local_git).exec();
assert!(fsck.ok(), "fsck after fetch: {}", fsck.stderr);
}
#[test]
fn fetch_local_into_empty_repo_links_remote_pack() {
let tmp = tempfile::tempdir().expect("tempdir");
let remote = tmp.path().join("remote");
let local = tmp.path().join("local");
std::fs::create_dir_all(&remote).unwrap();
std::fs::create_dir_all(&local).unwrap();
git(&remote, &["init", "-q", "-b", "main", "."]);
for i in 0..40 {
std::fs::write(remote.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
git(&remote, &["add", "."]);
git(&remote, &["commit", "-q", "-m", &format!("c{i}")]);
}
git(&remote, &["repack", "-a", "-d", "-f"]);
let remote_git = remote.join(".git");
let remote_packs = pack_files(&remote_git);
assert_eq!(remote_packs.len(), 1, "fixture must have one pack");
let remote_pack_name = remote_packs[0].file_name().expect("pack name").to_owned();
git(&local, &["init", "-q", "-b", "main", "."]);
let local_git = local.join(".git");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::All,
..Default::default()
};
fetch_local(&local_git, &remote_git, &opts).expect("fetch_local");
let local_packs = pack_files(&local_git);
assert_eq!(local_packs.len(), 1, "expected one linked pack");
assert_eq!(
local_packs[0].file_name().expect("local pack name"),
remote_pack_name.as_os_str(),
"initial fetch must link remote packfiles instead of rebuilding"
);
let fsck = git_cmd(&["fsck", "--strict"]).in_dir(&local_git).exec();
assert!(fsck.ok(), "fsck after linked fetch: {}", fsck.stderr);
}