1pub mod builder;
25mod commit;
26mod peel;
27pub mod store;
28mod stream;
29
30#[doc(inline)]
31pub use builder::OdbBuilder;
32
33pub(crate) use store::loose::{
34 decompress_zlib_loose_bytes, for_each_loose_object_id, parse_object_bytes,
35 read_loose_object_info,
36};
37#[doc(inline)]
38pub use store::LooseStore;
39pub use store::{CompositeStore, FilesSource, ObjectStream};
40#[doc(inline)]
41pub use store::{MemoryStore, ObjectStore, WritableObjectStore};
42
43use crate::git_path::CanonicalizeExt;
44use std::collections::{HashMap, HashSet};
45use std::fs;
46use std::hash::{Hash, Hasher};
47use std::ops::ControlFlow;
48use std::path::{Path, PathBuf};
49#[cfg(test)]
50use std::sync::atomic::AtomicUsize;
51use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
52use std::sync::{Arc, Mutex, OnceLock, RwLock};
53
54use crate::config::ConfigSet;
55use crate::diagnostics::NullDiagnostics;
56use crate::error::{Error, Result};
57use crate::hash;
58use crate::midx::validate_midx_referenced_packs_with_diagnostics;
59use crate::objects::{
60 oid_hash_set, HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind, OidHashMap, OidHashSet,
61};
62use crate::pack;
63use crate::pack_store::PackStore;
64use filetime::{set_file_mtime, FileTime};
65use flate2::Compression;
66
67pub(crate) use store::BoundedRead;
68use store::{LocalSizedRead, PackedObjects};
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub(crate) enum UnreachablePackRetention {
73 KeepAll,
75 KeepGraceProtected(std::time::SystemTime),
77 DropAll,
79}
80
81type MemOdbOverlayStore = Arc<RwLock<Option<Arc<MemoryStore>>>>;
82
83struct AlternateSourcesCache {
85 file_generation: u64,
86 env_fingerprint: u64,
87 submodule_generation: u64,
88 sources: Option<Arc<CompositeStore>>,
89}
90
91struct AlternateSourcesKey {
92 file_generation: u64,
93 env_fingerprint: u64,
94 submodule_generation: u64,
95}
96
97struct FileAlternatesCache {
99 generation: u64,
100 snapshot: Option<Arc<Vec<PathBuf>>>,
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105pub struct WriteOptions {
106 pub silent: bool,
108 pub assume_loose_only_existence: bool,
114 pub trust_new_loose: bool,
117 pub loose_prefix_dirs_ready: bool,
120}
121
122pub const BULK_NEW_LOOSE_WRITE: WriteOptions = WriteOptions {
124 assume_loose_only_existence: true,
125 trust_new_loose: true,
126 silent: false,
127 loose_prefix_dirs_ready: true,
128};
129
130pub const BULK_TRACKED_RESTAGE_LOOSE_WRITE: WriteOptions = WriteOptions {
132 assume_loose_only_existence: true,
133 trust_new_loose: false,
134 silent: false,
135 loose_prefix_dirs_ready: true,
136};
137
138fn exists_materialized_in_objects_dir(objects_dir: &Path, oid: &ObjectId) -> bool {
144 store::local_object_materialized(objects_dir, oid)
145}
146
147fn filter_oids(
149 oids: impl IntoIterator<Item = ObjectId>,
150 mut keep: impl FnMut(&ObjectId) -> Result<bool>,
151) -> Result<Vec<ObjectId>> {
152 oids.into_iter()
153 .filter_map(|oid| keep(&oid).map(|k| k.then_some(oid)).transpose())
154 .collect()
155}
156
157fn objects_dir_has_pack_index_files(objects_dir: &Path) -> bool {
158 let pack_dir = objects_dir.join("pack");
159 let Ok(entries) = fs::read_dir(pack_dir) else {
160 return false;
161 };
162 entries
163 .filter_map(|e| e.ok())
164 .any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
165}
166
167#[derive(Clone)]
187pub struct Odb {
188 objects_dir: PathBuf,
189 work_tree: Option<PathBuf>,
191 submodule_alternate_dirs: Arc<Mutex<Vec<PathBuf>>>,
193 config_git_dir: Option<PathBuf>,
195 core_multi_pack_index_cache: Arc<OnceLock<bool>>,
201 local_pack_indexes_seen: Arc<OnceLock<bool>>,
203 file_alternate_dirs_cache: Arc<RwLock<FileAlternatesCache>>,
205 #[cfg(test)]
206 exists_probe: Arc<AtomicUsize>,
207 #[cfg(test)]
208 pub(crate) hot_path_test_metrics: Arc<crate::hot_path_test_metrics::HotPathTestMetrics>,
209 env_alternate_dirs: Vec<PathBuf>,
211 env_alternate_lazy: Arc<OnceLock<Arc<Vec<PathBuf>>>>,
213 mem_overlay: MemOdbOverlayStore,
218 mem_overlay_active: Arc<AtomicBool>,
220 primary_source: Arc<OnceLock<Arc<FilesSource>>>,
222 alternate_sources_cache: Arc<RwLock<AlternateSourcesCache>>,
224 midx_packs_validated: Arc<OnceLock<()>>,
226 hash_algo_cache: Arc<OnceLock<HashAlgo>>,
230 loose_zlib_cache: Arc<OnceLock<Compression>>,
232 pack_zlib_cache: Arc<OnceLock<Compression>>,
234 big_file_threshold_cache: Arc<OnceLock<u64>>,
236 shared_config_state: Option<crate::repo::RepositoryConfigSnapshot>,
238 delta_base_cache_sync: Arc<OnceLock<()>>,
240 shared_environment: Option<Arc<crate::environment::Environment>>,
242 empty_config_env: Arc<OnceLock<crate::environment::Environment>>,
244 freshened_packs: Arc<Mutex<HashSet<PathBuf>>>,
247 pack_store: Arc<PackStore>,
249 alternate_pack_stores: Arc<Mutex<HashMap<PathBuf, Arc<PackStore>>>>,
251 submodule_sources_generation: Arc<AtomicU64>,
253 custom_primary: Option<Arc<dyn WritableObjectStore>>,
255 extra_read_sources: Vec<Arc<dyn ObjectStore>>,
257 alternates_enabled: bool,
259 session_loose_reads: Arc<std::sync::Mutex<SessionLooseReads>>,
261 session_tree_entries: Arc<Mutex<SessionTreeEntries>>,
263}
264
265const SESSION_BOUNDED_LOOSE_CACHE_MAX: usize = 64 * 1024;
269
270const SESSION_READ_BLOB_CACHE_MAX: usize = 4 * 1024;
274
275const SESSION_LOOSE_CACHE_MAX_ENTRIES: usize = 256;
277
278const SESSION_LOOSE_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
281
282#[derive(Default)]
285struct SessionLooseReads {
286 objects: OidHashMap<Object>,
287 bytes: usize,
288}
289
290impl SessionLooseReads {
291 fn get(&self, oid: &ObjectId) -> Option<&Object> {
292 self.objects.get(oid)
293 }
294
295 fn insert(&mut self, oid: ObjectId, obj: &Object) {
296 let len = obj.data.len();
297 if len > SESSION_LOOSE_CACHE_MAX_BYTES {
298 return;
299 }
300 if self.objects.len() > SESSION_LOOSE_CACHE_MAX_ENTRIES
301 || self.bytes.saturating_add(len) > SESSION_LOOSE_CACHE_MAX_BYTES
302 {
303 self.objects.clear();
304 self.bytes = 0;
305 }
306 if let Some(old) = self.objects.insert(oid, obj.clone()) {
307 self.bytes = self.bytes.saturating_sub(old.data.len());
308 }
309 self.bytes += len;
310 }
311
312 #[cfg(test)]
313 fn is_empty(&self) -> bool {
314 self.objects.is_empty()
315 }
316}
317
318#[derive(Default)]
320struct SessionTreeEntries {
321 trees: OidHashMap<Arc<Vec<crate::objects::TreeEntry>>>,
322 entries: usize,
323}
324
325impl SessionTreeEntries {
326 const ENTRY_BUDGET: usize = 1 << 16;
331
332 fn insert(&mut self, oid: ObjectId, tree: Arc<Vec<crate::objects::TreeEntry>>) {
333 if tree.len() > Self::ENTRY_BUDGET {
334 return;
335 }
336 if self.entries + tree.len() > Self::ENTRY_BUDGET {
337 self.trees.clear();
338 self.entries = 0;
339 }
340 self.entries += tree.len();
341 self.trees.insert(oid, tree);
342 }
343}
344
345#[cfg(test)]
346mod session_tree_entries_tests {
347 use super::*;
348 use crate::objects::TreeEntry;
349
350 fn tree(entries: usize) -> Arc<Vec<TreeEntry>> {
351 let entry = TreeEntry {
352 mode: 0o100644,
353 name: b"f".to_vec(),
354 oid: ObjectId::zero(),
355 };
356 Arc::new(vec![entry; entries])
357 }
358
359 fn oid(n: u8) -> ObjectId {
360 ObjectId::from_bytes(&[n; 20]).expect("oid")
361 }
362
363 #[test]
364 fn cache_starts_over_when_the_entry_budget_is_exceeded() {
365 let mut cache = SessionTreeEntries::default();
366 let half = SessionTreeEntries::ENTRY_BUDGET / 2;
367 cache.insert(oid(1), tree(half));
368 cache.insert(oid(2), tree(half));
369 assert_eq!(cache.entries, SessionTreeEntries::ENTRY_BUDGET);
370 assert_eq!(cache.trees.len(), 2);
371
372 cache.insert(oid(3), tree(1));
373 assert_eq!(cache.trees.len(), 1);
374 assert!(cache.trees.contains_key(&oid(3)));
375 assert_eq!(cache.entries, 1);
376
377 cache.insert(oid(4), tree(SessionTreeEntries::ENTRY_BUDGET + 1));
378 assert!(
379 !cache.trees.contains_key(&oid(4)),
380 "an oversized tree is not cached"
381 );
382 assert!(cache.trees.contains_key(&oid(3)));
383 assert!(cache.entries <= SessionTreeEntries::ENTRY_BUDGET);
384 }
385}
386
387const _: () = {
388 fn assert_send_sync<T: Send + Sync>() {}
389 let _ = assert_send_sync::<Odb>;
390};
391
392impl std::fmt::Debug for Odb {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.debug_struct("Odb")
395 .field("objects_dir", &self.objects_dir)
396 .field("work_tree", &self.work_tree)
397 .field("submodule_alternate_dirs", &"<mutex>")
398 .field("config_git_dir", &self.config_git_dir)
399 .finish()
400 }
401}
402
403#[must_use]
408pub fn hash_algo_for_git_dir(env: &crate::environment::Environment, git_dir: &Path) -> HashAlgo {
409 ConfigSet::load(env, Some(git_dir), true)
410 .ok()
411 .and_then(|cfg| cfg.get("extensions.objectformat"))
412 .and_then(|v| HashAlgo::from_name(&v))
413 .unwrap_or(HashAlgo::Sha1)
414}
415
416#[must_use]
418pub fn hash_algo_for_objects_dir(
419 env: &crate::environment::Environment,
420 objects_dir: &Path,
421) -> HashAlgo {
422 objects_dir
423 .parent()
424 .map(|d| hash_algo_for_git_dir(env, d))
425 .unwrap_or(HashAlgo::Sha1)
426}
427
428impl Odb {
429 #[must_use]
436 pub fn new(objects_dir: &Path) -> Self {
437 Self {
438 objects_dir: objects_dir.to_path_buf(),
439 work_tree: None,
440 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
441 config_git_dir: None,
442 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
443 local_pack_indexes_seen: Arc::new(OnceLock::new()),
444 file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
445 generation: 0,
446 snapshot: None,
447 })),
448 env_alternate_dirs: Vec::new(),
449 env_alternate_lazy: Arc::new(OnceLock::new()),
450 mem_overlay: Arc::new(RwLock::new(None)),
451 mem_overlay_active: Arc::new(AtomicBool::new(false)),
452 primary_source: Arc::new(OnceLock::new()),
453 alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
454 file_generation: 0,
455 env_fingerprint: 0,
456 submodule_generation: 0,
457 sources: None,
458 })),
459 midx_packs_validated: Arc::new(OnceLock::new()),
460 hash_algo_cache: Arc::new(OnceLock::new()),
461 loose_zlib_cache: Arc::new(OnceLock::new()),
462 pack_zlib_cache: Arc::new(OnceLock::new()),
463 big_file_threshold_cache: Arc::new(OnceLock::new()),
464 shared_config_state: None,
465 delta_base_cache_sync: Arc::new(OnceLock::new()),
466 shared_environment: None,
467 empty_config_env: Arc::new(OnceLock::new()),
468 freshened_packs: Arc::new(Mutex::new(HashSet::new())),
469 pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
470 alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
471 submodule_sources_generation: Arc::new(AtomicU64::new(0)),
472 custom_primary: None,
473 extra_read_sources: Vec::new(),
474 alternates_enabled: true,
475 session_loose_reads: Arc::new(Mutex::new(SessionLooseReads::default())),
476 session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
477 #[cfg(test)]
478 exists_probe: Arc::new(AtomicUsize::new(0)),
479 #[cfg(test)]
480 hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
481 }
482 }
483
484 #[must_use]
489 pub fn with_work_tree(objects_dir: &Path, work_tree: &Path) -> Self {
490 Self {
491 objects_dir: objects_dir.to_path_buf(),
492 work_tree: Some(work_tree.to_path_buf()),
493 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
494 config_git_dir: None,
495 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
496 local_pack_indexes_seen: Arc::new(OnceLock::new()),
497 file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
498 generation: 0,
499 snapshot: None,
500 })),
501 env_alternate_dirs: Vec::new(),
502 env_alternate_lazy: Arc::new(OnceLock::new()),
503 mem_overlay: Arc::new(RwLock::new(None)),
504 mem_overlay_active: Arc::new(AtomicBool::new(false)),
505 primary_source: Arc::new(OnceLock::new()),
506 alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
507 file_generation: 0,
508 env_fingerprint: 0,
509 submodule_generation: 0,
510 sources: None,
511 })),
512 midx_packs_validated: Arc::new(OnceLock::new()),
513 hash_algo_cache: Arc::new(OnceLock::new()),
514 loose_zlib_cache: Arc::new(OnceLock::new()),
515 pack_zlib_cache: Arc::new(OnceLock::new()),
516 big_file_threshold_cache: Arc::new(OnceLock::new()),
517 shared_config_state: None,
518 delta_base_cache_sync: Arc::new(OnceLock::new()),
519 shared_environment: None,
520 empty_config_env: Arc::new(OnceLock::new()),
521 freshened_packs: Arc::new(Mutex::new(HashSet::new())),
522 pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
523 alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
524 submodule_sources_generation: Arc::new(AtomicU64::new(0)),
525 custom_primary: None,
526 extra_read_sources: Vec::new(),
527 alternates_enabled: true,
528 session_loose_reads: Arc::new(Mutex::new(SessionLooseReads::default())),
529 session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
530 #[cfg(test)]
531 exists_probe: Arc::new(AtomicUsize::new(0)),
532 #[cfg(test)]
533 hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
534 }
535 }
536
537 pub(crate) fn cached_tree_entries(
539 &self,
540 oid: &ObjectId,
541 ) -> Result<Arc<Vec<crate::objects::TreeEntry>>> {
542 if let Ok(guard) = self.session_tree_entries.lock() {
543 if let Some(entries) = guard.trees.get(oid) {
544 return Ok(Arc::clone(entries));
545 }
546 }
547 let obj = self.read_impl(oid, false)?;
548 if obj.kind != ObjectKind::Tree {
549 return Err(Error::CorruptObject(format!(
550 "expected tree, got {}",
551 obj.kind.as_str()
552 )));
553 }
554 let parsed = crate::objects::parse_tree_with_oid_len(&obj.data, self.hash_algo().len())?;
555 let arc = Arc::new(parsed);
556 if let Ok(mut guard) = self.session_tree_entries.lock() {
557 guard.insert(*oid, Arc::clone(&arc));
558 }
559 Ok(arc)
560 }
561
562 fn session_cached_loose_read(&self, oid: &ObjectId) -> Option<Object> {
563 self.session_loose_reads.lock().ok()?.get(oid).cloned()
564 }
565
566 fn remember_session_loose_read(&self, oid: &ObjectId, obj: &Object) {
569 if let Ok(mut guard) = self.session_loose_reads.lock() {
570 guard.insert(*oid, obj);
571 }
572 }
573
574 fn finish_session_loose_read(
582 &self,
583 oid: &ObjectId,
584 obj: Object,
585 remember: bool,
586 ) -> Result<Object> {
587 if remember
588 && (obj.kind != ObjectKind::Blob || obj.data.len() <= SESSION_READ_BLOB_CACHE_MAX)
589 {
590 self.remember_session_loose_read(oid, &obj);
591 }
592 Ok(obj)
593 }
594
595 #[cfg(test)]
597 #[must_use]
598 pub(crate) fn hot_path_metrics(&self) -> &crate::hot_path_test_metrics::HotPathTestMetrics {
599 &self.hot_path_test_metrics
600 }
601
602 #[cfg(test)]
603 #[must_use]
604 pub(crate) fn hot_path_metrics_arc(
605 &self,
606 ) -> Arc<crate::hot_path_test_metrics::HotPathTestMetrics> {
607 Arc::clone(&self.hot_path_test_metrics)
608 }
609
610 #[must_use]
612 pub(crate) fn with_shared_config_state(
613 mut self,
614 state: crate::repo::RepositoryConfigSnapshot,
615 env: Arc<crate::environment::Environment>,
616 ) -> Self {
617 self.pack_store.set_repo_caches(Arc::clone(&state));
618 self.shared_config_state = Some(state);
619 self.shared_environment = Some(env);
620 self
621 }
622
623 fn diagnostics_handle(&self) -> crate::diagnostics::DiagnosticsHandle {
624 self.shared_config_state
625 .as_ref()
626 .map(|state| state.diagnostics_handle())
627 .unwrap_or_else(|| Arc::new(NullDiagnostics))
628 }
629
630 fn load_config_cascade(&self) -> Result<ConfigSet> {
631 if let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment) {
632 let git_dir = self
633 .config_git_dir
634 .as_deref()
635 .or_else(|| self.objects_dir.parent());
636 let cfg = caches.load_config(env.as_ref(), git_dir, true)?;
637 return Ok(cfg.as_ref().clone());
638 }
639 let git_dir = self
640 .config_git_dir
641 .as_deref()
642 .or_else(|| self.objects_dir.parent());
643 if let Some(git_dir) = git_dir {
644 ConfigSet::load(self.config_environment(), Some(git_dir), true)
645 } else {
646 Ok(ConfigSet::new())
647 }
648 }
649
650 fn config_environment(&self) -> &crate::environment::Environment {
651 if let Some(env) = self.shared_environment.as_deref() {
652 return env;
653 }
654 self.empty_config_env
655 .get_or_init(crate::environment::Environment::empty)
656 }
657
658 fn env_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
659 if !self.env_alternate_dirs.is_empty() {
660 return Arc::new(self.env_alternate_dirs.clone());
661 }
662 Arc::clone(self.env_alternate_lazy.get_or_init(|| {
663 Arc::new(Self::env_alternate_dirs_from_var(
664 self.config_environment(),
665 self.work_tree.as_deref(),
666 ))
667 }))
668 }
669
670 #[must_use]
674 pub fn env_alternate_dirs_from_var(
675 env: &crate::environment::Environment,
676 resolve_base: Option<&Path>,
677 ) -> Vec<PathBuf> {
678 match env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES") {
679 Some(val) if !val.is_empty() => {
680 let mut dirs = parse_alternate_env(&val);
681 if let Some(base) = resolve_base {
682 for dir in &mut dirs {
683 if dir.is_relative() {
684 *dir = base.join(&dir);
685 }
686 }
687 }
688 dirs
689 }
690 _ => Vec::new(),
691 }
692 }
693
694 #[must_use]
696 pub fn with_env_alternate_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
697 self.env_alternate_dirs = dirs;
698 self.clear_alternate_sources_cache();
699 self
700 }
701
702 fn clear_alternate_sources_cache(&self) {
703 if let Ok(mut guard) = self.alternate_sources_cache.write() {
704 guard.sources = None;
705 }
706 }
707
708 fn alternate_sources_key(&self) -> AlternateSourcesKey {
709 AlternateSourcesKey {
710 file_generation: self
711 .file_alternate_dirs_cache
712 .read()
713 .map(|g| g.generation)
714 .unwrap_or(0),
715 env_fingerprint: self.env_alternate_fingerprint(),
716 submodule_generation: self.submodule_sources_generation.load(Ordering::Relaxed),
717 }
718 }
719
720 fn env_alternate_fingerprint(&self) -> u64 {
721 let mut hasher = std::collections::hash_map::DefaultHasher::new();
722 for path in self.env_alternate_dirs_snapshot().iter() {
723 path.hash(&mut hasher);
724 }
725 hasher.finish()
726 }
727
728 pub fn invalidate_alternates_cache(&self) {
730 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
731 guard.generation = guard.generation.wrapping_add(1);
732 guard.snapshot = None;
733 }
734 self.clear_alternate_sources_cache();
735 }
736
737 pub fn refresh_file_alternates_from_disk(&self) -> Result<()> {
743 let snapshot = Arc::new(pack::read_alternates_recursive(&self.objects_dir)?);
744 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
745 guard.generation = guard.generation.wrapping_add(1);
746 guard.snapshot = Some(snapshot);
747 }
748 self.clear_alternate_sources_cache();
749 Ok(())
750 }
751
752 pub fn append_file_alternate(&self, alternate_objects_dir: &Path) -> Result<()> {
758 Self::append_alternate_objects_line(&self.objects_dir, alternate_objects_dir)?;
759 self.refresh_file_alternates_from_disk()?;
760 Ok(())
761 }
762
763 pub fn append_alternate_objects_line(
768 objects_dir: &Path,
769 alternate_objects_dir: &Path,
770 ) -> Result<()> {
771 let info = objects_dir.join("info");
772 fs::create_dir_all(&info)?;
773 let alt_path = info.join("alternates");
774 let alt_abs = alternate_objects_dir
775 .canonicalize_simplified()
776 .unwrap_or_else(|_| alternate_objects_dir.to_path_buf());
777 let line = format!("{}\n", alt_abs.display());
778 let existing = match fs::read_to_string(&alt_path) {
779 Ok(s) => s,
780 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
781 Err(e) => return Err(Error::Io(e)),
782 };
783 if existing
784 .lines()
785 .any(|l| l.trim() == alt_abs.to_string_lossy())
786 {
787 return Ok(());
788 }
789 let mut out = existing;
790 out.push_str(&line);
791 fs::write(alt_path, out).map_err(Error::Io)
792 }
793
794 fn file_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
795 loop {
796 let observed_gen = self
797 .file_alternate_dirs_cache
798 .read()
799 .map(|g| g.generation)
800 .unwrap_or(0);
801 if let Ok(guard) = self.file_alternate_dirs_cache.read() {
802 if guard.generation == observed_gen {
803 if let Some(snapshot) = &guard.snapshot {
804 return Arc::clone(snapshot);
805 }
806 }
807 }
808 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
809 if guard.generation != observed_gen {
810 continue;
811 }
812 if let Some(snapshot) = &guard.snapshot {
813 return Arc::clone(snapshot);
814 }
815 let snapshot = Arc::new(
816 pack::read_alternates_recursive(&self.objects_dir).unwrap_or_default(),
817 );
818 guard.snapshot = Some(Arc::clone(&snapshot));
819 return snapshot;
820 }
821 break;
822 }
823 Arc::new(Vec::new())
824 }
825
826 #[cfg(test)]
827 pub(crate) fn exists_probe_count(&self) -> usize {
828 self.exists_probe.load(Ordering::Relaxed)
829 }
830
831 #[cfg(test)]
832 pub(crate) fn reset_exists_probe(&self) {
833 self.exists_probe.store(0, Ordering::Relaxed);
834 }
835
836 pub fn enable_mem_overlay(&self) {
841 self.mem_overlay_active.store(true, Ordering::Release);
842 if let Ok(mut guard) = self.mem_overlay.write() {
843 if guard.is_none() {
844 *guard = Some(Arc::new(MemoryStore::new(self.hash_algo())));
845 }
846 }
847 }
848
849 pub fn disable_mem_overlay(&self) {
851 self.mem_overlay_active.store(false, Ordering::Release);
852 if let Ok(mut guard) = self.mem_overlay.write() {
853 *guard = None;
854 }
855 }
856
857 #[must_use]
859 pub(crate) fn is_mem_overlay_active(&self) -> bool {
860 self.mem_overlay_active.load(Ordering::Acquire)
861 }
862
863 fn overlay_active(&self) -> bool {
864 self.mem_overlay
865 .read()
866 .ok()
867 .and_then(|g| g.as_ref().map(|_| ()))
868 .is_some()
869 }
870
871 fn overlay_store(&self) -> Option<Arc<MemoryStore>> {
872 if !self.mem_overlay_active.load(Ordering::Acquire) {
873 return None;
874 }
875 self.mem_overlay.read().ok().and_then(|g| g.clone())
876 }
877
878 #[cfg(test)]
880 pub(crate) fn mem_overlay_len_for_tests(&self) -> Option<usize> {
881 let store = self.overlay_store()?;
882 let mut count = 0usize;
883 store
884 .for_each_object(&mut |_| {
885 count += 1;
886 ControlFlow::Continue(())
887 })
888 .ok()?;
889 Some(count)
890 }
891
892 fn overlay_store_object(&self, oid: ObjectId, kind: ObjectKind, data: &[u8]) -> bool {
895 let Some(store) = self.overlay_store() else {
896 return false;
897 };
898 store.insert_object(oid, kind, Arc::from(data.to_owned()));
899 true
900 }
901
902 fn overlay_read(&self, oid: &ObjectId) -> Option<Object> {
904 let store = self.overlay_store()?;
905 store.read(oid).ok().flatten()
906 }
907
908 pub fn primary(&self) -> Result<Arc<FilesSource>> {
914 self.require_files_primary("primary")?;
915 if let Some(src) = self.primary_source.get() {
916 return Ok(Arc::clone(src));
917 }
918 let src = self.build_files_source(
919 &self.objects_dir,
920 self.multi_pack_index_reads_enabled(),
921 self.midx_tip_listing_enabled(),
922 )?;
923 match self.primary_source.set(Arc::clone(&src)) {
924 Ok(()) => Ok(src),
925 Err(existing) => Ok(existing),
926 }
927 }
928
929 #[must_use]
931 fn alternate_object_lookup_needed(&self) -> bool {
932 if !self.file_alternate_dirs_snapshot().is_empty() {
933 return true;
934 }
935 if !self.env_alternate_dirs_snapshot().is_empty() {
936 return true;
937 }
938 self.submodule_alternate_dirs
939 .lock()
940 .ok()
941 .is_some_and(|guard| !guard.is_empty())
942 }
943
944 pub fn sources(&self) -> Result<Arc<CompositeStore>> {
950 if !self.alternates_enabled {
951 return Ok(Arc::new(CompositeStore::new(Vec::new(), self.hash_algo())));
952 }
953 let key = self.alternate_sources_key();
954 if let Ok(guard) = self.alternate_sources_cache.read() {
955 if guard.file_generation == key.file_generation
956 && guard.env_fingerprint == key.env_fingerprint
957 && guard.submodule_generation == key.submodule_generation
958 {
959 if let Some(sources) = guard.sources.as_ref() {
960 return Ok(Arc::clone(sources));
961 }
962 }
963 }
964 let use_midx = self.multi_pack_index_reads_enabled();
965 let midx_tip = self.midx_tip_listing_enabled();
966 let mut stores: Vec<Arc<dyn ObjectStore>> = Vec::new();
967 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
968 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
969 }
970 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
971 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
972 }
973 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
974 for alt_dir in guard.iter() {
975 stores.push(self.build_files_source(alt_dir, false, false)?);
976 }
977 }
978 let composite = Arc::new(CompositeStore::new(stores, self.hash_algo()));
979 if let Ok(mut guard) = self.alternate_sources_cache.write() {
980 guard.file_generation = key.file_generation;
981 guard.env_fingerprint = key.env_fingerprint;
982 guard.submodule_generation = key.submodule_generation;
983 guard.sources = Some(Arc::clone(&composite));
984 }
985 Ok(composite)
986 }
987
988 fn midx_tip_listing_enabled(&self) -> bool {
989 self.config_git_dir.is_some() && self.core_multi_pack_index_enabled()
990 }
991
992 fn build_files_source(
993 &self,
994 objects_dir: &Path,
995 use_midx: bool,
996 midx_tip_listing_for_exists: bool,
997 ) -> Result<Arc<FilesSource>> {
998 let loose = self.loose_at(objects_dir)?;
999 let pack_store = self.pack_store_for(objects_dir);
1000 let diagnostics = if objects_dir == self.objects_dir.as_path() {
1001 self.diagnostics_handle()
1002 } else {
1003 Arc::new(NullDiagnostics)
1004 };
1005 FilesSource::open(
1006 pack_store,
1007 loose,
1008 use_midx,
1009 midx_tip_listing_for_exists,
1010 diagnostics,
1011 )
1012 .map(Arc::new)
1013 }
1014
1015 pub fn register_submodule_object_directories_from_index(
1019 &self,
1020 work_tree: &Path,
1021 index: &crate::index::Index,
1022 ) {
1023 use crate::diff::submodule_embedded_git_dir;
1024
1025 let Ok(mut dirs) = self.submodule_alternate_dirs.lock() else {
1026 return;
1027 };
1028 dirs.clear();
1029 for e in &index.entries {
1030 if e.stage() != 0 || e.mode != crate::index::MODE_GITLINK {
1031 continue;
1032 }
1033 let path_str = String::from_utf8_lossy(&e.path);
1034 let abs = work_tree.join(path_str.as_ref());
1035 let Some(sub_git) = submodule_embedded_git_dir(&abs) else {
1036 continue;
1037 };
1038 let objects = sub_git.join("objects");
1039 if !objects.is_dir() {
1040 continue;
1041 }
1042 let canon = objects.canonicalize_simplified().unwrap_or(objects);
1043 if !dirs.iter().any(|p| p == &canon) {
1044 dirs.push(canon);
1045 }
1046 }
1047 self.submodule_sources_generation
1048 .fetch_add(1, Ordering::Relaxed);
1049 self.clear_alternate_sources_cache();
1050 }
1051
1052 #[must_use]
1054 pub fn with_config_git_dir(mut self, git_dir: PathBuf) -> Self {
1055 self.config_git_dir = Some(git_dir);
1056 self
1057 }
1058
1059 #[must_use]
1071 pub fn hash_algo(&self) -> HashAlgo {
1072 if let Some(primary) = &self.custom_primary {
1073 return primary.hash_algo();
1074 }
1075 *self.hash_algo_cache.get_or_init(|| {
1076 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1077 return HashAlgo::Sha1;
1078 }
1079 let cfg = self.load_config_cascade().unwrap_or_default();
1080 cfg.get("extensions.objectformat")
1081 .and_then(|v| HashAlgo::from_name(&v))
1082 .unwrap_or(HashAlgo::Sha1)
1083 })
1084 }
1085
1086 fn resolve_loose_zlib_level(&self) -> Result<u32> {
1087 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1088 return Ok(ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT as u32);
1089 }
1090 let cfg = self.load_config_cascade()?;
1091 Ok(cfg.loose_objects_zlib_level()? as u32)
1092 }
1093
1094 pub(crate) fn loose_compression(&self) -> Result<Compression> {
1101 if let Some(cached) = self.loose_zlib_cache.get() {
1102 return Ok(*cached);
1103 }
1104 let level = self.resolve_loose_zlib_level()?;
1105 let compression = Compression::new(level);
1106 let _ = self.loose_zlib_cache.set(compression);
1107 Ok(compression)
1108 }
1109
1110 fn resolve_pack_zlib_level(&self) -> Result<i32> {
1111 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1112 return Ok(6);
1113 }
1114 let cfg = self.load_config_cascade()?;
1115 cfg.pack_objects_zlib_level()
1116 }
1117
1118 pub(crate) fn pack_compression(&self) -> Result<Compression> {
1124 if let Some(cached) = self.pack_zlib_cache.get() {
1125 return Ok(*cached);
1126 }
1127 let level = self.resolve_pack_zlib_level()?;
1128 let compression = crate::pack_zlib::flate2_compression_for_pack_level(level);
1129 let _ = self.pack_zlib_cache.set(compression);
1130 Ok(compression)
1131 }
1132
1133 pub fn big_file_threshold(&self) -> Result<u64> {
1144 if let Some(cached) = self.big_file_threshold_cache.get() {
1145 return Ok(*cached);
1146 }
1147 let threshold = if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1148 ConfigSet::BIG_FILE_THRESHOLD_DEFAULT
1149 } else {
1150 self.load_config_cascade()?.big_file_threshold()?
1151 };
1152 let _ = self.big_file_threshold_cache.set(threshold);
1153 Ok(threshold)
1154 }
1155
1156 pub(crate) fn with_bounded_pack_cache(&self) -> Self {
1160 let mut odb = self.clone();
1161 odb.pack_store = Arc::new(PackStore::for_bounded_reads(self.objects_dir.clone()));
1162 odb.primary_source = Arc::new(OnceLock::new());
1163 odb.session_loose_reads = Arc::new(Mutex::new(SessionLooseReads::default()));
1164 odb.alternate_pack_stores = Arc::new(Mutex::new(HashMap::new()));
1165 odb.alternate_sources_cache = Arc::new(RwLock::new(AlternateSourcesCache {
1166 file_generation: 0,
1167 env_fingerprint: 0,
1168 submodule_generation: 0,
1169 sources: None,
1170 }));
1171 odb
1172 }
1173
1174 fn sync_delta_base_cache_limit(&self) {
1175 if self.pack_store.bounded_reads() {
1176 return;
1177 }
1178 self.delta_base_cache_sync.get_or_init(|| {
1179 if self.config_git_dir.is_some() {
1180 let cfg = self.load_config_cascade().unwrap_or_default();
1181 self.pack_store.configure_delta_base_from_config(Some(&cfg));
1182 }
1183 });
1184 }
1185
1186 pub(crate) fn pack_store_for(&self, objects_dir: &Path) -> Arc<PackStore> {
1187 if objects_dir == self.objects_dir.as_path() {
1188 return Arc::clone(&self.pack_store);
1189 }
1190 let new_store = || {
1191 Arc::new(if self.pack_store.bounded_reads() {
1192 PackStore::for_bounded_reads(objects_dir.to_path_buf())
1193 } else {
1194 PackStore::new(objects_dir.to_path_buf())
1195 })
1196 };
1197 if let Ok(mut guard) = self.alternate_pack_stores.lock() {
1198 return Arc::clone(
1199 guard
1200 .entry(objects_dir.to_path_buf())
1201 .or_insert_with(new_store),
1202 );
1203 }
1204 new_store()
1205 }
1206
1207 pub fn pack_store(&self) -> Result<&Arc<PackStore>> {
1209 self.require_files_primary("pack_store")?;
1210 Ok(&self.pack_store)
1211 }
1212
1213 pub fn largest_pack_index_path(&self) -> Result<Option<PathBuf>> {
1222 self.require_files_primary("largest_pack_index_path")?;
1223 let mut indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
1224 indexes.sort_by_key(|idx| std::cmp::Reverse(idx.len()));
1225 Ok(indexes.first().map(|idx| idx.idx_path.clone()))
1226 }
1227
1228 pub(crate) fn clear_delta_base_cache(&self) {
1232 self.pack_store.delta_bases().clear();
1233 }
1234
1235 pub fn invalidate_packs(&self) {
1242 self.pack_store.invalidate_all();
1243 PackStore::invalidate_legacy_for_objects_dir(&self.objects_dir);
1244 if let Ok(guard) = self.alternate_pack_stores.lock() {
1245 for store in guard.values() {
1246 store.invalidate_all();
1247 PackStore::invalidate_legacy_for_objects_dir(store.objects_dir());
1248 }
1249 }
1250 self.invalidate_change_id_header_probe();
1251 }
1252
1253 fn invalidate_change_id_header_probe(&self) {
1254 if let Some(caches) = self.shared_config_state.as_ref() {
1255 crate::change_id::invalidate_stored_change_id_header_probe(caches, self.objects_dir());
1256 }
1257 }
1258
1259 fn note_commit_object_written(&self, kind: ObjectKind) {
1260 if kind == ObjectKind::Commit {
1261 self.invalidate_change_id_header_probe();
1262 }
1263 }
1264
1265 pub(crate) fn loosen_unreachable_from_superseded_packs(
1279 &self,
1280 pack_dir: &Path,
1281 keep_pack: &Path,
1282 reachable: &crate::gc::LazyReachable<'_>,
1283 retention: UnreachablePackRetention,
1284 retained_packs: &HashSet<PathBuf>,
1285 ) -> Result<usize> {
1286 if matches!(retention, UnreachablePackRetention::DropAll) {
1287 return Ok(0);
1288 }
1289 self.require_files_primary("repack")?;
1290 let keep_idx = keep_pack.with_extension("idx");
1291 let keep_pack_oids = if keep_idx.is_file() {
1292 PackedObjects::object_ids_from_index_path(&keep_idx)?
1293 } else {
1294 HashSet::new()
1295 };
1296 let kept_pack_oids = oids_in_marker_kept_packs(pack_dir, keep_pack)?;
1297 let mut loosened = 0usize;
1298 for (idx_path, pack_mtime) in superseded_pack_indexes(pack_dir, keep_pack, retained_packs)?
1299 {
1300 let oids = PackedObjects::object_ids_from_index_path(&idx_path)?;
1301 for oid in oids {
1302 let keep = match retention {
1303 UnreachablePackRetention::KeepAll => true,
1304 UnreachablePackRetention::KeepGraceProtected(cutoff) => pack_mtime >= cutoff,
1305 UnreachablePackRetention::DropAll => false,
1306 };
1307 if !keep {
1308 continue;
1309 }
1310 if keep_pack_oids.contains(&oid) || kept_pack_oids.contains(&oid) {
1311 continue;
1312 }
1313 if reachable.get()?.contains(&oid) {
1314 continue;
1315 }
1316 let loose_path = oid.loose_path_in(&self.objects_dir);
1317 let pack_ft = system_time_to_file_time(pack_mtime);
1318 match retention {
1319 UnreachablePackRetention::KeepGraceProtected(cutoff) => {
1320 if loose_path.is_file() {
1321 let loose_mtime = fs::metadata(&loose_path)
1322 .and_then(|m| m.modified())
1323 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1324 if loose_mtime >= cutoff {
1325 continue;
1326 }
1327 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1328 loosened += 1;
1329 continue;
1330 }
1331 }
1332 UnreachablePackRetention::KeepAll => {
1333 if loose_path.is_file() {
1334 continue;
1335 }
1336 }
1337 UnreachablePackRetention::DropAll => {}
1338 }
1339 let obj = self.read(&oid)?;
1340 self.write_loose_materialize(obj.kind, &obj.data)?;
1341 let primary = self.primary()?;
1342 let loose_path = primary.loose_store().object_path(&oid);
1343 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1344 loosened += 1;
1345 }
1346 }
1347 Ok(loosened)
1348 }
1349
1350 fn core_multi_pack_index_enabled(&self) -> bool {
1351 *self.core_multi_pack_index_cache.get_or_init(|| {
1355 if self.config_git_dir.is_none() {
1356 return false;
1357 }
1358 let cfg = self.load_config_cascade().unwrap_or_default();
1359 match cfg.get_bool("core.multiPackIndex") {
1360 Some(Ok(b)) => b,
1361 Some(Err(_)) => true,
1362 None => true,
1363 }
1364 })
1365 }
1366
1367 fn multi_pack_index_reads_enabled(&self) -> bool {
1368 if self.custom_primary.is_some() {
1369 return false;
1370 }
1371 if self.config_git_dir.is_none() {
1372 return false;
1373 }
1374 if !*self
1375 .local_pack_indexes_seen
1376 .get_or_init(|| objects_dir_has_pack_index_files(&self.objects_dir))
1377 {
1378 return false;
1379 }
1380 self.core_multi_pack_index_enabled()
1381 }
1382
1383 fn ensure_midx_prepared(&self) {
1384 if !self.multi_pack_index_reads_enabled() {
1385 return;
1386 }
1387 let _ = self.midx_packs_validated.get_or_init(|| {
1388 let diagnostics = self.diagnostics_handle();
1389 self.with_pack_store_for(&self.objects_dir, || {
1390 validate_midx_referenced_packs_with_diagnostics(
1391 &self.objects_dir,
1392 diagnostics.as_ref(),
1393 );
1394 });
1395 });
1396 }
1397
1398 fn try_read_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<Object>> {
1403 if self.custom_primary.is_some()
1404 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1405 || self.multi_pack_index_reads_enabled()
1406 {
1407 return Ok(None);
1408 }
1409 self.sync_delta_base_cache_limit();
1410 match pack::try_read_object_from_packs_with_options(
1411 &self.objects_dir,
1412 oid,
1413 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1414 ) {
1415 Ok(obj) => Ok(Some(obj)),
1416 Err(Error::ObjectNotFound(_)) => Ok(None),
1417 Err(err) => Err(err),
1418 }
1419 }
1420
1421 fn try_read_info_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1422 if self.custom_primary.is_some()
1423 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1424 || self.multi_pack_index_reads_enabled()
1425 {
1426 return Ok(None);
1427 }
1428 match pack::try_read_object_info_from_packs_with_options(
1429 &self.objects_dir,
1430 oid,
1431 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1432 ) {
1433 Ok(info) => Ok(Some(info)),
1434 Err(Error::ObjectNotFound(_)) => Ok(None),
1435 Err(err) => Err(err),
1436 }
1437 }
1438
1439 #[must_use]
1446 pub fn objects_dir(&self) -> &Path {
1447 &self.objects_dir
1448 }
1449
1450 pub(crate) fn commit_graph_chain(
1455 &self,
1456 ) -> Option<Arc<crate::commit_graph_file::CommitGraphChain>> {
1457 self.shared_config_state
1458 .as_ref()?
1459 .commit_graph_chain(self.objects_dir())
1460 }
1461
1462 fn try_read_extra_sources(&self, oid: &ObjectId) -> Result<Option<Object>> {
1463 for src in &self.extra_read_sources {
1464 if let Some(obj) = src.read(oid)? {
1465 return Ok(Some(obj));
1466 }
1467 }
1468 Ok(None)
1469 }
1470
1471 fn try_read_info_extra_sources(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1472 for src in &self.extra_read_sources {
1473 if let Some(info) = src.read_info(oid)? {
1474 return Ok(Some(info));
1475 }
1476 }
1477 Ok(None)
1478 }
1479
1480 fn try_open_stream_extra_sources(
1481 &self,
1482 oid: &ObjectId,
1483 ) -> Result<Option<store::ObjectStream<'_>>> {
1484 for src in &self.extra_read_sources {
1485 if let Some(stream) = src.open_stream(oid)? {
1486 return Ok(Some(stream));
1487 }
1488 }
1489 Ok(None)
1490 }
1491
1492 #[must_use]
1498 pub fn config_git_dir(&self) -> Option<&Path> {
1499 self.config_git_dir.as_deref()
1500 }
1501
1502 #[must_use]
1504 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
1505 oid.loose_path_in(&self.objects_dir)
1506 }
1507
1508 pub(crate) fn local_pack_indexes(&self) -> Result<Vec<Arc<pack::PackIndex>>> {
1515 pack::read_local_pack_indexes_cached(&self.objects_dir)
1516 }
1517
1518 pub(crate) fn local_loose_file_size(&self, oid: &ObjectId) -> Option<u64> {
1521 fs::metadata(self.object_path(oid))
1522 .ok()
1523 .filter(fs::Metadata::is_file)
1524 .map(|meta| meta.len())
1525 }
1526
1527 #[must_use]
1543 pub fn exists_local(&self, oid: &ObjectId) -> bool {
1544 self.try_exists_local(oid).unwrap_or(false)
1545 }
1546
1547 pub(crate) fn try_exists_local(&self, oid: &ObjectId) -> Result<bool> {
1549 if self.is_empty_tree(oid) {
1550 return Ok(true);
1551 }
1552 if let Some(primary) = &self.custom_primary {
1553 return primary.contains(oid);
1554 }
1555 Ok(self.with_pack_store_for(&self.objects_dir, || {
1556 exists_materialized_in_objects_dir(&self.objects_dir, oid)
1557 }))
1558 }
1559
1560 #[must_use]
1568 pub fn exists_stored(&self, oid: &ObjectId) -> bool {
1569 self.try_exists_stored(oid).unwrap_or(false)
1570 }
1571
1572 pub(crate) fn try_exists_stored(&self, oid: &ObjectId) -> Result<bool> {
1574 if oid.is_canonical_empty_tree() || self.overlay_read(oid).is_some() {
1575 return Ok(true);
1576 }
1577 if let Some(primary) = &self.custom_primary {
1578 return primary.contains(oid);
1579 }
1580 Ok(self.with_pack_store_for(&self.objects_dir, || {
1581 store::local_object_stored(&self.objects_dir, oid)
1582 }))
1583 }
1584
1585 #[must_use]
1595 pub fn exists_materialized_local(&self, oid: &ObjectId) -> bool {
1596 if let Some(primary) = &self.custom_primary {
1597 return primary.contains(oid).unwrap_or(false);
1598 }
1599 self.with_pack_store_for(&self.objects_dir, || {
1600 exists_materialized_in_objects_dir(&self.objects_dir, oid)
1601 })
1602 }
1603
1604 pub(crate) fn reprepare_local_packs(&self) {
1609 if self.custom_primary.is_some() {
1610 return;
1611 }
1612 self.with_pack_store_for(&self.objects_dir, || {
1613 let _ = crate::pack::reprepare_pack_directory_on_miss(&self.objects_dir);
1614 });
1615 }
1616
1617 pub(crate) fn exists_materialized_local_without_rescan(&self, oid: &ObjectId) -> bool {
1625 if self.custom_primary.is_some() {
1626 return self.exists_materialized_local(oid);
1627 }
1628 self.with_pack_store_for(&self.objects_dir, || {
1629 store::local_object_materialized_without_rescan(&self.objects_dir, oid)
1630 })
1631 }
1632
1633 pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
1645 self.probe_sources(oid, Err)
1646 }
1647
1648 fn probe_sources(
1651 &self,
1652 oid: &ObjectId,
1653 mut on_err: impl FnMut(Error) -> Result<()>,
1654 ) -> Result<bool> {
1655 #[cfg(test)]
1656 self.exists_probe.fetch_add(1, Ordering::Relaxed);
1657 if self.is_empty_tree(oid) || self.overlay_read(oid).is_some() {
1658 return Ok(true);
1659 }
1660 let mut found = |probe: Result<bool>| match probe {
1661 Ok(present) => Ok(present),
1662 Err(err) => on_err(err).map(|()| false),
1663 };
1664 if found(self.primary_writable().and_then(|p| p.contains(oid)))? {
1665 return Ok(true);
1666 }
1667 for src in &self.extra_read_sources {
1668 if found(src.contains(oid))? {
1669 return Ok(true);
1670 }
1671 }
1672 if !self.alternate_object_lookup_needed() {
1673 return Ok(false);
1674 }
1675 found(self.sources().and_then(|s| s.contains(oid)))
1676 }
1677
1678 pub(crate) fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
1688 #[cfg(test)]
1689 self.exists_probe.fetch_add(oids.len(), Ordering::Relaxed);
1690 let mut present: Vec<bool> = oids
1691 .iter()
1692 .map(|oid| self.is_empty_tree(oid) || self.overlay_read(oid).is_some())
1693 .collect();
1694 let mut pending: Vec<usize> = (0..oids.len()).filter(|&i| !present[i]).collect();
1695 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1696 self.primary_writable()?.contains_many(batch)
1697 })?;
1698 for src in &self.extra_read_sources {
1699 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1700 src.contains_many(batch)
1701 })?;
1702 }
1703 if !pending.is_empty() && self.alternate_object_lookup_needed() {
1704 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1705 self.sources()?.contains_many(batch)
1706 })?;
1707 }
1708 Ok(present)
1709 }
1710
1711 pub(crate) fn present_oids(
1717 &self,
1718 oids: impl IntoIterator<Item = ObjectId>,
1719 ) -> Result<Vec<ObjectId>> {
1720 self.partition_oids(oids, true)
1721 }
1722
1723 pub(crate) fn missing_oids(
1729 &self,
1730 oids: impl IntoIterator<Item = ObjectId>,
1731 ) -> Result<Vec<ObjectId>> {
1732 self.partition_oids(oids, false)
1733 }
1734
1735 fn partition_oids(
1737 &self,
1738 oids: impl IntoIterator<Item = ObjectId>,
1739 want_present: bool,
1740 ) -> Result<Vec<ObjectId>> {
1741 let oids: Vec<ObjectId> = oids.into_iter().collect();
1742 let present = self.contains_many(&oids)?;
1743 Ok(oids
1744 .into_iter()
1745 .zip(present)
1746 .filter_map(|(oid, has)| (has == want_present).then_some(oid))
1747 .collect())
1748 }
1749
1750 pub(crate) fn missing_stored_oids(
1757 &self,
1758 oids: impl IntoIterator<Item = ObjectId>,
1759 ) -> Result<Vec<ObjectId>> {
1760 filter_oids(oids, |oid| self.try_exists_stored(oid).map(|has| !has))
1761 }
1762
1763 #[must_use]
1770 pub fn exists(&self, oid: &ObjectId) -> bool {
1771 self.probe_sources(oid, |_| Ok(())).unwrap_or(false)
1772 }
1773
1774 fn with_pack_store_for<R>(&self, objects_dir: &Path, f: impl FnOnce() -> R) -> R {
1776 let store = self.pack_store_for(objects_dir);
1777 PackStore::with_context(store, f)
1778 }
1779
1780 pub fn with_pack_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
1785 self.with_pack_store_for(&self.objects_dir, f)
1786 }
1787
1788 pub(crate) fn pack_read_context_guard(&self) -> crate::pack_store::PackReadContextGuard {
1791 crate::pack_store::PackReadContextGuard::install(self.pack_store_for(&self.objects_dir))
1792 }
1793
1794 pub(crate) fn open_session_pack_reader(&self) -> Result<Option<pack::SingleLocalPackReader>> {
1802 if self.custom_primary.is_some()
1803 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1804 {
1805 return Ok(None);
1806 }
1807 self.sync_delta_base_cache_limit();
1808 pack::SingleLocalPackReader::open_allow_loose(&self.objects_dir)
1809 }
1810
1811 #[must_use]
1816 pub fn freshen_object(&self, oid: &ObjectId) -> bool {
1817 if self.is_empty_tree(oid) {
1818 return false;
1819 }
1820
1821 let loose = self.object_path(oid);
1822 if loose.is_file() {
1823 return self.touch_object_mtime(&loose).is_some();
1824 }
1825
1826 if self.freshen_object_in_objects_dir(&self.objects_dir, oid) {
1827 return true;
1828 }
1829
1830 let file_alts = self.file_alternate_dirs_snapshot();
1831 for alt_dir in file_alts.iter() {
1832 if self.freshen_object_in_objects_dir(alt_dir, oid) {
1833 return true;
1834 }
1835 }
1836
1837 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1838 if self.freshen_object_in_objects_dir(alt_dir, oid) {
1839 return true;
1840 }
1841 }
1842
1843 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1844 for alt_dir in guard.iter() {
1845 if self.freshen_object_in_objects_dir(alt_dir, oid) {
1846 return true;
1847 }
1848 }
1849 }
1850
1851 false
1852 }
1853
1854 fn freshen_object_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
1855 let loose = objects_dir
1856 .join(oid.loose_prefix())
1857 .join(oid.loose_suffix());
1858 if loose.is_file() {
1859 return self.touch_object_mtime(&loose).is_some();
1860 }
1861 self.with_pack_store_for(objects_dir, || {
1862 let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
1863 return false;
1864 };
1865 for idx in &indexes {
1866 if idx.contains(oid).unwrap_or(false) {
1867 return self.freshen_pack_once(&idx.pack_path, idx.is_cruft);
1868 }
1869 }
1870 false
1871 })
1872 }
1873
1874 fn freshen_pack_once(&self, pack_path: &Path, is_cruft: bool) -> bool {
1879 let Ok(mut guard) = self.freshened_packs.lock() else {
1880 return false;
1881 };
1882 if guard.contains(pack_path) {
1883 return true;
1884 }
1885 if is_cruft {
1886 return false;
1887 }
1888 let Some(touched_at) = self.touch_object_mtime(pack_path) else {
1889 return false;
1890 };
1891 pack::refresh_pack_bytes_signature(pack_path, touched_at);
1892 guard.insert(pack_path.to_path_buf());
1893 true
1894 }
1895
1896 fn try_freshen_existing(&self, oid: &ObjectId) -> Option<ObjectId> {
1899 if let Ok(primary) = self.primary() {
1900 if let Ok(Some(existing)) =
1901 primary.try_finish_duplicate_write(oid, WriteOptions::default())
1902 {
1903 return Some(existing);
1904 }
1905 }
1906 if !self.exists(oid) {
1907 return None;
1908 }
1909 if self.freshen_object(oid) {
1910 return Some(*oid);
1911 }
1912 None
1913 }
1914
1915 fn try_freshen_existing_local(&self, oid: &ObjectId) -> Option<ObjectId> {
1916 let primary = self.primary().ok()?;
1917 primary
1918 .try_finish_duplicate_write(
1919 oid,
1920 WriteOptions {
1921 assume_loose_only_existence: true,
1922 ..WriteOptions::default()
1923 },
1924 )
1925 .ok()
1926 .flatten()
1927 }
1928
1929 fn object_exists_for_write_skip(&self, oid: &ObjectId) -> bool {
1934 if self.is_empty_tree(oid) {
1935 self.exists_materialized_local(oid)
1936 } else {
1937 self.exists(oid)
1938 }
1939 }
1940
1941 pub fn read_loose_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
1952 LooseStore::read_verify_oid(path, expected_oid)
1953 }
1954
1955 fn local_loose(&self) -> Result<LooseStore> {
1956 self.require_files_primary("loose_write")?;
1957 Ok(LooseStore::new(
1958 self.objects_dir.clone(),
1959 self.hash_algo(),
1960 self.loose_compression()?,
1961 ))
1962 }
1963
1964 fn loose_at(&self, objects_dir: &Path) -> Result<LooseStore> {
1965 Ok(LooseStore::new(
1966 objects_dir.to_path_buf(),
1967 self.hash_algo(),
1968 self.loose_compression()?,
1969 ))
1970 }
1971
1972 pub fn read(&self, oid: &ObjectId) -> Result<Object> {
1980 self.read_impl(oid, true)
1981 }
1982
1983 fn read_impl(&self, oid: &ObjectId, remember: bool) -> Result<Object> {
1986 if self.is_empty_tree(oid) {
1987 return Ok(Object {
1988 kind: ObjectKind::Tree,
1989 data: Vec::new(),
1990 });
1991 }
1992
1993 if let Some(obj) = self.session_cached_loose_read(oid) {
1994 return Ok(obj);
1995 }
1996
1997 if let Some(obj) = self.overlay_read(oid) {
1998 return Ok(obj);
1999 }
2000 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
2001 return Ok(obj);
2002 }
2003
2004 let use_midx = self.multi_pack_index_reads_enabled();
2005 if use_midx {
2006 self.ensure_midx_prepared();
2007 }
2008
2009 self.sync_delta_base_cache_limit();
2010
2011 let primary = self.primary_writable()?;
2012 let files_primary = self.primary().ok();
2013 let mut unreadable_local_loose = None;
2014 let read_primary = || primary.read(oid);
2015 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2016 read_primary()
2017 } else {
2018 self.with_pack_store_for(&self.objects_dir, read_primary)
2019 } {
2020 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj, remember),
2021 Ok(None) => {}
2022 Err(err)
2023 if files_primary
2024 .as_ref()
2025 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2026 {
2027 unreadable_local_loose = Some(err);
2028 }
2029 Err(err) => return Err(err),
2030 }
2031
2032 if let Some(obj) = self.try_read_extra_sources(oid)? {
2033 return self.finish_session_loose_read(oid, obj, remember);
2034 }
2035
2036 if self.alternates_enabled {
2037 match self.sources()?.read(oid) {
2038 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj, remember),
2039 Ok(None) => {}
2040 Err(err) => return Err(err),
2041 }
2042 }
2043
2044 if let Some(err) = unreadable_local_loose {
2045 return Err(err);
2046 }
2047
2048 self.read_after_promisor_miss(oid)
2049 }
2050
2051 #[cold]
2052 #[inline(never)]
2053 fn read_after_promisor_miss(&self, oid: &ObjectId) -> Result<Object> {
2054 let mut lazy_retries = 0u32;
2055 loop {
2056 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2057 return Err(Error::ObjectNotFound(oid.to_hex()));
2058 };
2059 match res {
2060 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2061 lazy_retries += 1;
2062 if let Some(obj) = self.try_read_once(oid)? {
2063 return Ok(obj);
2064 }
2065 }
2066 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2067 return Err(Error::ObjectNotFound(oid.to_hex()));
2068 }
2069 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2070 return Err(err);
2071 }
2072 }
2073 }
2074 }
2075
2076 pub(crate) fn try_read_once(&self, oid: &ObjectId) -> Result<Option<Object>> {
2079 if oid.is_well_known_empty_tree() {
2080 return Ok(Some(Object {
2081 kind: ObjectKind::Tree,
2082 data: Vec::new(),
2083 }));
2084 }
2085
2086 if let Some(obj) = self.session_cached_loose_read(oid) {
2087 return Ok(Some(obj));
2088 }
2089
2090 if let Some(obj) = self.overlay_read(oid) {
2091 return Ok(Some(obj));
2092 }
2093 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
2094 return Ok(Some(self.finish_session_loose_read(oid, obj, true)?));
2095 }
2096
2097 let use_midx = self.multi_pack_index_reads_enabled();
2098 if use_midx {
2099 self.ensure_midx_prepared();
2100 }
2101
2102 self.sync_delta_base_cache_limit();
2103
2104 let primary = self.primary_writable()?;
2105 let files_primary = self.primary().ok();
2106 let mut unreadable_local_loose = None;
2107 let read_primary = || primary.read(oid);
2108 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2109 read_primary()
2110 } else {
2111 self.with_pack_store_for(&self.objects_dir, read_primary)
2112 } {
2113 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj, true)?)),
2114 Ok(None) => {}
2115 Err(err)
2116 if files_primary
2117 .as_ref()
2118 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2119 {
2120 unreadable_local_loose = Some(err);
2121 }
2122 Err(err) => return Err(err),
2123 }
2124
2125 if let Some(obj) = self.try_read_extra_sources(oid)? {
2126 return Ok(Some(self.finish_session_loose_read(oid, obj, true)?));
2127 }
2128
2129 if self.alternates_enabled {
2130 match self.sources()?.read(oid) {
2131 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj, true)?)),
2132 Ok(None) => {}
2133 Err(err) => return Err(err),
2134 }
2135 }
2136
2137 if let Some(err) = unreadable_local_loose {
2138 return Err(err);
2139 }
2140
2141 Ok(None)
2142 }
2143
2144 fn resolve_promisor_miss(
2145 &self,
2146 oid: &ObjectId,
2147 lazy_retries: u32,
2148 ) -> Result<Option<crate::promisor_fetch::PromisorMissResolution>> {
2149 let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment)
2150 else {
2151 return Ok(None);
2152 };
2153 Ok(Some(crate::promisor_fetch::resolve_promisor_read_miss(
2154 caches,
2155 self,
2156 oid,
2157 env.as_ref(),
2158 lazy_retries,
2159 )?))
2160 }
2161
2162 pub fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<ObjectKind> {
2171 if self.is_empty_tree(oid) {
2172 buf.clear();
2173 return Ok(ObjectKind::Tree);
2174 }
2175
2176 if let Some(obj) = self.session_cached_loose_read(oid) {
2177 buf.clear();
2178 buf.extend_from_slice(&obj.data);
2179 return Ok(obj.kind);
2180 }
2181
2182 if let Some(obj) = self.overlay_read(oid) {
2183 buf.clear();
2184 buf.extend_from_slice(&obj.data);
2185 return Ok(obj.kind);
2186 }
2187
2188 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2189 return Ok(kind);
2190 }
2191
2192 let use_midx = self.multi_pack_index_reads_enabled();
2193 if use_midx {
2194 self.ensure_midx_prepared();
2195 }
2196
2197 self.sync_delta_base_cache_limit();
2198
2199 let primary = self.primary_writable()?;
2200 let files_primary = self.primary().ok();
2201 let mut unreadable_local_loose = None;
2202 let mut read_primary = || primary.read_into(oid, buf);
2203 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2204 read_primary()
2205 } else {
2206 self.with_pack_store_for(&self.objects_dir, read_primary)
2207 } {
2208 Ok(Some(kind)) => return Ok(kind),
2209 Ok(None) => {}
2210 Err(err)
2211 if files_primary
2212 .as_ref()
2213 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2214 {
2215 unreadable_local_loose = Some(err);
2216 }
2217 Err(err) => return Err(err),
2218 }
2219
2220 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2221 return Ok(kind);
2222 }
2223
2224 if self.alternates_enabled {
2225 match self.sources()?.read_into(oid, buf) {
2226 Ok(Some(kind)) => return Ok(kind),
2227 Ok(None) => {}
2228 Err(err) => return Err(err),
2229 }
2230 }
2231
2232 if let Some(err) = unreadable_local_loose {
2233 return Err(err);
2234 }
2235
2236 self.read_into_after_promisor_miss(oid, buf)
2237 }
2238
2239 pub(crate) fn read_into_caching_delta_bases(
2251 &self,
2252 oid: &ObjectId,
2253 buf: &mut Vec<u8>,
2254 ) -> Result<ObjectKind> {
2255 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, true, buf)? {
2256 return Ok(kind);
2257 }
2258 if self.custom_primary.is_none() && !self.is_empty_tree(oid) {
2259 if let Ok(files) = self.primary() {
2260 self.ensure_midx_prepared();
2261 self.sync_delta_base_cache_limit();
2262 let mut read = || files.read_into_with_delta_policy(oid, true, buf);
2263 let local = if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2264 read()
2265 } else {
2266 self.with_pack_store_for(&self.objects_dir, read)
2267 };
2268 if let Ok(Some(kind)) = local {
2269 return Ok(kind);
2270 }
2271 }
2272 }
2273 let object = self.read(oid)?;
2274 buf.clear();
2275 buf.extend_from_slice(&object.data);
2276 Ok(object.kind)
2277 }
2278
2279 #[cold]
2280 #[inline(never)]
2281 fn read_into_after_promisor_miss(
2282 &self,
2283 oid: &ObjectId,
2284 buf: &mut Vec<u8>,
2285 ) -> Result<ObjectKind> {
2286 let mut lazy_retries = 0u32;
2287 loop {
2288 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2289 return Err(Error::ObjectNotFound(oid.to_hex()));
2290 };
2291 match res {
2292 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2293 lazy_retries += 1;
2294 if let Some(kind) = self.try_read_into_once(oid, buf)? {
2295 return Ok(kind);
2296 }
2297 }
2298 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2299 return Err(Error::ObjectNotFound(oid.to_hex()));
2300 }
2301 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2302 return Err(err);
2303 }
2304 }
2305 }
2306 }
2307
2308 fn try_read_into_once(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
2309 if oid.is_well_known_empty_tree() {
2310 buf.clear();
2311 return Ok(Some(ObjectKind::Tree));
2312 }
2313
2314 if let Some(obj) = self.session_cached_loose_read(oid) {
2315 buf.clear();
2316 buf.extend_from_slice(&obj.data);
2317 return Ok(Some(obj.kind));
2318 }
2319
2320 if let Some(obj) = self.overlay_read(oid) {
2321 buf.clear();
2322 buf.extend_from_slice(&obj.data);
2323 return Ok(Some(obj.kind));
2324 }
2325
2326 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2327 return Ok(Some(kind));
2328 }
2329
2330 let use_midx = self.multi_pack_index_reads_enabled();
2331 if use_midx {
2332 self.ensure_midx_prepared();
2333 }
2334
2335 self.sync_delta_base_cache_limit();
2336
2337 let primary = self.primary_writable()?;
2338 let files_primary = self.primary().ok();
2339 let mut unreadable_local_loose = None;
2340 let mut read_primary = || primary.read_into(oid, buf);
2341 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2342 read_primary()
2343 } else {
2344 self.with_pack_store_for(&self.objects_dir, read_primary)
2345 } {
2346 Ok(Some(kind)) => return Ok(Some(kind)),
2347 Ok(None) => {}
2348 Err(err)
2349 if files_primary
2350 .as_ref()
2351 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2352 {
2353 unreadable_local_loose = Some(err);
2354 }
2355 Err(err) => return Err(err),
2356 }
2357
2358 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2359 return Ok(Some(kind));
2360 }
2361
2362 if self.alternates_enabled {
2363 match self.sources()?.read_into(oid, buf) {
2364 Ok(Some(kind)) => return Ok(Some(kind)),
2365 Ok(None) => {}
2366 Err(err) => return Err(err),
2367 }
2368 }
2369
2370 if let Some(err) = unreadable_local_loose {
2371 return Err(err);
2372 }
2373
2374 Ok(None)
2375 }
2376
2377 fn try_read_into_extra_sources(
2378 &self,
2379 oid: &ObjectId,
2380 buf: &mut Vec<u8>,
2381 ) -> Result<Option<ObjectKind>> {
2382 for src in &self.extra_read_sources {
2383 if let Some(kind) = src.read_into(oid, buf)? {
2384 return Ok(Some(kind));
2385 }
2386 }
2387 Ok(None)
2388 }
2389
2390 fn try_read_packed_in_batch_context_into(
2391 &self,
2392 oid: &ObjectId,
2393 populate_delta_lru: bool,
2394 buf: &mut Vec<u8>,
2395 ) -> Result<Option<ObjectKind>> {
2396 if self.custom_primary.is_some()
2397 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
2398 || self.multi_pack_index_reads_enabled()
2399 {
2400 return Ok(None);
2401 }
2402 self.sync_delta_base_cache_limit();
2403 match pack::try_read_object_from_packs_with_options_into(
2404 &self.objects_dir,
2405 oid,
2406 pack::PackLookupOptions::ALL_LOCAL_PACKS,
2407 populate_delta_lru,
2408 buf,
2409 ) {
2410 Ok(kind) => Ok(Some(kind)),
2411 Err(Error::ObjectNotFound(_)) => Ok(None),
2412 Err(err) => Err(err),
2413 }
2414 }
2415
2416 pub fn read_info(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2428 if self.is_empty_tree(oid) {
2429 return Ok(ObjectInfo {
2430 kind: ObjectKind::Tree,
2431 size: 0,
2432 });
2433 }
2434
2435 if let Some(obj) = self.overlay_read(oid) {
2436 return Ok(ObjectInfo {
2437 kind: obj.kind,
2438 size: u64::try_from(obj.data.len())
2439 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
2440 });
2441 }
2442
2443 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
2444 return Ok(info);
2445 }
2446
2447 let use_midx = self.multi_pack_index_reads_enabled();
2448 if use_midx {
2449 self.ensure_midx_prepared();
2450 }
2451
2452 let primary = self.primary_writable()?;
2453 let files_primary = self.primary().ok();
2454 let mut unreadable_local_loose = None;
2455 match primary.read_info(oid) {
2456 Ok(Some(info)) => return Ok(info),
2457 Ok(None) => {}
2458 Err(err)
2459 if files_primary
2460 .as_ref()
2461 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2462 {
2463 unreadable_local_loose = Some(err);
2464 }
2465 Err(err) => return Err(err),
2466 }
2467
2468 if let Some(info) = self.try_read_info_extra_sources(oid)? {
2469 return Ok(info);
2470 }
2471
2472 if self.alternates_enabled {
2473 match self.sources()?.read_info(oid) {
2474 Ok(Some(info)) => return Ok(info),
2475 Ok(None) => {}
2476 Err(err) => return Err(err),
2477 }
2478 }
2479
2480 if let Some(err) = unreadable_local_loose {
2481 return Err(err);
2482 }
2483
2484 self.read_info_after_promisor_miss(oid)
2485 }
2486
2487 pub(crate) fn read_info_without_fetch(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2492 self.try_read_info_once(oid)?
2493 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))
2494 }
2495
2496 #[cold]
2497 #[inline(never)]
2498 fn read_info_after_promisor_miss(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2499 let mut lazy_retries = 0u32;
2500 loop {
2501 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2502 return Err(Error::ObjectNotFound(oid.to_hex()));
2503 };
2504 match res {
2505 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2506 return Err(err);
2507 }
2508 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2509 return Err(Error::ObjectNotFound(oid.to_hex()));
2510 }
2511 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2512 lazy_retries += 1;
2513 if let Some(info) = self.try_read_info_once(oid)? {
2514 return Ok(info);
2515 }
2516 }
2517 }
2518 }
2519 }
2520
2521 fn try_read_info_once(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
2522 if oid.is_well_known_empty_tree() {
2523 return Ok(Some(ObjectInfo {
2524 kind: ObjectKind::Tree,
2525 size: 0,
2526 }));
2527 }
2528
2529 if let Some(obj) = self.overlay_read(oid) {
2530 return Ok(Some(ObjectInfo {
2531 kind: obj.kind,
2532 size: u64::try_from(obj.data.len())
2533 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
2534 }));
2535 }
2536
2537 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
2538 return Ok(Some(info));
2539 }
2540
2541 let use_midx = self.multi_pack_index_reads_enabled();
2542 if use_midx {
2543 self.ensure_midx_prepared();
2544 }
2545
2546 let primary = self.primary_writable()?;
2547 let files_primary = self.primary().ok();
2548 let mut unreadable_local_loose = None;
2549 match primary.read_info(oid) {
2550 Ok(Some(info)) => return Ok(Some(info)),
2551 Ok(None) => {}
2552 Err(err)
2553 if files_primary
2554 .as_ref()
2555 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2556 {
2557 unreadable_local_loose = Some(err);
2558 }
2559 Err(err) => return Err(err),
2560 }
2561
2562 if let Some(info) = self.try_read_info_extra_sources(oid)? {
2563 return Ok(Some(info));
2564 }
2565
2566 if self.alternates_enabled {
2567 match self.sources()?.read_info(oid) {
2568 Ok(Some(info)) => return Ok(Some(info)),
2569 Ok(None) => {}
2570 Err(err) => return Err(err),
2571 }
2572 }
2573
2574 if let Some(err) = unreadable_local_loose {
2575 return Err(err);
2576 }
2577
2578 Ok(None)
2579 }
2580
2581 pub(crate) fn read_unless_larger(
2597 &self,
2598 oid: &ObjectId,
2599 limit: u64,
2600 cache_small: bool,
2601 ) -> Result<BoundedRead> {
2602 if cache_small {
2603 if let Some(read) = self.session_loose_reads.lock().ok().and_then(|cache| {
2604 cache.get(oid).map(|obj| {
2605 if obj.data.len() as u64 > limit {
2606 BoundedRead::Larger
2607 } else {
2608 BoundedRead::Object(obj.clone())
2609 }
2610 })
2611 }) {
2612 return Ok(read);
2613 }
2614 }
2615 if self.custom_primary.is_none() && !self.multi_pack_index_reads_enabled() {
2616 if let Some(read) =
2617 pack::pack_cache::try_read_object_via_single_pack_reader_unless_whole_larger(
2618 &self.objects_dir,
2619 oid,
2620 limit,
2621 )?
2622 {
2623 return Ok(read);
2624 }
2625 }
2626 if let Ok(files) = self.primary() {
2627 if self.multi_pack_index_reads_enabled() {
2628 self.ensure_midx_prepared();
2629 }
2630 match files.read_unless_larger(oid, limit)? {
2631 LocalSizedRead::Packed(Some(size)) if size > limit => {
2632 return Ok(BoundedRead::Larger);
2633 }
2634 LocalSizedRead::Packed(_) => return self.read(oid).map(BoundedRead::Object),
2635 LocalSizedRead::Loose(BoundedRead::Object(obj)) => {
2636 if cache_small && obj.data.len() <= SESSION_BOUNDED_LOOSE_CACHE_MAX {
2637 self.remember_session_loose_read(oid, &obj);
2638 }
2639 return Ok(BoundedRead::Object(obj));
2640 }
2641 LocalSizedRead::Loose(read) => return Ok(read),
2642 LocalSizedRead::Missing => {}
2643 }
2644 }
2645 let info = self.read_info(oid)?;
2646 if info.size > limit {
2647 return Ok(BoundedRead::Larger);
2648 }
2649 self.read(oid).map(BoundedRead::Object)
2650 }
2651
2652 pub fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'static>>> {
2661 self.with_stream(oid, materialize_stream)
2662 }
2663
2664 pub fn with_stream<R>(
2681 &self,
2682 oid: &ObjectId,
2683 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
2684 ) -> Result<Option<R>> {
2685 use std::io::Cursor;
2686 if let Some(obj) = self.overlay_read(oid) {
2687 let size = u64::try_from(obj.data.len()).map_err(|_| {
2688 Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
2689 })?;
2690 return f(ObjectStream {
2691 kind: obj.kind,
2692 size,
2693 reader: Box::new(Cursor::new(obj.data)),
2694 })
2695 .map(Some);
2696 }
2697 if self.multi_pack_index_reads_enabled() {
2698 self.ensure_midx_prepared();
2699 }
2700 self.sync_delta_base_cache_limit();
2701 let primary = self.primary_writable()?;
2702 let files_primary = self.primary().ok();
2703 let mut unreadable_local_loose = None;
2704 match primary.open_stream(oid) {
2705 Ok(Some(stream)) => return f(stream).map(Some),
2706 Ok(None) => {}
2707 Err(err)
2708 if files_primary
2709 .as_ref()
2710 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2711 {
2712 unreadable_local_loose = Some(err);
2713 }
2714 Err(err) => return Err(err),
2715 }
2716 if let Some(stream) = self.try_open_stream_extra_sources(oid)? {
2717 return f(stream).map(Some);
2718 }
2719 if self.alternates_enabled {
2720 let sources = self.sources()?;
2721 let stream = sources.open_stream(oid)?;
2722 if let Some(stream) = stream {
2723 return f(stream).map(Some);
2724 }
2725 }
2726 if let Some(err) = unreadable_local_loose {
2727 return Err(err);
2728 }
2729 Ok(None)
2730 }
2731
2732 pub fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
2738 let mut seen = oid_hash_set();
2739 let mut visit = |store: &dyn ObjectStore| -> Result<bool> {
2740 store::for_each_propagate_break(store, &mut |oid| {
2741 if !seen.insert(*oid) {
2742 return ControlFlow::Continue(());
2743 }
2744 f(oid)
2745 })
2746 };
2747 if let Some(overlay) = self.overlay_store() {
2748 if visit(overlay.as_ref())? {
2749 return Ok(());
2750 }
2751 }
2752 if visit(self.primary_writable()?.as_ref())? {
2753 return Ok(());
2754 }
2755 for src in &self.extra_read_sources {
2756 if visit(src.as_ref())? {
2757 return Ok(());
2758 }
2759 }
2760 if self.alternates_enabled {
2761 let _ = visit(self.sources()?.as_ref())?;
2762 }
2763 Ok(())
2764 }
2765
2766 pub fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
2772 let mut seen: OidHashSet = out.iter().copied().collect();
2773 if let Some(overlay) = self.overlay_store() {
2774 append_lookup_prefix_layer(overlay.as_ref(), prefix, limit, out, &mut seen)?;
2775 if limit != 0 && out.len() >= limit {
2776 return Ok(());
2777 }
2778 }
2779 append_lookup_prefix_layer(
2780 self.primary_writable()?.as_ref(),
2781 prefix,
2782 limit,
2783 out,
2784 &mut seen,
2785 )?;
2786 if limit != 0 && out.len() >= limit {
2787 return Ok(());
2788 }
2789 for src in &self.extra_read_sources {
2790 append_lookup_prefix_layer(src.as_ref(), prefix, limit, out, &mut seen)?;
2791 if limit != 0 && out.len() >= limit {
2792 return Ok(());
2793 }
2794 }
2795 if self.alternates_enabled {
2796 append_lookup_prefix_layer(self.sources()?.as_ref(), prefix, limit, out, &mut seen)?;
2797 }
2798 Ok(())
2799 }
2800
2801 pub fn enumerate_local_loose_objects(&self) -> Result<Vec<(ObjectId, PathBuf)>> {
2809 let primary = self.primary()?;
2810 let loose = primary.loose_store();
2811 let mut out = Vec::new();
2812 loose.for_each_object(&mut |oid| {
2813 out.push((*oid, loose.object_path(oid)));
2814 ControlFlow::Continue(())
2815 })?;
2816 Ok(out)
2817 }
2818
2819 pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
2827 hash_object_data_with(self.hash_algo(), kind, data)
2828 }
2829
2830 pub fn hash_stream(
2841 &self,
2842 kind: ObjectKind,
2843 size: u64,
2844 reader: &mut dyn std::io::Read,
2845 ) -> Result<ObjectId> {
2846 #[cfg(test)]
2847 self.hot_path_metrics().record_stream_hash();
2848 stream::hash_stream(self.hash_algo(), kind, size, reader)
2849 }
2850
2851 pub fn write_stream(
2872 &self,
2873 kind: ObjectKind,
2874 size: u64,
2875 reader: &mut dyn std::io::Read,
2876 options: WriteOptions,
2877 ) -> Result<ObjectId> {
2878 #[cfg(test)]
2879 self.hot_path_metrics().record_stream_write();
2880 if let Some(primary) = &self.custom_primary {
2881 if !self.overlay_active() {
2882 let oid = primary.write_stream(kind, size, reader, options)?;
2883 self.note_commit_object_written(kind);
2884 return Ok(oid);
2885 }
2886 }
2887 if self.overlay_active() || size < self.big_file_threshold()? {
2888 let len = usize::try_from(size).map_err(|_| {
2889 Error::CorruptObject(format!("object size {size} exceeds address space"))
2890 })?;
2891 let mut data = Vec::with_capacity(len);
2892 let read =
2893 std::io::Read::read_to_end(&mut std::io::Read::take(reader, size), &mut data)?;
2894 if read != len {
2895 return Err(Error::ObjectStreamTruncated {
2896 expected: size,
2897 actual: read as u64,
2898 });
2899 }
2900 return self.write_with_options(kind, &data, options);
2901 }
2902 let staged = stream::StagedPack::write(
2903 &self.objects_dir.join("pack"),
2904 self.hash_algo(),
2905 kind,
2906 size,
2907 reader,
2908 self.pack_compression()?,
2909 )?;
2910 let oid = staged.oid();
2911 if self.object_exists_for_write_skip(&oid) {
2912 if !options.silent {
2913 let _ = self.freshen_object(&oid);
2914 }
2915 return Ok(oid);
2916 }
2917 staged.install()?;
2918 self.invalidate_packs();
2919 self.note_commit_object_written(kind);
2920 Ok(oid)
2921 }
2922
2923 pub fn empty_tree_oid(&self) -> Result<ObjectId> {
2931 self.hash(ObjectKind::Tree, b"")
2932 }
2933
2934 #[must_use]
2936 pub fn is_empty_tree(&self, oid: &ObjectId) -> bool {
2937 empty_tree_oid_matches(self.hash_algo(), oid)
2938 }
2939
2940 pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
2953 self.write_with_options(kind, data, WriteOptions::default())
2954 }
2955
2956 pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
2965 if self.custom_primary.is_some() {
2966 return Ok(());
2967 }
2968 self.local_loose()?.ensure_all_loose_prefix_dirs()
2969 }
2970
2971 #[cfg(test)]
2973 #[must_use]
2974 pub fn test_loose_object_path(&self, oid: &ObjectId) -> PathBuf {
2975 self.object_path(oid)
2976 }
2977
2978 #[cfg(test)]
2980 pub fn test_remove_loose_object(&self, oid: &ObjectId) {
2981 let _ = fs::remove_file(self.object_path(oid));
2982 }
2983
2984 pub fn count_loose_objects(&self) -> Result<usize> {
2990 let mut count = 0usize;
2991 for_each_loose_object_id(&self.objects_dir, self.hash_algo(), &mut |_| {
2992 count += 1;
2993 ControlFlow::Continue(())
2994 })?;
2995 Ok(count)
2996 }
2997
2998 pub fn approximate_object_count(&self) -> Result<usize> {
3008 let pack_objects: Result<usize> = self.with_pack_read_context(|| {
3009 let indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
3010 Ok(indexes.iter().map(|idx| idx.len()).sum())
3011 });
3012 let mut count = pack_objects?;
3013 count += self.count_loose_objects()?;
3014 Ok(count)
3015 }
3016
3017 pub fn zlib_compress_loose_store(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
3023 self.local_loose()?.zlib_compress_store_bytes(store_bytes)
3024 }
3025
3026 pub fn write_loose_zlib_prehashed(
3040 &self,
3041 oid: &ObjectId,
3042 zlib_store: &[u8],
3043 options: WriteOptions,
3044 ) -> Result<ObjectId> {
3045 if self.custom_primary.is_some() {
3046 let raw = decompress_zlib_loose_bytes(zlib_store)?;
3047 let obj = parse_object_bytes(&raw)?;
3048 return self.write_with_options(obj.kind, &obj.data, options);
3049 }
3050 let primary = self.primary()?;
3051 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
3052 return Ok(existing);
3053 }
3054
3055 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3056 && self.object_exists_for_write_skip(oid);
3057
3058 if self.overlay_active() && !already_exists {
3059 if let Ok(raw) = decompress_zlib_loose_bytes(zlib_store) {
3060 if let Ok(obj) = parse_object_bytes(&raw) {
3061 if self.overlay_store_object(*oid, obj.kind, &obj.data) {
3062 return Ok(*oid);
3063 }
3064 }
3065 }
3066 }
3067
3068 if already_exists {
3069 if !options.silent {
3070 let _ = self.freshen_object(oid);
3071 }
3072 return Ok(*oid);
3073 }
3074
3075 primary.write_zlib_prehashed(oid, zlib_store, options)
3076 }
3077
3078 pub(crate) fn write_blob_prehashed(
3084 &self,
3085 oid: &ObjectId,
3086 data: &[u8],
3087 options: WriteOptions,
3088 ) -> Result<ObjectId> {
3089 if self.custom_primary.is_some() {
3090 return self.write_with_options(ObjectKind::Blob, data, options);
3091 }
3092 let primary = self.primary()?;
3093 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
3094 return Ok(existing);
3095 }
3096 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3097 && self.object_exists_for_write_skip(oid);
3098 if self.overlay_active()
3099 && !already_exists
3100 && self.overlay_store_object(*oid, ObjectKind::Blob, data)
3101 {
3102 return Ok(*oid);
3103 }
3104 if already_exists {
3105 if !options.silent {
3106 let _ = self.freshen_object(oid);
3107 }
3108 return Ok(*oid);
3109 }
3110 primary
3111 .loose_store()
3112 .write_prehashed(oid, ObjectKind::Blob, data, options)
3113 }
3114 pub fn write_with_options(
3116 &self,
3117 kind: ObjectKind,
3118 data: &[u8],
3119 options: WriteOptions,
3120 ) -> Result<ObjectId> {
3121 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3122
3123 if let Some(primary) = &self.custom_primary {
3124 let already_exists = if options.trust_new_loose {
3125 false
3126 } else if options.assume_loose_only_existence {
3127 primary.contains(&oid)?
3128 } else {
3129 self.object_exists_for_write_skip(&oid)
3130 };
3131 if self.overlay_active()
3132 && !already_exists
3133 && self.overlay_store_object(oid, kind, data)
3134 {
3135 return Ok(oid);
3136 }
3137 if already_exists {
3138 return Ok(oid);
3139 }
3140 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3141 self.note_commit_object_written(kind);
3142 return Ok(oid);
3143 }
3144
3145 let primary = self.primary()?;
3146 if let Some(existing) = primary.try_finish_duplicate_write(&oid, options)? {
3147 return Ok(existing);
3148 }
3149
3150 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3151 && self.object_exists_for_write_skip(&oid);
3152
3153 if self.overlay_active() && !already_exists && self.overlay_store_object(oid, kind, data) {
3156 return Ok(oid);
3157 }
3158
3159 if already_exists {
3160 if !options.silent {
3161 let _ = self.freshen_object(&oid);
3162 }
3163 return Ok(oid);
3164 }
3165
3166 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3167 self.note_commit_object_written(kind);
3168 Ok(oid)
3169 }
3170
3171 pub fn write_local(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3187 if self.custom_primary.is_some() {
3188 return Err(Error::UnsupportedObjectStore {
3189 operation: "write_local",
3190 });
3191 }
3192 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3193
3194 if let Some(existing) = self.try_freshen_existing_local(&oid) {
3195 return Ok(existing);
3196 }
3197
3198 let oid = self
3199 .local_loose()?
3200 .write(kind, data, WriteOptions::default())?;
3201 self.note_commit_object_written(kind);
3202 Ok(oid)
3203 }
3204
3205 pub(crate) fn write_local_stream_with(
3207 &self,
3208 kind: ObjectKind,
3209 size: u64,
3210 supply: impl FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()>,
3211 ) -> Result<ObjectId> {
3212 if self.custom_primary.is_some() {
3213 return Err(Error::UnsupportedObjectStore {
3214 operation: "write_local_stream",
3215 });
3216 }
3217 let loose = self.local_loose()?;
3218 let oid = loose.write_payload_stream_with(kind, size, WriteOptions::default(), supply)?;
3219 self.note_commit_object_written(kind);
3220 Ok(oid)
3221 }
3222
3223 pub fn write_loose_materialize(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3230 if self.custom_primary.is_some() {
3231 return Err(Error::UnsupportedObjectStore {
3232 operation: "write_loose_materialize",
3233 });
3234 }
3235 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3236 if let Ok(primary) = self.primary() {
3237 if let Ok(Some(existing)) = primary
3238 .loose_store()
3239 .try_finish_if_loose_present(&oid, WriteOptions::default())
3240 {
3241 return Ok(existing);
3242 }
3243 }
3244
3245 self.local_loose()?
3246 .write(kind, data, WriteOptions::default())
3247 }
3248
3249 pub fn write_raw(&self, store_bytes: &[u8]) -> Result<ObjectId> {
3259 let obj = parse_object_bytes(store_bytes)?;
3260 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
3261
3262 if let Some(primary) = &self.custom_primary {
3263 if self.exists(&oid) {
3264 return Ok(oid);
3265 }
3266 if self.overlay_active() && self.overlay_store_object(oid, obj.kind, &obj.data) {
3267 return Ok(oid);
3268 }
3269 return WritableObjectStore::write(
3270 primary.as_ref(),
3271 obj.kind,
3272 &obj.data,
3273 WriteOptions::default(),
3274 );
3275 }
3276
3277 if let Some(existing) = self.try_freshen_existing(&oid) {
3278 return Ok(existing);
3279 }
3280
3281 self.local_loose()?
3282 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
3283 }
3284
3285 pub fn write_raw_local(&self, store_bytes: &[u8]) -> Result<ObjectId> {
3293 if self.custom_primary.is_some() {
3294 return Err(Error::UnsupportedObjectStore {
3295 operation: "write_raw_local",
3296 });
3297 }
3298 let obj = parse_object_bytes(store_bytes)?;
3299 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
3300 if let Some(existing) = self.try_freshen_existing_local(&oid) {
3301 return Ok(existing);
3302 }
3303
3304 self.local_loose()?
3305 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
3306 }
3307
3308 fn touch_object_mtime(&self, path: &Path) -> Option<std::time::SystemTime> {
3310 #[cfg(test)]
3311 self.hot_path_test_metrics.record_freshen();
3312 let now = filetime::FileTime::now();
3314 filetime::set_file_times(path, now, now).ok()?;
3315 Some(
3316 std::time::UNIX_EPOCH
3317 + std::time::Duration::new(now.unix_seconds() as u64, now.nanoseconds()),
3318 )
3319 }
3320}
3321
3322fn hash_object_data_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3325 hash::hash_object(algo, kind, data)
3326}
3327
3328#[must_use]
3330pub(crate) fn empty_tree_oid_matches(algo: HashAlgo, oid: &ObjectId) -> bool {
3331 oid.is_empty_tree_for(algo)
3332}
3333
3334pub(crate) fn zlib_compress_blob_loose(
3337 data: &[u8],
3338 compression: flate2::Compression,
3339) -> Result<Vec<u8>> {
3340 use std::io::Write as _;
3341 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), compression);
3342 let zlib_err = |e: std::io::Error| Error::Zlib(e.to_string());
3343 write!(encoder, "{} {}\0", ObjectKind::Blob, data.len()).map_err(zlib_err)?;
3344 encoder.write_all(data).map_err(zlib_err)?;
3345 encoder.finish().map_err(zlib_err)
3346}
3347
3348fn append_lookup_prefix_layer(
3349 store: &dyn ObjectStore,
3350 prefix: &str,
3351 limit: usize,
3352 out: &mut Vec<ObjectId>,
3353 seen: &mut OidHashSet,
3354) -> Result<()> {
3355 let mut scratch = Vec::new();
3356 store.lookup_prefix(prefix, 0, &mut scratch)?;
3357 for oid in scratch {
3358 if seen.insert(oid) {
3359 out.push(oid);
3360 if limit != 0 && out.len() >= limit {
3361 return Ok(());
3362 }
3363 }
3364 }
3365 Ok(())
3366}
3367
3368fn materialize_stream(stream: ObjectStream<'_>) -> Result<ObjectStream<'static>> {
3369 use std::io::{Cursor, Read};
3370 let ObjectStream {
3371 kind,
3372 size,
3373 mut reader,
3374 } = stream;
3375 let mut payload = Vec::new();
3376 reader.read_to_end(&mut payload).map_err(Error::Io)?;
3377 Ok(ObjectStream {
3378 kind,
3379 size,
3380 reader: Box::new(Cursor::new(payload)),
3381 })
3382}
3383
3384fn parse_alternate_env(val: &str) -> Vec<PathBuf> {
3387 let mut result = Vec::new();
3388 let mut chars = val.chars().peekable();
3389 while chars.peek().is_some() {
3390 if chars.peek() == Some(&':') {
3391 chars.next();
3392 continue;
3393 }
3394 if chars.peek() == Some(&'"') {
3395 chars.next(); let saved: Vec<char> = chars.clone().collect();
3399 let mut path = String::new();
3400 let mut properly_closed = false;
3401 loop {
3402 match chars.next() {
3403 None => break,
3404 Some('"') => {
3405 properly_closed = true;
3406 break;
3407 }
3408 Some('\\') => match chars.peek() {
3409 Some(c) if c.is_ascii_digit() => {
3410 let mut oct = String::new();
3411 for _ in 0..3 {
3412 if let Some(&c) = chars.peek() {
3413 if c.is_ascii_digit() {
3414 oct.push(c);
3415 chars.next();
3416 } else {
3417 break;
3418 }
3419 } else {
3420 break;
3421 }
3422 }
3423 if let Ok(byte) = u8::from_str_radix(&oct, 8) {
3424 path.push(byte as char);
3425 }
3426 }
3427 Some(_) => {
3428 if let Some(c) = chars.next() {
3429 match c {
3430 'n' => path.push('\n'),
3431 't' => path.push('\t'),
3432 'r' => path.push('\r'),
3433 _ => path.push(c),
3434 }
3435 }
3436 }
3437 None => {}
3438 },
3439 Some(c) => path.push(c),
3440 }
3441 }
3442 if !properly_closed {
3443 let raw: String = std::iter::once('"').chain(saved).collect();
3446 let raw_path = raw.split(':').next().unwrap_or(&raw);
3448 if !raw_path.is_empty() {
3449 result.push(PathBuf::from(raw_path));
3450 }
3451 let remainder = &raw[raw_path.len()..];
3455 if let Some(rest) = remainder.strip_prefix(':') {
3456 result.extend(parse_alternate_env(rest));
3458 }
3459 return result;
3460 } else if !path.is_empty() {
3461 result.push(PathBuf::from(path));
3462 }
3463 } else {
3464 let mut path = String::new();
3465 while let Some(&c) = chars.peek() {
3466 if c == ':' {
3467 break;
3468 }
3469 path.push(c);
3470 chars.next();
3471 }
3472 if !path.is_empty() {
3473 result.push(PathBuf::from(path));
3474 }
3475 }
3476 }
3477 result
3478}
3479
3480fn system_time_to_file_time(t: std::time::SystemTime) -> FileTime {
3481 let dur = t
3482 .duration_since(std::time::SystemTime::UNIX_EPOCH)
3483 .unwrap_or_default();
3484 FileTime::from_unix_time(
3485 i64::try_from(dur.as_secs()).unwrap_or(i64::MAX),
3486 dur.subsec_nanos(),
3487 )
3488}
3489
3490fn superseded_pack_indexes(
3491 pack_dir: &Path,
3492 keep_pack: &Path,
3493 retained_packs: &HashSet<PathBuf>,
3494) -> Result<Vec<(PathBuf, std::time::SystemTime)>> {
3495 let keep_stem = keep_pack
3496 .file_stem()
3497 .and_then(|s| s.to_str())
3498 .ok_or_else(|| {
3499 Error::Io(std::io::Error::new(
3500 std::io::ErrorKind::InvalidInput,
3501 "bad pack path",
3502 ))
3503 })?;
3504 if !pack_dir.is_dir() {
3505 return Ok(Vec::new());
3506 }
3507 let mut out = Vec::new();
3508 for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
3509 let entry = entry.map_err(Error::Io)?;
3510 let name = entry.file_name();
3511 let name_str = name.to_string_lossy();
3512 if name_str == keep_stem || name_str.starts_with(&format!("{keep_stem}.")) {
3513 continue;
3514 }
3515 if !name_str.ends_with(".idx") {
3516 continue;
3517 }
3518 let idx_path = entry.path();
3519 let pack_path = idx_path.with_extension("pack");
3520 if !pack_path.is_file() {
3521 continue;
3522 }
3523 if superseded_pack_has_keep_marker(pack_dir, &pack_path)
3524 || pack_path.with_extension("promisor").is_file()
3525 || retained_packs.contains(&pack_path)
3526 {
3527 continue;
3528 }
3529 let pack_mtime = fs::metadata(&pack_path)
3530 .and_then(|m| m.modified())
3531 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
3532 out.push((idx_path, pack_mtime));
3533 }
3534 Ok(out)
3535}
3536
3537fn superseded_pack_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
3538 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
3539 return false;
3540 };
3541 pack_dir.join(format!("{stem}.keep")).is_file()
3542}
3543
3544fn oids_in_marker_kept_packs(pack_dir: &Path, keep_pack: &Path) -> Result<OidHashSet> {
3545 let keep_stem = keep_pack.file_stem().and_then(|s| s.to_str()).unwrap_or("");
3546 let mut oids = oid_hash_set();
3547 if !pack_dir.is_dir() {
3548 return Ok(oids);
3549 }
3550 for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
3551 let entry = entry.map_err(Error::Io)?;
3552 let pack_path = entry.path();
3553 if pack_path.extension().is_none_or(|e| e != "pack") {
3554 continue;
3555 }
3556 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
3557 continue;
3558 };
3559 if stem == keep_stem {
3560 continue;
3561 }
3562 if !superseded_pack_has_keep_marker(pack_dir, &pack_path) {
3563 continue;
3564 }
3565 let idx_path = pack_path.with_extension("idx");
3566 if idx_path.is_file() {
3567 oids.extend(PackedObjects::object_ids_from_index_path(&idx_path)?);
3568 }
3569 }
3570 Ok(oids)
3571}
3572
3573#[cfg(test)]
3574mod tests {
3575 #![allow(clippy::expect_used, clippy::unwrap_used)]
3576
3577 use super::*;
3578 use tempfile::TempDir;
3579
3580 #[test]
3581 fn round_trip_blob() {
3582 let dir = TempDir::new().unwrap();
3583 let odb = Odb::new(dir.path());
3584 let data = b"hello world";
3585 let oid = odb.write(ObjectKind::Blob, data).unwrap();
3586 let obj = odb.read(&oid).unwrap();
3587 assert_eq!(obj.kind, ObjectKind::Blob);
3588 assert_eq!(obj.data, data);
3589 }
3590
3591 #[test]
3592 fn known_blob_hash() {
3593 let oid = HashAlgo::Sha1
3596 .hash_object(ObjectKind::Blob, b"hello")
3597 .expect("hash");
3598 assert_eq!(oid.to_hex(), "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0");
3599 }
3600
3601 fn pack_objects(objects: &Path, oids: &[ObjectId]) {
3603 let odb = Odb::new(objects);
3604 let pack = crate::pack_objects::build_pack_from_send_list(
3605 &odb,
3606 oids,
3607 &HashSet::new(),
3608 &crate::pack_objects::PackBuildOptions::default(),
3609 &HashMap::new(),
3610 )
3611 .unwrap();
3612 for oid in oids {
3613 fs::remove_file(oid.loose_path_in(objects)).unwrap();
3614 }
3615 crate::index_pack::install_pack_bytes(
3616 pack,
3617 &odb,
3618 &crate::index_pack::IngestPackOptions::default(),
3619 )
3620 .unwrap();
3621 }
3622
3623 #[test]
3624 fn general_reads_cache_trees_and_tiny_blobs_but_not_larger_blobs() {
3625 let dir = TempDir::new().unwrap();
3626 let odb = Odb::new(dir.path());
3627 let blob = odb
3628 .write(
3629 ObjectKind::Blob,
3630 &vec![b'x'; SESSION_READ_BLOB_CACHE_MAX + 1],
3631 )
3632 .unwrap();
3633 let tiny = odb.write(ObjectKind::Blob, b"tiny\n").unwrap();
3634 let tree = odb
3635 .write(
3636 ObjectKind::Tree,
3637 &crate::objects::serialize_tree(&[crate::objects::TreeEntry {
3638 mode: 0o100644,
3639 name: b"f".to_vec(),
3640 oid: blob,
3641 }]),
3642 )
3643 .unwrap();
3644 assert_eq!(
3645 odb.read(&blob).unwrap().data.len(),
3646 SESSION_READ_BLOB_CACHE_MAX + 1
3647 );
3648 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
3649 let tree_bytes = odb.read(&tree).unwrap().data;
3650 assert_eq!(odb.read(&tiny).unwrap().data, b"tiny\n");
3651 for oid in [blob, tree, tiny] {
3652 let hex = oid.to_hex();
3653 fs::remove_file(dir.path().join(&hex[..2]).join(&hex[2..])).unwrap();
3654 }
3655 assert_eq!(odb.read(&tree).unwrap().data, tree_bytes, "tree reused");
3656 assert_eq!(odb.read(&tiny).unwrap().data, b"tiny\n", "tiny blob reused");
3657 assert!(odb.read(&blob).is_err(), "blob was not retained");
3658 }
3659
3660 #[test]
3661 fn session_read_cache_is_bounded_by_bytes_and_entries() {
3662 let mut cache = SessionLooseReads::default();
3663 let chunk = SESSION_LOOSE_CACHE_MAX_BYTES / 4;
3664 let object = |len: usize| Object {
3665 kind: ObjectKind::Tree,
3666 data: vec![0; len],
3667 };
3668 let oid = |n: u32| {
3669 let mut raw = [0u8; 20];
3670 raw[..4].copy_from_slice(&n.to_be_bytes());
3671 ObjectId::from_bytes(&raw).unwrap()
3672 };
3673 for n in 0..4 {
3674 cache.insert(oid(n), &object(chunk));
3675 }
3676 assert_eq!(cache.bytes, SESSION_LOOSE_CACHE_MAX_BYTES);
3677 cache.insert(oid(4), &object(1));
3678 assert_eq!(cache.bytes, 1, "exceeding the byte budget starts over");
3679 assert!(cache.get(&oid(0)).is_none());
3680 cache.insert(oid(4), &object(10));
3681 assert_eq!(cache.bytes, 10, "replacing an entry drops its old size");
3682 cache.insert(oid(5), &object(SESSION_LOOSE_CACHE_MAX_BYTES + 1));
3683 assert!(
3684 cache.get(&oid(5)).is_none(),
3685 "oversized objects are never kept"
3686 );
3687 assert!(cache.get(&oid(4)).is_some());
3688 for n in 10..(10 + SESSION_LOOSE_CACHE_MAX_ENTRIES as u32 + 2) {
3689 cache.insert(oid(n), &object(1));
3690 }
3691 assert!(cache.objects.len() <= SESSION_LOOSE_CACHE_MAX_ENTRIES + 1);
3692 assert!(cache.bytes <= SESSION_LOOSE_CACHE_MAX_BYTES);
3693 }
3694
3695 fn bounded_read_fixture() -> (TempDir, PathBuf, ObjectId, ObjectId) {
3696 let dir = TempDir::new().unwrap();
3697 let objects = dir.path().to_path_buf();
3698 let odb = Odb::new(&objects);
3699 let small = odb.write(ObjectKind::Blob, b"small blob\n").unwrap();
3700 let big: Vec<u8> = (0..300_000u32).flat_map(|i| i.to_le_bytes()).collect();
3701 let big = odb.write(ObjectKind::Blob, &big).unwrap();
3702 (dir, objects, small, big)
3703 }
3704
3705 fn assert_bounded_reads(odb: &Odb, small: &ObjectId, big: &ObjectId) {
3706 match odb.read_unless_larger(small, 1000, false).unwrap() {
3707 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
3708 BoundedRead::Larger => panic!("small blob is within the limit"),
3709 }
3710 assert!(matches!(
3711 odb.read_unless_larger(big, 1000, false).unwrap(),
3712 BoundedRead::Larger
3713 ));
3714 match odb.read_unless_larger(big, 1_200_000, false).unwrap() {
3715 BoundedRead::Object(obj) => assert_eq!(obj.data.len(), 1_200_000),
3716 BoundedRead::Larger => panic!("limit equal to the size reads the object"),
3717 }
3718 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
3719 assert!(matches!(
3720 odb.read_unless_larger(&missing, 1000, false),
3721 Err(Error::ObjectNotFound(_))
3722 ));
3723 }
3724
3725 #[test]
3726 fn read_unless_larger_sizes_loose_objects_from_their_header() {
3727 let (_dir, objects, small, big) = bounded_read_fixture();
3728 assert_bounded_reads(&Odb::new(&objects), &small, &big);
3729 }
3730
3731 #[test]
3732 fn read_unless_larger_sizes_packed_objects_in_and_out_of_read_context() {
3733 let (_dir, objects, small, big) = bounded_read_fixture();
3734 pack_objects(&objects, &[small, big]);
3735 let odb = Odb::new(&objects);
3736 assert_bounded_reads(&odb, &small, &big);
3737 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
3738 }
3739
3740 #[test]
3741 fn parsed_tree_reads_keep_no_raw_copy_in_the_session_cache() {
3742 let (_dir, objects, small, _big) = bounded_read_fixture();
3743 let odb = Odb::new(&objects);
3744 let entries = [crate::objects::TreeEntry {
3745 mode: 0o100644,
3746 name: b"small".to_vec(),
3747 oid: small,
3748 }];
3749 let tree = odb
3750 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
3751 .unwrap();
3752 for packed in [false, true] {
3753 if packed {
3754 pack_objects(&objects, &[tree]);
3755 }
3756 let odb = Odb::new(&objects);
3757 assert_eq!(*odb.cached_tree_entries(&tree).unwrap(), entries);
3758 assert!(odb.session_cached_loose_read(&tree).is_none());
3759 assert!(Arc::ptr_eq(
3760 &odb.cached_tree_entries(&tree).unwrap(),
3761 &odb.cached_tree_entries(&tree).unwrap()
3762 ));
3763 assert_eq!(odb.read(&tree).unwrap().kind, ObjectKind::Tree);
3764 assert!(
3765 odb.session_cached_loose_read(&tree).is_some(),
3766 "plain reads still use the session cache (packed: {packed})"
3767 );
3768 }
3769 }
3770
3771 #[test]
3772 fn read_unless_larger_reuses_small_loose_reads_but_never_caches_big_ones() {
3773 let (_dir, objects, small, big) = bounded_read_fixture();
3774 let odb = Odb::new(&objects);
3775 assert_bounded_reads(&odb, &small, &big);
3776 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
3777 assert!(matches!(
3778 odb.read_unless_larger(&small, 1000, true).unwrap(),
3779 BoundedRead::Object(_)
3780 ));
3781 assert!(matches!(
3782 odb.read_unless_larger(&big, 1_200_000, true).unwrap(),
3783 BoundedRead::Object(_)
3784 ));
3785 for oid in [small, big] {
3786 let hex = oid.to_hex();
3787 fs::remove_file(objects.join(&hex[..2]).join(&hex[2..])).unwrap();
3788 }
3789 match odb.read_unless_larger(&small, 1000, true).unwrap() {
3790 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
3791 BoundedRead::Larger => panic!("small blob is within the limit"),
3792 }
3793 assert!(matches!(
3794 odb.read_unless_larger(&small, 4, true),
3795 Ok(BoundedRead::Larger)
3796 ));
3797 assert!(matches!(
3798 odb.read_unless_larger(&big, 1_200_000, true),
3799 Err(Error::ObjectNotFound(_))
3800 ));
3801 }
3802
3803 #[test]
3804 fn read_unless_larger_finds_objects_across_several_packs() {
3805 let (_dir, objects, small, big) = bounded_read_fixture();
3806 pack_objects(&objects, &[small]);
3807 pack_objects(&objects, &[big]);
3808 let odb = Odb::new(&objects);
3809 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
3810 }
3811
3812 #[test]
3813 fn parse_object_header_prefix_unknown_kind_is_typed_error() {
3814 let err = crate::odb::store::loose::parse_object_header_prefix(b"not-a-git-kind 0\0")
3815 .unwrap_err();
3816 assert!(matches!(err, Error::UnknownObjectType(_)));
3817 }
3818
3819 fn git_dir_with_config(config: &str) -> (TempDir, PathBuf, PathBuf) {
3820 let dir = TempDir::new().unwrap();
3821 let git_dir = dir.path().join(".git");
3822 let objects = git_dir.join("objects");
3823 fs::create_dir_all(&objects).unwrap();
3824 fs::write(git_dir.join("config"), config).unwrap();
3825 (dir, git_dir, objects)
3826 }
3827
3828 fn zlib_flevel(compressed: &[u8]) -> u8 {
3830 assert!(compressed.len() >= 2);
3831 (compressed[1] >> 6) & 3
3832 }
3833
3834 fn tmp_loose_object_paths(objects: &Path) -> Vec<PathBuf> {
3835 let mut tmp = Vec::new();
3836 let Ok(shards) = fs::read_dir(objects) else {
3837 return tmp;
3838 };
3839 for shard in shards.flatten() {
3840 if !shard.file_type().is_ok_and(|t| t.is_dir()) {
3841 continue;
3842 }
3843 let Ok(files) = fs::read_dir(shard.path()) else {
3844 continue;
3845 };
3846 for file in files.flatten() {
3847 let name = file.file_name();
3848 if name.to_string_lossy().starts_with("tmp_") {
3849 tmp.push(file.path());
3850 }
3851 }
3852 }
3853 tmp
3854 }
3855
3856 #[test]
3857 fn loose_write_honours_level() {
3858 let payload: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
3859
3860 let (_dir, git_dir, objects) = git_dir_with_config("");
3861 let odb = Odb::new(&objects).with_config_git_dir(git_dir.clone());
3862 let oid_default = odb.write(ObjectKind::Blob, &payload).unwrap();
3863 let bytes_default = fs::read(odb.object_path(&oid_default)).unwrap();
3864 assert!(zlib_flevel(&bytes_default) <= 1);
3865 assert_eq!(odb.read(&oid_default).unwrap().data, payload);
3866
3867 for level in -1i32..=9 {
3868 let config = format!("[core]\n\tloosecompression = {level}\n");
3869 let (_dir, git_dir, objects) = git_dir_with_config(&config);
3870 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3871 let byte = u8::try_from(level.rem_euclid(256)).unwrap_or(0);
3872 let bytes: Vec<u8> = vec![byte; 512];
3873 let oid = odb.write(ObjectKind::Blob, &bytes).unwrap();
3874 let on_disk = fs::read(odb.object_path(&oid)).unwrap();
3875 assert!(on_disk.len() >= 2, "level {level}");
3876 assert_eq!(odb.read(&oid).unwrap().data, bytes.as_slice());
3877 }
3878
3879 let config = "[core]\n\tcompression = 9\n";
3880 let (_dir, git_dir, objects) = git_dir_with_config(config);
3881 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3882 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
3883 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 3);
3884
3885 let config = "[core]\n\tcompression = 0\n\tloosecompression = 1\n";
3886 let (_dir, git_dir, objects) = git_dir_with_config(config);
3887 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3888 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
3889 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 0);
3890
3891 let config = "[core]\n\tloosecompression = 99\n";
3892 let (_dir, git_dir, objects) = git_dir_with_config(config);
3893 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3894 assert!(odb.write(ObjectKind::Blob, b"x").is_err());
3895 assert!(tmp_loose_object_paths(&objects).is_empty());
3896 }
3897
3898 fn git_run(dir: &Path, args: &[&str]) -> std::process::Output {
3899 std::process::Command::new("git") .current_dir(dir)
3901 .args(args)
3902 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3903 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3904 .env("GIT_AUTHOR_NAME", "T")
3905 .env("GIT_AUTHOR_EMAIL", "t@example.com")
3906 .env("GIT_COMMITTER_NAME", "T")
3907 .env("GIT_COMMITTER_EMAIL", "t@example.com")
3908 .output()
3909 .unwrap()
3910 }
3911
3912 fn git_in(dir: &Path, args: &[&str]) {
3913 let out = git_run(dir, args);
3914 assert!(
3915 out.status.success(),
3916 "git {:?}: {}",
3917 args,
3918 String::from_utf8_lossy(&out.stderr)
3919 );
3920 }
3921
3922 fn git_rev_parse_oid(dir: &Path, spec: &str) -> ObjectId {
3923 let out = git_run(dir, &["rev-parse", spec]);
3924 assert!(
3925 out.status.success(),
3926 "git rev-parse {spec}: {}",
3927 String::from_utf8_lossy(&out.stderr)
3928 );
3929 ObjectId::from_hex(std::str::from_utf8(&out.stdout).unwrap().trim()).unwrap()
3930 }
3931
3932 #[test]
3933 fn write_loose_zlib_prehashed_matches_write() {
3934 let dir = TempDir::new().unwrap();
3935 let objects = dir.path().join("objects");
3936 let odb = Odb::new(&objects);
3937 let data = b"parallel prehash path";
3938 let oid_normal = odb.write(ObjectKind::Blob, data).unwrap();
3939
3940 let dir2 = TempDir::new().unwrap();
3941 let objects2 = dir2.path().join("objects");
3942 let odb2 = Odb::new(&objects2);
3943 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, data).expect("hash");
3944 assert_eq!(oid, oid_normal);
3945 let zlib =
3946 super::zlib_compress_blob_loose(data, odb2.loose_compression().unwrap()).unwrap();
3947 let oid2 = odb2
3948 .write_loose_zlib_prehashed(
3949 &oid,
3950 &zlib,
3951 WriteOptions {
3952 assume_loose_only_existence: true,
3953 ..WriteOptions::default()
3954 },
3955 )
3956 .unwrap();
3957 assert_eq!(oid2, oid_normal);
3958 assert_eq!(
3959 fs::read(odb2.object_path(&oid2)).unwrap(),
3960 fs::read(odb.object_path(&oid_normal)).unwrap()
3961 );
3962 }
3963
3964 #[test]
3965 fn write_blob_prehashed_matches_write_and_skips_existing_objects() {
3966 let dir = TempDir::new().unwrap();
3967 let odb = Odb::new(&dir.path().join("objects"));
3968 let data = vec![7u8; 100_000];
3969 let oid_normal = odb.write(ObjectKind::Blob, &data).unwrap();
3970
3971 let dir2 = TempDir::new().unwrap();
3972 let odb2 = Odb::new(&dir2.path().join("objects"));
3973 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, &data).unwrap();
3974 let options = WriteOptions {
3975 assume_loose_only_existence: true,
3976 ..WriteOptions::default()
3977 };
3978 assert_eq!(
3979 odb2.write_blob_prehashed(&oid, &data, options).unwrap(),
3980 oid
3981 );
3982 assert_eq!(oid, oid_normal);
3983 assert!(odb2.object_path(&oid).is_file());
3984 let obj = odb2.read(&oid).unwrap();
3985 assert_eq!(
3986 (obj.kind, obj.data.as_slice()),
3987 (ObjectKind::Blob, data.as_slice())
3988 );
3989
3990 assert_eq!(
3992 odb2.write_blob_prehashed(&oid, b"other", options).unwrap(),
3993 oid
3994 );
3995 assert_eq!(odb2.read(&oid).unwrap().data, data);
3996 }
3997
3998 #[test]
3999 fn write_loose_zlib_prehashed_routes_to_custom_primary() {
4000 use crate::objects::HashAlgo;
4001 use std::sync::Arc;
4002
4003 let dir = TempDir::new().unwrap();
4004 let objects = dir.path().join("objects");
4005 fs::create_dir_all(&objects).unwrap();
4006 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
4007 let odb = OdbBuilder::files(&objects)
4008 .primary(store.clone())
4009 .alternates(false)
4010 .build();
4011 let data = b"staged through a custom primary";
4012 let oid = hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, data).unwrap();
4013 let zlib = super::zlib_compress_blob_loose(data, Compression::fast()).unwrap();
4014
4015 odb.ensure_all_loose_prefix_dirs().unwrap();
4016 let written = odb
4017 .write_loose_zlib_prehashed(&oid, &zlib, WriteOptions::default())
4018 .unwrap();
4019 assert_eq!(written, oid);
4020 assert!(store.contains(&oid).unwrap());
4021 assert_eq!(odb.read(&oid).unwrap().data, data);
4022 assert_eq!(
4023 fs::read_dir(&objects).unwrap().count(),
4024 0,
4025 "no loose shard directories or files beside a custom primary"
4026 );
4027
4028 assert!(odb
4029 .write_loose_zlib_prehashed(&oid, b"not zlib", WriteOptions::default())
4030 .is_err());
4031 }
4032
4033 #[test]
4036 fn write_existing_loose_does_not_rewrite() {
4037 let dir = TempDir::new().unwrap();
4038 let objects = dir.path().join("objects");
4039 fs::create_dir_all(&objects).unwrap();
4040 let odb = Odb::new(&objects);
4041 let payload = b"already loose";
4042 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4043 let path = odb.object_path(&oid);
4044 let before_bytes = fs::read(&path).unwrap();
4045 let meta_before = fs::metadata(&path).unwrap();
4046 let before_mtime = filetime::FileTime::from_last_modification_time(&meta_before);
4047 #[cfg(unix)]
4048 let ino_before = {
4049 use std::os::unix::fs::MetadataExt;
4050 meta_before.ino()
4051 };
4052 std::thread::sleep(std::time::Duration::from_millis(20));
4053 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4054 assert_eq!(oid, oid2);
4055 assert_eq!(fs::read(&path).unwrap(), before_bytes);
4056 let meta_after = fs::metadata(&path).unwrap();
4057 let after_mtime = filetime::FileTime::from_last_modification_time(&meta_after);
4058 assert!(
4059 after_mtime >= before_mtime,
4060 "expected freshen to bump or preserve mtime"
4061 );
4062 #[cfg(unix)]
4063 {
4064 use std::os::unix::fs::MetadataExt;
4065 assert_eq!(
4066 meta_after.ino(),
4067 ino_before,
4068 "rewrite must not replace the loose object file"
4069 );
4070 }
4071 }
4072
4073 #[cfg_attr(
4075 not(feature = "full-tests"),
4076 ignore = "starts system git or is heavy; run with --features full-tests"
4077 )]
4078 #[test]
4079 fn write_skips_loose_when_object_in_pack_only() {
4080 let dir = TempDir::new().unwrap();
4081 git_in(dir.path(), &["init", "-q"]);
4082 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4083 git_in(dir.path(), &["config", "user.name", "T"]);
4084 fs::write(dir.path().join("f"), b"pack-only payload").unwrap();
4085 git_in(dir.path(), &["add", "f"]);
4086 git_in(dir.path(), &["commit", "-m", "c"]);
4087 git_in(dir.path(), &["repack", "-ad"]);
4088 let objects = dir.path().join(".git").join("objects");
4089 let odb = Odb::new(&objects);
4090 let oid = odb.write(ObjectKind::Blob, b"pack-only payload").unwrap();
4091 assert!(!odb.object_path(&oid).exists());
4092 assert!(odb.exists(&oid));
4093 }
4094
4095 #[cfg_attr(
4097 not(feature = "full-tests"),
4098 ignore = "starts system git or is heavy; run with --features full-tests"
4099 )]
4100 #[test]
4101 fn write_freshens_loose_object_in_alternate() {
4102 use filetime::FileTime;
4103 use std::thread;
4104 use std::time::Duration;
4105
4106 let alt = TempDir::new().unwrap();
4107 git_in(alt.path(), &["init", "-q", "--bare"]);
4108 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
4109 let alt_objects = alt.path().join("objects");
4110 let alt_odb = Odb::new(&alt_objects);
4111 let payload = b"alternate-loose";
4112 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
4113 let alt_loose = alt_odb.object_path(&oid);
4114
4115 let primary = TempDir::new().unwrap();
4116 let primary_objects = primary.path().join("objects");
4117 fs::create_dir_all(primary_objects.join("info")).unwrap();
4118 fs::write(
4119 primary_objects.join("info").join("alternates"),
4120 format!("{}\n", alt_objects.display()),
4121 )
4122 .unwrap();
4123 let odb = Odb::new(&primary_objects);
4124 thread::sleep(Duration::from_millis(50));
4125 let before = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
4126 filetime::set_file_mtime(&alt_loose, before).unwrap();
4127
4128 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4129 assert_eq!(oid, oid2);
4130 assert!(!odb.object_path(&oid).exists());
4131 let after = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
4132 assert!(after > before, "write must freshen alternate loose object");
4133 }
4134
4135 #[cfg_attr(
4136 not(feature = "full-tests"),
4137 ignore = "starts system git or is heavy; run with --features full-tests"
4138 )]
4139 #[test]
4140 fn write_freshens_packed_object_in_alternate() {
4141 use filetime::FileTime;
4142 use std::thread;
4143 use std::time::Duration;
4144
4145 let alt = TempDir::new().unwrap();
4146 git_in(alt.path(), &["init", "-q"]);
4147 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
4148 git_in(alt.path(), &["config", "user.name", "T"]);
4149 let payload = b"alternate-pack";
4150 fs::write(alt.path().join("f"), payload).unwrap();
4151 git_in(alt.path(), &["add", "f"]);
4152 git_in(alt.path(), &["commit", "-m", "c"]);
4153 git_in(alt.path(), &["repack", "-ad"]);
4154 let alt_objects = alt.path().join(".git").join("objects");
4155 let alt_odb = Odb::new(&alt_objects);
4156 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
4157 let pack_path = fs::read_dir(alt_objects.join("pack"))
4158 .unwrap()
4159 .flatten()
4160 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
4161 .unwrap()
4162 .path();
4163 thread::sleep(Duration::from_millis(50));
4164 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4165
4166 let primary = TempDir::new().unwrap();
4167 let primary_objects = primary.path().join("objects");
4168 fs::create_dir_all(primary_objects.join("info")).unwrap();
4169 fs::write(
4170 primary_objects.join("info").join("alternates"),
4171 format!("{}\n", alt_objects.display()),
4172 )
4173 .unwrap();
4174 pack::clear_pack_cache();
4175 let odb = Odb::new(&primary_objects);
4176 assert!(odb.exists(&oid));
4177 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4178 assert_eq!(oid, oid2);
4179 assert!(!odb.object_path(&oid).exists());
4180 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4181 assert!(after >= before, "write must freshen alternate pack");
4182 }
4183
4184 #[cfg_attr(
4185 not(feature = "full-tests"),
4186 ignore = "starts system git or is heavy; run with --features full-tests"
4187 )]
4188 #[test]
4189 fn write_skips_loose_when_object_only_in_alternate() {
4190 let alt = TempDir::new().unwrap();
4191 git_in(alt.path(), &["init", "-q", "--bare"]);
4192 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
4193 let alt_objects = alt.path().join("objects");
4194 let alt_odb = Odb::new(&alt_objects);
4195 let payload = b"alternate-only";
4196 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
4197
4198 let primary = TempDir::new().unwrap();
4199 let primary_objects = primary.path().join("objects");
4200 fs::create_dir_all(primary.path().join("objects").join("info")).unwrap();
4201 fs::write(
4202 primary
4203 .path()
4204 .join("objects")
4205 .join("info")
4206 .join("alternates"),
4207 format!("{}\n", alt_objects.display()),
4208 )
4209 .unwrap();
4210 let odb = Odb::new(&primary_objects);
4211 assert!(odb.exists(&oid));
4212 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4213 assert_eq!(oid, oid2);
4214 assert!(!odb.object_path(&oid).exists());
4215 }
4216
4217 #[cfg_attr(
4218 not(feature = "full-tests"),
4219 ignore = "starts system git or is heavy; run with --features full-tests"
4220 )]
4221 #[test]
4222 fn write_with_mem_overlay_stays_in_memory() {
4223 let dir = TempDir::new().unwrap();
4224 let objects = dir.path().join("objects");
4225 fs::create_dir_all(&objects).unwrap();
4226 let odb = Odb::new(&objects);
4227 odb.enable_mem_overlay();
4228 let payload = b"overlay blob";
4229 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4230 assert!(!odb.object_path(&oid).exists());
4231 assert_eq!(odb.read(&oid).unwrap().data, payload);
4232 odb.disable_mem_overlay();
4233 assert!(odb.read(&oid).is_err());
4234 }
4235
4236 #[test]
4237 fn read_alternates_relative_path_and_symlinked_objects_dir() {
4238 use crate::pack::read_alternates_recursive;
4239
4240 let layout = TempDir::new().unwrap();
4241 let alt_objects = layout.path().join("alt-store").join("objects");
4242 fs::create_dir_all(&alt_objects).unwrap();
4243 let primary_objects = layout.path().join("repo").join("objects");
4244 fs::create_dir_all(primary_objects.join("info")).unwrap();
4245 fs::write(
4246 primary_objects.join("info").join("alternates"),
4247 "../../alt-store/objects\n",
4248 )
4249 .unwrap();
4250 let resolved = read_alternates_recursive(&primary_objects).unwrap();
4251 let alt_canonical = alt_objects
4252 .canonicalize_simplified()
4253 .unwrap_or(alt_objects.clone());
4254 assert!(
4255 resolved.iter().any(|p| {
4256 p.canonicalize_simplified().unwrap_or_else(|_| p.clone()) == alt_canonical
4257 }),
4258 "relative alternate entry should resolve: {resolved:?}"
4259 );
4260
4261 #[cfg(unix)]
4262 {
4263 let link_parent = TempDir::new().unwrap();
4264 std::os::unix::fs::symlink(&primary_objects, link_parent.path().join("objects-link"))
4265 .unwrap();
4266 let via_link =
4267 read_alternates_recursive(&link_parent.path().join("objects-link")).unwrap();
4268 assert!(
4269 !via_link.is_empty(),
4270 "alternates through symlinked objects dir: {via_link:?}"
4271 );
4272 }
4273 }
4274 #[test]
4275 fn empty_tree_exists_without_hex() {
4276 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
4277 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4278 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").unwrap();
4279 assert!(odb.exists(&canon));
4280 assert!(odb.exists(&legacy));
4281 assert!(odb.exists_local(&canon));
4282 assert!(!odb.freshen_object(&canon));
4283 }
4284
4285 #[test]
4286 fn sha256_empty_tree_virtual_read_without_loose_file() {
4287 use crate::objects::HashAlgo;
4288 use std::sync::Arc;
4289
4290 let dir = TempDir::new().unwrap();
4291 let objects = dir.path().join("objects");
4292 fs::create_dir_all(&objects).unwrap();
4293 let store = Arc::new(MemoryStore::new(HashAlgo::Sha256));
4294 let odb = OdbBuilder::files(&objects)
4295 .primary(store)
4296 .alternates(false)
4297 .build();
4298 let empty = odb.empty_tree_oid().expect("hash empty tree");
4299 assert!(!odb.exists_materialized_local(&empty));
4300 assert!(odb.exists_local(&empty));
4301 assert!(odb.is_empty_tree(&empty));
4302 let obj = odb.read(&empty).expect("virtual read empty tree");
4303 assert_eq!(obj.kind, ObjectKind::Tree);
4304 assert!(obj.data.is_empty());
4305 }
4306
4307 #[test]
4308 fn empty_tree_oid_and_object_id_empty_tree_match_sha1() {
4309 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
4310 let from_odb = odb.empty_tree_oid().expect("empty tree oid");
4311 assert_eq!(from_odb, ObjectId::empty_tree());
4312 assert!(odb.is_empty_tree(&ObjectId::empty_tree()));
4313 }
4314
4315 #[test]
4316 fn empty_tree_virtual_presence_skips_materialized_local() {
4317 let dir = TempDir::new().unwrap();
4318 let objects = dir.path().join("objects");
4319 fs::create_dir_all(&objects).unwrap();
4320 let odb = Odb::new(&objects);
4321 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4322 assert!(odb.exists_local(&canon));
4323 assert!(!odb.exists_materialized_local(&canon));
4324 }
4325
4326 #[test]
4327 fn write_materializes_well_known_empty_tree() {
4328 let dir = TempDir::new().unwrap();
4329 let objects = dir.path().join("objects");
4330 fs::create_dir_all(&objects).unwrap();
4331 let odb = Odb::new(&objects);
4332 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4333 assert!(!odb.exists_materialized_local(&canon));
4334 let oid = odb
4335 .write(crate::objects::ObjectKind::Tree, &[])
4336 .expect("write empty tree");
4337 assert_eq!(oid, canon);
4338 assert!(odb.exists_materialized_local(&canon));
4339 }
4340
4341 #[test]
4342 fn append_alternate_objects_line_errors_on_unreadable_existing_file() {
4343 let dir = TempDir::new().unwrap();
4344 let objects = dir.path().join("objects");
4345 fs::create_dir_all(objects.join("info")).unwrap();
4346 let alt_path = objects.join("info/alternates");
4347 fs::write(&alt_path, b"not valid utf-8 \xff\n").unwrap();
4348 let err = Odb::append_alternate_objects_line(&objects, Path::new("/tmp/other/objects"))
4349 .unwrap_err();
4350 assert!(matches!(err, Error::Io(_)));
4351 assert_eq!(fs::read(&alt_path).unwrap(), b"not valid utf-8 \xff\n");
4352 }
4353
4354 #[test]
4355 fn append_file_alternate_refreshes_cached_snapshot() {
4356 let primary = TempDir::new().unwrap();
4357 let alternate = TempDir::new().unwrap();
4358 let alt_objects = alternate.path().join("objects");
4359 std::fs::create_dir_all(&alt_objects).unwrap();
4360 let alt_odb = Odb::new(&alt_objects);
4361 let oid = alt_odb.write(ObjectKind::Blob, b"via-alternate").unwrap();
4362
4363 let primary_objects = primary.path().join("objects");
4364 std::fs::create_dir_all(primary_objects.join("info")).unwrap();
4365 let primary_odb = Odb::new(&primary_objects);
4366 assert!(!primary_odb.exists(&oid));
4367 let _ = primary_odb.file_alternate_dirs_snapshot();
4368 primary_odb.append_file_alternate(&alt_objects).unwrap();
4369 assert!(primary_odb.exists(&oid));
4370 }
4371
4372 #[test]
4373 fn exists_via_env_alternate_dirs_at_construction() {
4374 let primary = TempDir::new().unwrap();
4375 let alternate = TempDir::new().unwrap();
4376 let alt_objects = alternate.path().join("objects");
4377 std::fs::create_dir_all(&alt_objects).unwrap();
4378 let alt_odb = Odb::new(&alt_objects);
4379 let oid = alt_odb.write(ObjectKind::Blob, b"env-alt").unwrap();
4380
4381 let primary_objects = primary.path().join("objects");
4382 std::fs::create_dir_all(&primary_objects).unwrap();
4383 let primary_odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
4384 assert!(primary_odb.exists(&oid));
4385 }
4386
4387 fn flaky_odb(objects: &Path) -> (Odb, Arc<store::flaky::FlakyStore>) {
4388 let flaky = store::flaky::FlakyStore::new();
4389 let odb = OdbBuilder::files(objects)
4390 .primary(flaky.clone())
4391 .alternates(false)
4392 .build();
4393 (odb, flaky)
4394 }
4395
4396 #[test]
4397 fn contains_reports_primary_failure_instead_of_missing() {
4398 let dir = TempDir::new().unwrap();
4399 let (odb, flaky) = flaky_odb(dir.path());
4400 let oid = odb.write(ObjectKind::Blob, b"stored").unwrap();
4401 assert!(odb.contains(&oid).unwrap());
4402
4403 flaky.fail_all(true);
4404 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
4405 assert!(store::flaky::is_injected(&odb.contains(&oid).unwrap_err()));
4406 assert!(
4407 !odb.exists(&oid),
4408 "exists() keeps treating failures as absent"
4409 );
4410 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
4411
4412 let try_local = odb.try_exists_local(&oid).unwrap_err();
4413 assert!(store::flaky::is_injected(&try_local));
4414 assert!(!odb.exists_local(&oid));
4415 let try_stored = odb.try_exists_stored(&oid).unwrap_err();
4416 assert!(store::flaky::is_injected(&try_stored));
4417 assert!(!odb.exists_stored(&oid));
4418 }
4419
4420 #[test]
4421 fn contains_reports_extra_read_source_failure() {
4422 let dir = TempDir::new().unwrap();
4423 let flaky = store::flaky::FlakyStore::new();
4424 let odb = OdbBuilder::files(dir.path())
4425 .push_read_source(flaky.clone())
4426 .alternates(false)
4427 .build();
4428 let local = odb.write(ObjectKind::Blob, b"in primary").unwrap();
4429 let absent = HashAlgo::Sha1
4430 .hash_object(ObjectKind::Blob, b"nowhere")
4431 .unwrap();
4432 assert!(!odb.contains(&absent).unwrap());
4433
4434 flaky.fail_contains(true);
4435 assert!(
4436 odb.contains(&local).unwrap(),
4437 "primary hit wins over a failing source"
4438 );
4439 assert!(store::flaky::is_injected(
4440 &odb.contains(&absent).unwrap_err()
4441 ));
4442 assert!(!odb.exists(&absent));
4443 }
4444
4445 #[test]
4446 fn contains_reports_primary_failure_even_when_later_source_has_object() {
4447 let dir = TempDir::new().unwrap();
4448 let extra = Arc::new(store::MemoryStore::new(HashAlgo::Sha1));
4449 let flaky = store::flaky::FlakyStore::new();
4450 let odb = OdbBuilder::files(dir.path())
4451 .primary(flaky.clone())
4452 .push_read_source(extra.clone())
4453 .alternates(false)
4454 .build();
4455 let oid = extra
4456 .write(ObjectKind::Blob, b"extra", WriteOptions::default())
4457 .unwrap();
4458 assert!(odb.contains(&oid).unwrap());
4459
4460 flaky.fail_all(true);
4461 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
4462 assert!(
4463 store::flaky::is_injected(&odb.contains(&oid).unwrap_err()),
4464 "contains must agree with read"
4465 );
4466 assert!(odb.exists(&oid), "exists() skips the failing primary");
4467 }
4468
4469 #[test]
4470 fn contains_reports_alternate_failure() {
4471 let primary = TempDir::new().unwrap();
4472 let alternate = TempDir::new().unwrap();
4473 let alt_objects = alternate.path().join("objects");
4474 std::fs::create_dir_all(&alt_objects).unwrap();
4475 let primary_objects = primary.path().join("objects");
4476 std::fs::create_dir_all(&primary_objects).unwrap();
4477 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
4478 let local = odb.write(ObjectKind::Blob, b"local").unwrap();
4479 let absent = HashAlgo::Sha1
4480 .hash_object(ObjectKind::Blob, b"nowhere")
4481 .unwrap();
4482
4483 let flaky = store::flaky::FlakyStore::new();
4486 odb.sources().unwrap();
4487 odb.alternate_sources_cache.write().unwrap().sources = Some(Arc::new(CompositeStore::new(
4488 vec![flaky.clone()],
4489 HashAlgo::Sha1,
4490 )));
4491 assert!(!odb.contains(&absent).unwrap());
4492
4493 flaky.fail_contains(true);
4494 assert!(odb.contains(&local).unwrap());
4495 assert!(store::flaky::is_injected(
4496 &odb.contains(&absent).unwrap_err()
4497 ));
4498 assert!(!odb.exists(&absent));
4499 }
4500
4501 #[test]
4502 fn contains_on_files_backend_covers_present_absent_empty_tree_and_alternate() {
4503 let primary = TempDir::new().unwrap();
4504 let alternate = TempDir::new().unwrap();
4505 let alt_objects = alternate.path().join("objects");
4506 std::fs::create_dir_all(&alt_objects).unwrap();
4507 let alt_oid = Odb::new(&alt_objects)
4508 .write(ObjectKind::Blob, b"alternate")
4509 .unwrap();
4510 let primary_objects = primary.path().join("objects");
4511 std::fs::create_dir_all(&primary_objects).unwrap();
4512 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
4513 let present = odb.write(ObjectKind::Blob, b"present").unwrap();
4514 let absent = HashAlgo::Sha1
4515 .hash_object(ObjectKind::Blob, b"absent")
4516 .unwrap();
4517
4518 assert!(odb.contains(&present).unwrap());
4519 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
4520 assert!(odb.contains(&alt_oid).unwrap());
4521 assert!(!odb.contains(&absent).unwrap());
4522 let empty_tree = odb.empty_tree_oid().unwrap();
4523 assert_eq!(
4524 odb.contains_many(&[present, absent, alt_oid, empty_tree])
4525 .unwrap(),
4526 vec![true, false, true, true]
4527 );
4528 assert!(odb.contains_many(&[]).unwrap().is_empty());
4529 assert_eq!(
4530 odb.present_oids([present, absent, alt_oid]).unwrap(),
4531 vec![present, alt_oid]
4532 );
4533 assert_eq!(
4534 odb.missing_oids([present, absent, alt_oid]).unwrap(),
4535 vec![absent]
4536 );
4537 assert_eq!(
4538 odb.missing_stored_oids([present, absent, alt_oid]).unwrap(),
4539 vec![absent, alt_oid],
4540 "stored ignores alternates"
4541 );
4542 }
4543
4544 #[test]
4545 fn oid_filters_propagate_backend_failure() {
4546 let dir = TempDir::new().unwrap();
4547 let (odb, flaky) = flaky_odb(dir.path());
4548 let oid = odb.write(ObjectKind::Blob, b"x").unwrap();
4549 assert_eq!(odb.contains_many(&[oid]).unwrap(), vec![true]);
4550 flaky.fail_contains(true);
4551 assert!(store::flaky::is_injected(
4552 &odb.contains_many(&[oid]).unwrap_err()
4553 ));
4554 assert!(store::flaky::is_injected(
4555 &odb.present_oids([oid]).unwrap_err()
4556 ));
4557 assert!(store::flaky::is_injected(
4558 &odb.missing_oids([oid]).unwrap_err()
4559 ));
4560 assert!(store::flaky::is_injected(
4561 &odb.missing_stored_oids([oid]).unwrap_err()
4562 ));
4563 }
4564
4565 #[test]
4566 fn write_existing_loose_uses_path_stat_not_exists() {
4567 let dir = TempDir::new().unwrap();
4568 let odb = Odb::new(dir.path());
4569 let oid = odb.write(ObjectKind::Blob, b"once").unwrap();
4570 let path = odb.object_path(&oid);
4571 assert!(path.is_file());
4572 odb.reset_exists_probe();
4573 let oid2 = odb.write(ObjectKind::Blob, b"once").unwrap();
4574 assert_eq!(oid, oid2);
4575 assert_eq!(
4576 odb.exists_probe_count(),
4577 0,
4578 "loose fast path must not call exists()"
4579 );
4580 }
4581
4582 #[cfg_attr(
4583 not(feature = "full-tests"),
4584 ignore = "starts system git or is heavy; run with --features full-tests"
4585 )]
4586 #[test]
4587 fn exists_and_write_after_invalidate_packs_use_odb_store() {
4588 let dir = TempDir::new().unwrap();
4589 git_in(dir.path(), &["init", "-q"]);
4590 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4591 git_in(dir.path(), &["config", "user.name", "T"]);
4592 let payload = b"invalidate-pack-exists";
4593 fs::write(dir.path().join("f"), payload).unwrap();
4594 git_in(dir.path(), &["add", "f"]);
4595 git_in(dir.path(), &["commit", "-m", "c"]);
4596 git_in(dir.path(), &["repack", "-ad"]);
4597 let objects = dir.path().join(".git").join("objects");
4598 let odb = Odb::new(&objects);
4599 let oid = odb.hash(ObjectKind::Blob, payload).expect("hash");
4600 assert!(odb.exists(&oid), "blob must live in pack after repack");
4601 let pack_dir = objects.join("pack");
4602 for entry in fs::read_dir(&pack_dir).unwrap().flatten() {
4603 let path = entry.path();
4604 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
4605 fs::remove_file(path).unwrap();
4606 }
4607 }
4608 odb.invalidate_packs();
4609 assert!(
4610 !odb.exists(&oid),
4611 "exists must not read stale legacy pack listing after invalidate_packs"
4612 );
4613 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4614 assert_eq!(oid, oid2);
4615 assert!(
4616 odb.object_path(&oid).is_file(),
4617 "write must materialize loose object when pack copy is gone"
4618 );
4619 odb.read(&oid).expect("read after write");
4620 }
4621
4622 #[cfg_attr(
4623 not(feature = "full-tests"),
4624 ignore = "starts system git or is heavy; run with --features full-tests"
4625 )]
4626 #[test]
4627 fn write_uses_single_exists_probe_when_not_loose() {
4628 let dir = TempDir::new().unwrap();
4629 git_in(dir.path(), &["init", "-q"]);
4630 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4631 git_in(dir.path(), &["config", "user.name", "T"]);
4632 fs::write(dir.path().join("f"), b"probe-once").unwrap();
4633 git_in(dir.path(), &["add", "f"]);
4634 git_in(dir.path(), &["commit", "-m", "c"]);
4635 git_in(dir.path(), &["repack", "-ad"]);
4636 let objects = dir.path().join(".git").join("objects");
4637 let odb = Odb::new(&objects);
4638 let oid = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
4639 assert!(!odb.object_path(&oid).exists());
4640 odb.reset_exists_probe();
4641 let oid2 = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
4642 assert_eq!(oid, oid2);
4643 assert_eq!(
4644 odb.exists_probe_count(),
4645 1,
4646 "pack/alternate write path must call exists() exactly once"
4647 );
4648 }
4649 #[test]
4650 fn write_existing_object_freshens() {
4651 use filetime::FileTime;
4652 use std::fs;
4653 use std::thread;
4654 use std::time::Duration;
4655
4656 let dir = TempDir::new().unwrap();
4657 let odb = Odb::new(dir.path());
4658 let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
4659 let path = odb.object_path(&oid);
4660 thread::sleep(Duration::from_millis(50));
4661 let before = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
4662 filetime::set_file_mtime(&path, before).unwrap();
4663
4664 let _ = odb.write(ObjectKind::Blob, b"hello").unwrap();
4665 let after = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
4666 assert!(
4667 after > before,
4668 "rewriting an existing loose object must freshen its mtime"
4669 );
4670 }
4671
4672 #[test]
4673 fn freshen_pack_once_per_odb() {
4674 use filetime::FileTime;
4675 use flate2::write::ZlibEncoder;
4676 use flate2::Compression;
4677 use std::fs;
4678 use std::io::Write;
4679 use std::thread;
4680 use std::time::Duration;
4681
4682 fn append_pack_object(buf: &mut Vec<u8>, type_code: u8, data: &[u8]) {
4683 let mut size = data.len();
4684 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
4685 size >>= 4;
4686 if size > 0 {
4687 buf.push(first | 0x80);
4688 while size > 0 {
4689 let b = (size & 0x7f) as u8;
4690 size >>= 7;
4691 buf.push(if size > 0 { b | 0x80 } else { b });
4692 }
4693 } else {
4694 buf.push(first);
4695 }
4696 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
4697 enc.write_all(data).unwrap();
4698 buf.extend_from_slice(&enc.finish().unwrap());
4699 }
4700
4701 fn write_v2_idx(
4702 idx_path: &Path,
4703 pack_path: &Path,
4704 entries: &[(ObjectId, u64)],
4705 ) -> Result<()> {
4706 let mut sorted = entries.to_vec();
4707 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
4708 let n = sorted.len();
4709 let mut fanout = [0u32; 256];
4710 for byte in 0u32..256 {
4711 let count = sorted
4712 .iter()
4713 .filter(|(oid, _)| u32::from(oid.as_bytes()[0]) <= byte)
4714 .count();
4715 fanout[byte as usize] = u32::try_from(count).unwrap_or(u32::MAX);
4716 }
4717 let mut buf = Vec::new();
4718 buf.extend_from_slice(b"\xfftOc");
4719 buf.extend_from_slice(&2u32.to_be_bytes());
4720 for f in fanout {
4721 buf.extend_from_slice(&f.to_be_bytes());
4722 }
4723 for (oid, _) in &sorted {
4724 buf.extend_from_slice(oid.as_bytes());
4725 }
4726 for _ in 0..n {
4727 buf.extend_from_slice(&0u32.to_be_bytes());
4728 }
4729 for (_, off) in &sorted {
4730 buf.extend_from_slice(&u32::try_from(*off).unwrap_or(0x8000_0000).to_be_bytes());
4731 }
4732 let pack_bytes = fs::read(pack_path)?;
4733 buf.extend_from_slice(&pack_bytes[pack_bytes.len() - 20..]);
4734 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
4735 fs::write(idx_path, buf)?;
4736 Ok(())
4737 }
4738
4739 let dir = TempDir::new().unwrap();
4740 let objects_dir = dir.path();
4741 let odb = Odb::new(objects_dir);
4742
4743 let oid_a = odb.write(ObjectKind::Blob, b"alpha").unwrap();
4744 let oid_b = odb.write(ObjectKind::Blob, b"beta").unwrap();
4745 fs::remove_file(odb.object_path(&oid_a)).unwrap();
4746 fs::remove_file(odb.object_path(&oid_b)).unwrap();
4747
4748 let mut pack = Vec::new();
4749 pack.extend_from_slice(b"PACK");
4750 pack.extend_from_slice(&2u32.to_be_bytes());
4751 pack.extend_from_slice(&2u32.to_be_bytes());
4752 let off_a = pack.len() as u64;
4753 append_pack_object(&mut pack, 3, b"alpha");
4754 let off_b = pack.len() as u64;
4755 append_pack_object(&mut pack, 3, b"beta");
4756 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
4757
4758 let pack_dir = objects_dir.join("pack");
4759 fs::create_dir_all(&pack_dir).unwrap();
4760 let pack_path = pack_dir.join("test-177.pack");
4761 let idx_path = pack_dir.join("test-177.idx");
4762 fs::write(&pack_path, &pack).unwrap();
4763 write_v2_idx(&idx_path, &pack_path, &[(oid_a, off_a), (oid_b, off_b)]).unwrap();
4764 odb.invalidate_packs();
4765
4766 thread::sleep(Duration::from_millis(50));
4767 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4768
4769 assert!(odb.freshen_object(&oid_a));
4770 let after_first = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4771 assert!(after_first > before);
4772
4773 assert!(odb.freshen_object(&oid_b));
4774 let after_second =
4775 FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4776 assert_eq!(
4777 after_second, after_first,
4778 "second freshen of another object in the same pack must not touch the pack again"
4779 );
4780 }
4781
4782 #[test]
4783 fn write_existing_packed_object_freshens_pack() {
4784 use filetime::FileTime;
4785 use flate2::write::ZlibEncoder;
4786 use flate2::Compression;
4787 use std::fs;
4788 use std::io::Write;
4789 use std::thread;
4790 use std::time::Duration;
4791
4792 let dir = TempDir::new().unwrap();
4793 let objects_dir = dir.path();
4794 let odb = Odb::new(objects_dir);
4795 let oid = odb.write(ObjectKind::Blob, b"packed-only").unwrap();
4796 fs::remove_file(odb.object_path(&oid)).unwrap();
4797
4798 let mut pack = Vec::new();
4799 pack.extend_from_slice(b"PACK");
4800 pack.extend_from_slice(&2u32.to_be_bytes());
4801 pack.extend_from_slice(&1u32.to_be_bytes());
4802 let off = pack.len() as u64;
4803 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
4804 enc.write_all(b"packed-only").unwrap();
4805 let compressed = enc.finish().unwrap();
4806 pack.push(0x30 | 0x03); pack.extend_from_slice(&compressed);
4808 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
4809
4810 let pack_dir = objects_dir.join("pack");
4811 fs::create_dir_all(&pack_dir).unwrap();
4812 let pack_path = pack_dir.join("single-177.pack");
4813 fs::write(&pack_path, &pack).unwrap();
4814 let idx_path = pack_dir.join("single-177.idx");
4816 let mut sorted = [(oid, off)];
4817 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
4818 let mut fanout = [0u32; 256];
4819 for byte in 0u32..256 {
4820 fanout[byte as usize] = if u32::from(oid.as_bytes()[0]) <= byte {
4821 1
4822 } else {
4823 0
4824 };
4825 }
4826 let mut buf = Vec::new();
4827 buf.extend_from_slice(b"\xfftOc");
4828 buf.extend_from_slice(&2u32.to_be_bytes());
4829 for f in fanout {
4830 buf.extend_from_slice(&f.to_be_bytes());
4831 }
4832 buf.extend_from_slice(oid.as_bytes());
4833 buf.extend_from_slice(&0u32.to_be_bytes());
4834 buf.extend_from_slice(&(off as u32).to_be_bytes());
4835 buf.extend_from_slice(&pack[pack.len() - 20..]);
4836 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
4837 fs::write(&idx_path, buf).unwrap();
4838 odb.invalidate_packs();
4839
4840 thread::sleep(Duration::from_millis(50));
4841 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4842
4843 odb.write(ObjectKind::Blob, b"packed-only").unwrap();
4844
4845 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4846 assert!(
4847 after > before,
4848 "write of an existing packed object must freshen the pack mtime"
4849 );
4850 }
4851
4852 #[cfg_attr(
4856 not(feature = "full-tests"),
4857 ignore = "starts system git or is heavy; run with --features full-tests"
4858 )]
4859 #[test]
4860 fn write_materializes_loose_when_pack_freshen_fails() {
4861 let dir = TempDir::new().unwrap();
4862 git_in(dir.path(), &["init", "-q"]);
4863 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4864 git_in(dir.path(), &["config", "user.name", "T"]);
4865 let payload = b"packed-fallback";
4866 fs::write(dir.path().join("f"), payload).unwrap();
4867 git_in(dir.path(), &["add", "f"]);
4868 git_in(dir.path(), &["commit", "-m", "c"]);
4869 git_in(dir.path(), &["repack", "-ad"]);
4870 let objects = dir.path().join(".git").join("objects");
4871 let odb = Odb::new(&objects);
4872 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4873 assert!(!odb.object_path(&oid).exists());
4874 assert!(odb.exists(&oid));
4875
4876 let pack_path = fs::read_dir(objects.join("pack"))
4877 .unwrap()
4878 .flatten()
4879 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
4880 .unwrap()
4881 .path();
4882 fs::remove_file(&pack_path).unwrap();
4883 fs::create_dir(&pack_path).unwrap();
4884 odb.invalidate_packs();
4885 assert!(!odb.freshen_object(&oid));
4886
4887 odb.write(ObjectKind::Blob, payload).unwrap();
4888 assert!(odb.object_path(&oid).exists());
4889 }
4890 #[test]
4891 fn write_with_options_silent_skips_freshen_on_existing_loose_object() {
4892 let dir = TempDir::new().unwrap();
4893 let odb = Odb::new(dir.path());
4894 let oid = odb.write(ObjectKind::Blob, b"stable").unwrap();
4895 let path = odb.object_path(&oid);
4896 let before = std::fs::metadata(&path).unwrap().modified().unwrap();
4897 std::thread::sleep(std::time::Duration::from_millis(20));
4898 let again = odb
4899 .write_with_options(
4900 ObjectKind::Blob,
4901 b"stable",
4902 WriteOptions {
4903 silent: true,
4904 ..WriteOptions::default()
4905 },
4906 )
4907 .unwrap();
4908 assert_eq!(oid, again);
4909 let after = std::fs::metadata(&path).unwrap().modified().unwrap();
4910 assert_eq!(before, after);
4911
4912 let touched = odb
4913 .write_with_options(
4914 ObjectKind::Blob,
4915 b"stable",
4916 WriteOptions {
4917 silent: false,
4918 ..WriteOptions::default()
4919 },
4920 )
4921 .unwrap();
4922 assert_eq!(oid, touched);
4923 let freshened = std::fs::metadata(&path).unwrap().modified().unwrap();
4924 assert!(freshened >= before);
4925 }
4926
4927 #[cfg_attr(
4928 not(feature = "full-tests"),
4929 ignore = "starts system git or is heavy; run with --features full-tests"
4930 )]
4931 #[test]
4932 fn promisor_marker_checked_once_per_prepare() {
4933 use crate::pack::{
4934 clear_pack_cache, pack_cache_test_guard, test_pack_marker_stat_count,
4935 test_reset_pack_marker_stat_count,
4936 };
4937
4938 let _guard = pack_cache_test_guard();
4939 let dir = TempDir::new().unwrap();
4940 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4941 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4942 git_in(dir.path(), &["config", "user.name", "T"]);
4943 for i in 0..64 {
4944 fs::write(dir.path().join(format!("f{i}.txt")), format!("body {i}")).unwrap();
4945 git_in(dir.path(), &["add", "."]);
4946 git_in(dir.path(), &["commit", "-m", &format!("c{i}")]);
4947 }
4948 git_in(dir.path(), &["repack", "-a", "-d"]);
4949 let objects = dir.path().join(".git/objects");
4950 let oid = git_rev_parse_oid(dir.path(), "HEAD");
4951
4952 clear_pack_cache();
4953 test_reset_pack_marker_stat_count();
4954 let pack_count = fs::read_dir(objects.join("pack"))
4955 .unwrap()
4956 .filter_map(|e| e.ok())
4957 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
4958 .count();
4959 let expected_prepare_stats = u64::try_from(pack_count * 2).unwrap();
4960 let odb = Odb::new(&objects);
4961 let before_prepare = test_pack_marker_stat_count();
4962 assert!(odb.exists_local(&oid));
4963 assert_eq!(
4964 test_pack_marker_stat_count().saturating_sub(before_prepare),
4965 expected_prepare_stats,
4966 "prepare should stat promisor and mtimes once per pack"
4967 );
4968
4969 test_reset_pack_marker_stat_count();
4970 for _ in 0..10_000 {
4971 let before_probe = test_pack_marker_stat_count();
4972 assert!(odb.exists_local(&oid));
4973 assert_eq!(
4974 test_pack_marker_stat_count(),
4975 before_probe,
4976 "cached sidecar flags must not stat markers on each exists_local probe"
4977 );
4978 }
4979 }
4980
4981 #[cfg_attr(
4982 not(feature = "full-tests"),
4983 ignore = "starts system git or is heavy; run with --features full-tests"
4984 )]
4985 #[test]
4986 fn promisor_marker_added_after_prepare_is_seen_after_reprepare() {
4987 use crate::pack::{
4988 clear_pack_cache, pack_cache_test_guard, reprepare_pack_directory_on_miss,
4989 };
4990
4991 let _guard = pack_cache_test_guard();
4992 let dir = TempDir::new().unwrap();
4993 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4994 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4995 git_in(dir.path(), &["config", "user.name", "T"]);
4996 fs::write(dir.path().join("one.txt"), b"x").unwrap();
4997 git_in(dir.path(), &["add", "one.txt"]);
4998 git_in(dir.path(), &["commit", "-m", "one"]);
4999 git_in(dir.path(), &["repack", "-a", "-d"]);
5000 let objects = dir.path().join(".git/objects");
5001 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5002
5003 clear_pack_cache();
5004 let odb = Odb::new(&objects);
5005 assert!(odb.exists_local(&oid));
5006
5007 let pack_path = fs::read_dir(objects.join("pack"))
5008 .unwrap()
5009 .filter_map(|e| e.ok())
5010 .map(|e| e.path())
5011 .find(|p| p.extension().is_some_and(|x| x == "pack"))
5012 .unwrap();
5013 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
5014 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now()).unwrap();
5015
5016 assert!(
5017 odb.with_pack_store_for(&objects, || {
5018 reprepare_pack_directory_on_miss(&objects).unwrap()
5019 }),
5020 "pack directory change must require reprepare"
5021 );
5022 assert!(
5023 !odb.exists_local(&oid),
5024 "promisor marker must hide pack objects after reprepare refreshed sidecar flags"
5025 );
5026 }
5027
5028 #[cfg_attr(
5029 not(feature = "full-tests"),
5030 ignore = "starts system git or is heavy; run with --features full-tests"
5031 )]
5032 #[test]
5033 fn git_promisor_marker_pack_not_materialized_for_exists_local() {
5034 use crate::pack::clear_pack_cache;
5035
5036 let dir = TempDir::new().unwrap();
5037 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5038 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5039 git_in(dir.path(), &["config", "user.name", "T"]);
5040 fs::write(dir.path().join("blob.txt"), b"git promisor compat").unwrap();
5041 git_in(dir.path(), &["add", "blob.txt"]);
5042 git_in(dir.path(), &["commit", "-m", "seed"]);
5043 git_in(dir.path(), &["repack", "-a", "-d"]);
5044 let objects = dir.path().join(".git/objects");
5045 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5046 let pack_path = fs::read_dir(objects.join("pack"))
5047 .unwrap()
5048 .filter_map(|e| e.ok())
5049 .map(|e| e.path())
5050 .find(|p| p.extension().is_some_and(|x| x == "pack"))
5051 .unwrap();
5052 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
5053
5054 clear_pack_cache();
5055 let odb = Odb::new(&objects);
5056 assert!(
5057 !odb.exists_local(&oid),
5058 "git-written pack with .promisor must not count as local materialization"
5059 );
5060
5061 let cat = git_run(dir.path(), &["cat-file", "-e", &oid.to_hex()]);
5062 assert!(
5063 cat.status.success(),
5064 "git still resolves the object in a promisor-marked pack"
5065 );
5066 }
5067
5068 #[cfg_attr(
5069 not(feature = "full-tests"),
5070 ignore = "starts system git or is heavy; run with --features full-tests"
5071 )]
5072 #[test]
5073 fn packed_object_read_avoids_loose_path_open() {
5074 use crate::hot_path_test_metrics::HotPathMetricsScope;
5075 use crate::pack::clear_pack_cache;
5076
5077 let dir = TempDir::new().unwrap();
5078 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5079 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5080 git_in(dir.path(), &["config", "user.name", "T"]);
5081 fs::write(dir.path().join("packed.txt"), b"packed-only payload").unwrap();
5082 git_in(dir.path(), &["add", "packed.txt"]);
5083 git_in(dir.path(), &["commit", "-m", "pack"]);
5084 git_in(dir.path(), &["repack", "-a", "-d"]);
5085 let objects = dir.path().join(".git/objects");
5086 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5087
5088 clear_pack_cache();
5089 let odb = Odb::new(&objects);
5090 odb.hot_path_metrics().set_loose_open_counting(true);
5091 odb.hot_path_metrics().reset_loose_path_open_attempts();
5092 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
5093
5094 let obj = odb.read(&oid).expect("packed read");
5095 assert_eq!(obj.kind, ObjectKind::Commit);
5096 assert_eq!(
5097 odb.hot_path_metrics().loose_path_open_attempts(),
5098 0,
5099 "packed objects must not attempt a loose-path open"
5100 );
5101 }
5102
5103 #[test]
5104 fn loose_only_read_records_loose_path_open() {
5105 use crate::hot_path_test_metrics::HotPathMetricsScope;
5106
5107 let dir = TempDir::new().unwrap();
5108 let objects = dir.path().join("objects");
5109 fs::create_dir_all(&objects).unwrap();
5110 let odb = Odb::new(&objects);
5111 let oid = odb
5112 .write(ObjectKind::Blob, b"loose-only")
5113 .expect("write loose");
5114 odb.hot_path_metrics().set_loose_open_counting(true);
5115 odb.hot_path_metrics().reset_loose_path_open_attempts();
5116 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
5117
5118 odb.read(&oid).expect("loose read");
5119 assert_eq!(
5120 odb.hot_path_metrics().loose_path_open_attempts(),
5121 1,
5122 "loose fallback must record a path open attempt"
5123 );
5124 }
5125
5126 #[cfg_attr(
5127 not(feature = "full-tests"),
5128 ignore = "starts system git or is heavy; run with --features full-tests"
5129 )]
5130 #[test]
5131 fn loose_and_packed_copies_return_identical_bytes() {
5132 use crate::pack::clear_pack_cache;
5133
5134 let dir = TempDir::new().unwrap();
5135 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5136 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5137 git_in(dir.path(), &["config", "user.name", "T"]);
5138 fs::write(dir.path().join("dup.txt"), b"duplicate loose and packed\n").unwrap();
5139 git_in(dir.path(), &["add", "dup.txt"]);
5140 git_in(dir.path(), &["commit", "-m", "seed"]);
5141 let objects = dir.path().join(".git/objects");
5142 let tree_oid = git_rev_parse_oid(dir.path(), "HEAD^{tree}");
5143 git_in(dir.path(), &["repack", "-a"]);
5144 clear_pack_cache();
5145 let odb = Odb::new(&objects);
5146 let loose_path = tree_oid.loose_path_in(&objects);
5147 assert!(
5148 loose_path.is_file(),
5149 "loose copy must remain after repack -a"
5150 );
5151 let loose = Odb::read_loose_verify_oid(&loose_path, &tree_oid).expect("loose tree");
5152 let packed = odb.read(&tree_oid).expect("packed tree");
5153 assert_eq!(packed.data, loose.data);
5154 assert_eq!(packed.kind, loose.kind);
5155 }
5156
5157 #[cfg_attr(
5158 not(feature = "full-tests"),
5159 ignore = "starts system git or is heavy; run with --features full-tests"
5160 )]
5161 #[test]
5162 fn freshly_written_loose_object_visible_before_repack() {
5163 let dir = TempDir::new().unwrap();
5164 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5165 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5166 git_in(dir.path(), &["config", "user.name", "T"]);
5167 fs::write(dir.path().join("seed.txt"), b"seed").unwrap();
5168 git_in(dir.path(), &["add", "seed.txt"]);
5169 git_in(dir.path(), &["commit", "-m", "seed"]);
5170 git_in(dir.path(), &["repack", "-a", "-d"]);
5171 let objects = dir.path().join(".git/objects");
5172 let odb = Odb::new(&objects);
5173 let fresh = b"brand-new loose blob\n";
5174 let oid = odb.write(ObjectKind::Blob, fresh).expect("loose write");
5175 let obj = odb.read(&oid).expect("immediate loose read");
5176 assert_eq!(obj.data.as_slice(), fresh);
5177 }
5178
5179 #[cfg_attr(
5180 not(feature = "full-tests"),
5181 ignore = "starts system git or is heavy; run with --features full-tests"
5182 )]
5183 #[test]
5184 fn pack_added_after_first_read_found_via_odb_reprepare() {
5185 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
5186
5187 let _guard = pack_cache_test_guard();
5188 clear_pack_cache();
5189 let dir = TempDir::new().unwrap();
5190 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5191 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5192 git_in(dir.path(), &["config", "user.name", "T"]);
5193 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
5194 git_in(dir.path(), &["add", "a.txt"]);
5195 git_in(dir.path(), &["commit", "-m", "a"]);
5196 git_in(dir.path(), &["repack", "-a", "-d"]);
5197 let objects = dir.path().join(".git/objects");
5198 let pack_dir = objects.join("pack");
5199 let git_dir = dir.path().join(".git");
5200
5201 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
5202 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
5203 odb.read(&warm_oid).expect("warm read");
5204
5205 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
5206 git_in(dir.path(), &["add", "b.txt"]);
5207 git_in(dir.path(), &["commit", "-m", "b"]);
5208 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
5209
5210 std::thread::sleep(std::time::Duration::from_secs(1));
5211 filetime::set_file_mtime(&pack_dir, filetime::FileTime::now()).unwrap();
5212
5213 let obj = odb.read(&new_oid).expect("new pack via reprepare");
5214 assert_eq!(obj.kind, ObjectKind::Commit);
5215 }
5216
5217 #[cfg_attr(
5218 not(feature = "full-tests"),
5219 ignore = "starts system git or is heavy; run with --features full-tests"
5220 )]
5221 #[test]
5222 fn late_midx_after_warm_read_resolves_new_commit() {
5223 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
5224
5225 let _guard = pack_cache_test_guard();
5226 clear_pack_cache();
5227 let dir = TempDir::new().unwrap();
5228 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5229 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5230 git_in(dir.path(), &["config", "user.name", "T"]);
5231 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
5232 git_in(dir.path(), &["add", "a.txt"]);
5233 git_in(dir.path(), &["commit", "-m", "a"]);
5234 git_in(dir.path(), &["repack", "-a", "-d"]);
5235 let objects = dir.path().join(".git/objects");
5236 let git_dir = dir.path().join(".git");
5237 assert!(
5238 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_none(),
5239 "fixture starts without a MIDX"
5240 );
5241
5242 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
5243 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
5244 odb.read(&warm_oid).expect("warm read before MIDX exists");
5245
5246 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
5247 git_in(dir.path(), &["add", "b.txt"]);
5248 git_in(dir.path(), &["commit", "-m", "b"]);
5249 git_in(dir.path(), &["repack", "-a", "-d"]);
5250 git_in(dir.path(), &["multi-pack-index", "write"]);
5251 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
5252
5253 let cat = git_run(dir.path(), &["cat-file", "-e", &new_oid.to_hex()]);
5254 assert!(
5255 cat.status.success(),
5256 "system git must resolve the new commit"
5257 );
5258
5259 let obj = odb
5260 .read(&new_oid)
5261 .expect("MIDX-covered commit after late MIDX write");
5262 assert_eq!(obj.kind, ObjectKind::Commit);
5263 assert!(
5264 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_some(),
5265 "successful read after late MIDX must refresh the tip cache"
5266 );
5267 }
5268}