Skip to main content

sley_odb/
lib.rs

1// sley#7: untrusted-input parsing crate — fallible ops propagate errors;
2// the only retained `expect`s would be documented compile-time invariants.
3#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
4
5use sley_core::{GitError, MissingObjectContext, ObjectFormat, ObjectId, Result};
6use sley_object::{EncodedObject, ObjectType};
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11mod install;
12mod loose;
13mod pack;
14mod reachability;
15mod registry;
16mod repack;
17
18pub use install::*;
19pub use loose::*;
20// Re-exported so callers of the progress-aware installer can name the counter
21// struct without depending on `sley-pack` directly.
22pub use sley_pack::PackStreamProgress;
23pub use pack::*;
24pub use reachability::*;
25pub use registry::*;
26pub use repack::*;
27
28static TEMPFILE_COUNTER: AtomicU64 = AtomicU64::new(0);
29
30pub trait ObjectReader {
31    fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>>;
32
33    /// Graft-points seam (shallow clones today, replace refs/grafts later):
34    /// `true` when history is cut at `oid`, so every walk must treat the
35    /// commit as parentless even though its raw body still names parents.
36    ///
37    /// [`FileObjectDatabase`] answers from `$GIT_DIR/shallow`; readers that
38    /// are not backed by a repository (in-memory stores, pack overlays)
39    /// keep the default "no grafts".
40    fn is_shallow_graft(&self, _oid: &ObjectId) -> bool {
41        false
42    }
43
44    /// Whether this reader has any shallow/graft boundaries at all. Walkers can
45    /// use this to choose dense graph-only traversal when no boundary can cut
46    /// parent edges.
47    fn has_shallow_grafts(&self) -> bool {
48        false
49    }
50
51    /// True when `oid` is covered by a promisor pack. Partial clones are
52    /// allowed to omit promised objects until a later on-demand fetch hydrates
53    /// them; ordinary readers keep the default "no promised objects".
54    fn is_promised_object(&self, _oid: &ObjectId) -> bool {
55        false
56    }
57}
58pub trait ObjectWriter {
59    /// Write `object`, returning its id. Takes `&self`: every implementation's
60    /// write state (in-memory map, loose-object cache) is behind interior
61    /// mutability, so a single handle can interleave reads and writes without a
62    /// `&mut` borrow. This lets the merge engine read and write through one `db`
63    /// instead of opening a second read-only handle that re-warms the caches.
64    fn write_object(&self, object: EncodedObject) -> Result<ObjectId>;
65}
66
67pub(crate) fn implied_empty_tree_object(format: ObjectFormat, oid: &ObjectId) -> Option<Arc<EncodedObject>> {
68    (*oid == ObjectId::empty_tree(format)).then(|| Arc::new(EncodedObject::new(ObjectType::Tree, Vec::new())))
69}
70
71pub(crate) fn with_missing_object_context(err: GitError, oid: ObjectId, context: MissingObjectContext) -> GitError {
72    let kind = err.not_found_kind().and_then(sley_core::NotFoundKind::missing_object_kind);
73    match kind { Some(kind) => GitError::object_kind_not_found_in(oid, kind, context), None => err }
74}
75
76/// Parents of a parsed commit with the graft seam applied: empty when the
77/// reader cuts history at `oid` (shallow boundary), the raw parsed parents
78/// otherwise.
79pub fn grafted_parents<R: ObjectReader + ?Sized>(
80    reader: &R,
81    oid: &ObjectId,
82    parents: Vec<ObjectId>,
83) -> Vec<ObjectId> {
84    if reader.is_shallow_graft(oid) {
85        Vec::new()
86    } else {
87        parents
88    }
89}
90pub(crate) fn unique_temp_path(parent: &Path) -> PathBuf {
91    let id = TEMPFILE_COUNTER.fetch_add(1, Ordering::Relaxed);
92    parent.join(format!("tmp_obj_{}_{}", std::process::id(), id))
93}
94
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::collections::{HashMap, HashSet};
100    use std::io::Read;
101    use std::fs;
102    use sley_core::BString;
103    use sley_formats::{Bundle, BundleReference, RepositoryLayout};
104    use sley_object::{Commit, EncodedObject, ObjectType, Tag, Tree, TreeEntry};
105    use sley_pack::{MultiPackIndex, PackFile, PackIndex, PackWrite, PackWriteOptions};
106
107    fn blob_of(byte: u8, len: usize) -> EncodedObject {
108        EncodedObject::new(ObjectType::Blob, vec![byte; len])
109    }
110
111    fn cached_blob_of(byte: u8, len: usize) -> Arc<EncodedObject> {
112        Arc::new(blob_of(byte, len))
113    }
114
115    fn read_object_for_assert(reader: &impl ObjectReader, oid: &ObjectId) -> EncodedObject {
116        reader
117            .read_object(oid)
118            .expect("test operation should succeed")
119            .as_ref()
120            .clone()
121    }
122
123    #[test]
124    fn lru_cache_evicts_by_byte_budget_least_recently_used_first() {
125        // Budget holds two ~1 KiB objects but not three.
126        let one = cached_object_cost(&blob_of(0, 1000));
127        let mut cache = LruCache::<u32>::new(one * 2 + 8);
128        cache.put(1, cached_blob_of(b'a', 1000));
129        cache.put(2, cached_blob_of(b'b', 1000));
130        // Touch key 1 so key 2 becomes least-recently-used.
131        assert!(cache.get(&1).is_some());
132        cache.put(3, cached_blob_of(b'c', 1000));
133        // Key 2 (LRU) is evicted; 1 and 3 remain.
134        assert!(cache.get(&1).is_some());
135        assert!(cache.get(&2).is_none());
136        assert!(cache.get(&3).is_some());
137    }
138
139    #[test]
140    fn lru_cache_zero_budget_is_inert() {
141        let mut cache = LruCache::<u32>::new(0);
142        cache.put(1, cached_blob_of(b'a', 16));
143        assert!(cache.get(&1).is_none());
144    }
145
146    #[test]
147    fn lru_cache_skips_object_larger_than_budget_and_clears_stale_entry() {
148        let mut cache = LruCache::<u32>::new(cached_object_cost(&blob_of(0, 100)));
149        cache.put(1, cached_blob_of(b'a', 50));
150        assert!(cache.get(&1).is_some());
151        // An object that cannot fit is not cached, and it evicts the prior entry
152        // stored under the same key (so we never serve a stale value for it).
153        cache.put(1, cached_blob_of(b'b', 10_000));
154        assert!(cache.get(&1).is_none());
155        // A subsequent fitting insert under another key still works and accounting
156        // is not corrupted by the oversized insert.
157        cache.put(2, cached_blob_of(b'c', 50));
158        assert!(cache.get(&2).is_some());
159    }
160
161    #[test]
162    fn lru_cache_replacing_entry_updates_byte_accounting() {
163        // Budget holds two 500-byte objects (plus headroom) but not a 500 + a
164        // ~1900-byte object.
165        let small = cached_object_cost(&blob_of(0, 500));
166        let mut cache = LruCache::<u32>::new(small * 2 + 200);
167        cache.put(1, cached_blob_of(b'a', 500));
168        cache.put(2, cached_blob_of(b'b', 500));
169        assert!(cache.get(&1).is_some());
170        assert!(cache.get(&2).is_some());
171        // Replace key 2 (now MRU after the gets above re-ordered 1 then 2) with a
172        // bigger value that still fits the budget alone but makes the running total
173        // exceed it; the LRU (key 1) is evicted while the replaced key 2 stays.
174        // This exercises the replace-path accounting.
175        cache.put(2, cached_blob_of(b'b', 1000));
176        assert!(cache.get(&2).is_some());
177        assert!(cache.get(&1).is_none());
178    }
179
180    #[test]
181    fn write_and_validate_blob() {
182        let db = ObjectDatabase::new(ObjectFormat::Sha1);
183        let oid = db
184            .write_object(EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec()))
185            .expect("test operation should succeed");
186        assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
187        db.validate(&oid).expect("test operation should succeed");
188    }
189
190    #[test]
191    fn loose_store_writes_and_reads_object() {
192        let root = std::env::temp_dir().join(format!(
193            "sley-loose-store-{}-{}",
194            std::process::id(),
195            TEMPFILE_COUNTER.fetch_add(1, Ordering::Relaxed)
196        ));
197        let store = LooseObjectStore::new(root.join("objects"), ObjectFormat::Sha1);
198        let object = EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec());
199        let oid = store
200            .write_object(object.clone())
201            .expect("test operation should succeed");
202        assert_eq!(read_object_for_assert(&store, &oid), object);
203        assert!(
204            store
205                .object_path(&oid)
206                .expect("test operation should succeed")
207                .exists()
208        );
209        fs::remove_dir_all(root).expect("test operation should succeed");
210    }
211
212    #[test]
213    fn read_header_detects_corruption_within_gits_header_window() {
214        // git's `unpack_loose_header` inflates only the first MAX_HEADER_LEN (32)
215        // bytes of output; a zlib data error inside that window makes `cat-file
216        // -s`/`-t` fail (ULHR_BAD → "unable to unpack header"). A byte-by-byte
217        // header read that stopped at the NUL would never inflate into the corrupt
218        // region and would silently return a bogus size — the t1060 "getting type
219        // of a corrupt blob fails" bug. Corrupt a byte inside the inflate stream of
220        // a tiny object so the damage lands within the first 32 inflated bytes.
221        let root = temp_root("sley-loose-header-corrupt");
222        let store = LooseObjectStore::new(root.join("objects"), ObjectFormat::Sha1);
223        let object = EncodedObject::new(ObjectType::Blob, b"content\n".to_vec());
224        let oid = store
225            .write_object(object)
226            .expect("test operation should succeed");
227        let path = store
228            .object_path(&oid)
229            .expect("test operation should succeed");
230        let mut bytes = fs::read(&path).expect("test operation should succeed");
231        // Offset 10 is inside the deflate stream (past the 2-byte zlib header) and,
232        // for an 8-byte blob, decodes into the first 32 output bytes. Zero it to
233        // break inflation, mirroring t1060's `corrupt_byte HEAD:content.t 10`.
234        bytes[10] = 0;
235        fs::write(&path, &bytes).expect("test operation should succeed");
236        store.invalidate_cache();
237        let err = store
238            .read_header(&oid)
239            .expect_err("corrupt loose header must fail like git's ULHR_BAD");
240        let msg = err.to_string();
241        assert!(
242            msg.contains("unable to unpack") && msg.contains(&oid.to_hex()),
243            "expected git's ULHR_BAD message, got: {msg}"
244        );
245        fs::remove_dir_all(root).expect("test operation should succeed");
246    }
247
248    #[test]
249    fn read_header_ignores_corruption_past_gits_header_window() {
250        // Mirror git: corruption deeper than the 32-byte header window is NOT
251        // detected by a header-only read (`cat-file -s` still returns the size);
252        // the full-object read path catches it instead. Over-detecting here would
253        // diverge from upstream on large objects with a clean header.
254        let root = temp_root("sley-loose-header-deep-corrupt");
255        let store = LooseObjectStore::new(root.join("objects"), ObjectFormat::Sha1);
256        // Incompressible body so the deflate stream is long and a deep byte is well
257        // past the 32 inflated header-window bytes.
258        let body: Vec<u8> = (0..4096u32)
259            .map(|i| (i.wrapping_mul(2654435761)) as u8)
260            .collect();
261        let object = EncodedObject::new(ObjectType::Blob, body.clone());
262        let oid = store
263            .write_object(object)
264            .expect("test operation should succeed");
265        let path = store
266            .object_path(&oid)
267            .expect("test operation should succeed");
268        let mut bytes = fs::read(&path).expect("test operation should succeed");
269        let deep = bytes.len() / 2;
270        bytes[deep] ^= 0xff;
271        fs::write(&path, &bytes).expect("test operation should succeed");
272        store.invalidate_cache();
273        let header = store
274            .read_header(&oid)
275            .expect("header-only read must still succeed for deep body corruption");
276        assert_eq!(header, Some((ObjectType::Blob, body.len() as u64)));
277        fs::remove_dir_all(root).expect("test operation should succeed");
278    }
279
280    #[test]
281    fn file_database_reads_object_from_pack_index() {
282        let root = temp_root("sley-file-odb-pack");
283        let git_dir = root.join(".git");
284        let pack_dir = git_dir.join("objects").join("pack");
285        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
286        let object = EncodedObject::new(ObjectType::Blob, b"packed\n".to_vec());
287        let oid = object
288            .object_id(ObjectFormat::Sha1)
289            .expect("test operation should succeed");
290        let written = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
291            .expect("test operation should succeed");
292        let pack_name = written.checksum.to_hex();
293        fs::write(
294            pack_dir.join(format!("pack-{pack_name}.pack")),
295            written.pack,
296        )
297        .expect("test operation should succeed");
298        fs::write(
299            pack_dir.join(format!("pack-{pack_name}.idx")),
300            written.index,
301        )
302        .expect("test operation should succeed");
303
304        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
305        assert!(db.contains(&oid).expect("test operation should succeed"));
306        assert_eq!(read_object_for_assert(&db, &oid), object);
307        fs::remove_dir_all(root).expect("test operation should succeed");
308    }
309
310    #[test]
311    fn file_database_loose_cache_observes_same_process_write_after_miss() {
312        let root = temp_root("sley-file-odb-loose-cache-write");
313        let git_dir = root.join(".git");
314        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
315        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
316
317        let object = EncodedObject::new(ObjectType::Blob, b"written after miss\n".to_vec());
318        let oid = object
319            .object_id(ObjectFormat::Sha1)
320            .expect("test operation should succeed");
321
322        assert!(matches!(db.read_object(&oid), Err(GitError::NotFound(_))));
323        db.loose()
324            .write_object(object.clone())
325            .expect("test operation should succeed");
326
327        assert_eq!(read_object_for_assert(&db, &oid), object);
328        fs::remove_dir_all(root).expect("test operation should succeed");
329    }
330
331    #[test]
332    fn present_loose_fanouts_lists_only_existing_two_hex_dirs() {
333        let root = temp_root("sley-present-fanouts");
334        let objects = root.join("objects");
335        fs::create_dir_all(objects.join("ab")).expect("test operation should succeed");
336        fs::create_dir_all(objects.join("0f")).expect("test operation should succeed");
337        // Non-fanout siblings git keeps under objects/ must be ignored.
338        fs::create_dir_all(objects.join("pack")).expect("test operation should succeed");
339        fs::create_dir_all(objects.join("info")).expect("test operation should succeed");
340        // A 2-char-but-non-hex dir, and a regular file with a 2-hex name, are not
341        // fanouts.
342        fs::create_dir_all(objects.join("zz")).expect("test operation should succeed");
343        fs::write(objects.join("ff"), b"not a dir").expect("test operation should succeed");
344
345        let present = present_loose_fanouts(&objects).expect("test operation should succeed");
346        assert_eq!(
347            present,
348            HashSet::from([0xab, 0x0f]),
349            "only the genuine two-hex fanout directories should be reported"
350        );
351        fs::remove_dir_all(root).expect("test operation should succeed");
352    }
353
354    #[test]
355    fn present_loose_fanouts_empty_when_objects_dir_absent() {
356        let root = temp_root("sley-present-fanouts-absent");
357        fs::create_dir_all(&root).expect("test operation should succeed");
358        // No objects dir at all (e.g. an all-packed bare layout before any loose
359        // write): the helper reports an empty set rather than erroring.
360        let present =
361            present_loose_fanouts(&root.join("objects")).expect("test operation should succeed");
362        assert!(present.is_empty());
363        fs::remove_dir_all(root).expect("test operation should succeed");
364    }
365
366    #[test]
367    fn packed_only_repo_read_does_not_probe_loose_fanout_dirs() {
368        // Regression for the loose-first statx floor: an all-packed repo must read
369        // objects without ever opendir()-ing a per-id `objects/XX/` fanout, because
370        // none exist. The present-fanout set is learned from one `read_dir(objects/)`.
371        let root = temp_root("sley-packed-no-loose-probe");
372        let git_dir = root.join(".git");
373        let pack_dir = git_dir.join("objects").join("pack");
374        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
375        let object = EncodedObject::new(ObjectType::Blob, b"packed only\n".to_vec());
376        let oid = object
377            .object_id(ObjectFormat::Sha1)
378            .expect("test operation should succeed");
379        let written = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
380            .expect("test operation should succeed");
381        let pack_name = written.checksum.to_hex();
382        fs::write(
383            pack_dir.join(format!("pack-{pack_name}.pack")),
384            written.pack,
385        )
386        .expect("test operation should succeed");
387        fs::write(
388            pack_dir.join(format!("pack-{pack_name}.idx")),
389            written.index,
390        )
391        .expect("test operation should succeed");
392
393        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
394        // Header read takes the loose-first path; it must still resolve from the pack
395        // and learn that the object's fanout dir is absent.
396        assert_eq!(
397            db.read_object_header(&oid)
398                .expect("test operation should succeed"),
399            Some((ObjectType::Blob, object.body.len() as u64))
400        );
401        assert_eq!(read_object_for_assert(&db, &oid), object);
402
403        // No fanout dir was created by the read (we never wrote loose), and the
404        // cached present-fanout set is the empty set — so further probes short-circuit.
405        let fanout_hex = format!("{:02x}", oid.as_bytes()[0]);
406        assert!(
407            !git_dir.join("objects").join(&fanout_hex).exists(),
408            "reading a packed object must not create its loose fanout dir"
409        );
410        if let Ok(guard) = db.loose().loose_cache.lock() {
411            assert_eq!(
412                guard.present_fanouts.as_ref(),
413                Some(&HashSet::new()),
414                "an all-packed repo must learn zero present fanouts"
415            );
416        }
417        fs::remove_dir_all(root).expect("test operation should succeed");
418    }
419
420    #[test]
421    fn loose_object_in_existing_fanout_still_resolves_through_cache() {
422        // The optimization must not hide a real loose object: when its fanout dir
423        // exists, the per-fanout scan still runs and the read succeeds.
424        let root = temp_root("sley-loose-resolves");
425        let git_dir = root.join(".git");
426        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
427        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
428        let object = EncodedObject::new(ObjectType::Blob, b"a genuine loose object\n".to_vec());
429        let oid = db
430            .write_object(object.clone())
431            .expect("test operation should succeed");
432        // Drop all in-memory state so the read must re-learn fanouts from disk.
433        db.refresh_read_cache();
434        assert_eq!(
435            db.read_object_header(&oid)
436                .expect("test operation should succeed"),
437            Some((ObjectType::Blob, object.body.len() as u64))
438        );
439        assert_eq!(read_object_for_assert(&db, &oid), object);
440        fs::remove_dir_all(root).expect("test operation should succeed");
441    }
442
443    #[test]
444    fn loose_write_into_previously_absent_fanout_is_found_after_cache_built() {
445        // Cache-coherence gate: a packed-only read first learns "all fanouts
446        // absent". A subsequent loose write must NOT be permanently masked by that
447        // negative present-fanout set — the just-written object reads back.
448        let root = temp_root("sley-new-loose-after-cache");
449        let git_dir = root.join(".git");
450        let pack_dir = git_dir.join("objects").join("pack");
451        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
452        // Seed one packed object and read it, which warms the present-fanout set to
453        // empty (no loose dirs exist yet).
454        let packed = EncodedObject::new(ObjectType::Blob, b"seed packed\n".to_vec());
455        let written = PackFile::write_undeltified_sha1(std::slice::from_ref(&packed))
456            .expect("test operation should succeed");
457        let pack_name = written.checksum.to_hex();
458        fs::write(
459            pack_dir.join(format!("pack-{pack_name}.pack")),
460            written.pack,
461        )
462        .expect("test operation should succeed");
463        fs::write(
464            pack_dir.join(format!("pack-{pack_name}.idx")),
465            written.index,
466        )
467        .expect("test operation should succeed");
468        let packed_oid = packed
469            .object_id(ObjectFormat::Sha1)
470            .expect("test operation should succeed");
471
472        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
473        // Read the packed object so the loose cache learns "zero present fanouts".
474        assert_eq!(read_object_for_assert(&db, &packed_oid), packed);
475
476        // Now write a NEW loose object through the same handle. Its fanout dir did
477        // not exist when the cache learned the present set, but `note_loose_write`
478        // must keep the read path coherent.
479        let loose = EncodedObject::new(ObjectType::Blob, b"new loose into empty fanout\n".to_vec());
480        let loose_oid = db
481            .write_object(loose.clone())
482            .expect("test operation should succeed");
483        assert_eq!(
484            db.read_object_header(&loose_oid)
485                .expect("test operation should succeed"),
486            Some((ObjectType::Blob, loose.body.len() as u64)),
487            "a loose object written after the present-fanout cache was built must be found"
488        );
489        assert_eq!(read_object_for_assert(&db, &loose_oid), loose);
490        // And the original packed object still resolves.
491        assert_eq!(read_object_for_assert(&db, &packed_oid), packed);
492        fs::remove_dir_all(root).expect("test operation should succeed");
493    }
494
495    #[test]
496    fn loose_copy_is_consulted_when_packed_copy_is_corrupt() {
497        // Loose-shadows-packed precedence: git's `oid_object_info_extended` keeps a
498        // good loose copy authoritative when the packed copy is unreadable. The
499        // present-fanout optimization must not change this — the loose fanout dir
500        // exists (we wrote it), so it is scanned and consulted.
501        let root = temp_root("sley-loose-shadows-corrupt-pack");
502        let git_dir = root.join(".git");
503        let pack_dir = git_dir.join("objects").join("pack");
504        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
505        let object = EncodedObject::new(ObjectType::Blob, b"shadow me\n".to_vec());
506        let oid = object
507            .object_id(ObjectFormat::Sha1)
508            .expect("test operation should succeed");
509
510        // Write the object both packed and loose (content-addressed: same oid).
511        let written = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
512            .expect("test operation should succeed");
513        let pack_name = written.checksum.to_hex();
514        let pack_path = pack_dir.join(format!("pack-{pack_name}.pack"));
515        fs::write(&pack_path, written.pack).expect("test operation should succeed");
516        fs::write(
517            pack_dir.join(format!("pack-{pack_name}.idx")),
518            written.index,
519        )
520        .expect("test operation should succeed");
521        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
522        db.loose()
523            .write_object(object.clone())
524            .expect("test operation should succeed");
525
526        // Corrupt the pack body so the packed read fails; the good loose copy must
527        // still satisfy the read.
528        let mut pack_bytes = fs::read(&pack_path).expect("test operation should succeed");
529        let mid = pack_bytes.len() / 2;
530        pack_bytes[mid] ^= 0xff;
531        fs::write(&pack_path, &pack_bytes).expect("test operation should succeed");
532        db.refresh_read_cache();
533
534        assert_eq!(
535            read_object_for_assert(&db, &oid),
536            object,
537            "a good loose copy must shadow a corrupt packed copy"
538        );
539        fs::remove_dir_all(root).expect("test operation should succeed");
540    }
541
542    #[test]
543    fn loose_ofs_delta_base_recovers_child_from_corrupt_pack_base() {
544        let root = temp_root("sley-ofs-delta-base-corrupt-loose-recovery");
545        let git_dir = root.join(".git");
546        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
547        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
548
549        let mut base_body = Vec::new();
550        for idx in 0..256u16 {
551            base_body.extend_from_slice(format!("shared line {idx:03}\n").as_bytes());
552        }
553        let base = EncodedObject::new(ObjectType::Blob, base_body.clone());
554        let mut first_body = base_body;
555        first_body.extend_from_slice(b"first delta payload\n");
556        let first = EncodedObject::new(ObjectType::Blob, first_body.clone());
557        let mut second_body = first_body;
558        second_body.extend_from_slice(b"second delta payload\n");
559        let second = EncodedObject::new(ObjectType::Blob, second_body);
560        let base_oid = base
561            .object_id(ObjectFormat::Sha1)
562            .expect("test operation should succeed");
563        let first_oid = first
564            .object_id(ObjectFormat::Sha1)
565            .expect("test operation should succeed");
566        let second_oid = second
567            .object_id(ObjectFormat::Sha1)
568            .expect("test operation should succeed");
569
570        let options = PackWriteOptions::new()
571            .with_prefer_ofs_delta(true)
572            .with_reorder(false);
573        let written = PackFile::write_packed_with_options(
574            &[base, first.clone(), second.clone()],
575            ObjectFormat::Sha1,
576            &options,
577        )
578        .expect("test operation should succeed");
579        let stats = PackFile::verify_pack_stats(&written.pack, ObjectFormat::Sha1)
580            .expect("test operation should succeed");
581        let first_stat = stats
582            .objects
583            .iter()
584            .find(|stat| stat.oid == first_oid)
585            .expect("first object should be packed");
586        let second_stat = stats
587            .objects
588            .iter()
589            .find(|stat| stat.oid == second_oid)
590            .expect("second object should be packed");
591        assert_eq!(first_stat.base_oid, Some(base_oid));
592        assert_eq!(second_stat.base_oid, Some(first_oid));
593
594        let installed = db
595            .install_pack(&written)
596            .expect("test operation should succeed");
597        db.loose()
598            .write_object(first.clone())
599            .expect("test operation should succeed");
600
601        let mut corrupt_pack = written.pack;
602        let base_reference = ofs_delta_base_reference_position(&corrupt_pack, first_stat.offset);
603        corrupt_pack[base_reference] = if corrupt_pack[base_reference] == 1 {
604            2
605        } else {
606            1
607        };
608        assert!(PackFile::verify_pack_stats(&corrupt_pack, ObjectFormat::Sha1).is_err());
609        fs::write(&installed.pack_path, &corrupt_pack).expect("test operation should succeed");
610        db.refresh_read_cache();
611
612        assert_eq!(read_object_for_assert(&db, &first_oid), first);
613        assert_eq!(read_object_for_assert(&db, &second_oid), second);
614        fs::remove_dir_all(root).expect("test operation should succeed");
615    }
616
617    fn ofs_delta_base_reference_position(pack: &[u8], offset: u64) -> usize {
618        let mut cursor = usize::try_from(offset).expect("test operation should succeed");
619        let first = pack[cursor];
620        cursor += 1;
621        let kind = (first >> 4) & 0x07;
622        let mut byte = first;
623        while byte & 0x80 != 0 {
624            byte = pack[cursor];
625            cursor += 1;
626        }
627        assert_eq!(kind, 6, "expected an ofs-delta entry");
628        cursor
629    }
630
631    #[test]
632    fn object_presence_checker_observes_same_process_loose_write_after_miss() {
633        let root = temp_root("sley-presence-checker-loose-cache-write");
634        let git_dir = root.join(".git");
635        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
636        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
637        let mut checker = db.presence_checker();
638
639        let object = EncodedObject::new(ObjectType::Blob, b"checker loose after miss\n".to_vec());
640        let oid = object
641            .object_id(ObjectFormat::Sha1)
642            .expect("test operation should succeed");
643
644        assert!(
645            !checker
646                .contains(&oid)
647                .expect("test operation should succeed")
648        );
649        db.loose()
650            .write_object(object)
651            .expect("test operation should succeed");
652
653        assert!(
654            checker
655                .contains(&oid)
656                .expect("test operation should succeed")
657        );
658        fs::remove_dir_all(root).expect("test operation should succeed");
659    }
660
661    #[test]
662    fn read_object_header_matches_full_read_for_loose_and_packed_and_delta() {
663        let root = temp_root("sley-read-object-header");
664        let git_dir = root.join(".git");
665        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
666        let format = ObjectFormat::Sha1;
667        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
668
669        // Loose object: the header read inflates only the framing, not the body.
670        let loose = EncodedObject::new(ObjectType::Blob, b"loose header object\n".to_vec());
671        let loose_oid = db
672            .write_object(loose.clone())
673            .expect("test operation should succeed");
674
675        // Packed objects, including an ofs-delta whose *result* size lives in the
676        // delta stream (not the pack entry header) and whose type is inherited from
677        // its base at the end of the chain.
678        let base = EncodedObject::new(ObjectType::Blob, vec![b'a'; 4096]);
679        let mut child_body = vec![b'a'; 4096];
680        child_body.extend_from_slice(b" plus a deltified tail\n");
681        let child = EncodedObject::new(ObjectType::Blob, child_body);
682        let commitish =
683            EncodedObject::new(ObjectType::Commit, b"header-only type probe\n".to_vec());
684        let base_oid = base
685            .object_id(format)
686            .expect("test operation should succeed");
687        let child_oid = child
688            .object_id(format)
689            .expect("test operation should succeed");
690        let commit_oid = commitish
691            .object_id(format)
692            .expect("test operation should succeed");
693        let options = PackWriteOptions::new()
694            .with_prefer_ofs_delta(true)
695            .with_reorder(false);
696        let pack = PackFile::write_packed_with_options(
697            &[base.clone(), child.clone(), commitish.clone()],
698            format,
699            &options,
700        )
701        .expect("test operation should succeed");
702        db.install_pack(&pack)
703            .expect("test operation should succeed");
704
705        // The header read agrees with a full decode for every object and storage
706        // class, without ever materializing the body.
707        for (oid, want_type, want_len) in [
708            (&loose_oid, ObjectType::Blob, loose.body.len()),
709            (&base_oid, ObjectType::Blob, base.body.len()),
710            (&child_oid, ObjectType::Blob, child.body.len()),
711            (&commit_oid, ObjectType::Commit, commitish.body.len()),
712        ] {
713            assert_eq!(
714                db.read_object_header(oid)
715                    .expect("test operation should succeed"),
716                Some((want_type, want_len as u64)),
717                "header for {oid}"
718            );
719            let full = db.read_object(oid).expect("test operation should succeed");
720            assert_eq!(
721                db.read_object_header(oid)
722                    .expect("test operation should succeed"),
723                Some((full.object_type, full.body.len() as u64))
724            );
725        }
726
727        let missing = ObjectId::from_hex(format, "0000000000000000000000000000000000000001")
728            .expect("test operation should succeed");
729        assert_eq!(
730            db.read_object_header(&missing)
731                .expect("test operation should succeed"),
732            None
733        );
734        fs::remove_dir_all(root).expect("test operation should succeed");
735    }
736
737    #[test]
738    fn object_storage_info_reports_loose_packed_and_delta_metadata() {
739        let root = temp_root("sley-object-storage-info");
740        let git_dir = root.join(".git");
741        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
742        let format = ObjectFormat::Sha1;
743        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
744
745        let loose = EncodedObject::new(ObjectType::Blob, b"loose storage object\n".to_vec());
746        let loose_oid = db
747            .write_object(loose)
748            .expect("test operation should succeed");
749        let loose_size = fs::metadata(
750            db.loose()
751                .object_path(&loose_oid)
752                .expect("test operation should succeed"),
753        )
754        .expect("test operation should succeed")
755        .len();
756        let loose_info = db
757            .object_storage_info(&loose_oid)
758            .expect("test operation should succeed")
759            .expect("test operation should succeed");
760        assert_eq!(loose_info.disk_size, loose_size);
761        assert_eq!(
762            loose_info.deltabase,
763            zero_oid(format).expect("test operation should succeed")
764        );
765
766        let base = EncodedObject::new(ObjectType::Blob, vec![b'a'; 4096]);
767        let mut child_body = vec![b'a'; 4096];
768        child_body.extend_from_slice(b" changed tail\n");
769        let child = EncodedObject::new(ObjectType::Blob, child_body);
770        let base_oid = base
771            .object_id(format)
772            .expect("test operation should succeed");
773        let child_oid = child
774            .object_id(format)
775            .expect("test operation should succeed");
776        let options = PackWriteOptions::new()
777            .with_prefer_ofs_delta(true)
778            .with_reorder(false);
779        let pack = PackFile::write_packed_with_options(&[base, child], format, &options)
780            .expect("test operation should succeed");
781        db.install_pack(&pack)
782            .expect("test operation should succeed");
783
784        let base_info = db
785            .object_storage_info(&base_oid)
786            .expect("test operation should succeed")
787            .expect("test operation should succeed");
788        assert!(base_info.disk_size > 0);
789        assert_eq!(
790            base_info.deltabase,
791            zero_oid(format).expect("test operation should succeed")
792        );
793
794        let child_info = db
795            .object_storage_info(&child_oid)
796            .expect("test operation should succeed")
797            .expect("test operation should succeed");
798        assert!(child_info.disk_size > 0);
799        assert_eq!(child_info.deltabase, base_oid);
800
801        let missing = ObjectId::from_hex(format, "0000000000000000000000000000000000000001")
802            .expect("test operation should succeed");
803        assert_eq!(
804            db.object_storage_info(&missing)
805                .expect("test operation should succeed"),
806            None
807        );
808        fs::remove_dir_all(root).expect("test operation should succeed");
809    }
810
811    #[test]
812    fn object_storage_info_uses_midx_when_pack_sidecar_is_missing() {
813        let root = temp_root("sley-object-storage-midx-fallback");
814        let git_dir = root.join(".git");
815        let pack_dir = git_dir.join("objects").join("pack");
816        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
817        let format = ObjectFormat::Sha1;
818        let first = EncodedObject::new(ObjectType::Blob, b"first packed object\n".to_vec());
819        let second = EncodedObject::new(ObjectType::Blob, b"second packed object\n".to_vec());
820        let written = PackFile::write_undeltified_sha1(&[first, second])
821            .expect("test operation should succeed");
822        let pack_name = written.checksum.to_hex();
823        let pack_path = pack_dir.join(format!("pack-{pack_name}.pack"));
824        let idx_path = pack_dir.join(format!("pack-{pack_name}.idx"));
825        fs::write(&pack_path, &written.pack).expect("test operation should succeed");
826        fs::write(&idx_path, &written.index).expect("test operation should succeed");
827
828        let idx_name = idx_path
829            .file_name()
830            .and_then(|name| name.to_str())
831            .expect("test operation should succeed")
832            .to_string();
833        let midx_objects = written
834            .entries
835            .iter()
836            .map(|entry| sley_pack::MultiPackIndexEntry {
837                oid: entry.oid,
838                pack_int_id: 0,
839                offset: entry.offset,
840                force_large_offset: false,
841            })
842            .collect::<Vec<_>>();
843        let midx = MultiPackIndex::write(format, 1, &[idx_name], &midx_objects)
844            .expect("test operation should succeed");
845        fs::write(pack_dir.join("multi-pack-index"), midx).expect("test operation should succeed");
846
847        let target = written
848            .entries
849            .iter()
850            .min_by_key(|entry| entry.offset)
851            .expect("test operation should succeed")
852            .oid;
853        let indexed_info = FileObjectDatabase::from_git_dir(&git_dir, format)
854            .object_storage_info(&target)
855            .expect("test operation should succeed")
856            .expect("test operation should succeed");
857
858        fs::remove_file(&idx_path).expect("test operation should succeed");
859        let missing_idx_info = FileObjectDatabase::from_git_dir(&git_dir, format)
860            .object_storage_info(&target)
861            .expect("test operation should succeed")
862            .expect("test operation should succeed");
863        assert_eq!(missing_idx_info, indexed_info);
864
865        fs::write(&idx_path, &written.index).expect("test operation should succeed");
866        fs::remove_file(&pack_path).expect("test operation should succeed");
867        let missing_pack_info = FileObjectDatabase::from_git_dir(&git_dir, format)
868            .object_storage_info(&target)
869            .expect("test operation should succeed")
870            .expect("test operation should succeed");
871        assert_eq!(missing_pack_info.disk_size, indexed_info.disk_size);
872        assert_eq!(
873            missing_pack_info.deltabase,
874            zero_oid(format).expect("test operation should succeed")
875        );
876
877        fs::remove_dir_all(root).expect("test operation should succeed");
878    }
879
880    #[test]
881    fn file_database_resolves_unique_loose_object_prefix() {
882        let root = temp_root("sley-file-odb-prefix-loose");
883        let git_dir = root.join(".git");
884        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
885        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
886        let object = EncodedObject::new(ObjectType::Blob, b"prefix loose\n".to_vec());
887        let oid = db
888            .write_object(object)
889            .expect("test operation should succeed");
890        let prefix = &oid.to_hex()[..8];
891
892        assert_eq!(
893            db.resolve_prefix(prefix)
894                .expect("test operation should succeed"),
895            ObjectPrefixResolution::Unique(oid)
896        );
897        assert!(
898            db.object_ids()
899                .expect("test operation should succeed")
900                .contains(&oid)
901        );
902        fs::remove_dir_all(root).expect("test operation should succeed");
903    }
904
905    #[test]
906    fn file_database_resolves_unique_packed_object_prefix() {
907        let root = temp_root("sley-file-odb-prefix-packed");
908        let git_dir = root.join(".git");
909        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
910        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
911        let object = EncodedObject::new(ObjectType::Blob, b"prefix packed\n".to_vec());
912        let oid = object
913            .object_id(ObjectFormat::Sha1)
914            .expect("test operation should succeed");
915        let pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
916            .expect("test operation should succeed");
917        db.install_pack(&pack)
918            .expect("test operation should succeed");
919        let prefix = &oid.to_hex()[..8];
920
921        assert_eq!(
922            db.resolve_prefix(prefix)
923                .expect("test operation should succeed"),
924            ObjectPrefixResolution::Unique(oid)
925        );
926        fs::remove_dir_all(root).expect("test operation should succeed");
927    }
928
929    #[test]
930    fn file_database_reports_ambiguous_object_prefix() {
931        let root = temp_root("sley-file-odb-prefix-ambiguous");
932        let git_dir = root.join(".git");
933        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
934        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
935        let mut seen = HashMap::new();
936        let (prefix, first, second) = (0..10_000)
937            .find_map(|idx| {
938                let object =
939                    EncodedObject::new(ObjectType::Blob, format!("ambiguous {idx}\n").into_bytes());
940                let oid = db
941                    .write_object(object)
942                    .expect("test operation should succeed");
943                let prefix = oid.to_hex()[..4].to_string();
944                seen.insert(prefix.clone(), oid)
945                    .map(|first| (prefix, first, oid))
946            })
947            .expect("test should find a 4-hex collision");
948
949        let ObjectPrefixResolution::Ambiguous(mut matches) = db
950            .resolve_prefix(&prefix)
951            .expect("test operation should succeed")
952        else {
953            panic!("expected ambiguous prefix {prefix}");
954        };
955        matches.sort_by_key(ObjectId::to_hex);
956        let mut expected = vec![first, second];
957        expected.sort_by_key(ObjectId::to_hex);
958        assert_eq!(matches, expected);
959        fs::remove_dir_all(root).expect("test operation should succeed");
960    }
961
962    #[test]
963    fn file_database_rejects_too_short_object_prefix() {
964        let root = temp_root("sley-file-odb-prefix-short");
965        let git_dir = root.join(".git");
966        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
967        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
968
969        assert!(matches!(
970            db.resolve_prefix("abc"),
971            Err(GitError::InvalidObjectId(_))
972        ));
973        fs::remove_dir_all(root).expect("test operation should succeed");
974    }
975
976    #[test]
977    fn file_database_reads_sha256_object_from_pack_index() {
978        let root = temp_root("sley-file-odb-pack-sha256");
979        let git_dir = root.join(".git");
980        let pack_dir = git_dir.join("objects").join("pack");
981        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
982        let object = EncodedObject::new(ObjectType::Blob, b"packed sha256\n".to_vec());
983        let oid = object
984            .object_id(ObjectFormat::Sha256)
985            .expect("test operation should succeed");
986        let written =
987            PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha256)
988                .expect("test operation should succeed");
989        let pack_name = written.checksum.to_hex();
990        fs::write(
991            pack_dir.join(format!("pack-{pack_name}.pack")),
992            written.pack,
993        )
994        .expect("test operation should succeed");
995        fs::write(
996            pack_dir.join(format!("pack-{pack_name}.idx")),
997            written.index,
998        )
999        .expect("test operation should succeed");
1000
1001        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha256);
1002        assert!(db.contains(&oid).expect("test operation should succeed"));
1003        assert_eq!(read_object_for_assert(&db, &oid), object);
1004        fs::remove_dir_all(root).expect("test operation should succeed");
1005    }
1006
1007    #[test]
1008    fn file_database_installs_sha256_pack_without_loose_objects() {
1009        let root = temp_root("sley-file-odb-install-pack");
1010        let git_dir = root.join(".git");
1011        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1012        let object = EncodedObject::new(ObjectType::Blob, b"installed sha256 pack\n".to_vec());
1013        let oid = object
1014            .object_id(ObjectFormat::Sha256)
1015            .expect("test operation should succeed");
1016        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha256)
1017            .expect("test operation should succeed");
1018        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha256);
1019
1020        let result = db
1021            .install_pack(&pack)
1022            .expect("test operation should succeed");
1023
1024        assert_eq!(result.pack_name, format!("pack-{}", pack.checksum.to_hex()));
1025        assert_eq!(result.object_ids, vec![oid]);
1026        assert!(result.pack_path.exists());
1027        assert!(result.index_path.exists());
1028        assert_eq!(result.promisor_path, None);
1029        assert!(
1030            !db.loose()
1031                .object_path(&oid)
1032                .expect("test operation should succeed")
1033                .exists()
1034        );
1035        assert!(db.contains(&oid).expect("test operation should succeed"));
1036        assert_eq!(read_object_for_assert(&db, &oid), object);
1037        fs::remove_dir_all(root).expect("test operation should succeed");
1038    }
1039
1040    #[test]
1041    fn file_database_installs_raw_sha256_pack_without_loose_objects() {
1042        let root = temp_root("sley-file-odb-install-raw-pack");
1043        let git_dir = root.join(".git");
1044        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1045        let object = EncodedObject::new(ObjectType::Blob, b"installed raw sha256 pack\n".to_vec());
1046        let oid = object
1047            .object_id(ObjectFormat::Sha256)
1048            .expect("test operation should succeed");
1049        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha256)
1050            .expect("test operation should succeed");
1051        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha256);
1052        let mut reader = pack.pack.as_slice();
1053
1054        let result = db
1055            .install_raw_pack_from_reader(&mut reader)
1056            .expect("test operation should succeed");
1057
1058        assert_eq!(result.pack_name, format!("pack-{}", pack.checksum.to_hex()));
1059        assert_eq!(result.object_ids, vec![oid]);
1060        assert!(result.pack_path.exists());
1061        assert!(result.index_path.exists());
1062        assert_eq!(result.promisor_path, None);
1063        assert!(
1064            !db.loose()
1065                .object_path(&oid)
1066                .expect("test operation should succeed")
1067                .exists()
1068        );
1069        assert!(db.contains(&oid).expect("test operation should succeed"));
1070        assert_eq!(read_object_for_assert(&db, &oid), object);
1071        fs::remove_dir_all(root).expect("test operation should succeed");
1072    }
1073
1074    #[test]
1075    fn file_database_streams_raw_pack_install_to_packfile() {
1076        use std::io::Write as _;
1077
1078        let root = temp_root("sley-file-odb-stream-raw-pack");
1079        let git_dir = root.join(".git");
1080        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1081        let object = EncodedObject::new(ObjectType::Blob, b"streamed raw pack\n".to_vec());
1082        let oid = object
1083            .object_id(ObjectFormat::Sha1)
1084            .expect("test operation should succeed");
1085        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha1)
1086            .expect("test operation should succeed");
1087        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1088
1089        let mut install = db
1090            .begin_raw_pack_install(pack.checksum, pack.pack.len() as u64)
1091            .expect("test operation should succeed");
1092        for chunk in pack.pack.chunks(5) {
1093            install
1094                .write_all(chunk)
1095                .expect("test operation should succeed");
1096        }
1097        let result = install.finish().expect("test operation should succeed");
1098
1099        assert_eq!(result.pack_name, format!("pack-{}", pack.checksum.to_hex()));
1100        assert_eq!(result.object_ids, vec![oid]);
1101        assert_eq!(
1102            fs::read(&result.pack_path).expect("test operation should succeed"),
1103            pack.pack
1104        );
1105        assert!(result.index_path.exists());
1106        assert!(db.contains(&oid).expect("test operation should succeed"));
1107        assert_eq!(read_object_for_assert(&db, &oid), object);
1108
1109        let bad_id = ObjectId::from_raw(ObjectFormat::Sha1, &[0x42; 20])
1110            .expect("test operation should succeed");
1111        let mut bad_install = db
1112            .begin_raw_pack_install(bad_id, pack.pack.len() as u64)
1113            .expect("test operation should succeed");
1114        bad_install
1115            .write_all(&pack.pack)
1116            .expect("test operation should succeed");
1117        assert!(
1118            bad_install.finish().is_err(),
1119            "checksum mismatch should reject the streamed pack"
1120        );
1121        assert!(
1122            !git_dir
1123                .join("objects")
1124                .join("pack")
1125                .join(format!("pack-{}.pack", bad_id.to_hex()))
1126                .exists()
1127        );
1128
1129        fs::remove_dir_all(root).expect("test operation should succeed");
1130    }
1131
1132    #[test]
1133    fn file_database_installs_unknown_length_raw_pack_from_reader() {
1134        let root = temp_root("sley-file-odb-install-raw-pack-reader");
1135        let git_dir = root.join(".git");
1136        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1137        let object = EncodedObject::new(ObjectType::Blob, b"reader streamed raw pack\n".to_vec());
1138        let oid = object
1139            .object_id(ObjectFormat::Sha1)
1140            .expect("test operation should succeed");
1141        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha1)
1142            .expect("test operation should succeed");
1143        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1144        let mut reader = pack.pack.as_slice();
1145
1146        let result = db
1147            .install_raw_pack_from_reader(&mut reader)
1148            .expect("test operation should succeed");
1149
1150        assert_eq!(result.pack_name, format!("pack-{}", pack.checksum.to_hex()));
1151        assert_eq!(result.object_ids, vec![oid]);
1152        assert_eq!(
1153            fs::read(&result.pack_path).expect("test operation should succeed"),
1154            pack.pack
1155        );
1156        assert!(result.index_path.exists());
1157        assert!(db.contains(&oid).expect("test operation should succeed"));
1158        assert_eq!(read_object_for_assert(&db, &oid), object);
1159        fs::remove_dir_all(root).expect("test operation should succeed");
1160    }
1161
1162    #[test]
1163    fn file_database_rejects_raw_pack_stream_exceeding_max_input_size() {
1164        let root = temp_root("sley-file-odb-install-raw-pack-max-size");
1165        let git_dir = root.join(".git");
1166        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1167        let object = EncodedObject::new(ObjectType::Blob, b"bounded raw pack install\n".to_vec());
1168        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha1)
1169            .expect("test operation should succeed");
1170        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1171        let pack_dir = git_dir.join("objects").join("pack");
1172        let before = fs::read_dir(&pack_dir)
1173            .map(|entries| entries.count())
1174            .unwrap_or_default();
1175        let mut reader = pack.pack.as_slice();
1176        let limit = 32u64;
1177
1178        let err = db
1179            .install_raw_pack_from_reader_with_options(
1180                &mut reader,
1181                RawPackInstallOptions {
1182                    max_input_size: Some(limit),
1183                    ..Default::default()
1184                },
1185            )
1186            .expect_err("oversized stream should be rejected");
1187
1188        assert!(
1189            err.to_string().contains("pack exceeds maximum allowed size"),
1190            "unexpected error: {err}"
1191        );
1192        let temp_files = fs::read_dir(&pack_dir)
1193            .expect("pack dir should exist")
1194            .filter_map(|entry| entry.ok())
1195            .map(|entry| entry.path())
1196            .filter(|path| {
1197                path.file_name()
1198                    .and_then(|name| name.to_str())
1199                    .is_some_and(|name| name.starts_with("tmp_obj_"))
1200            })
1201            .collect::<Vec<_>>();
1202        assert!(
1203            temp_files.is_empty(),
1204            "temp pack staging file should be removed on failure"
1205        );
1206        let after = fs::read_dir(&pack_dir)
1207            .map(|entries| entries.count())
1208            .unwrap_or_default();
1209        assert_eq!(after, before, "no durable pack files should be installed");
1210        let installed = fs::read_dir(&pack_dir)
1211            .map(|entries| {
1212                entries
1213                    .filter_map(|entry| entry.ok())
1214                    .filter(|entry| {
1215                        entry
1216                            .path()
1217                            .extension()
1218                            .and_then(|ext| ext.to_str())
1219                            == Some("pack")
1220                    })
1221                    .count()
1222            })
1223            .unwrap_or_default();
1224        assert_eq!(installed, 0);
1225        fs::remove_dir_all(root).expect("test operation should succeed");
1226    }
1227
1228    #[test]
1229    fn file_database_rejects_unknown_length_raw_pack_with_trailing_bytes() {
1230        let root = temp_root("sley-file-odb-install-raw-pack-reader-trailing");
1231        let git_dir = root.join(".git");
1232        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1233        let object = EncodedObject::new(ObjectType::Blob, b"trailing streamed raw pack\n".to_vec());
1234        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha1)
1235            .expect("test operation should succeed");
1236        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1237        let mut bytes = pack.pack;
1238        bytes.extend_from_slice(b"not part of the pack");
1239        let mut reader = bytes.as_slice();
1240
1241        let err = db
1242            .install_raw_pack_from_reader(&mut reader)
1243            .expect_err("trailing bytes should be rejected");
1244
1245        assert!(err.to_string().contains("trailing bytes after checksum"));
1246        let pack_dir = git_dir.join("objects").join("pack");
1247        let pack_entries = fs::read_dir(&pack_dir)
1248            .map(|entries| entries.count())
1249            .unwrap_or_default();
1250        assert_eq!(pack_entries, 0);
1251        fs::remove_dir_all(root).expect("test operation should succeed");
1252    }
1253
1254    #[test]
1255    fn file_database_rejects_noncanonical_pack_index() {
1256        let root = temp_root("sley-file-odb-install-bad-index");
1257        let git_dir = root.join(".git");
1258        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1259        let object = EncodedObject::new(ObjectType::Blob, b"bad index crc\n".to_vec());
1260        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha1)
1261            .expect("test operation should succeed");
1262        let mut entries = pack.entries.clone();
1263        entries[0].crc32 ^= 1;
1264        let mut bad_pack = pack.clone();
1265        bad_pack.index = PackIndex::write_v2(ObjectFormat::Sha1, &entries, &pack.checksum)
1266            .expect("test operation should succeed");
1267        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1268
1269        assert!(db.install_pack(&bad_pack).is_err());
1270
1271        fs::remove_dir_all(root).expect("test operation should succeed");
1272    }
1273
1274    #[test]
1275    fn file_database_installs_raw_promisor_pack_with_sidecar() {
1276        let root = temp_root("sley-file-odb-install-raw-promisor-pack");
1277        let git_dir = root.join(".git");
1278        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1279        let object = EncodedObject::new(ObjectType::Blob, b"installed promisor pack\n".to_vec());
1280        let oid = object
1281            .object_id(ObjectFormat::Sha1)
1282            .expect("test operation should succeed");
1283        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha1)
1284            .expect("test operation should succeed");
1285        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1286        let mut reader = pack.pack.as_slice();
1287
1288        let result = db
1289            .install_raw_pack_from_reader_with_options(
1290                &mut reader,
1291                RawPackInstallOptions {
1292                    promisor: true,
1293                    ..Default::default()
1294                },
1295            )
1296            .expect("test operation should succeed");
1297
1298        let promisor_path = result.promisor_path.expect("promisor sidecar");
1299        assert_eq!(promisor_path.file_stem(), result.pack_path.file_stem());
1300        assert_eq!(
1301            promisor_path.extension().and_then(|ext| ext.to_str()),
1302            Some("promisor")
1303        );
1304        assert!(promisor_path.exists());
1305        assert_eq!(
1306            fs::read(&promisor_path).expect("test operation should succeed"),
1307            b""
1308        );
1309        assert!(result.pack_path.exists());
1310        assert!(result.index_path.exists());
1311        assert!(
1312            !db.loose()
1313                .object_path(&oid)
1314                .expect("test operation should succeed")
1315                .exists()
1316        );
1317        assert_eq!(read_object_for_assert(&db, &oid), object);
1318        fs::remove_dir_all(root).expect("test operation should succeed");
1319    }
1320
1321    #[test]
1322    fn repository_objects_dir_uses_linked_worktree_common_dir() {
1323        let root = temp_root("sley-odb-common-dir");
1324        let common = root.join(".git");
1325        let admin = common.join("worktrees").join("linked");
1326        fs::create_dir_all(&admin).expect("test operation should succeed");
1327        fs::write(admin.join("commondir"), "../..\n").expect("test operation should succeed");
1328
1329        let common = fs::canonicalize(common).expect("test operation should succeed");
1330        assert_eq!(repository_common_dir(&admin), common);
1331        assert_eq!(repository_objects_dir(&admin), common.join("objects"));
1332
1333        fs::remove_dir_all(root).expect("test operation should succeed");
1334    }
1335
1336    #[test]
1337    fn reachable_object_helpers_walk_graph_and_install_pack() {
1338        let root = temp_root("sley-reachable-pack");
1339        let source_git_dir = root.join("source.git");
1340        let destination_git_dir = root.join("destination.git");
1341        fs::create_dir_all(source_git_dir.join("objects")).expect("test operation should succeed");
1342        fs::create_dir_all(destination_git_dir.join("objects"))
1343            .expect("test operation should succeed");
1344        let format = ObjectFormat::Sha1;
1345        let source = FileObjectDatabase::from_git_dir(&source_git_dir, format);
1346        let destination = FileObjectDatabase::from_git_dir(&destination_git_dir, format);
1347
1348        let blob = EncodedObject::new(ObjectType::Blob, b"reachable payload\n".to_vec());
1349        let blob_oid = source
1350            .write_object(blob.clone())
1351            .expect("test operation should succeed");
1352        let tree = EncodedObject::new(
1353            ObjectType::Tree,
1354            Tree {
1355                entries: vec![TreeEntry {
1356                    mode: 0o100644,
1357                    name: BString::from(b"payload.txt"),
1358                    oid: blob_oid,
1359                }],
1360            }
1361            .write(),
1362        );
1363        let tree_oid = source
1364            .write_object(tree.clone())
1365            .expect("test operation should succeed");
1366        let identity = b"Example <example@example.invalid> 0 +0000".to_vec();
1367        let commit = EncodedObject::new(
1368            ObjectType::Commit,
1369            Commit {
1370                tree: tree_oid,
1371                parents: Vec::new(),
1372                author: identity.clone(),
1373                committer: identity,
1374                encoding: None,
1375                message: b"initial\n".to_vec(),
1376            }
1377            .write(),
1378        );
1379        let commit_oid = source
1380            .write_object(commit.clone())
1381            .expect("test operation should succeed");
1382
1383        let reachable = collect_reachable_object_ids(&source, format, std::iter::once(commit_oid))
1384            .expect("test operation should succeed");
1385        assert!(reachable.contains(&commit_oid));
1386        assert!(reachable.contains(&tree_oid));
1387        assert!(reachable.contains(&blob_oid));
1388
1389        let install =
1390            install_reachable_pack(&source, &destination, format, std::iter::once(commit_oid))
1391                .expect("test operation should succeed")
1392                .expect("reachable pack should be written");
1393        assert_eq!(install.object_ids.len(), 3);
1394        for (oid, object) in [
1395            (&commit_oid, &commit),
1396            (&tree_oid, &tree),
1397            (&blob_oid, &blob),
1398        ] {
1399            assert!(
1400                !destination
1401                    .loose()
1402                    .object_path(oid)
1403                    .expect("test operation should succeed")
1404                    .exists()
1405            );
1406            assert!(
1407                destination
1408                    .contains(oid)
1409                    .expect("test operation should succeed")
1410            );
1411            assert_eq!(read_object_for_assert(&destination, oid), *object);
1412        }
1413        fs::remove_dir_all(root).expect("test operation should succeed");
1414    }
1415
1416    #[test]
1417    fn reachable_object_helpers_respect_exclusions_and_duplicate_starts() {
1418        let root = temp_root("sley-reachable-exclusions");
1419        let git_dir = root.join("repo.git");
1420        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1421        let format = ObjectFormat::Sha1;
1422        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
1423
1424        let blob = EncodedObject::new(ObjectType::Blob, b"excluded payload\n".to_vec());
1425        let blob_oid = db
1426            .write_object(blob)
1427            .expect("test operation should succeed");
1428        let tree = EncodedObject::new(
1429            ObjectType::Tree,
1430            Tree {
1431                entries: vec![TreeEntry {
1432                    mode: 0o100644,
1433                    name: BString::from(b"payload.txt"),
1434                    oid: blob_oid,
1435                }],
1436            }
1437            .write(),
1438        );
1439        let tree_oid = db
1440            .write_object(tree)
1441            .expect("test operation should succeed");
1442        let identity = b"Example <example@example.invalid> 0 +0000".to_vec();
1443        let commit = EncodedObject::new(
1444            ObjectType::Commit,
1445            Commit {
1446                tree: tree_oid,
1447                parents: Vec::new(),
1448                author: identity.clone(),
1449                committer: identity,
1450                encoding: None,
1451                message: b"initial\n".to_vec(),
1452            }
1453            .write(),
1454        );
1455        let commit_oid = db
1456            .write_object(commit)
1457            .expect("test operation should succeed");
1458        let excluded = HashSet::from([tree_oid]);
1459
1460        let objects = collect_reachable_objects(&db, format, [commit_oid, commit_oid], &excluded)
1461            .expect("test operation should succeed");
1462
1463        assert_eq!(objects.len(), 1);
1464        assert_eq!(
1465            objects[0]
1466                .object_id(format)
1467                .expect("test operation should succeed"),
1468            commit_oid
1469        );
1470        fs::remove_dir_all(root).expect("test operation should succeed");
1471    }
1472
1473    #[test]
1474    fn build_reachable_pack_returns_raw_pack_and_respects_empty_exclusions() {
1475        let root = temp_root("sley-build-reachable-pack");
1476        let git_dir = root.join("repo.git");
1477        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1478        let format = ObjectFormat::Sha1;
1479        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
1480
1481        let object = EncodedObject::new(ObjectType::Blob, b"raw reachable pack\n".to_vec());
1482        let oid = db
1483            .write_object(object.clone())
1484            .expect("test operation should succeed");
1485        let pack = build_reachable_pack(&db, format, std::iter::once(oid), &HashSet::new())
1486            .expect("test operation should succeed")
1487            .expect("reachable pack should be built");
1488        assert!(pack.pack.starts_with(b"PACK"));
1489        assert_eq!(pack.entries.len(), 1);
1490        assert_eq!(pack.entries[0].oid, oid);
1491
1492        let pack_path = root.join("reachable.pack");
1493        let pack_file = build_reachable_pack_file(
1494            &db,
1495            format,
1496            std::iter::once(oid),
1497            &HashSet::new(),
1498            &pack_path,
1499        )
1500        .expect("test operation should succeed")
1501        .expect("reachable pack file should be built");
1502        assert_eq!(pack_file.checksum, pack.checksum);
1503        assert_eq!(pack_file.pack_size, pack.pack.len() as u64);
1504        assert_eq!(pack_file.object_count, 1);
1505        assert_eq!(
1506            fs::read(&pack_file.pack_path).expect("test operation should succeed"),
1507            pack.pack
1508        );
1509
1510        let mut streamed_pack = Vec::new();
1511        let streamed = write_reachable_pack_to_writer(
1512            &db,
1513            format,
1514            std::iter::once(oid),
1515            &HashSet::new(),
1516            &mut streamed_pack,
1517        )
1518        .expect("test operation should succeed")
1519        .expect("reachable pack should be streamed");
1520        assert_eq!(streamed.checksum, pack.checksum);
1521        assert_eq!(streamed.pack_size, pack.pack.len() as u64);
1522        assert_eq!(streamed.object_count, 1);
1523        assert_eq!(streamed_pack, pack.pack);
1524
1525        let mut sink = std::io::sink();
1526        let dry_run = write_reachable_pack_to_writer(
1527            &db,
1528            format,
1529            std::iter::once(oid),
1530            &HashSet::new(),
1531            &mut sink,
1532        )
1533        .expect("test operation should succeed")
1534        .expect("reachable pack should stream to sink");
1535        assert_eq!(dry_run.checksum, pack.checksum);
1536        assert_eq!(dry_run.pack_size, pack.pack.len() as u64);
1537        assert_eq!(dry_run.object_count, 1);
1538
1539        let excluded = HashSet::from([oid]);
1540        assert!(
1541            build_reachable_pack(
1542                &db,
1543                format,
1544                pack.entries.into_iter().map(|entry| entry.oid),
1545                &excluded
1546            )
1547            .expect("test operation should succeed")
1548            .is_none()
1549        );
1550        fs::remove_dir_all(root).expect("test operation should succeed");
1551    }
1552
1553    #[test]
1554    fn large_reachable_pack_streams_objects_by_id_windows() {
1555        let root = temp_root("sley-reachable-pack-streamed-large");
1556        let git_dir = root.join(".git");
1557        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1558        let format = ObjectFormat::Sha1;
1559        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
1560
1561        let mut roots = Vec::new();
1562        for idx in 0..(REACHABLE_PACK_STREAMING_MIN_OBJECTS + 5) {
1563            let object = EncodedObject::new(
1564                ObjectType::Blob,
1565                format!("streamed reachable blob {idx:04}\n").into_bytes(),
1566            );
1567            roots.push(
1568                db.write_object(object)
1569                    .expect("test operation should succeed"),
1570            );
1571        }
1572
1573        let mut pack_bytes = Vec::new();
1574        let summary = write_reachable_pack_to_writer(
1575            &db,
1576            format,
1577            roots.iter().copied(),
1578            &HashSet::new(),
1579            &mut pack_bytes,
1580        )
1581        .expect("test operation should succeed")
1582        .expect("reachable pack should be streamed");
1583        assert_eq!(summary.object_count, roots.len());
1584        assert!(
1585            summary.delta_count > 0,
1586            "streamed large packs should still find deltas"
1587        );
1588        assert_eq!(summary.pack_size, pack_bytes.len() as u64);
1589
1590        let parsed = PackFile::parse(&pack_bytes, format).expect("test operation should succeed");
1591        let expected_oids = roots.iter().copied().collect::<HashSet<_>>();
1592        let parsed_oids = parsed
1593            .entries
1594            .iter()
1595            .map(|entry| entry.entry.oid)
1596            .collect::<HashSet<_>>();
1597        assert_eq!(parsed.checksum, summary.checksum);
1598        assert_eq!(parsed_oids, expected_oids);
1599
1600        fs::remove_dir_all(root).expect("test operation should succeed");
1601    }
1602
1603    #[test]
1604    fn index_raw_pack_returns_validated_pack_metadata() {
1605        let root = temp_root("sley-index-raw-pack");
1606        let git_dir = root.join("repo.git");
1607        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1608        let format = ObjectFormat::Sha1;
1609        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
1610        let graph = write_commit_graph(&mut db, b"pack indexed\n");
1611        let commit_oid = graph[0].0;
1612        let expected = graph
1613            .iter()
1614            .map(|(oid, object)| (*oid, (object.object_type, object.body.len() as u64)))
1615            .collect::<HashMap<_, _>>();
1616        let pack = build_reachable_pack(&db, format, std::iter::once(commit_oid), &HashSet::new())
1617            .expect("test operation should succeed")
1618            .expect("reachable pack should be built");
1619
1620        let indexed = index_raw_pack(&pack.pack, format).expect("test operation should succeed");
1621        let mut cursor = std::io::Cursor::new(pack.pack.clone());
1622        let streamed = index_raw_pack_from_reader(&mut cursor, format)
1623            .expect("streamed pack indexing should match in-memory indexing");
1624        assert_eq!(streamed, indexed);
1625        let pack_path = root.join("reachable.pack");
1626        fs::write(&pack_path, &pack.pack).expect("test operation should succeed");
1627        let file_indexed = index_raw_pack_file(&pack_path, format)
1628            .expect("file-backed pack indexing should match in-memory indexing");
1629        assert_eq!(file_indexed, indexed);
1630
1631        assert_eq!(indexed.pack_id, pack.checksum);
1632        assert_eq!(indexed.index, pack.index);
1633        assert_eq!(indexed.objects.len(), 3);
1634        for object in indexed.objects {
1635            let (expected_type, expected_size) = expected
1636                .get(&object.oid)
1637                .copied()
1638                .expect("indexed object should be reachable");
1639            assert_eq!(object.object_type, expected_type);
1640            assert_eq!(object.size, expected_size);
1641            assert!(object.offset > 0);
1642        }
1643        fs::remove_dir_all(root).expect("test operation should succeed");
1644    }
1645
1646    #[test]
1647    fn reachable_object_helpers_follow_tags_and_report_missing_objects() {
1648        let root = temp_root("sley-reachable-tags");
1649        let git_dir = root.join("repo.git");
1650        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1651        let format = ObjectFormat::Sha1;
1652        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
1653
1654        let blob = EncodedObject::new(ObjectType::Blob, b"tagged payload\n".to_vec());
1655        let blob_oid = db
1656            .write_object(blob)
1657            .expect("test operation should succeed");
1658        let tag = EncodedObject::new(
1659            ObjectType::Tag,
1660            Tag {
1661                object: blob_oid,
1662                object_type: ObjectType::Blob,
1663                name: b"v1".to_vec(),
1664                tagger: Some(b"Example <example@example.invalid> 0 +0000".to_vec()),
1665                message: b"tag message\n".to_vec(),
1666                raw_body: None,
1667            }
1668            .write(),
1669        );
1670        let tag_oid = db.write_object(tag).expect("test operation should succeed");
1671
1672        let reachable = collect_reachable_object_ids(&db, format, std::iter::once(tag_oid))
1673            .expect("test operation should succeed");
1674        assert!(reachable.contains(&tag_oid));
1675        assert!(reachable.contains(&blob_oid));
1676
1677        let missing = ObjectId::from_hex(format, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
1678            .expect("test operation should succeed");
1679        let err = collect_reachable_object_ids(&db, format, std::iter::once(missing))
1680            .expect_err("missing traversal root should error");
1681        let kind = err.not_found_kind().expect("typed not found");
1682        assert_eq!(kind.object_id(), Some(missing));
1683        assert_eq!(
1684            kind.missing_object_context(),
1685            Some(MissingObjectContext::Traversal)
1686        );
1687        fs::remove_dir_all(root).expect("test operation should succeed");
1688    }
1689
1690    #[test]
1691    fn install_reachable_pack_empty_starts_create_no_pack() {
1692        let root = temp_root("sley-reachable-empty");
1693        let source_git_dir = root.join("source.git");
1694        let destination_git_dir = root.join("destination.git");
1695        fs::create_dir_all(source_git_dir.join("objects")).expect("test operation should succeed");
1696        fs::create_dir_all(destination_git_dir.join("objects"))
1697            .expect("test operation should succeed");
1698        let format = ObjectFormat::Sha1;
1699        let source = FileObjectDatabase::from_git_dir(&source_git_dir, format);
1700        let destination = FileObjectDatabase::from_git_dir(&destination_git_dir, format);
1701
1702        let result = install_reachable_pack(&source, &destination, format, Vec::<ObjectId>::new())
1703            .expect("test operation should succeed");
1704
1705        assert!(result.is_none());
1706        assert!(!destination_git_dir.join("objects").join("pack").exists());
1707        fs::remove_dir_all(root).expect("test operation should succeed");
1708    }
1709
1710    #[test]
1711    fn install_reachable_pack_excluding_skips_fully_excluded_starts() {
1712        let root = temp_root("sley-reachable-install-excluding");
1713        let source_git_dir = root.join("source.git");
1714        let destination_git_dir = root.join("destination.git");
1715        fs::create_dir_all(source_git_dir.join("objects")).expect("test operation should succeed");
1716        fs::create_dir_all(destination_git_dir.join("objects"))
1717            .expect("test operation should succeed");
1718        let format = ObjectFormat::Sha1;
1719        let source = FileObjectDatabase::from_git_dir(&source_git_dir, format);
1720        let destination = FileObjectDatabase::from_git_dir(&destination_git_dir, format);
1721        let object = EncodedObject::new(ObjectType::Blob, b"excluded install\n".to_vec());
1722        let oid = source
1723            .write_object(object)
1724            .expect("test operation should succeed");
1725        let excluded = HashSet::from([oid]);
1726
1727        let result = install_reachable_pack_excluding(
1728            &source,
1729            &destination,
1730            format,
1731            std::iter::once(oid),
1732            &excluded,
1733        )
1734        .expect("test operation should succeed");
1735
1736        assert!(result.is_none());
1737        assert!(!destination_git_dir.join("objects").join("pack").exists());
1738        fs::remove_dir_all(root).expect("test operation should succeed");
1739    }
1740
1741    #[test]
1742    fn install_reachable_pack_supports_sha256() {
1743        let root = temp_root("sley-reachable-pack-sha256");
1744        let source_git_dir = root.join("source.git");
1745        let destination_git_dir = root.join("destination.git");
1746        fs::create_dir_all(source_git_dir.join("objects")).expect("test operation should succeed");
1747        fs::create_dir_all(destination_git_dir.join("objects"))
1748            .expect("test operation should succeed");
1749        let format = ObjectFormat::Sha256;
1750        let source = FileObjectDatabase::from_git_dir(&source_git_dir, format);
1751        let destination = FileObjectDatabase::from_git_dir(&destination_git_dir, format);
1752        let object = EncodedObject::new(ObjectType::Blob, b"sha256 reachable pack\n".to_vec());
1753        let oid = source
1754            .write_object(object.clone())
1755            .expect("test operation should succeed");
1756
1757        let pack = build_reachable_pack(&source, format, std::iter::once(oid), &HashSet::new())
1758            .expect("test operation should succeed")
1759            .expect("sha256 reachable pack should be built");
1760        assert!(pack.pack.starts_with(b"PACK"));
1761        assert_eq!(pack.entries[0].oid, oid);
1762
1763        let result = install_reachable_pack(&source, &destination, format, std::iter::once(oid))
1764            .expect("test operation should succeed")
1765            .expect("sha256 reachable pack should be written");
1766
1767        assert_eq!(result.object_ids, vec![oid]);
1768        assert!(
1769            !destination
1770                .loose()
1771                .object_path(&oid)
1772                .expect("test operation should succeed")
1773                .exists()
1774        );
1775        assert_eq!(read_object_for_assert(&destination, &oid), object);
1776        fs::remove_dir_all(root).expect("test operation should succeed");
1777    }
1778
1779    #[test]
1780    fn install_helpers_accept_custom_raw_pack_installer() {
1781        #[derive(Default)]
1782        struct RecordingInstaller {
1783            packs: std::cell::RefCell<Vec<Vec<u8>>>,
1784            installed: std::cell::RefCell<Vec<ObjectId>>,
1785        }
1786
1787        impl RawPackInstaller for RecordingInstaller {
1788            fn install_raw_pack_from_reader_with_options<R>(
1789                &self,
1790                reader: &mut R,
1791                _options: RawPackInstallOptions,
1792            ) -> Result<RawPackInstallResult>
1793            where
1794                R: Read,
1795            {
1796                let mut pack_bytes = Vec::new();
1797                reader.read_to_end(&mut pack_bytes)?;
1798                self.packs.borrow_mut().push(pack_bytes.to_vec());
1799                let object_ids = self.installed.borrow().clone();
1800                Ok(RawPackInstallResult { object_ids })
1801            }
1802        }
1803
1804        let format = ObjectFormat::Sha1;
1805        let source = ObjectDatabase::new(format);
1806        let object = EncodedObject::new(ObjectType::Blob, b"custom raw installer\n".to_vec());
1807        let oid = source
1808            .write_object(object)
1809            .expect("test operation should succeed");
1810        let installer = RecordingInstaller::default();
1811        installer.installed.borrow_mut().push(oid);
1812
1813        let result = install_reachable_pack(&source, &installer, format, std::iter::once(oid))
1814            .expect("test operation should succeed")
1815            .expect("custom installer should receive pack");
1816
1817        assert_eq!(result.object_ids, installer.installed.into_inner());
1818        let packs = installer.packs.into_inner();
1819        assert_eq!(packs.len(), 1);
1820        assert!(packs[0].starts_with(b"PACK"));
1821    }
1822
1823    #[test]
1824    fn file_database_reads_object_from_multi_pack_index() {
1825        let root = temp_root("sley-file-odb-midx");
1826        let git_dir = root.join(".git");
1827        let pack_dir = git_dir.join("objects").join("pack");
1828        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
1829        let first = EncodedObject::new(ObjectType::Blob, b"first packed\n".to_vec());
1830        let second = EncodedObject::new(ObjectType::Blob, b"second packed\n".to_vec());
1831        let first_oid = first
1832            .object_id(ObjectFormat::Sha1)
1833            .expect("test operation should succeed");
1834        let second_oid = second
1835            .object_id(ObjectFormat::Sha1)
1836            .expect("test operation should succeed");
1837        let first_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&first))
1838            .expect("test operation should succeed");
1839        let second_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&second))
1840            .expect("test operation should succeed");
1841        let first_pack_name = format!("pack-{}.idx", first_pack.checksum.to_hex());
1842        let second_pack_name = format!("pack-{}.idx", second_pack.checksum.to_hex());
1843        fs::write(
1844            pack_dir.join(first_pack_name.replace(".idx", ".pack")),
1845            first_pack.pack,
1846        )
1847        .expect("test operation should succeed");
1848        fs::write(
1849            pack_dir.join(second_pack_name.replace(".idx", ".pack")),
1850            second_pack.pack,
1851        )
1852        .expect("test operation should succeed");
1853        let midx = MultiPackIndex::write(
1854            ObjectFormat::Sha1,
1855            2,
1856            &[first_pack_name, second_pack_name],
1857            &[
1858                sley_pack::MultiPackIndexEntry {
1859                    oid: first_oid,
1860                    pack_int_id: 0,
1861                    offset: first_pack.entries[0].offset,
1862                    force_large_offset: false,
1863                },
1864                sley_pack::MultiPackIndexEntry {
1865                    oid: second_oid,
1866                    pack_int_id: 1,
1867                    offset: second_pack.entries[0].offset,
1868                    force_large_offset: false,
1869                },
1870            ],
1871        )
1872        .expect("test operation should succeed");
1873        fs::write(pack_dir.join("multi-pack-index"), midx).expect("test operation should succeed");
1874
1875        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1876        assert!(
1877            db.contains(&second_oid)
1878                .expect("test operation should succeed")
1879        );
1880        assert_eq!(
1881            db.resolve_prefix(&second_oid.to_hex()[..8])
1882                .expect("test operation should succeed"),
1883            ObjectPrefixResolution::Unique(second_oid)
1884        );
1885        assert_eq!(read_object_for_assert(&db, &second_oid), second);
1886        assert_eq!(read_object_for_assert(&db, &first_oid), first);
1887        fs::remove_dir_all(root).expect("test operation should succeed");
1888    }
1889
1890    #[test]
1891    fn file_database_finds_pack_added_after_registry_was_cached() {
1892        // Regression guard for the cached pack-directory registry: a pack written
1893        // after the registry was first cached (via a prior read) must still be
1894        // discovered by the same handle, because a miss triggers a re-scan.
1895        let root = temp_root("sley-file-odb-pack-added-late");
1896        let git_dir = root.join(".git");
1897        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1898        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1899
1900        // First pack + object; reading it populates the registry cache.
1901        let first = EncodedObject::new(ObjectType::Blob, b"first late\n".to_vec());
1902        let first_oid = first
1903            .object_id(ObjectFormat::Sha1)
1904            .expect("test operation should succeed");
1905        let first_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&first))
1906            .expect("test operation should succeed");
1907        db.install_pack(&first_pack)
1908            .expect("test operation should succeed");
1909        assert_eq!(read_object_for_assert(&db, &first_oid), first);
1910
1911        // A second object that the cached registry does not yet know about.
1912        let second = EncodedObject::new(ObjectType::Blob, b"second late\n".to_vec());
1913        let second_oid = second
1914            .object_id(ObjectFormat::Sha1)
1915            .expect("test operation should succeed");
1916        // It is genuinely absent right now.
1917        assert!(matches!(
1918            db.read_object(&second_oid),
1919            Err(GitError::NotFound(_))
1920        ));
1921
1922        // Install its pack through the same handle; the next read must find it via
1923        // a re-scan, not be masked by the stale registry.
1924        let second_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&second))
1925            .expect("test operation should succeed");
1926        db.install_pack(&second_pack)
1927            .expect("test operation should succeed");
1928        assert!(
1929            db.contains(&second_oid)
1930                .expect("test operation should succeed")
1931        );
1932        assert_eq!(read_object_for_assert(&db, &second_oid), second);
1933        // The original object still resolves too.
1934        assert_eq!(read_object_for_assert(&db, &first_oid), first);
1935
1936        fs::remove_dir_all(root).expect("test operation should succeed");
1937    }
1938
1939    #[test]
1940    fn object_presence_checker_finds_pack_added_after_registry_was_cached() {
1941        let root = temp_root("sley-presence-checker-pack-added-late");
1942        let git_dir = root.join(".git");
1943        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1944        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1945
1946        let first = EncodedObject::new(ObjectType::Blob, b"checker first late\n".to_vec());
1947        let first_oid = first
1948            .object_id(ObjectFormat::Sha1)
1949            .expect("test operation should succeed");
1950        let first_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&first))
1951            .expect("test operation should succeed");
1952        db.install_pack(&first_pack)
1953            .expect("test operation should succeed");
1954
1955        let second = EncodedObject::new(ObjectType::Blob, b"checker second late\n".to_vec());
1956        let second_oid = second
1957            .object_id(ObjectFormat::Sha1)
1958            .expect("test operation should succeed");
1959        let mut checker = db.presence_checker();
1960        assert!(
1961            checker
1962                .contains(&first_oid)
1963                .expect("test operation should succeed")
1964        );
1965        assert!(
1966            !checker
1967                .contains(&second_oid)
1968                .expect("test operation should succeed")
1969        );
1970
1971        let second_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&second))
1972            .expect("test operation should succeed");
1973        db.install_pack(&second_pack)
1974            .expect("test operation should succeed");
1975
1976        assert!(
1977            checker
1978                .contains(&second_oid)
1979                .expect("test operation should succeed")
1980        );
1981        fs::remove_dir_all(root).expect("test operation should succeed");
1982    }
1983
1984    #[test]
1985    fn file_database_pack_registry_loads_indexes_lazily_and_refreshes_after_count_change() {
1986        let root = temp_root("sley-file-odb-pack-registry-refresh");
1987        let git_dir = root.join(".git");
1988        let pack_dir = git_dir.join("objects").join("pack");
1989        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1990        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
1991
1992        let first = EncodedObject::new(ObjectType::Blob, b"registry first\n".to_vec());
1993        let first_oid = first
1994            .object_id(ObjectFormat::Sha1)
1995            .expect("test operation should succeed");
1996        let first_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&first))
1997            .expect("test operation should succeed");
1998        db.install_pack(&first_pack)
1999            .expect("test operation should succeed");
2000
2001        let first_registry = db
2002            .cached_pack_registry(&pack_dir, false)
2003            .expect("test operation should succeed");
2004        assert_eq!(first_registry.fingerprint.idx_count, 1);
2005        assert_eq!(first_registry.fingerprint.pack_count, 1);
2006        assert_eq!(first_registry.packs.len(), 1);
2007        assert!(
2008            first_registry.packs[0]
2009                .index.read().is_none()
2010        );
2011        assert!(
2012            first_registry.packs[0]
2013                .data
2014                .lock()
2015                .expect("test operation should succeed")
2016                .is_none()
2017        );
2018
2019        // Existence checks use the parsed index directly and do not load pack
2020        // bytes; a full read fills the registry-owned pack data handle.
2021        assert!(
2022            db.contains(&first_oid)
2023                .expect("test operation should succeed")
2024        );
2025        assert!(
2026            first_registry.packs[0]
2027                .index.read().is_some()
2028        );
2029        assert!(
2030            first_registry.packs[0]
2031                .data
2032                .lock()
2033                .expect("test operation should succeed")
2034                .is_none()
2035        );
2036        assert_eq!(read_object_for_assert(&db, &first_oid), first);
2037        assert!(
2038            first_registry.packs[0]
2039                .data
2040                .lock()
2041                .expect("test operation should succeed")
2042                .is_some()
2043        );
2044
2045        let second = EncodedObject::new(ObjectType::Blob, b"registry second\n".to_vec());
2046        let second_oid = second
2047            .object_id(ObjectFormat::Sha1)
2048            .expect("test operation should succeed");
2049        let second_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&second))
2050            .expect("test operation should succeed");
2051        db.install_pack(&second_pack)
2052            .expect("test operation should succeed");
2053
2054        let refreshed = db
2055            .cached_pack_registry(&pack_dir, true)
2056            .expect("test operation should succeed");
2057        assert!(!Arc::ptr_eq(&first_registry, &refreshed));
2058        assert_eq!(refreshed.fingerprint.idx_count, 2);
2059        assert_eq!(refreshed.fingerprint.pack_count, 2);
2060        assert_eq!(refreshed.packs.len(), 2);
2061        assert_eq!(read_object_for_assert(&db, &second_oid), second);
2062
2063        fs::remove_dir_all(root).expect("test operation should succeed");
2064    }
2065
2066    #[test]
2067    fn file_database_pack_search_hint_rebuilds_after_pack_added() {
2068        // Regression guard for the recent-pack search hint: it is tied to the
2069        // cached pack registry, so a miss followed by a changed registry must not
2070        // hide newly-added packs.
2071        let root = temp_root("sley-file-odb-pack-lookup-added-late");
2072        let git_dir = root.join(".git");
2073        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2074        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
2075
2076        let first = EncodedObject::new(ObjectType::Blob, b"first lookup\n".to_vec());
2077        let second = EncodedObject::new(ObjectType::Blob, b"second lookup\n".to_vec());
2078        let third = EncodedObject::new(ObjectType::Blob, b"third lookup\n".to_vec());
2079        let first_oid = first
2080            .object_id(ObjectFormat::Sha1)
2081            .expect("test operation should succeed");
2082        let second_oid = second
2083            .object_id(ObjectFormat::Sha1)
2084            .expect("test operation should succeed");
2085        let third_oid = third
2086            .object_id(ObjectFormat::Sha1)
2087            .expect("test operation should succeed");
2088
2089        let first_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&first))
2090            .expect("test operation should succeed");
2091        let second_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&second))
2092            .expect("test operation should succeed");
2093        db.install_pack(&first_pack)
2094            .expect("test operation should succeed");
2095        db.install_pack(&second_pack)
2096            .expect("test operation should succeed");
2097
2098        // With two packs, these reads establish a cached registry and pack hint.
2099        assert_eq!(read_object_for_assert(&db, &first_oid), first);
2100        assert_eq!(read_object_for_assert(&db, &second_oid), second);
2101        assert!(matches!(
2102            db.read_object(&third_oid),
2103            Err(GitError::NotFound(_))
2104        ));
2105
2106        let third_pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&third))
2107            .expect("test operation should succeed");
2108        db.install_pack(&third_pack)
2109            .expect("test operation should succeed");
2110
2111        assert_eq!(read_object_for_assert(&db, &third_oid), third);
2112        assert_eq!(read_object_for_assert(&db, &first_oid), first);
2113
2114        fs::remove_dir_all(root).expect("test operation should succeed");
2115    }
2116
2117    #[test]
2118    fn file_database_prefers_loose_object_over_packed_object() {
2119        let root = temp_root("sley-file-odb-prefer-loose");
2120        let git_dir = root.join(".git");
2121        let pack_dir = git_dir.join("objects").join("pack");
2122        fs::create_dir_all(&pack_dir).expect("test operation should succeed");
2123        let object = EncodedObject::new(ObjectType::Blob, b"same\n".to_vec());
2124        let written = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
2125            .expect("test operation should succeed");
2126        let pack_name = written.checksum.to_hex();
2127        fs::write(
2128            pack_dir.join(format!("pack-{pack_name}.pack")),
2129            written.pack,
2130        )
2131        .expect("test operation should succeed");
2132        fs::write(
2133            pack_dir.join(format!("pack-{pack_name}.idx")),
2134            written.index,
2135        )
2136        .expect("test operation should succeed");
2137
2138        let db = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
2139        let oid = db
2140            .write_object(object.clone())
2141            .expect("test operation should succeed");
2142        assert_eq!(read_object_for_assert(&db, &oid), object);
2143        fs::remove_dir_all(root).expect("test operation should succeed");
2144    }
2145
2146    #[test]
2147    fn bundle_prerequisite_verification_reads_existing_objects() {
2148        let db = ObjectDatabase::new(ObjectFormat::Sha1);
2149        let oid = db
2150            .write_object(EncodedObject::new(ObjectType::Blob, b"base\n".to_vec()))
2151            .expect("test operation should succeed");
2152        let bundle_bytes = format!("# v2 git bundle\n-{oid} base\n\n").into_bytes();
2153        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
2154            .expect("test operation should succeed");
2155
2156        verify_bundle_prerequisites(&bundle, &db).expect("test operation should succeed");
2157    }
2158
2159    #[test]
2160    fn bundle_prerequisite_verification_reports_missing_objects() {
2161        let db = ObjectDatabase::new(ObjectFormat::Sha1);
2162        let missing = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"missing\n")
2163            .expect("test operation should succeed");
2164        let bundle_bytes = format!("# v2 git bundle\n-{missing} missing\n\n").into_bytes();
2165        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
2166            .expect("test operation should succeed");
2167
2168        assert!(verify_bundle_prerequisites(&bundle, &db).is_err());
2169    }
2170
2171    #[test]
2172    fn unbundle_objects_writes_pack_entries_and_returns_refs() {
2173        let prerequisite_reader = ObjectDatabase::new(ObjectFormat::Sha1);
2174        let mut writer = ObjectDatabase::new(ObjectFormat::Sha1);
2175        let object = EncodedObject::new(ObjectType::Blob, b"bundle object\n".to_vec());
2176        let oid = object
2177            .object_id(ObjectFormat::Sha1)
2178            .expect("test operation should succeed");
2179        let pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
2180            .expect("test operation should succeed");
2181        let bundle_bytes = format!("# v2 git bundle\n{oid} refs/heads/main\n\n")
2182            .into_bytes()
2183            .into_iter()
2184            .chain(pack.pack)
2185            .collect::<Vec<_>>();
2186        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
2187            .expect("test operation should succeed");
2188
2189        let result = unbundle_objects(&bundle, &prerequisite_reader, &mut writer)
2190            .expect("test operation should succeed");
2191        assert_eq!(result.written_objects, vec![oid]);
2192        assert_eq!(result.references, bundle.references);
2193        assert_eq!(read_object_for_assert(&writer, &oid), object);
2194    }
2195
2196    #[test]
2197    fn install_bundle_pack_writes_pack_and_returns_refs() {
2198        let root = temp_root("sley-install-bundle-pack");
2199        let git_dir = root.join(".git");
2200        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2201        let prerequisite_reader = ObjectDatabase::new(ObjectFormat::Sha1);
2202        let database = FileObjectDatabase::from_git_dir(&git_dir, ObjectFormat::Sha1);
2203        let object = EncodedObject::new(ObjectType::Blob, b"bundle pack object\n".to_vec());
2204        let oid = object
2205            .object_id(ObjectFormat::Sha1)
2206            .expect("test operation should succeed");
2207        let pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
2208            .expect("test operation should succeed");
2209        let bundle_bytes = format!("# v2 git bundle\n{oid} refs/heads/main\n\n")
2210            .into_bytes()
2211            .into_iter()
2212            .chain(pack.pack)
2213            .collect::<Vec<_>>();
2214        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
2215            .expect("test operation should succeed");
2216
2217        let result = install_bundle_pack(&bundle, &prerequisite_reader, &database)
2218            .expect("test operation should succeed");
2219
2220        assert_eq!(result.written_objects, vec![oid]);
2221        assert_eq!(result.references, bundle.references);
2222        assert!(
2223            database
2224                .contains(&oid)
2225                .expect("test operation should succeed")
2226        );
2227        assert_eq!(read_object_for_assert(&database, &oid), object);
2228        assert!(
2229            !database
2230                .loose()
2231                .object_path(&oid)
2232                .expect("test operation should succeed")
2233                .exists()
2234        );
2235        fs::remove_dir_all(root).expect("test operation should succeed");
2236    }
2237
2238    #[test]
2239    fn unpack_packfile_objects_writes_sha256_pack_entries() {
2240        let writer = ObjectDatabase::new(ObjectFormat::Sha256);
2241        let object = EncodedObject::new(ObjectType::Blob, b"transport pack object\n".to_vec());
2242        let oid = object
2243            .object_id(ObjectFormat::Sha256)
2244            .expect("test operation should succeed");
2245        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha256)
2246            .expect("test operation should succeed");
2247
2248        let result = unpack_packfile_objects(&pack.pack, ObjectFormat::Sha256, &writer)
2249            .expect("test operation should succeed");
2250
2251        assert_eq!(result.written_objects, vec![oid]);
2252        assert_eq!(read_object_for_assert(&writer, &oid), object);
2253    }
2254
2255    #[test]
2256    fn unbundle_objects_rejects_missing_prerequisites_before_writing() {
2257        let prerequisite_reader = ObjectDatabase::new(ObjectFormat::Sha1);
2258        let mut writer = ObjectDatabase::new(ObjectFormat::Sha1);
2259        let missing = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"missing\n")
2260            .expect("test operation should succeed");
2261        let object = EncodedObject::new(ObjectType::Blob, b"bundle object\n".to_vec());
2262        let oid = object
2263            .object_id(ObjectFormat::Sha1)
2264            .expect("test operation should succeed");
2265        let pack = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
2266            .expect("test operation should succeed");
2267        let bundle_bytes =
2268            format!("# v2 git bundle\n-{missing} missing\n{oid} refs/heads/main\n\n")
2269                .into_bytes()
2270                .into_iter()
2271                .chain(pack.pack)
2272                .collect::<Vec<_>>();
2273        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
2274            .expect("test operation should succeed");
2275
2276        assert!(unbundle_objects(&bundle, &prerequisite_reader, &mut writer).is_err());
2277        assert!(!writer.contains(&oid));
2278    }
2279
2280    /// Build a commit -> tree -> blob graph in `db`, returning the three object
2281    /// ids and their canonical encodings as `(oid, object)` pairs.
2282    fn write_commit_graph(
2283        db: &mut FileObjectDatabase,
2284        payload: &[u8],
2285    ) -> Vec<(ObjectId, EncodedObject)> {
2286        let blob = EncodedObject::new(ObjectType::Blob, payload.to_vec());
2287        let blob_oid = db
2288            .write_object(blob.clone())
2289            .expect("test operation should succeed");
2290        let tree = EncodedObject::new(
2291            ObjectType::Tree,
2292            Tree {
2293                entries: vec![TreeEntry {
2294                    mode: 0o100644,
2295                    name: BString::from(b"payload.txt"),
2296                    oid: blob_oid,
2297                }],
2298            }
2299            .write(),
2300        );
2301        let tree_oid = db
2302            .write_object(tree.clone())
2303            .expect("test operation should succeed");
2304        let identity = b"Example <example@example.invalid> 0 +0000".to_vec();
2305        let commit = EncodedObject::new(
2306            ObjectType::Commit,
2307            Commit {
2308                tree: tree_oid,
2309                parents: Vec::new(),
2310                author: identity.clone(),
2311                committer: identity,
2312                encoding: None,
2313                message: b"initial\n".to_vec(),
2314            }
2315            .write(),
2316        );
2317        let commit_oid = db
2318            .write_object(commit.clone())
2319            .expect("test operation should succeed");
2320        vec![(commit_oid, commit), (tree_oid, tree), (blob_oid, blob)]
2321    }
2322
2323    fn repack_all_objects_consolidates_loose_and_pack(format: ObjectFormat) {
2324        let root = temp_root("sley-repack-all");
2325        let git_dir = root.join(".git");
2326        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2327        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
2328
2329        // A pre-existing pack holds one blob; the rest of the graph is loose.
2330        let packed_blob = EncodedObject::new(ObjectType::Blob, b"already packed\n".to_vec());
2331        let packed_oid = packed_blob
2332            .object_id(format)
2333            .expect("test operation should succeed");
2334        let existing_pack = PackFile::write_undeltified(std::slice::from_ref(&packed_blob), format)
2335            .expect("test operation should succeed");
2336        let existing = db
2337            .install_pack(&existing_pack)
2338            .expect("test operation should succeed");
2339
2340        let graph = write_commit_graph(&mut db, b"repack payload\n");
2341
2342        let mut expected: HashMap<ObjectId, EncodedObject> = graph.iter().cloned().collect();
2343        expected.insert(packed_oid, packed_blob.clone());
2344
2345        let result = repack_all_objects(&git_dir, format)
2346            .expect("test operation should succeed")
2347            .expect("repository has objects");
2348
2349        // The new pack round-trips and contains every original object byte-for-byte.
2350        assert_eq!(result.object_count, expected.len());
2351        let parsed = PackFile::parse(&result.pack, format).expect("test operation should succeed");
2352        assert_eq!(parsed.entries.len(), expected.len());
2353        for entry in &parsed.entries {
2354            let want = expected
2355                .get(&entry.entry.oid)
2356                .expect("packed object was in the repository");
2357            assert_eq!(&entry.object, want);
2358            assert_eq!(
2359                entry
2360                    .object
2361                    .object_id(format)
2362                    .expect("test operation should succeed"),
2363                entry.entry.oid
2364            );
2365        }
2366        // The generated index parses and agrees with the pack checksum.
2367        let idx = PackIndex::parse(&result.idx, format).expect("test operation should succeed");
2368        assert_eq!(idx.pack_checksum, parsed.checksum);
2369        assert_eq!(idx.entries.len(), expected.len());
2370
2371        // The pre-existing pack is reported obsolete (by its .pack path).
2372        assert_eq!(result.obsolete_packs, vec![existing.pack_path.clone()]);
2373        // Every loose object id is reported as now packed.
2374        let mut want_loose: Vec<ObjectId> = graph.iter().map(|(oid, _)| *oid).collect();
2375        want_loose.sort_by_key(ObjectId::to_hex);
2376        assert_eq!(result.packed_loose, want_loose);
2377        assert!(!result.packed_loose.contains(&packed_oid));
2378
2379        fs::remove_dir_all(root).expect("test operation should succeed");
2380    }
2381
2382    #[test]
2383    fn repack_all_objects_consolidates_loose_and_pack_sha1() {
2384        repack_all_objects_consolidates_loose_and_pack(ObjectFormat::Sha1);
2385    }
2386
2387    #[test]
2388    fn repack_all_objects_consolidates_loose_and_pack_sha256() {
2389        repack_all_objects_consolidates_loose_and_pack(ObjectFormat::Sha256);
2390    }
2391
2392    #[test]
2393    fn repack_all_objects_returns_none_for_empty_repository() {
2394        let root = temp_root("sley-repack-empty");
2395        let git_dir = root.join(".git");
2396        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2397
2398        assert!(
2399            repack_all_objects(&git_dir, ObjectFormat::Sha1)
2400                .expect("test operation should succeed")
2401                .is_none()
2402        );
2403
2404        fs::remove_dir_all(root).expect("test operation should succeed");
2405    }
2406
2407    #[test]
2408    fn install_repack_result_writes_pack_without_pruning_by_default() {
2409        let root = temp_root("sley-repack-install-nodelete");
2410        let git_dir = root.join(".git");
2411        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2412        let format = ObjectFormat::Sha1;
2413        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
2414        let graph = write_commit_graph(&mut db, b"install no prune\n");
2415
2416        let result = repack_all_objects(&git_dir, format)
2417            .expect("test operation should succeed")
2418            .expect("test operation should succeed");
2419        install_repack_result(&git_dir, format, &result, false)
2420            .expect("test operation should succeed");
2421
2422        // New pack is on disk and readable.
2423        let parsed = PackFile::parse(&result.pack, format).expect("test operation should succeed");
2424        let pack_dir = git_dir.join("objects").join("pack");
2425        let pack_path = pack_dir.join(format!("pack-{}.pack", parsed.checksum.to_hex()));
2426        let idx_path = pack_dir.join(format!("pack-{}.idx", parsed.checksum.to_hex()));
2427        assert!(pack_path.exists());
2428        assert!(idx_path.exists());
2429        // Loose objects survive because prune was not requested.
2430        for (oid, object) in &graph {
2431            assert!(
2432                db.loose()
2433                    .object_path(oid)
2434                    .expect("test operation should succeed")
2435                    .exists()
2436            );
2437            assert_eq!(read_object_for_assert(&db, oid), *object);
2438        }
2439
2440        fs::remove_dir_all(root).expect("test operation should succeed");
2441    }
2442
2443    #[test]
2444    fn install_repack_result_prunes_obsolete_packs_and_loose_objects() {
2445        let root = temp_root("sley-repack-install-prune");
2446        let git_dir = root.join(".git");
2447        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2448        let format = ObjectFormat::Sha1;
2449        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
2450
2451        let packed_blob = EncodedObject::new(ObjectType::Blob, b"prune packed\n".to_vec());
2452        let existing_pack = PackFile::write_undeltified(std::slice::from_ref(&packed_blob), format)
2453            .expect("test operation should succeed");
2454        let existing = db
2455            .install_pack(&existing_pack)
2456            .expect("test operation should succeed");
2457        let graph = write_commit_graph(&mut db, b"prune payload\n");
2458
2459        let result = repack_all_objects(&git_dir, format)
2460            .expect("test operation should succeed")
2461            .expect("test operation should succeed");
2462        let new_pack_checksum = PackFile::parse(&result.pack, format)
2463            .expect("test operation should succeed")
2464            .checksum;
2465        install_repack_result(&git_dir, format, &result, true)
2466            .expect("test operation should succeed");
2467
2468        // Obsolete pack and its index are gone.
2469        assert!(!existing.pack_path.exists());
2470        assert!(!existing.index_path.exists());
2471        // Packed loose objects are gone from disk.
2472        for (oid, _) in &graph {
2473            assert!(
2474                !db.loose()
2475                    .object_path(oid)
2476                    .expect("test operation should succeed")
2477                    .exists()
2478            );
2479        }
2480        // The new consolidated pack remains and still serves every object.
2481        let pack_dir = git_dir.join("objects").join("pack");
2482        assert!(
2483            pack_dir
2484                .join(format!("pack-{}.pack", new_pack_checksum.to_hex()))
2485                .exists()
2486        );
2487        let reopened = FileObjectDatabase::from_git_dir(&git_dir, format);
2488        for (oid, object) in &graph {
2489            assert!(
2490                reopened
2491                    .contains(oid)
2492                    .expect("test operation should succeed")
2493            );
2494            assert_eq!(read_object_for_assert(&reopened, oid), *object);
2495        }
2496        let packed_oid = packed_blob
2497            .object_id(format)
2498            .expect("test operation should succeed");
2499        assert_eq!(read_object_for_assert(&reopened, &packed_oid), packed_blob);
2500
2501        fs::remove_dir_all(root).expect("test operation should succeed");
2502    }
2503
2504    #[test]
2505    fn install_repack_result_preserves_keep_and_promisor_packs() {
2506        let root = temp_root("sley-repack-install-keep-promisor");
2507        let git_dir = root.join(".git");
2508        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2509        let format = ObjectFormat::Sha1;
2510        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
2511
2512        let keep_blob = EncodedObject::new(ObjectType::Blob, b"keep protected\n".to_vec());
2513        let keep_pack = PackFile::write_undeltified(std::slice::from_ref(&keep_blob), format)
2514            .expect("test operation should succeed");
2515        let keep_install = db
2516            .install_pack(&keep_pack)
2517            .expect("test operation should succeed");
2518        let keep_sidecar = keep_install.pack_path.with_extension("keep");
2519        fs::write(&keep_sidecar, b"").expect("test operation should succeed");
2520
2521        let promisor_blob = EncodedObject::new(ObjectType::Blob, b"promisor protected\n".to_vec());
2522        let promisor_pack =
2523            PackFile::write_undeltified(std::slice::from_ref(&promisor_blob), format)
2524                .expect("test operation should succeed");
2525        let promisor_install = db
2526            .install_pack_with_options(
2527                &promisor_pack,
2528                RawPackInstallOptions {
2529                    promisor: true,
2530                    ..Default::default()
2531                },
2532            )
2533            .expect("test operation should succeed");
2534        let promisor_sidecar = promisor_install
2535            .promisor_path
2536            .clone()
2537            .expect("promisor sidecar");
2538
2539        let graph = write_commit_graph(&mut db, b"new consolidated payload\n");
2540        let result = repack_all_objects(&git_dir, format)
2541            .expect("test operation should succeed")
2542            .expect("test operation should succeed");
2543        assert!(result.obsolete_packs.contains(&keep_install.pack_path));
2544        assert!(result.obsolete_packs.contains(&promisor_install.pack_path));
2545
2546        install_repack_result(&git_dir, format, &result, true)
2547            .expect("test operation should succeed");
2548
2549        for path in [
2550            &keep_install.pack_path,
2551            &keep_install.index_path,
2552            &keep_sidecar,
2553            &promisor_install.pack_path,
2554            &promisor_install.index_path,
2555            &promisor_sidecar,
2556        ] {
2557            assert!(path.exists(), "{} should be preserved", path.display());
2558        }
2559        for (oid, _) in &graph {
2560            assert!(
2561                !db.loose()
2562                    .object_path(oid)
2563                    .expect("test operation should succeed")
2564                    .exists()
2565            );
2566        }
2567
2568        fs::remove_dir_all(root).expect("test operation should succeed");
2569    }
2570
2571    #[test]
2572    fn install_repack_result_keeps_loose_object_absent_from_new_pack() {
2573        // Safety: a loose object whose id is not in the new pack must survive
2574        // pruning even if the caller lists it in `packed_loose`.
2575        let root = temp_root("sley-repack-install-safety");
2576        let git_dir = root.join(".git");
2577        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2578        let format = ObjectFormat::Sha1;
2579        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
2580        let graph = write_commit_graph(&mut db, b"safety packed\n");
2581
2582        let mut result = repack_all_objects(&git_dir, format)
2583            .expect("test operation should succeed")
2584            .expect("test operation should succeed");
2585
2586        // A loose object that is NOT in the new pack, but mislabeled as packed.
2587        let stray = EncodedObject::new(ObjectType::Blob, b"never packed\n".to_vec());
2588        let stray_oid = db
2589            .write_object(stray.clone())
2590            .expect("test operation should succeed");
2591        assert!(!result.packed_loose.contains(&stray_oid));
2592        result.packed_loose.push(stray_oid);
2593
2594        install_repack_result(&git_dir, format, &result, true)
2595            .expect("test operation should succeed");
2596
2597        // The stray loose object is untouched because it is not in the new pack.
2598        assert!(
2599            db.loose()
2600                .object_path(&stray_oid)
2601                .expect("test operation should succeed")
2602                .exists()
2603        );
2604        assert_eq!(read_object_for_assert(&db, &stray_oid), stray);
2605        // Genuinely packed loose objects were still removed.
2606        for (oid, _) in &graph {
2607            assert!(
2608                !db.loose()
2609                    .object_path(oid)
2610                    .expect("test operation should succeed")
2611                    .exists()
2612            );
2613        }
2614
2615        fs::remove_dir_all(root).expect("test operation should succeed");
2616    }
2617
2618    #[test]
2619    fn prune_unreachable_loose_reports_and_deletes_only_unreachable() {
2620        let root = temp_root("sley-prune-unreachable");
2621        let git_dir = root.join(".git");
2622        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2623        let format = ObjectFormat::Sha1;
2624        let mut db = FileObjectDatabase::from_git_dir(&git_dir, format);
2625        let graph = write_commit_graph(&mut db, b"reachable payload\n");
2626        let commit_oid = graph[0].0.clone();
2627
2628        // A dangling loose blob not referenced by the commit graph.
2629        let dangling = EncodedObject::new(ObjectType::Blob, b"dangling\n".to_vec());
2630        let dangling_oid = db
2631            .write_object(dangling)
2632            .expect("test operation should succeed");
2633
2634        // Report-only pass leaves everything on disk.
2635        let reported = prune_unreachable_loose(&git_dir, format, [commit_oid], false)
2636            .expect("test operation should succeed");
2637        assert_eq!(reported, vec![dangling_oid]);
2638        assert!(
2639            db.loose()
2640                .object_path(&dangling_oid)
2641                .expect("test operation should succeed")
2642                .exists()
2643        );
2644
2645        // Deleting pass removes only the unreachable object.
2646        let deleted = prune_unreachable_loose(&git_dir, format, [commit_oid], true)
2647            .expect("test operation should succeed");
2648        assert_eq!(deleted, vec![dangling_oid]);
2649        assert!(
2650            !db.loose()
2651                .object_path(&dangling_oid)
2652                .expect("test operation should succeed")
2653                .exists()
2654        );
2655        for (oid, object) in &graph {
2656            assert!(
2657                db.loose()
2658                    .object_path(oid)
2659                    .expect("test operation should succeed")
2660                    .exists()
2661            );
2662            assert_eq!(read_object_for_assert(&db, oid), *object);
2663        }
2664
2665        fs::remove_dir_all(root).expect("test operation should succeed");
2666    }
2667
2668    #[test]
2669    fn prune_unreachable_loose_ignores_gitlink_targets() {
2670        let root = temp_root("sley-prune-gitlink");
2671        let git_dir = root.join(".git");
2672        fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
2673        let format = ObjectFormat::Sha1;
2674        let db = FileObjectDatabase::from_git_dir(&git_dir, format);
2675
2676        let submodule_oid = ObjectId::from_hex(format, "1111111111111111111111111111111111111111")
2677            .expect("test operation should succeed");
2678        let tree = EncodedObject::new(
2679            ObjectType::Tree,
2680            Tree {
2681                entries: vec![TreeEntry {
2682                    mode: 0o160000,
2683                    name: BString::from(b"submodule"),
2684                    oid: submodule_oid,
2685                }],
2686            }
2687            .write(),
2688        );
2689        let tree_oid = db
2690            .write_object(tree)
2691            .expect("test operation should succeed");
2692        let identity = b"Example <example@example.invalid> 0 +0000".to_vec();
2693        let commit = EncodedObject::new(
2694            ObjectType::Commit,
2695            Commit {
2696                tree: tree_oid,
2697                parents: Vec::new(),
2698                author: identity.clone(),
2699                committer: identity,
2700                encoding: None,
2701                message: b"gitlink\n".to_vec(),
2702            }
2703            .write(),
2704        );
2705        let commit_oid = db
2706            .write_object(commit)
2707            .expect("test operation should succeed");
2708        let dangling = EncodedObject::new(ObjectType::Blob, b"dangling with gitlink\n".to_vec());
2709        let dangling_oid = db
2710            .write_object(dangling)
2711            .expect("test operation should succeed");
2712
2713        let deleted = prune_unreachable_loose(&git_dir, format, [commit_oid], true)
2714            .expect("test operation should succeed");
2715
2716        assert_eq!(deleted, vec![dangling_oid]);
2717        assert!(
2718            !db.loose()
2719                .object_path(&dangling_oid)
2720                .expect("test operation should succeed")
2721                .exists()
2722        );
2723
2724        fs::remove_dir_all(root).expect("test operation should succeed");
2725    }
2726
2727    fn temp_root(prefix: &str) -> PathBuf {
2728        std::env::temp_dir().join(format!(
2729            "{prefix}-{}-{}",
2730            std::process::id(),
2731            TEMPFILE_COUNTER.fetch_add(1, Ordering::Relaxed)
2732        ))
2733    }
2734}