#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::Path;
use grit_lib::error::Error;
use grit_lib::maintenance::{offload_blobs, OffloadError, OffloadOptions};
use grit_lib::objects::pack::clear_pack_cache;
use grit_lib::remote::partial_clone::ObjectFilterSpec;
use grit_lib::repo::Repository;
use grit_test_support::{git_cmd, Output};
const NOW: i64 = 4_000_000_000;
fn git_out(dir: &Path, args: &[&str]) -> Output {
git_cmd(args)
.in_dir(dir)
.env("GIT_NO_LAZY_FETCH", "1")
.exec()
}
fn git(dir: &Path, args: &[&str]) -> String {
let out = git_out(dir, args);
assert!(out.ok(), "git {args:?}: {}", out.stderr);
out.stdout
}
fn assert_stored(work: &Path, repo: &Path, revs: &[&str], lop: &Path, offloaded: &[&str]) {
for (dir, list) in [(repo, revs), (lop, offloaded)] {
for rev in list {
let oid = git(work, &["rev-parse", rev]);
let present = git_out(dir, &["cat-file", "-e", oid.trim()]);
assert!(
present.ok(),
"{rev} ({}) missing from {}",
oid.trim(),
dir.display()
);
}
}
}
fn noise(seed: u64, len: usize) -> Vec<u8> {
let mut state = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1;
(0..len)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state.to_le_bytes()[0]
})
.collect()
}
fn offloaded_and_pushed(root: &Path) -> std::path::PathBuf {
git(root, &["init", "-q", "-b", "main", "work"]);
let work = root.join("work");
std::fs::write(work.join("small.txt"), b"small\n").unwrap();
std::fs::write(work.join("big.bin"), noise(1, 200 * 1024)).unwrap();
git(&work, &["add", "."]);
git(&work, &["commit", "-q", "-m", "one"]);
git(
root,
&["clone", "-q", "--bare", "--no-local", "work", "main.git"],
);
git(root, &["init", "-q", "--bare", "lop.git"]);
let main_dir = root.join("main.git");
git(&main_dir, &["remote", "add", "lop", "../lop.git"]);
let main = Repository::open(&main_dir, &Default::default()).unwrap();
let mut opts = OffloadOptions::new("lop", NOW);
opts.filter = Some(ObjectFilterSpec::BlobLimit(100 * 1024));
let report = offload_blobs(&main, &opts).unwrap();
assert_eq!(report.dropped_from_main, 1);
drop(main);
clear_pack_cache();
assert_eq!(
git(&main_dir, &["config", "repack.writeBitmaps"]).trim(),
"false"
);
std::fs::write(work.join("small.txt"), b"small, again\n").unwrap();
git(&work, &["commit", "-q", "-am", "two"]);
git(&work, &["push", "-q", "../main.git", "main"]);
main_dir
}
#[cfg_attr(
not(feature = "full-tests"),
ignore = "starts system git or is heavy; run with --features full-tests"
)]
#[test]
fn git_maintenance_keeps_every_blob_offload_leaves_in_main() {
for maintenance in [&["repack", "-a", "-d"][..], &["gc", "--prune=now"], &["gc"]] {
let tmp = tempfile::tempdir().unwrap();
let main_dir = offloaded_and_pushed(tmp.path());
let (work, lop) = (tmp.path().join("work"), tmp.path().join("lop.git"));
let kept = ["HEAD:small.txt", "HEAD~1:small.txt"];
git(&main_dir, maintenance);
assert_stored(&work, &main_dir, &kept, &lop, &["HEAD:big.bin"]);
let main = Repository::open(&main_dir, &Default::default()).unwrap();
let mut opts = OffloadOptions::new("lop", NOW);
opts.filter = Some(ObjectFilterSpec::BlobLimit(100 * 1024));
offload_blobs(&main, &opts).unwrap();
drop(main);
clear_pack_cache();
git(&main_dir, maintenance);
assert_stored(&work, &main_dir, &kept, &lop, &["HEAD:big.bin"]);
git(&main_dir, &["fsck", "--no-dangling"]);
}
}
#[cfg_attr(
not(feature = "full-tests"),
ignore = "starts system git or is heavy; run with --features full-tests"
)]
#[test]
fn git_cannot_write_a_bitmap_once_an_offloaded_repository_takes_a_push() {
let tmp = tempfile::tempdir().unwrap();
let main_dir = offloaded_and_pushed(tmp.path());
let gc = git_out(&main_dir, &["-c", "repack.writeBitmaps=true", "gc"]);
assert!(!gc.ok(), "git gc with bitmaps succeeded:\n{}", gc.stderr);
assert!(gc.stderr.contains("full closure"), "{}", gc.stderr);
let midx = git_out(&main_dir, &["repack", "-a", "-d", "-b", "--write-midx"]);
assert!(!midx.ok(), "multi-pack bitmap written:\n{}", midx.stderr);
assert!(midx.stderr.contains("full closure"), "{}", midx.stderr);
git(&main_dir, &["gc"]);
git(&main_dir, &["fsck", "--no-dangling"]);
let packs = std::fs::read_dir(main_dir.join("objects/pack")).unwrap();
assert!(
!packs
.flatten()
.any(|e| e.path().extension().is_some_and(|x| x == "bitmap")),
"a bitmap was written"
);
assert_stored(
&tmp.path().join("work"),
&main_dir,
&["HEAD:small.txt", "HEAD~1:small.txt"],
&tmp.path().join("lop.git"),
&["HEAD:big.bin"],
);
}
#[cfg_attr(
not(feature = "full-tests"),
ignore = "starts system git or is heavy; run with --features full-tests"
)]
#[test]
fn git_maintenance_keeps_objects_of_a_kept_pack_the_promisor_pack_names() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
git(root, &["init", "-q", "-b", "main", "work"]);
let work = root.join("work");
std::fs::write(work.join("small.txt"), b"small\n").unwrap();
git(&work, &["add", "."]);
git(&work, &["commit", "-q", "-m", "one"]);
git(
root,
&["clone", "-q", "--bare", "--no-local", "work", "main.git"],
);
let main_dir = root.join("main.git");
let kept_pack = std::fs::read_dir(main_dir.join("objects/pack"))
.unwrap()
.flatten()
.map(|e| e.path())
.find(|p| p.extension().is_some_and(|x| x == "pack"))
.unwrap();
let keep = kept_pack.with_extension("keep");
std::fs::write(&keep, b"held by an operator\n").unwrap();
std::fs::write(work.join("big.bin"), noise(2, 200 * 1024)).unwrap();
git(&work, &["add", "."]);
git(&work, &["commit", "-q", "-m", "two"]);
git(&work, &["push", "-q", "../main.git", "main"]);
git(root, &["init", "-q", "--bare", "lop.git"]);
git(&main_dir, &["remote", "add", "lop", "../lop.git"]);
let mut opts = OffloadOptions::new("lop", NOW);
opts.filter = Some(ObjectFilterSpec::BlobLimit(100 * 1024));
let offload = |opts: &OffloadOptions| {
let main = Repository::open(&main_dir, &Default::default()).unwrap();
let result = offload_blobs(&main, opts);
drop(main);
clear_pack_cache();
result
};
match offload(&opts) {
Err(Error::Offload(OffloadError::KeptPackObjectPromised { pack, .. })) => {
assert_eq!(pack.file_name(), kept_pack.file_name());
}
other => panic!("expected a refusal, got {other:?}"),
}
assert!(
keep.is_file() && kept_pack.is_file(),
"the kept pack was touched"
);
let lop = root.join("lop.git");
assert_stored(&work, &main_dir, &["HEAD:big.bin"], &lop, &[]);
std::fs::remove_file(&keep).unwrap();
assert_eq!(offload(&opts).unwrap().dropped_from_main, 1);
let kept = ["HEAD:small.txt", "HEAD~1", "HEAD~1^{tree}"];
for _ in 0..2 {
git(&main_dir, &["repack", "-a", "-d"]);
assert_stored(&work, &main_dir, &kept, &lop, &["HEAD:big.bin"]);
}
git(&main_dir, &["fsck", "--no-dangling"]);
}