use super::*;
fn git(dir: &Path, args: &[&str]) -> String {
let output = crate::test_git()
.current_dir(dir)
.args(["-c", "user.name=t", "-c", "user.email=t@t"])
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
fn format_of(name: &str) -> ObjectFormat {
ObjectFormat::from_capability(name).unwrap()
}
fn packed_repo(dir: &Path, format: &str, offsets: bool) -> PathBuf {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q", &format!("--object-format={format}")]);
let mut lines: Vec<String> = (0..400).map(|n| format!("line {n}\n")).collect();
for n in 0..40 {
lines[(n * 37) % 400] = format!("line changed in {n}\n");
lines.push(format!("added in {n}\n"));
std::fs::write(dir.join("file.txt"), lines.concat()).unwrap();
std::fs::write(
dir.join(format!("f{}.txt", n % 7)),
format!("{n}\n").repeat(n + 1),
)
.unwrap();
git(dir, &["add", "-A"]);
git(dir, &["commit", "-q", "-m", &n.to_string()]);
}
git(
dir,
&[
"-c",
&format!("repack.useDeltaBaseOffset={offsets}"),
"repack",
"-adfq",
"--depth=50",
"--window=50",
],
);
let packs = dir.join(".git/objects/pack");
std::fs::read_dir(&packs)
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|path| path.extension().is_some_and(|ext| ext == "pack"))
.unwrap()
}
fn delta_kinds(pack: &Path, index: &index::File) -> (usize, usize) {
let bytes = std::fs::read(pack).unwrap();
let kinds: Vec<u8> = (0..index.num_objects())
.map(|at| (bytes[index.pack_offset_at_index(at) as usize] >> 4) & 7)
.collect();
let count = |kind| kinds.iter().filter(|&&k| k == kind).count();
(count(OFS_DELTA), count(REF_DELTA))
}
fn pick(at: u32, case: u64) -> u64 {
(u64::from(at) ^ (case << 32)).wrapping_mul(0x9e37_79b9_7f4a_7c15) >> 40
}
#[test]
fn a_filtered_copy_has_the_index_git_writes_for_it() {
for format in ["sha1", "sha256"] {
for offsets in [true, false] {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("source");
let pack = packed_repo(&source, format, offsets);
let repo = gix::open(&source).unwrap();
let index =
index::File::at(pack.with_extension("idx"), format_of(format).kind()).unwrap();
let (ofs, refs) = delta_kinds(&pack, &index);
assert!(
if offsets {
ofs > 0 && refs == 0
} else {
refs > 0 && ofs == 0
},
"{ofs} {refs}"
);
for (case, percent) in [(0, 100), (1, 97), (2, 70), (3, 30)] {
let label = format!("{format} offsets={offsets} {percent}%");
let keep = |at: u32| pick(at, case) % 100 < percent;
let export = root.path().join(format!("export-{case}"));
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let copied = filter_copy(&repo, &pack, &keep, &export, format_of(format))
.unwrap()
.unwrap();
let check = root.path().join(format!("check-{case}"));
git(
root.path(),
&[
"init",
"-q",
"--bare",
&format!("--object-format={format}"),
check.to_str().unwrap(),
],
);
let indexed = check.join("objects/pack/copied.pack");
std::fs::copy(copied.path(&export, "pack"), &indexed).unwrap();
// not --strict, since the copy drops objects that trees still point at.
git(
&check,
&["index-pack", "--rev-index", indexed.to_str().unwrap()],
);
for ext in ["idx", "rev"] {
assert!(
std::fs::read(copied.path(&export, ext)).unwrap()
== std::fs::read(indexed.with_extension(ext)).unwrap(),
"{label}: our .{ext} isn't git's"
);
}
let listed = git(
&check,
&[
"cat-file",
"--batch-all-objects",
"--batch-check=%(objectname)",
],
);
let mut kept: Vec<String> = (0..index.num_objects())
.filter(|&at| keep(at))
.map(|at| index.oid_at_index(at).to_string())
.collect();
kept.sort();
assert_eq!(listed.lines().collect::<Vec<_>>(), kept, "{label}");
}
}
}
}
fn staged_files(export: &Path) -> Vec<String> {
std::fs::read_dir(export.join("objects/pack"))
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect()
}
#[test]
fn a_copy_of_nothing_writes_nothing() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("source");
let pack = packed_repo(&source, "sha1", true);
let repo = gix::open(&source).unwrap();
let export = root.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let copied = filter_copy(&repo, &pack, &|_| false, &export, format_of("sha1")).unwrap();
assert!(copied.is_none());
assert!(staged_files(&export).is_empty());
}
#[test]
fn a_failed_copy_leaves_nothing_behind() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("source");
let pack = packed_repo(&source, "sha1", true);
let repo = gix::open(&source).unwrap();
let cut = root.path().join("cut");
std::fs::create_dir_all(&cut).unwrap();
let short = cut.join("pack-cut.pack");
let bytes = std::fs::read(&pack).unwrap();
std::fs::write(&short, &bytes[..bytes.len() / 2]).unwrap();
std::fs::copy(pack.with_extension("idx"), short.with_extension("idx")).unwrap();
let export = root.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let error = filter_copy(&repo, &short, &|_| true, &export, format_of("sha1")).unwrap_err();
assert!(format!("{error:#}").contains("read"), "{error:#}");
assert!(
staged_files(&export).is_empty(),
"{:?}",
staged_files(&export)
);
}
/// the offsets here are made up, since no test writes a 2 GiB pack.
#[test]
fn offsets_past_2_gib_go_in_the_large_offset_table() {
let root = tempfile::tempdir().unwrap();
let pack = packed_repo(&root.path().join("source"), "sha1", true);
let source = index::File::at(pack.with_extension("idx"), gix::hash::Kind::Sha1).unwrap();
let offsets = [12, MAX_SMALL_OFFSET, MAX_SMALL_OFFSET + 1, 1 << 32, 5 << 32];
let mut written = vec![0; source.num_objects() as usize];
written[..offsets.len()].copy_from_slice(&offsets);
let crcs: Vec<u32> = (0..source.num_objects()).collect();
let entries = Filtered {
index: &source,
kept: (0..offsets.len() as u32).collect(),
written: &written,
crcs: &crcs,
};
let path = root.path().join("big.idx");
let checksum = gix::ObjectId::from_bytes_or_panic(&[7; 20]);
entries
.write_idx(&checksum, File::create(&path).unwrap())
.unwrap();
let index = index::File::at(&path, gix::hash::Kind::Sha1).unwrap();
assert_eq!(index.num_objects() as usize, offsets.len());
for (at, &offset) in offsets.iter().enumerate() {
let at = at as u32;
assert_eq!(index.oid_at_index(at), source.oid_at_index(at));
assert_eq!(index.crc32_at_index(at), Some(at));
assert_eq!(index.pack_offset_at_index(at), offset);
assert_eq!(index.lookup(source.oid_at_index(at)), Some(at));
}
assert_eq!(index.pack_checksum(), checksum);
let bytes = std::fs::read(&path).unwrap();
let large = offsets.iter().filter(|&&o| o > MAX_SMALL_OFFSET).count();
assert_eq!(
bytes.len(),
8 + 256 * 4 + offsets.len() * (20 + 4 + 4) + large * 8 + 20 + 20
);
}
#[test]
fn a_broken_source_is_an_error() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("source");
let pack = packed_repo(&source, "sha1", true);
let repo = gix::open(&source).unwrap();
let index = index::File::at(pack.with_extension("idx"), gix::hash::Kind::Sha1).unwrap();
let last = (0..index.num_objects())
.map(|at| index.pack_offset_at_index(at))
.max()
.unwrap();
let cut = root.path().join("cut");
std::fs::create_dir_all(&cut).unwrap();
let short = cut.join("pack-cut.pack");
std::fs::write(&short, &std::fs::read(&pack).unwrap()[..last as usize + 2]).unwrap();
std::fs::copy(pack.with_extension("idx"), short.with_extension("idx")).unwrap();
let export = root.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let error = filter_copy(&repo, &short, &|_| true, &export, format_of("sha1")).unwrap_err();
assert!(
format!("{error:#}").contains("ends inside an entry"),
"{error:#}"
);
assert!(staged_files(&export).is_empty());
let mut miscounted = std::fs::read(&pack).unwrap();
miscounted[11] ^= 1;
std::fs::write(&short, miscounted).unwrap();
let error = filter_copy(&repo, &short, &|_| true, &export, format_of("sha1")).unwrap_err();
assert!(format!("{error:#}").contains("version 2 pack"), "{error:#}");
let mut far = vec![(OFS_DELTA << 4) | 1];
far.extend([0xff; 9]);
far.extend([0x7f, 0x78, 0x9c]);
let error = Entry::parse(&far, format_of("sha1")).err().unwrap();
assert!(format!("{error:#}").contains("distance"), "{error:#}");
let mut near = far.clone();
near.drain(1..2);
assert!(Entry::parse(&near, format_of("sha1")).is_ok());
for kind in [0, 5] {
assert!(Entry::parse(&[kind << 4, 0x78, 0x9c], format_of("sha1")).is_err());
}
}