1#![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::*;
20pub 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 fn is_shallow_graft(&self, _oid: &ObjectId) -> bool {
41 false
42 }
43
44 fn has_shallow_grafts(&self) -> bool {
48 false
49 }
50
51 fn is_promised_object(&self, _oid: &ObjectId) -> bool {
55 false
56 }
57}
58pub trait ObjectWriter {
59 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
76pub 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 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 assert!(cache.get(&1).is_some());
132 cache.put(3, cached_blob_of(b'c', 1000));
133 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 cache.put(1, cached_blob_of(b'b', 10_000));
154 assert!(cache.get(&1).is_none());
155 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 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 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 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 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 let root = temp_root("sley-loose-header-deep-corrupt");
255 let store = LooseObjectStore::new(root.join("objects"), ObjectFormat::Sha1);
256 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(read_object_for_assert(&db, &packed_oid), packed);
475
476 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 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 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 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 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 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 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 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 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 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 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 assert!(matches!(
1918 db.read_object(&second_oid),
1919 Err(GitError::NotFound(_))
1920 ));
1921
1922 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 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 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 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 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 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 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 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 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 assert_eq!(result.obsolete_packs, vec![existing.pack_path.clone()]);
2373 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 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 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 assert!(!existing.pack_path.exists());
2470 assert!(!existing.index_path.exists());
2471 for (oid, _) in &graph {
2473 assert!(
2474 !db.loose()
2475 .object_path(oid)
2476 .expect("test operation should succeed")
2477 .exists()
2478 );
2479 }
2480 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 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 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 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 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 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 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 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}