use std::path::Path;
use std::process::Command;
use grit_lib::objects::ObjectId;
use grit_lib::odb::Odb;
use grit_lib::transfer::{build_pack, PackBuildOptions};
use grit_lib::unpack_objects::pack_bytes_to_object_map;
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "T")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_COMMITTER_NAME", "T")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("run git");
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).expect("utf8 git output")
}
fn rev_parse(dir: &Path, rev: &str) -> ObjectId {
let hex = git(dir, &["rev-parse", rev]);
ObjectId::from_hex(hex.trim()).expect("valid oid")
}
fn pack_header_count(bytes: &[u8]) -> u32 {
assert_eq!(&bytes[0..4], b"PACK", "must start with PACK magic");
assert_eq!(u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), 2);
u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]])
}
struct Fixture {
dir: tempfile::TempDir,
c1: ObjectId,
c2: ObjectId,
c1_objects: Vec<ObjectId>,
c2_new_objects: Vec<ObjectId>,
}
fn build_fixture() -> Fixture {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
git(dir, &["init", "-q", "-b", "main", "."]);
std::fs::write(dir.join("a.txt"), b"hello\n").unwrap();
git(dir, &["add", "a.txt"]);
git(dir, &["commit", "-q", "-m", "c1"]);
let c1 = rev_parse(dir, "HEAD");
let c1_tree = rev_parse(dir, "HEAD^{tree}");
let c1_blob = rev_parse(dir, "HEAD:a.txt");
std::fs::write(dir.join("b.txt"), b"world\n").unwrap();
git(dir, &["add", "b.txt"]);
git(dir, &["commit", "-q", "-m", "c2"]);
let c2 = rev_parse(dir, "HEAD");
let c2_tree = rev_parse(dir, "HEAD^{tree}");
let c2_blob = rev_parse(dir, "HEAD:b.txt");
Fixture {
dir: tmp,
c1,
c2,
c1_objects: vec![c1, c1_tree, c1_blob],
c2_new_objects: vec![c2, c2_tree, c2_blob],
}
}
fn open_odb(dir: &Path) -> Odb {
let git_dir = dir.join(".git");
Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir)
}
#[test]
fn build_pack_selects_only_new_objects() {
let fx = build_fixture();
let odb = open_odb(fx.dir.path());
let pack = build_pack(&odb, &[fx.c2], &[fx.c1], &PackBuildOptions::default()).expect("build");
assert_eq!(&pack[0..4], b"PACK");
let count = pack_header_count(&pack);
assert_eq!(
count,
fx.c2_new_objects.len() as u32,
"expected exactly the 3 objects new in C2, got {count}"
);
let map = pack_bytes_to_object_map(&pack, &odb).expect("re-parse pack");
assert_eq!(map.len(), fx.c2_new_objects.len());
for oid in &fx.c2_new_objects {
assert!(map.contains_key(oid), "pack missing new object {oid}");
}
for oid in &fx.c1_objects {
assert!(
!map.contains_key(oid),
"pack should not contain have-reachable object {oid}"
);
}
}
#[test]
fn build_pack_with_empty_haves_packs_full_closure() {
let fx = build_fixture();
let odb = open_odb(fx.dir.path());
let pack = build_pack(&odb, &[fx.c2], &[], &PackBuildOptions::default()).expect("build");
assert_eq!(&pack[0..4], b"PACK");
let map = pack_bytes_to_object_map(&pack, &odb).expect("re-parse pack");
assert_eq!(
pack_header_count(&pack) as usize,
map.len(),
"header count must match resolved object count"
);
assert_eq!(map.len(), 6, "full closure of C2 is 6 objects, got {}", map.len());
for oid in fx.c1_objects.iter().chain(fx.c2_new_objects.iter()) {
assert!(map.contains_key(oid), "full pack missing {oid}");
}
assert!(map.contains_key(&fx.c1));
assert!(map.contains_key(&fx.c2));
}
struct DeltaFixture {
dir: tempfile::TempDir,
tips: Vec<ObjectId>,
}
fn build_delta_fixture() -> DeltaFixture {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
git(dir, &["init", "-q", "-b", "main", "."]);
let mut body = String::new();
for i in 0..4000 {
body.push_str(&format!("line {i:05} lorem ipsum dolor sit amet consectetur\n"));
}
let mut tips = Vec::new();
for rev in 0..5 {
body.push_str(&format!("--- edit number {rev} appended at the end ---\n"));
std::fs::write(dir.join("big.txt"), body.as_bytes()).unwrap();
git(dir, &["add", "big.txt"]);
git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
tips.push(rev_parse(dir, "HEAD"));
}
DeltaFixture { dir: tmp, tips }
}
fn pack_delta_stats(bytes: &[u8], algo_len: usize) -> (usize, usize, usize) {
let nr = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
let mut pos = 12usize;
let mut ofs = 0usize;
let mut ref_bases: Vec<Vec<u8>> = Vec::new();
for _ in 0..nr {
let (type_code, _size, consumed) = read_type_size(&bytes[pos..]);
pos += consumed;
match type_code {
6 => {
ofs += 1;
while bytes[pos] & 0x80 != 0 {
pos += 1;
}
pos += 1;
}
7 => {
ref_bases.push(bytes[pos..pos + algo_len].to_vec());
pos += algo_len;
}
_ => {}
}
pos += zlib_consume(&bytes[pos..]);
}
(ref_bases.len(), ofs, nr)
}
fn read_type_size(b: &[u8]) -> (u8, u64, usize) {
let mut c = b[0];
let type_code = (c >> 4) & 0x7;
let mut size = (c & 0x0f) as u64;
let mut shift = 4u32;
let mut i = 1usize;
while c & 0x80 != 0 {
c = b[i];
size |= ((c & 0x7f) as u64) << shift;
shift += 7;
i += 1;
}
(type_code, size, i)
}
fn zlib_consume(b: &[u8]) -> usize {
use std::io::Read;
let mut dec = flate2::bufread::ZlibDecoder::new(b);
let mut sink = Vec::new();
dec.read_to_end(&mut sink).expect("zlib decode");
dec.total_in() as usize
}
#[test]
fn delta_pack_is_smaller_and_reparses() {
let fx = build_delta_fixture();
let odb = open_odb(fx.dir.path());
let tip = *fx.tips.last().unwrap();
let whole = build_pack(&odb, &[tip], &[], &PackBuildOptions::default()).expect("whole");
let delta = build_pack(
&odb,
&[tip],
&[],
&PackBuildOptions {
delta: true,
..PackBuildOptions::default()
},
)
.expect("delta");
assert!(
delta.len() * 2 < whole.len(),
"delta pack ({}) should be < half the whole pack ({})",
delta.len(),
whole.len()
);
let whole_map = pack_bytes_to_object_map(&whole, &odb).expect("reparse whole");
let delta_map = pack_bytes_to_object_map(&delta, &odb).expect("reparse delta");
assert_eq!(
whole_map.keys().collect::<std::collections::BTreeSet<_>>(),
delta_map.keys().collect::<std::collections::BTreeSet<_>>(),
"delta pack must resolve to the same object set as the whole pack"
);
let (ref_n, ofs_n, _total) = pack_delta_stats(&delta, 20);
assert!(
ref_n + ofs_n >= 3,
"expected several delta entries, got ref={ref_n} ofs={ofs_n}"
);
assert!(
git_index_pack_ok(fx.dir.path(), &delta, false),
"system git index-pack rejected the delta pack"
);
}
#[test]
fn thin_pack_omits_peer_held_base_and_resolves() {
let fx = build_delta_fixture();
let odb = open_odb(fx.dir.path());
let want = *fx.tips.last().unwrap();
let have = fx.tips[fx.tips.len() - 2];
let thin = build_pack(
&odb,
&[want],
&[have],
&PackBuildOptions {
delta: true,
thin: true,
..PackBuildOptions::default()
},
)
.expect("thin pack");
assert!(
grit_lib::unpack_objects::pack_is_thin(&thin, grit_lib::objects::HashAlgo::Sha1),
"pack should be detected as thin"
);
let map = pack_bytes_to_object_map(&thin, &odb).expect("thin pack resolves via odb");
let in_pack: std::collections::HashSet<ObjectId> = map.keys().copied().collect();
let (ref_n, _ofs_n, _total) = pack_delta_stats(&thin, 20);
assert!(ref_n >= 1, "thin pack must use at least one REF_DELTA");
let external = ref_delta_external_bases(&thin, 20, &in_pack);
assert!(
!external.is_empty(),
"thin pack must reference at least one base NOT in the pack"
);
let big_oid = rev_parse(fx.dir.path(), &format!("{}:big.txt", want.to_hex()));
assert!(
map.contains_key(&big_oid),
"resolved thin pack must contain the newest big.txt blob"
);
assert!(
git_index_pack_ok(fx.dir.path(), &thin, true),
"system git index-pack --fix-thin rejected the thin pack"
);
}
fn ref_delta_external_bases(
bytes: &[u8],
algo_len: usize,
in_pack: &std::collections::HashSet<ObjectId>,
) -> Vec<ObjectId> {
let nr = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
let mut pos = 12usize;
let mut out = Vec::new();
for _ in 0..nr {
let (type_code, _size, consumed) = read_type_size(&bytes[pos..]);
pos += consumed;
match type_code {
6 => {
while bytes[pos] & 0x80 != 0 {
pos += 1;
}
pos += 1;
}
7 => {
let base = ObjectId::from_bytes(&bytes[pos..pos + algo_len]).unwrap();
if !in_pack.contains(&base) {
out.push(base);
}
pos += algo_len;
}
_ => {}
}
pos += zlib_consume(&bytes[pos..]);
}
out
}
fn git_index_pack_ok(repo: &Path, pack: &[u8], fix_thin: bool) -> bool {
use std::io::Write as _;
use std::process::Stdio;
if fix_thin {
let mut child = Command::new("git")
.current_dir(repo)
.args(["index-pack", "-v", "--fix-thin", "--stdin"])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn git index-pack --stdin");
child
.stdin
.take()
.expect("stdin")
.write_all(pack)
.expect("write pack to stdin");
let out = child.wait_with_output().expect("wait git index-pack");
if !out.status.success() {
eprintln!(
"git index-pack --fix-thin failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
return out.status.success();
}
let scratch = tempfile::tempdir().expect("scratch");
let pack_path = scratch.path().join("in.pack");
std::fs::write(&pack_path, pack).unwrap();
let out = Command::new("git")
.current_dir(scratch.path())
.args(["index-pack", "-v", &pack_path.to_string_lossy()])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("run git index-pack");
if !out.status.success() {
eprintln!(
"git index-pack failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
out.status.success()
}
fn git_index_and_fsck_ok(pack: &[u8]) -> bool {
let scratch = tempfile::tempdir().expect("scratch repo");
let init = Command::new("git")
.current_dir(scratch.path())
.args(["init", "-q", "--bare", "."])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("git init --bare");
if !init.status.success() {
return false;
}
let pack_path = scratch.path().join("objects/pack/in.pack");
std::fs::write(&pack_path, pack).unwrap();
let idx = Command::new("git")
.current_dir(scratch.path())
.args(["index-pack", &pack_path.to_string_lossy()])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("git index-pack");
if !idx.status.success() {
eprintln!(
"index-pack into bare repo failed: {}",
String::from_utf8_lossy(&idx.stderr)
);
return false;
}
let fsck = Command::new("git")
.current_dir(scratch.path())
.args(["fsck", "--strict", "--no-dangling"])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("git fsck");
if !fsck.status.success() {
eprintln!(
"git fsck failed: {}\n{}",
String::from_utf8_lossy(&fsck.stdout),
String::from_utf8_lossy(&fsck.stderr)
);
}
fsck.status.success()
}
fn git_pack_objects_size(repo: &Path, tips: &[ObjectId]) -> usize {
use std::io::Write as _;
use std::process::Stdio;
let mut child = Command::new("git")
.current_dir(repo)
.args([
"pack-objects",
"--stdout",
"--revs",
"--delta-base-offset",
])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn git pack-objects");
{
let mut stdin = child.stdin.take().expect("stdin");
for t in tips {
writeln!(stdin, "{}", t.to_hex()).unwrap();
}
}
let out = child.wait_with_output().expect("wait git pack-objects");
assert!(
out.status.success(),
"git pack-objects failed: {}",
String::from_utf8_lossy(&out.stderr)
);
out.stdout.len()
}
struct IslandFixture {
dir: tempfile::TempDir,
tip_a: ObjectId,
tip_b: ObjectId,
blob_a: ObjectId,
blob_b: ObjectId,
}
fn build_island_fixture() -> IslandFixture {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
git(dir, &["init", "-q", "-b", "a", "."]);
let mut prefix = String::new();
for i in 0..3000 {
prefix.push_str(&format!("shared line {i:05} lorem ipsum dolor sit amet\n"));
}
let mut a_body = prefix.clone();
for i in 0..1500 {
a_body.push_str(&format!("branch-a tail {i:05} aaaaaaaaaaaaaaaaaaaaaaaa\n"));
}
std::fs::write(dir.join("data.txt"), a_body.as_bytes()).unwrap();
git(dir, &["add", "data.txt"]);
git(dir, &["commit", "-q", "-m", "a"]);
let tip_a = rev_parse(dir, "HEAD");
let blob_a = rev_parse(dir, "HEAD:data.txt");
git(dir, &["checkout", "-q", "--orphan", "b"]);
git(dir, &["rm", "-q", "-f", "--cached", "data.txt"]);
std::fs::write(dir.join("data.txt"), prefix.as_bytes()).unwrap();
git(dir, &["add", "data.txt"]);
git(dir, &["commit", "-q", "-m", "b"]);
let tip_b = rev_parse(dir, "HEAD");
let blob_b = rev_parse(dir, "HEAD:data.txt");
assert_ne!(blob_a, blob_b, "the two branches must have distinct blobs");
IslandFixture {
dir: tmp,
tip_a,
tip_b,
blob_a,
blob_b,
}
}
#[test]
fn delta_pack_reindexes_fscks_and_is_near_pack_objects_size() {
let fx = build_delta_fixture();
let odb = open_odb(fx.dir.path());
let tip = *fx.tips.last().unwrap();
let whole = build_pack(&odb, &[tip], &[], &PackBuildOptions::default()).expect("whole");
let delta = build_pack(
&odb,
&[tip],
&[],
&PackBuildOptions {
delta: true,
..PackBuildOptions::default()
},
)
.expect("delta");
assert!(
git_index_and_fsck_ok(&delta),
"delta pack failed index-pack + fsck --strict"
);
assert!(
delta.len() * 2 < whole.len(),
"delta pack ({}) should be < half the whole pack ({})",
delta.len(),
whole.len()
);
let git_size = git_pack_objects_size(fx.dir.path(), &[tip]);
assert!(
delta.len() <= git_size * 6,
"delta pack ({}) is more than 6x git pack-objects ({}), ratio {:.2}",
delta.len(),
git_size,
delta.len() as f64 / git_size as f64
);
eprintln!(
"delta_pack parity: ours={} git={} ratio={:.2} whole={}",
delta.len(),
git_size,
delta.len() as f64 / git_size as f64,
whole.len()
);
}
#[test]
fn islands_forbid_cross_island_blob_delta() {
let fx = build_island_fixture();
let dir = fx.dir.path();
let odb = open_odb(dir);
let a_data = odb.read(&fx.blob_a).expect("read blob_a").data;
let b_data = odb.read(&fx.blob_b).expect("read blob_b").data;
assert!(
a_data.starts_with(&b_data) && a_data.len() > b_data.len(),
"fixture invariant: blob_b must be a strict prefix of blob_a"
);
let wants = [fx.tip_a, fx.tip_b];
let no_islands = build_pack(
&odb,
&wants,
&[],
&PackBuildOptions {
delta: true,
..PackBuildOptions::default()
},
)
.expect("pack without islands");
let edges_off = pack_delta_edges_resolved(&no_islands, &odb);
assert!(
edges_off
.iter()
.any(|(t, b)| *t == fx.blob_b && *b == fx.blob_a),
"without islands, blob_b should delta against blob_a (got edges {:?})",
edges_off
);
git(dir, &["config", "pack.island", "refs/heads/(.*)"]);
let islands = build_pack(
&odb,
&wants,
&[],
&PackBuildOptions {
delta: true,
respect_islands: true,
..PackBuildOptions::default()
},
)
.expect("pack with islands");
let edges = pack_delta_edges_resolved(&islands, &odb);
assert!(
!edges.iter().any(|(t, b)| *t == fx.blob_b && *b == fx.blob_a),
"island rule violated: blob_b ({}) was delta'd against cross-island base blob_a ({}); edges {:?}",
fx.blob_b.to_hex(),
fx.blob_a.to_hex(),
edges
);
assert!(
git_index_and_fsck_ok(&islands),
"island delta pack failed index-pack + fsck"
);
let m1 = pack_bytes_to_object_map(&no_islands, &odb).expect("reparse no-islands");
let m2 = pack_bytes_to_object_map(&islands, &odb).expect("reparse islands");
assert_eq!(
m1.keys().collect::<std::collections::BTreeSet<_>>(),
m2.keys().collect::<std::collections::BTreeSet<_>>(),
"island toggle must not change the packed object set"
);
}
struct RawEntry {
payload_start: usize,
type_code: u8,
base: EdgeBase,
}
enum EdgeBase {
None,
OfsAt(usize),
Ref(ObjectId),
}
fn walk_pack_entries(pack: &[u8], algo_len: usize) -> Vec<RawEntry> {
let nr = u32::from_be_bytes([pack[8], pack[9], pack[10], pack[11]]) as usize;
let mut pos = 12usize;
let mut out = Vec::with_capacity(nr);
for _ in 0..nr {
let start = pos;
let (type_code, _size, consumed) = read_type_size(&pack[pos..]);
pos += consumed;
let base = match type_code {
6 => {
let mut c = pack[pos];
pos += 1;
let mut rel = (c & 0x7f) as usize;
while c & 0x80 != 0 {
rel += 1;
c = pack[pos];
pos += 1;
rel = (rel << 7) + (c & 0x7f) as usize;
}
EdgeBase::OfsAt(start - rel)
}
7 => {
let oid = ObjectId::from_bytes(&pack[pos..pos + algo_len]).unwrap();
pos += algo_len;
EdgeBase::Ref(oid)
}
_ => EdgeBase::None,
};
let payload_start = pos;
pos += zlib_consume(&pack[pos..]);
out.push(RawEntry {
payload_start,
type_code,
base,
});
}
out
}
fn pack_delta_edges_resolved(pack: &[u8], odb: &Odb) -> Vec<(ObjectId, ObjectId)> {
use std::collections::HashMap;
let algo = odb.hash_algo();
let algo_len = algo.len();
let entries = walk_pack_entries(pack, algo_len);
let mut start_to_idx: HashMap<usize, usize> = HashMap::new();
{
let mut pos = 12usize;
for (i, _) in entries.iter().enumerate() {
let start = pos;
let (type_code, _s, consumed) = read_type_size(&pack[pos..]);
pos += consumed;
match type_code {
6 => {
while pack[pos] & 0x80 != 0 {
pos += 1;
}
pos += 1;
}
7 => pos += algo_len,
_ => {}
}
pos += zlib_consume(&pack[pos..]);
start_to_idx.insert(start, i);
}
}
let mut content_at: HashMap<usize, (Vec<u8>, u8)> = HashMap::new();
fn resolve(
idx: usize,
entries: &[RawEntry],
pack: &[u8],
start_to_idx: &HashMap<usize, usize>,
content_at: &mut HashMap<usize, (Vec<u8>, u8)>,
odb: &Odb,
) -> (Vec<u8>, u8) {
if let Some(v) = content_at.get(&idx) {
return v.clone();
}
let e = &entries[idx];
let payload = {
let z = zlib_consume(&pack[e.payload_start..]);
inflate(&pack[e.payload_start..e.payload_start + z])
};
let result = match &e.base {
EdgeBase::None => (payload, e.type_code),
EdgeBase::OfsAt(off) => {
let bidx = start_to_idx[off];
let (base_content, base_type) =
resolve(bidx, entries, pack, start_to_idx, content_at, odb);
(apply_delta(&base_content, &payload), base_type)
}
EdgeBase::Ref(oid) => {
let (base_content, base_type) =
if let Ok(obj) = odb.read(oid) {
let tc = match obj.kind {
grit_lib::objects::ObjectKind::Commit => 1,
grit_lib::objects::ObjectKind::Tree => 2,
grit_lib::objects::ObjectKind::Blob => 3,
grit_lib::objects::ObjectKind::Tag => 4,
};
(obj.data, tc)
} else {
(Vec::new(), 3)
};
(apply_delta(&base_content, &payload), base_type)
}
};
content_at.insert(idx, result.clone());
result
}
let mut oid_at: Vec<ObjectId> = Vec::with_capacity(entries.len());
for i in 0..entries.len() {
let (content, type_code) = resolve(i, &entries, pack, &start_to_idx, &mut content_at, odb);
let kind = match type_code {
1 => "commit",
2 => "tree",
4 => "tag",
_ => "blob",
};
oid_at.push(git_hash_object(kind, &content, algo));
}
let mut out = Vec::new();
for (i, e) in entries.iter().enumerate() {
let t = oid_at[i];
match &e.base {
EdgeBase::None => {}
EdgeBase::OfsAt(off) => {
let bidx = start_to_idx[off];
out.push((t, oid_at[bidx]));
}
EdgeBase::Ref(oid) => out.push((t, *oid)),
}
}
out
}
fn inflate(b: &[u8]) -> Vec<u8> {
use std::io::Read as _;
let mut dec = flate2::bufread::ZlibDecoder::new(b);
let mut out = Vec::new();
dec.read_to_end(&mut out).expect("inflate");
out
}
fn git_hash_object(kind: &str, content: &[u8], algo: grit_lib::objects::HashAlgo) -> ObjectId {
use sha1::{Digest as _, Sha1};
use sha2::Sha256;
let header = format!("{kind} {}\0", content.len());
match algo {
grit_lib::objects::HashAlgo::Sha1 => {
let mut h = Sha1::new();
h.update(header.as_bytes());
h.update(content);
ObjectId::from_bytes(&h.finalize()).unwrap()
}
grit_lib::objects::HashAlgo::Sha256 => {
let mut h = Sha256::new();
h.update(header.as_bytes());
h.update(content);
ObjectId::from_bytes(&h.finalize()).unwrap()
}
}
}
fn apply_delta(base: &[u8], delta: &[u8]) -> Vec<u8> {
let mut i = 0usize;
while delta[i] & 0x80 != 0 {
i += 1;
}
i += 1;
while delta[i] & 0x80 != 0 {
i += 1;
}
i += 1;
let mut out = Vec::new();
while i < delta.len() {
let op = delta[i];
i += 1;
if op & 0x80 != 0 {
let mut off = 0usize;
let mut len = 0usize;
for s in 0..4 {
if op & (1 << s) != 0 {
off |= (delta[i] as usize) << (8 * s);
i += 1;
}
}
for s in 0..3 {
if op & (1 << (4 + s)) != 0 {
len |= (delta[i] as usize) << (8 * s);
i += 1;
}
}
if len == 0 {
len = 0x10000;
}
out.extend_from_slice(&base[off..off + len]);
} else if op != 0 {
let len = op as usize;
out.extend_from_slice(&delta[i..i + len]);
i += len;
}
}
out
}
#[test]
fn reuse_deltas_produces_valid_pack() {
let fx = build_delta_fixture();
let dir = fx.dir.path();
git(dir, &["repack", "-q", "-a", "-d", "-f", "--window=10", "--depth=50"]);
let odb = open_odb(dir);
let tip = *fx.tips.last().unwrap();
let reuse = build_pack(
&odb,
&[tip],
&[],
&PackBuildOptions {
delta: true,
reuse_deltas: true,
..PackBuildOptions::default()
},
)
.expect("reuse pack");
assert!(
git_index_and_fsck_ok(&reuse),
"reuse-delta pack failed index-pack + fsck"
);
let fresh = build_pack(
&odb,
&[tip],
&[],
&PackBuildOptions {
delta: true,
..PackBuildOptions::default()
},
)
.expect("fresh pack");
let m1 = pack_bytes_to_object_map(&reuse, &odb).expect("reparse reuse");
let m2 = pack_bytes_to_object_map(&fresh, &odb).expect("reparse fresh");
assert_eq!(
m1.keys().collect::<std::collections::BTreeSet<_>>(),
m2.keys().collect::<std::collections::BTreeSet<_>>(),
"reuse_deltas must not change the packed object set"
);
let (ref_n, ofs_n, _t) = pack_delta_stats(&reuse, 20);
assert!(
ref_n + ofs_n >= 1,
"reuse pack should still contain delta entries (ref={ref_n} ofs={ofs_n})"
);
}