1pub mod builder;
25mod commit;
26mod peel;
27mod read_store;
28pub mod store;
29mod stream;
30mod tree;
31
32pub(crate) use stream::deflate_stream_to_writer;
33
34#[doc(inline)]
35pub use builder::OdbBuilder;
36
37pub(crate) use store::loose::{
38 decompress_zlib_loose_bytes, for_each_loose_object_id, parse_object_bytes,
39 read_loose_object_info,
40};
41#[doc(inline)]
42pub use store::LooseStore;
43pub use store::{CompositeStore, FilesSource, ObjectStream};
44#[doc(inline)]
45pub use store::{MemoryStore, ObjectStore, WritableObjectStore};
46
47use crate::git_path::CanonicalizeExt;
48use std::collections::{HashMap, HashSet};
49use std::fs;
50use std::hash::{Hash, Hasher};
51use std::ops::ControlFlow;
52use std::path::{Path, PathBuf};
53#[cfg(test)]
54use std::sync::atomic::AtomicUsize;
55use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
56use std::sync::{Arc, Mutex, OnceLock, RwLock};
57
58use crate::config::ConfigSet;
59use crate::diagnostics::NullDiagnostics;
60use crate::error::{Error, Result};
61use crate::hash;
62use crate::midx::validate_midx_referenced_packs_with_diagnostics;
63use crate::objects::{
64 oid_hash_set, HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind, OidHashMap, OidHashSet,
65};
66use crate::pack;
67use crate::pack_store::PackStore;
68use filetime::{set_file_mtime, FileTime};
69use flate2::Compression;
70
71pub(crate) use store::BoundedRead;
72use store::{LocalSizedRead, PackedObjects};
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub(crate) enum UnreachablePackRetention {
77 KeepAll,
79 KeepGraceProtected(std::time::SystemTime),
81 DropAll,
83}
84
85pub(crate) struct RepackPackInventorySnapshot {
87 pub pack_paths: HashSet<PathBuf>,
88 pub kept_at_inventory: HashSet<PathBuf>,
89}
90
91type MemOdbOverlayStore = Arc<RwLock<Option<Arc<MemoryStore>>>>;
92
93struct AlternateSourcesCache {
95 file_generation: u64,
96 env_fingerprint: u64,
97 submodule_generation: u64,
98 sources: Option<Arc<CompositeStore>>,
99}
100
101struct AlternateSourcesKey {
102 file_generation: u64,
103 env_fingerprint: u64,
104 submodule_generation: u64,
105}
106
107struct FileAlternatesCache {
109 generation: u64,
110 snapshot: Option<Arc<Vec<PathBuf>>>,
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
115pub struct WriteOptions {
116 pub silent: bool,
118 pub assume_loose_only_existence: bool,
124 pub trust_new_loose: bool,
127 pub loose_prefix_dirs_ready: bool,
130 pub freshen_existing: bool,
140}
141
142pub const BULK_NEW_LOOSE_WRITE: WriteOptions = WriteOptions {
147 assume_loose_only_existence: true,
148 trust_new_loose: true,
149 silent: false,
150 loose_prefix_dirs_ready: true,
151 freshen_existing: false,
152};
153
154pub const BULK_TRACKED_RESTAGE_LOOSE_WRITE: WriteOptions = WriteOptions {
156 assume_loose_only_existence: true,
157 trust_new_loose: false,
158 silent: false,
159 loose_prefix_dirs_ready: true,
160 freshen_existing: false,
161};
162
163fn exists_materialized_in_objects_dir(objects_dir: &Path, oid: &ObjectId) -> bool {
169 store::local_object_materialized(objects_dir, oid)
170}
171
172fn filter_oids(
174 oids: impl IntoIterator<Item = ObjectId>,
175 mut keep: impl FnMut(&ObjectId) -> Result<bool>,
176) -> Result<Vec<ObjectId>> {
177 oids.into_iter()
178 .filter_map(|oid| keep(&oid).map(|k| k.then_some(oid)).transpose())
179 .collect()
180}
181
182fn objects_dir_has_pack_index_files(objects_dir: &Path) -> bool {
183 let pack_dir = objects_dir.join("pack");
184 let Ok(entries) = fs::read_dir(pack_dir) else {
185 return false;
186 };
187 entries
188 .filter_map(|e| e.ok())
189 .any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
190}
191
192#[derive(Clone)]
212pub struct Odb {
213 objects_dir: PathBuf,
214 work_tree: Option<PathBuf>,
216 submodule_alternate_dirs: Arc<Mutex<Vec<PathBuf>>>,
218 config_git_dir: Option<PathBuf>,
220 core_multi_pack_index_cache: Arc<OnceLock<bool>>,
226 local_pack_indexes_seen: Arc<OnceLock<bool>>,
228 file_alternate_dirs_cache: Arc<RwLock<FileAlternatesCache>>,
230 #[cfg(test)]
231 exists_probe: Arc<AtomicUsize>,
232 #[cfg(test)]
233 pub(crate) hot_path_test_metrics: Arc<crate::hot_path_test_metrics::HotPathTestMetrics>,
234 env_alternate_dirs: Vec<PathBuf>,
236 env_alternate_lazy: Arc<OnceLock<Arc<Vec<PathBuf>>>>,
238 mem_overlay: MemOdbOverlayStore,
243 mem_overlay_active: Arc<AtomicBool>,
245 primary_source: Arc<OnceLock<Arc<FilesSource>>>,
247 alternate_sources_cache: Arc<RwLock<AlternateSourcesCache>>,
249 midx_packs_validated: Arc<OnceLock<()>>,
251 hash_algo_cache: Arc<OnceLock<HashAlgo>>,
255 loose_zlib_cache: Arc<OnceLock<Compression>>,
257 pack_zlib_cache: Arc<OnceLock<Compression>>,
259 big_file_threshold_cache: Arc<OnceLock<u64>>,
261 shared_config_state: Option<crate::repo::RepositoryConfigSnapshot>,
263 delta_base_cache_sync: Arc<OnceLock<()>>,
265 shared_environment: Option<Arc<crate::environment::Environment>>,
267 empty_config_env: Arc<OnceLock<crate::environment::Environment>>,
269 freshened_packs: Arc<Mutex<HashSet<PathBuf>>>,
272 pack_store: Arc<PackStore>,
274 alternate_pack_stores: Arc<Mutex<HashMap<PathBuf, Arc<PackStore>>>>,
276 submodule_sources_generation: Arc<AtomicU64>,
278 custom_primary: Option<Arc<dyn WritableObjectStore>>,
280 extra_read_sources: Vec<Arc<dyn ObjectStore>>,
282 alternates_enabled: bool,
284 attached_commit_graph: Option<Arc<crate::commit_graph_file::CommitGraphChain>>,
286 session_loose_reads: Arc<std::sync::Mutex<SessionLooseReads>>,
288 session_tree_entries: Arc<Mutex<SessionTreeEntries>>,
290 loose_negative_caches: Arc<Mutex<HashMap<PathBuf, store::SharedLooseNegativeCache>>>,
292}
293
294const SESSION_BOUNDED_LOOSE_CACHE_MAX: usize = 64 * 1024;
298
299const SESSION_READ_BLOB_CACHE_MAX: usize = 4 * 1024;
303
304const SESSION_LOOSE_CACHE_MAX_ENTRIES: usize = 256;
306
307const SESSION_LOOSE_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
310
311#[derive(Default)]
314struct SessionLooseReads {
315 objects: OidHashMap<Object>,
316 bytes: usize,
317}
318
319impl SessionLooseReads {
320 fn get(&self, oid: &ObjectId) -> Option<&Object> {
321 self.objects.get(oid)
322 }
323
324 fn insert(&mut self, oid: ObjectId, obj: &Object) {
325 let len = obj.data.len();
326 if len > SESSION_LOOSE_CACHE_MAX_BYTES {
327 return;
328 }
329 if self.objects.len() > SESSION_LOOSE_CACHE_MAX_ENTRIES
330 || self.bytes.saturating_add(len) > SESSION_LOOSE_CACHE_MAX_BYTES
331 {
332 self.objects.clear();
333 self.bytes = 0;
334 }
335 if let Some(old) = self.objects.insert(oid, obj.clone()) {
336 self.bytes = self.bytes.saturating_sub(old.data.len());
337 }
338 self.bytes += len;
339 }
340
341 #[cfg(test)]
342 fn is_empty(&self) -> bool {
343 self.objects.is_empty()
344 }
345}
346
347#[derive(Clone)]
350pub(crate) enum SessionTree {
351 Parsed(Arc<Vec<crate::objects::TreeEntry>>),
353 Raw(Arc<Vec<u8>>),
355}
356
357impl SessionTree {
358 const PARSED_ENTRY_BYTES: usize = 80;
360
361 fn cost(&self) -> usize {
362 match self {
363 Self::Parsed(entries) => entries.len().saturating_mul(Self::PARSED_ENTRY_BYTES),
364 Self::Raw(data) => data.len(),
365 }
366 }
367}
368
369#[derive(Default)]
372struct SessionTreeEntries {
373 trees: OidHashMap<SessionTree>,
374 bytes: usize,
375}
376
377impl SessionTreeEntries {
378 const BYTE_BUDGET: usize = (1 << 16) * SessionTree::PARSED_ENTRY_BYTES;
382
383 fn insert(&mut self, oid: ObjectId, tree: SessionTree) {
384 let cost = tree.cost();
385 if cost > Self::BYTE_BUDGET {
386 return;
387 }
388 if let Some(old) = self.trees.remove(&oid) {
389 self.bytes -= old.cost();
390 }
391 if self.bytes + cost > Self::BYTE_BUDGET {
392 self.trees.clear();
393 self.bytes = 0;
394 }
395 self.bytes += cost;
396 self.trees.insert(oid, tree);
397 }
398}
399
400#[cfg(test)]
401mod session_tree_entries_tests {
402 use super::*;
403 use crate::objects::TreeEntry;
404
405 fn tree(entries: usize) -> SessionTree {
406 let entry = TreeEntry {
407 mode: 0o100644,
408 name: b"f".to_vec(),
409 oid: ObjectId::zero(),
410 };
411 SessionTree::Parsed(Arc::new(vec![entry; entries]))
412 }
413
414 fn raw(bytes: usize) -> SessionTree {
415 SessionTree::Raw(Arc::new(vec![0u8; bytes]))
416 }
417
418 fn oid(n: u8) -> ObjectId {
419 ObjectId::from_bytes(&[n; 20]).expect("oid")
420 }
421
422 const ENTRY_BUDGET: usize = SessionTreeEntries::BYTE_BUDGET / SessionTree::PARSED_ENTRY_BYTES;
423
424 #[test]
425 fn cache_starts_over_when_the_budget_is_exceeded() {
426 let mut cache = SessionTreeEntries::default();
427 let half = ENTRY_BUDGET / 2;
428 cache.insert(oid(1), tree(half));
429 cache.insert(oid(2), tree(half));
430 assert_eq!(cache.bytes, SessionTreeEntries::BYTE_BUDGET);
431 assert_eq!(cache.trees.len(), 2);
432
433 cache.insert(oid(3), tree(1));
434 assert_eq!(cache.trees.len(), 1);
435 assert!(cache.trees.contains_key(&oid(3)));
436 assert_eq!(cache.bytes, SessionTree::PARSED_ENTRY_BYTES);
437
438 cache.insert(oid(4), tree(ENTRY_BUDGET + 1));
439 assert!(
440 !cache.trees.contains_key(&oid(4)),
441 "an oversized tree is not cached"
442 );
443 assert!(cache.trees.contains_key(&oid(3)));
444 assert!(cache.bytes <= SessionTreeEntries::BYTE_BUDGET);
445 }
446
447 #[test]
448 fn raw_trees_count_their_payload_bytes() {
449 let mut cache = SessionTreeEntries::default();
450 cache.insert(oid(1), raw(1000));
451 cache.insert(oid(2), tree(10));
452 assert_eq!(cache.bytes, 1000 + 10 * SessionTree::PARSED_ENTRY_BYTES);
453
454 cache.insert(oid(3), raw(SessionTreeEntries::BYTE_BUDGET + 1));
455 assert!(
456 !cache.trees.contains_key(&oid(3)),
457 "an oversized payload is not cached"
458 );
459 assert_eq!(cache.trees.len(), 2);
460 }
461
462 #[test]
463 fn replacing_a_tree_releases_its_old_cost() {
464 let mut cache = SessionTreeEntries::default();
465 cache.insert(oid(1), raw(4000));
466 cache.insert(oid(1), tree(2));
467 assert_eq!(cache.trees.len(), 1);
468 assert_eq!(cache.bytes, 2 * SessionTree::PARSED_ENTRY_BYTES);
469 assert!(matches!(
470 cache.trees.get(&oid(1)),
471 Some(SessionTree::Parsed(_))
472 ));
473 }
474}
475
476const _: () = {
477 fn assert_send_sync<T: Send + Sync>() {}
478 let _ = assert_send_sync::<Odb>;
479};
480
481impl std::fmt::Debug for Odb {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("Odb")
484 .field("objects_dir", &self.objects_dir)
485 .field("work_tree", &self.work_tree)
486 .field("submodule_alternate_dirs", &"<mutex>")
487 .field("config_git_dir", &self.config_git_dir)
488 .finish()
489 }
490}
491
492#[must_use]
497pub fn hash_algo_for_git_dir(env: &crate::environment::Environment, git_dir: &Path) -> HashAlgo {
498 ConfigSet::load(env, Some(git_dir), true)
499 .ok()
500 .and_then(|cfg| cfg.get("extensions.objectformat"))
501 .and_then(|v| HashAlgo::from_name(&v))
502 .unwrap_or(HashAlgo::Sha1)
503}
504
505#[must_use]
507pub fn hash_algo_for_objects_dir(
508 env: &crate::environment::Environment,
509 objects_dir: &Path,
510) -> HashAlgo {
511 objects_dir
512 .parent()
513 .map(|d| hash_algo_for_git_dir(env, d))
514 .unwrap_or(HashAlgo::Sha1)
515}
516
517impl Odb {
518 #[must_use]
525 pub fn new(objects_dir: &Path) -> Self {
526 Self {
527 objects_dir: objects_dir.to_path_buf(),
528 work_tree: None,
529 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
530 config_git_dir: None,
531 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
532 local_pack_indexes_seen: Arc::new(OnceLock::new()),
533 file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
534 generation: 0,
535 snapshot: None,
536 })),
537 env_alternate_dirs: Vec::new(),
538 env_alternate_lazy: Arc::new(OnceLock::new()),
539 mem_overlay: Arc::new(RwLock::new(None)),
540 mem_overlay_active: Arc::new(AtomicBool::new(false)),
541 primary_source: Arc::new(OnceLock::new()),
542 alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
543 file_generation: 0,
544 env_fingerprint: 0,
545 submodule_generation: 0,
546 sources: None,
547 })),
548 midx_packs_validated: Arc::new(OnceLock::new()),
549 hash_algo_cache: Arc::new(OnceLock::new()),
550 loose_zlib_cache: Arc::new(OnceLock::new()),
551 pack_zlib_cache: Arc::new(OnceLock::new()),
552 big_file_threshold_cache: Arc::new(OnceLock::new()),
553 shared_config_state: None,
554 delta_base_cache_sync: Arc::new(OnceLock::new()),
555 shared_environment: None,
556 empty_config_env: Arc::new(OnceLock::new()),
557 freshened_packs: Arc::new(Mutex::new(HashSet::new())),
558 pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
559 alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
560 submodule_sources_generation: Arc::new(AtomicU64::new(0)),
561 custom_primary: None,
562 extra_read_sources: Vec::new(),
563 alternates_enabled: true,
564 attached_commit_graph: None,
565 session_loose_reads: Arc::new(Mutex::new(SessionLooseReads::default())),
566 session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
567 loose_negative_caches: Arc::new(Mutex::new(HashMap::new())),
568 #[cfg(test)]
569 exists_probe: Arc::new(AtomicUsize::new(0)),
570 #[cfg(test)]
571 hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
572 }
573 }
574
575 #[must_use]
580 pub fn with_work_tree(objects_dir: &Path, work_tree: &Path) -> Self {
581 Self {
582 objects_dir: objects_dir.to_path_buf(),
583 work_tree: Some(work_tree.to_path_buf()),
584 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
585 config_git_dir: None,
586 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
587 local_pack_indexes_seen: Arc::new(OnceLock::new()),
588 file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
589 generation: 0,
590 snapshot: None,
591 })),
592 env_alternate_dirs: Vec::new(),
593 env_alternate_lazy: Arc::new(OnceLock::new()),
594 mem_overlay: Arc::new(RwLock::new(None)),
595 mem_overlay_active: Arc::new(AtomicBool::new(false)),
596 primary_source: Arc::new(OnceLock::new()),
597 alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
598 file_generation: 0,
599 env_fingerprint: 0,
600 submodule_generation: 0,
601 sources: None,
602 })),
603 midx_packs_validated: Arc::new(OnceLock::new()),
604 hash_algo_cache: Arc::new(OnceLock::new()),
605 loose_zlib_cache: Arc::new(OnceLock::new()),
606 pack_zlib_cache: Arc::new(OnceLock::new()),
607 big_file_threshold_cache: Arc::new(OnceLock::new()),
608 shared_config_state: None,
609 delta_base_cache_sync: Arc::new(OnceLock::new()),
610 shared_environment: None,
611 empty_config_env: Arc::new(OnceLock::new()),
612 freshened_packs: Arc::new(Mutex::new(HashSet::new())),
613 pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
614 alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
615 submodule_sources_generation: Arc::new(AtomicU64::new(0)),
616 custom_primary: None,
617 extra_read_sources: Vec::new(),
618 alternates_enabled: true,
619 attached_commit_graph: None,
620 session_loose_reads: Arc::new(Mutex::new(SessionLooseReads::default())),
621 session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
622 loose_negative_caches: Arc::new(Mutex::new(HashMap::new())),
623 #[cfg(test)]
624 exists_probe: Arc::new(AtomicUsize::new(0)),
625 #[cfg(test)]
626 hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
627 }
628 }
629
630 fn session_tree(&self, oid: &ObjectId) -> Option<SessionTree> {
631 self.session_tree_entries
632 .lock()
633 .ok()?
634 .trees
635 .get(oid)
636 .cloned()
637 }
638
639 fn remember_session_tree(&self, oid: &ObjectId, tree: SessionTree) {
640 if let Ok(mut guard) = self.session_tree_entries.lock() {
641 guard.insert(*oid, tree);
642 }
643 }
644
645 fn read_tree_payload(&self, oid: &ObjectId) -> Result<Vec<u8>> {
646 let obj = self.read_impl(oid, false)?;
647 if obj.kind != ObjectKind::Tree {
648 return Err(Error::CorruptObject(format!(
649 "expected tree, got {}",
650 obj.kind.as_str()
651 )));
652 }
653 Ok(obj.data)
654 }
655
656 pub(crate) fn cached_tree_entries(
658 &self,
659 oid: &ObjectId,
660 ) -> Result<Arc<Vec<crate::objects::TreeEntry>>> {
661 let parsed = match self.session_tree(oid) {
662 Some(SessionTree::Parsed(entries)) => return Ok(entries),
663 Some(SessionTree::Raw(data)) => {
664 crate::objects::parse_tree_with_oid_len(&data, self.hash_algo().len())?
665 }
666 None => crate::objects::parse_tree_with_oid_len(
667 &self.read_tree_payload(oid)?,
668 self.hash_algo().len(),
669 )?,
670 };
671 let arc = Arc::new(parsed);
672 self.remember_session_tree(oid, SessionTree::Parsed(Arc::clone(&arc)));
673 Ok(arc)
674 }
675
676 pub(crate) fn cached_tree_for_walk(&self, oid: &ObjectId) -> Result<SessionTree> {
681 if let Some(tree) = self.session_tree(oid) {
682 return Ok(tree);
683 }
684 let tree = SessionTree::Raw(Arc::new(self.read_tree_payload(oid)?));
685 self.remember_session_tree(oid, tree.clone());
686 Ok(tree)
687 }
688
689 fn session_cached_loose_read(&self, oid: &ObjectId) -> Option<Object> {
690 self.session_loose_reads.lock().ok()?.get(oid).cloned()
691 }
692
693 #[cfg(test)]
695 pub(crate) fn session_read_is_cached(&self, oid: &ObjectId) -> bool {
696 self.session_cached_loose_read(oid).is_some()
697 }
698
699 fn remember_session_loose_read(&self, oid: &ObjectId, obj: &Object) {
702 if let Ok(mut guard) = self.session_loose_reads.lock() {
703 guard.insert(*oid, obj);
704 }
705 }
706
707 fn finish_session_loose_read(
715 &self,
716 oid: &ObjectId,
717 obj: Object,
718 remember: bool,
719 ) -> Result<Object> {
720 if remember
721 && (obj.kind != ObjectKind::Blob || obj.data.len() <= SESSION_READ_BLOB_CACHE_MAX)
722 {
723 self.remember_session_loose_read(oid, &obj);
724 }
725 Ok(obj)
726 }
727
728 #[cfg(test)]
730 #[must_use]
731 pub(crate) fn hot_path_metrics(&self) -> &crate::hot_path_test_metrics::HotPathTestMetrics {
732 &self.hot_path_test_metrics
733 }
734
735 #[cfg(test)]
736 #[must_use]
737 pub(crate) fn hot_path_metrics_arc(
738 &self,
739 ) -> Arc<crate::hot_path_test_metrics::HotPathTestMetrics> {
740 Arc::clone(&self.hot_path_test_metrics)
741 }
742
743 #[must_use]
745 pub(crate) fn with_shared_config_state(
746 mut self,
747 state: crate::repo::RepositoryConfigSnapshot,
748 env: Arc<crate::environment::Environment>,
749 ) -> Self {
750 self.pack_store.set_repo_caches(Arc::clone(&state));
751 self.shared_config_state = Some(state);
752 self.shared_environment = Some(env);
753 self
754 }
755
756 fn diagnostics_handle(&self) -> crate::diagnostics::DiagnosticsHandle {
757 self.shared_config_state
758 .as_ref()
759 .map(|state| state.diagnostics_handle())
760 .unwrap_or_else(|| Arc::new(NullDiagnostics))
761 }
762
763 pub(crate) fn load_config_cascade(&self) -> Result<Arc<ConfigSet>> {
769 let git_dir = self
770 .config_git_dir
771 .as_deref()
772 .or_else(|| self.objects_dir.parent());
773 if let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment) {
774 let opts = crate::config::LoadConfigOptions::for_repository(
775 env,
776 git_dir,
777 Some(Arc::clone(caches)),
778 );
779 return caches.load_config_with_options(env.as_ref(), git_dir, &opts);
780 }
781 match git_dir {
782 Some(git_dir) => {
783 ConfigSet::load(self.config_environment(), Some(git_dir), true).map(Arc::new)
784 }
785 None => Ok(Arc::new(ConfigSet::new())),
786 }
787 }
788
789 fn config_environment(&self) -> &crate::environment::Environment {
790 if let Some(env) = self.shared_environment.as_deref() {
791 return env;
792 }
793 self.empty_config_env
794 .get_or_init(crate::environment::Environment::empty)
795 }
796
797 fn env_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
798 if !self.env_alternate_dirs.is_empty() {
799 return Arc::new(self.env_alternate_dirs.clone());
800 }
801 Arc::clone(self.env_alternate_lazy.get_or_init(|| {
802 Arc::new(Self::env_alternate_dirs_from_var(
803 self.config_environment(),
804 self.work_tree.as_deref(),
805 ))
806 }))
807 }
808
809 #[must_use]
813 pub fn env_alternate_dirs_from_var(
814 env: &crate::environment::Environment,
815 resolve_base: Option<&Path>,
816 ) -> Vec<PathBuf> {
817 match env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES") {
818 Some(val) if !val.is_empty() => {
819 let mut dirs = parse_alternate_env(&val);
820 if let Some(base) = resolve_base {
821 for dir in &mut dirs {
822 if dir.is_relative() {
823 *dir = base.join(&dir);
824 }
825 }
826 }
827 dirs
828 }
829 _ => Vec::new(),
830 }
831 }
832
833 #[must_use]
835 pub fn with_env_alternate_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
836 self.env_alternate_dirs = dirs;
837 self.clear_alternate_sources_cache();
838 self
839 }
840
841 fn clear_alternate_sources_cache(&self) {
842 if let Ok(mut guard) = self.alternate_sources_cache.write() {
843 guard.sources = None;
844 }
845 }
846
847 fn alternate_sources_key(&self) -> AlternateSourcesKey {
848 AlternateSourcesKey {
849 file_generation: self
850 .file_alternate_dirs_cache
851 .read()
852 .map(|g| g.generation)
853 .unwrap_or(0),
854 env_fingerprint: self.env_alternate_fingerprint(),
855 submodule_generation: self.submodule_sources_generation.load(Ordering::Relaxed),
856 }
857 }
858
859 fn env_alternate_fingerprint(&self) -> u64 {
860 let mut hasher = std::collections::hash_map::DefaultHasher::new();
861 for path in self.env_alternate_dirs_snapshot().iter() {
862 path.hash(&mut hasher);
863 }
864 hasher.finish()
865 }
866
867 pub fn refresh_file_alternates_from_disk(&self) -> Result<()> {
873 let snapshot = Arc::new(pack::read_alternates_recursive(&self.objects_dir)?);
874 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
875 guard.generation = guard.generation.wrapping_add(1);
876 guard.snapshot = Some(snapshot);
877 }
878 self.clear_alternate_sources_cache();
879 Ok(())
880 }
881
882 pub fn append_file_alternate(&self, alternate_objects_dir: &Path) -> Result<()> {
888 Self::append_alternate_objects_line(&self.objects_dir, alternate_objects_dir)?;
889 self.refresh_file_alternates_from_disk()?;
890 Ok(())
891 }
892
893 pub fn append_alternate_objects_line(
898 objects_dir: &Path,
899 alternate_objects_dir: &Path,
900 ) -> Result<()> {
901 let info = objects_dir.join("info");
902 fs::create_dir_all(&info)?;
903 let alt_path = info.join("alternates");
904 let alt_abs = alternate_objects_dir
905 .canonicalize_simplified()
906 .unwrap_or_else(|_| alternate_objects_dir.to_path_buf());
907 let line = format!("{}\n", alt_abs.display());
908 let existing = match fs::read_to_string(&alt_path) {
909 Ok(s) => s,
910 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
911 Err(e) => return Err(Error::Io(e)),
912 };
913 if existing
914 .lines()
915 .any(|l| l.trim() == alt_abs.to_string_lossy())
916 {
917 return Ok(());
918 }
919 let mut out = existing;
920 out.push_str(&line);
921 fs::write(alt_path, out).map_err(Error::Io)
922 }
923
924 fn file_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
925 loop {
926 let observed_gen = self
927 .file_alternate_dirs_cache
928 .read()
929 .map(|g| g.generation)
930 .unwrap_or(0);
931 if let Ok(guard) = self.file_alternate_dirs_cache.read() {
932 if guard.generation == observed_gen {
933 if let Some(snapshot) = &guard.snapshot {
934 return Arc::clone(snapshot);
935 }
936 }
937 }
938 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
939 if guard.generation != observed_gen {
940 continue;
941 }
942 if let Some(snapshot) = &guard.snapshot {
943 return Arc::clone(snapshot);
944 }
945 let snapshot = Arc::new(
946 pack::read_alternates_recursive(&self.objects_dir).unwrap_or_default(),
947 );
948 guard.snapshot = Some(Arc::clone(&snapshot));
949 return snapshot;
950 }
951 break;
952 }
953 Arc::new(Vec::new())
954 }
955
956 pub(crate) fn local_store_validation_object_dirs(&self) -> Vec<PathBuf> {
960 if !self.uses_files_primary() {
961 return Vec::new();
962 }
963 let mut dirs = Vec::new();
964 if let Some(primary) = self.files_objects_dir() {
965 dirs.push(primary.to_path_buf());
966 }
967 if self.alternates_enabled {
968 for alt in self.file_alternate_dirs_snapshot().iter() {
969 if !dirs.iter().any(|d| d == alt) {
970 dirs.push(alt.clone());
971 }
972 }
973 for alt in self.env_alternate_dirs_snapshot().iter() {
974 if !dirs.iter().any(|d| d == alt) {
975 dirs.push(alt.clone());
976 }
977 }
978 }
979 dirs
980 }
981
982 fn loose_negative_cache_for(&self, objects_dir: &Path) -> store::SharedLooseNegativeCache {
983 let key = objects_dir.to_path_buf();
984 if let Ok(guard) = self.loose_negative_caches.lock() {
985 if let Some(hit) = guard.get(&key) {
986 return Arc::clone(hit);
987 }
988 }
989 let mut guard = self
990 .loose_negative_caches
991 .lock()
992 .unwrap_or_else(|e| e.into_inner());
993 guard
994 .entry(key)
995 .or_insert_with(|| Arc::new(Mutex::new(store::LooseNegativeCache::default())))
996 .clone()
997 }
998
999 #[cfg(test)]
1000 pub(crate) fn exists_probe_count(&self) -> usize {
1001 self.exists_probe.load(Ordering::Relaxed)
1002 }
1003
1004 #[cfg(test)]
1005 pub(crate) fn reset_exists_probe(&self) {
1006 self.exists_probe.store(0, Ordering::Relaxed);
1007 }
1008
1009 pub fn enable_mem_overlay(&self) {
1014 self.mem_overlay_active.store(true, Ordering::Release);
1015 if let Ok(mut guard) = self.mem_overlay.write() {
1016 if guard.is_none() {
1017 *guard = Some(Arc::new(MemoryStore::new(self.hash_algo())));
1018 }
1019 }
1020 }
1021
1022 pub fn disable_mem_overlay(&self) {
1024 self.mem_overlay_active.store(false, Ordering::Release);
1025 if let Ok(mut guard) = self.mem_overlay.write() {
1026 *guard = None;
1027 }
1028 }
1029
1030 pub(crate) fn persist_mem_overlay(&self) -> Result<()> {
1037 use std::ops::ControlFlow;
1038
1039 let store = match self.overlay_store() {
1040 Some(store) => store,
1041 None => return Ok(()),
1042 };
1043 let mut oids = Vec::new();
1044 store.for_each_object(&mut |oid| {
1045 oids.push(*oid);
1046 ControlFlow::Continue(())
1047 })?;
1048 let mut payloads = Vec::with_capacity(oids.len());
1049 for oid in oids {
1050 let obj = store
1051 .read(&oid)?
1052 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))?;
1053 payloads.push((obj.kind, obj.data));
1054 }
1055 self.mem_overlay_active.store(false, Ordering::Release);
1056 if let Ok(mut guard) = self.mem_overlay.write() {
1057 *guard = None;
1058 }
1059 let custom_primary = self.custom_primary.is_some();
1060 for (kind, data) in payloads {
1061 self.write_into_primary(kind, &data)?;
1062 }
1063 if custom_primary {
1064 self.flush()?;
1065 }
1066 Ok(())
1067 }
1068
1069 pub(crate) fn persist_mem_overlay_objects(
1077 &self,
1078 oids: impl IntoIterator<Item = ObjectId>,
1079 ) -> Result<()> {
1080 let store = match self.overlay_store() {
1081 Some(store) => store,
1082 None => return Ok(()),
1083 };
1084 for oid in oids {
1085 if self.exists_local(&oid) {
1086 continue;
1087 }
1088 let Some(obj) = store.read(&oid)? else {
1089 continue;
1090 };
1091 self.write_into_primary(obj.kind, &obj.data)?;
1092 }
1093 if self.custom_primary.is_some() {
1094 self.flush()?;
1095 }
1096 Ok(())
1097 }
1098
1099 #[must_use]
1101 pub(crate) fn is_mem_overlay_active(&self) -> bool {
1102 self.mem_overlay_active.load(Ordering::Acquire)
1103 }
1104
1105 fn overlay_active(&self) -> bool {
1106 self.mem_overlay
1107 .read()
1108 .ok()
1109 .and_then(|g| g.as_ref().map(|_| ()))
1110 .is_some()
1111 }
1112
1113 fn overlay_store(&self) -> Option<Arc<MemoryStore>> {
1114 if !self.mem_overlay_active.load(Ordering::Acquire) {
1115 return None;
1116 }
1117 self.mem_overlay.read().ok().and_then(|g| g.clone())
1118 }
1119
1120 #[cfg(test)]
1122 pub(crate) fn mem_overlay_len_for_tests(&self) -> Option<usize> {
1123 let store = self.overlay_store()?;
1124 let mut count = 0usize;
1125 store
1126 .for_each_object(&mut |_| {
1127 count += 1;
1128 ControlFlow::Continue(())
1129 })
1130 .ok()?;
1131 Some(count)
1132 }
1133
1134 fn overlay_store_object(&self, oid: ObjectId, kind: ObjectKind, data: &[u8]) -> bool {
1137 let Some(store) = self.overlay_store() else {
1138 return false;
1139 };
1140 store.insert_object(oid, kind, Arc::from(data.to_owned()));
1141 true
1142 }
1143
1144 fn overlay_read(&self, oid: &ObjectId) -> Option<Object> {
1146 let store = self.overlay_store()?;
1147 store.read(oid).ok().flatten()
1148 }
1149
1150 pub fn primary(&self) -> Result<Arc<FilesSource>> {
1162 self.require_files_primary("primary")?;
1163 if let Some(src) = self.primary_source.get() {
1164 return Ok(Arc::clone(src));
1165 }
1166 let src = self.build_files_source(
1167 &self.objects_dir,
1168 self.multi_pack_index_reads_enabled(),
1169 self.midx_tip_listing_enabled(),
1170 )?;
1171 match self.primary_source.set(Arc::clone(&src)) {
1172 Ok(()) => Ok(src),
1173 Err(existing) => Ok(existing),
1174 }
1175 }
1176
1177 #[must_use]
1182 pub(crate) fn has_secondary_object_stores(&self) -> bool {
1183 if !self.extra_read_sources.is_empty() {
1184 return true;
1185 }
1186 self.alternates_enabled && self.alternate_object_lookup_needed()
1187 }
1188
1189 #[must_use]
1191 fn alternate_object_lookup_needed(&self) -> bool {
1192 if !self.file_alternate_dirs_snapshot().is_empty() {
1193 return true;
1194 }
1195 if !self.env_alternate_dirs_snapshot().is_empty() {
1196 return true;
1197 }
1198 self.submodule_alternate_dirs
1199 .lock()
1200 .ok()
1201 .is_some_and(|guard| !guard.is_empty())
1202 }
1203
1204 pub fn sources(&self) -> Result<Arc<CompositeStore>> {
1214 if !self.alternates_enabled {
1215 return Ok(Arc::new(CompositeStore::new(Vec::new(), self.hash_algo())));
1216 }
1217 let key = self.alternate_sources_key();
1218 if let Ok(guard) = self.alternate_sources_cache.read() {
1219 if guard.file_generation == key.file_generation
1220 && guard.env_fingerprint == key.env_fingerprint
1221 && guard.submodule_generation == key.submodule_generation
1222 {
1223 if let Some(sources) = guard.sources.as_ref() {
1224 return Ok(Arc::clone(sources));
1225 }
1226 }
1227 }
1228 let use_midx = self.multi_pack_index_reads_enabled();
1229 let midx_tip = self.midx_tip_listing_enabled();
1230 let mut stores: Vec<Arc<dyn ObjectStore>> = Vec::new();
1231 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
1232 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
1233 }
1234 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1235 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
1236 }
1237 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1238 for alt_dir in guard.iter() {
1239 stores.push(self.build_files_source(alt_dir, false, false)?);
1240 }
1241 }
1242 let composite = Arc::new(CompositeStore::new(stores, self.hash_algo()));
1243 if let Ok(mut guard) = self.alternate_sources_cache.write() {
1244 guard.file_generation = key.file_generation;
1245 guard.env_fingerprint = key.env_fingerprint;
1246 guard.submodule_generation = key.submodule_generation;
1247 guard.sources = Some(Arc::clone(&composite));
1248 }
1249 Ok(composite)
1250 }
1251
1252 fn midx_tip_listing_enabled(&self) -> bool {
1253 self.config_git_dir.is_some() && self.core_multi_pack_index_enabled()
1254 }
1255
1256 fn build_files_source(
1257 &self,
1258 objects_dir: &Path,
1259 use_midx: bool,
1260 midx_tip_listing_for_exists: bool,
1261 ) -> Result<Arc<FilesSource>> {
1262 let loose = self.loose_at(objects_dir)?;
1263 let pack_store = self.pack_store_for(objects_dir);
1264 let diagnostics = if objects_dir == self.objects_dir.as_path() {
1265 self.diagnostics_handle()
1266 } else {
1267 Arc::new(NullDiagnostics)
1268 };
1269 FilesSource::open(
1270 pack_store,
1271 loose,
1272 use_midx,
1273 midx_tip_listing_for_exists,
1274 diagnostics,
1275 )
1276 .map(Arc::new)
1277 }
1278
1279 pub fn register_submodule_object_directories_from_index(
1283 &self,
1284 work_tree: &Path,
1285 index: &crate::index::Index,
1286 ) {
1287 use crate::diff::submodule_embedded_git_dir;
1288
1289 let Ok(mut dirs) = self.submodule_alternate_dirs.lock() else {
1290 return;
1291 };
1292 dirs.clear();
1293 for e in &index.entries {
1294 if e.stage() != 0 || e.mode != crate::index::MODE_GITLINK {
1295 continue;
1296 }
1297 let path_str = String::from_utf8_lossy(&e.path);
1298 let abs = work_tree.join(path_str.as_ref());
1299 let Some(sub_git) = submodule_embedded_git_dir(&abs) else {
1300 continue;
1301 };
1302 let objects = sub_git.join("objects");
1303 if !objects.is_dir() {
1304 continue;
1305 }
1306 let canon = objects.canonicalize_simplified().unwrap_or(objects);
1307 if !dirs.iter().any(|p| p == &canon) {
1308 dirs.push(canon);
1309 }
1310 }
1311 self.submodule_sources_generation
1312 .fetch_add(1, Ordering::Relaxed);
1313 self.clear_alternate_sources_cache();
1314 }
1315
1316 #[must_use]
1318 pub fn with_config_git_dir(mut self, git_dir: PathBuf) -> Self {
1319 self.config_git_dir = Some(git_dir);
1320 self
1321 }
1322
1323 #[must_use]
1335 pub fn hash_algo(&self) -> HashAlgo {
1336 if let Some(primary) = &self.custom_primary {
1337 return primary.hash_algo();
1338 }
1339 *self.hash_algo_cache.get_or_init(|| {
1340 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1341 return HashAlgo::Sha1;
1342 }
1343 let cfg = self.load_config_cascade().unwrap_or_default();
1344 cfg.get("extensions.objectformat")
1345 .and_then(|v| HashAlgo::from_name(&v))
1346 .unwrap_or(HashAlgo::Sha1)
1347 })
1348 }
1349
1350 fn resolve_loose_zlib_level(&self) -> Result<u32> {
1351 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1352 return Ok(ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT as u32);
1353 }
1354 let cfg = self.load_config_cascade()?;
1355 Ok(cfg.loose_objects_zlib_level()? as u32)
1356 }
1357
1358 pub(crate) fn loose_compression(&self) -> Result<Compression> {
1365 if let Some(cached) = self.loose_zlib_cache.get() {
1366 return Ok(*cached);
1367 }
1368 let level = self.resolve_loose_zlib_level()?;
1369 let compression = Compression::new(level);
1370 let _ = self.loose_zlib_cache.set(compression);
1371 Ok(compression)
1372 }
1373
1374 fn resolve_pack_zlib_level(&self) -> Result<i32> {
1375 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1376 return Ok(6);
1377 }
1378 let cfg = self.load_config_cascade()?;
1379 cfg.pack_objects_zlib_level()
1380 }
1381
1382 pub(crate) fn pack_compression(&self) -> Result<Compression> {
1388 if let Some(cached) = self.pack_zlib_cache.get() {
1389 return Ok(*cached);
1390 }
1391 let level = self.resolve_pack_zlib_level()?;
1392 let compression = crate::pack_zlib::flate2_compression_for_pack_level(level);
1393 let _ = self.pack_zlib_cache.set(compression);
1394 Ok(compression)
1395 }
1396
1397 pub fn big_file_threshold(&self) -> Result<u64> {
1408 if let Some(cached) = self.big_file_threshold_cache.get() {
1409 return Ok(*cached);
1410 }
1411 let threshold = if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1412 ConfigSet::BIG_FILE_THRESHOLD_DEFAULT
1413 } else {
1414 self.load_config_cascade()?.big_file_threshold()?
1415 };
1416 let _ = self.big_file_threshold_cache.set(threshold);
1417 Ok(threshold)
1418 }
1419
1420 pub(crate) fn with_bounded_pack_cache(&self) -> Self {
1424 let mut odb = self.clone();
1425 odb.pack_store = Arc::new(PackStore::for_bounded_reads(self.objects_dir.clone()));
1426 odb.primary_source = Arc::new(OnceLock::new());
1427 odb.session_loose_reads = Arc::new(Mutex::new(SessionLooseReads::default()));
1428 odb.alternate_pack_stores = Arc::new(Mutex::new(HashMap::new()));
1429 odb.alternate_sources_cache = Arc::new(RwLock::new(AlternateSourcesCache {
1430 file_generation: 0,
1431 env_fingerprint: 0,
1432 submodule_generation: 0,
1433 sources: None,
1434 }));
1435 odb
1436 }
1437
1438 fn sync_delta_base_cache_limit(&self) {
1439 if self.pack_store.bounded_reads() {
1440 return;
1441 }
1442 self.delta_base_cache_sync.get_or_init(|| {
1443 if self.config_git_dir.is_some() {
1444 let cfg = self.load_config_cascade().unwrap_or_default();
1445 self.pack_store
1446 .configure_delta_base_from_config(Some(cfg.as_ref()));
1447 }
1448 });
1449 }
1450
1451 pub(crate) fn pack_store_for(&self, objects_dir: &Path) -> Arc<PackStore> {
1452 if objects_dir == self.objects_dir.as_path() {
1453 return Arc::clone(&self.pack_store);
1454 }
1455 let new_store = || {
1456 Arc::new(if self.pack_store.bounded_reads() {
1457 PackStore::for_bounded_reads(objects_dir.to_path_buf())
1458 } else {
1459 PackStore::new(objects_dir.to_path_buf())
1460 })
1461 };
1462 if let Ok(mut guard) = self.alternate_pack_stores.lock() {
1463 return Arc::clone(
1464 guard
1465 .entry(objects_dir.to_path_buf())
1466 .or_insert_with(new_store),
1467 );
1468 }
1469 new_store()
1470 }
1471
1472 pub fn pack_store(&self) -> Result<&Arc<PackStore>> {
1474 self.require_files_primary("pack_store")?;
1475 Ok(&self.pack_store)
1476 }
1477
1478 pub fn largest_pack_index_path(&self) -> Result<Option<PathBuf>> {
1487 self.require_files_primary("largest_pack_index_path")?;
1488 let mut indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
1489 indexes.sort_by_key(|idx| std::cmp::Reverse(idx.len()));
1490 Ok(indexes.first().map(|idx| idx.idx_path.clone()))
1491 }
1492
1493 pub(crate) fn clear_delta_base_cache(&self) {
1497 self.pack_store.delta_bases().clear();
1498 }
1499
1500 pub fn invalidate_packs(&self) {
1507 self.pack_store.invalidate_all();
1508 PackStore::invalidate_legacy_for_objects_dir(&self.objects_dir);
1509 if let Ok(guard) = self.alternate_pack_stores.lock() {
1510 for store in guard.values() {
1511 store.invalidate_all();
1512 PackStore::invalidate_legacy_for_objects_dir(store.objects_dir());
1513 }
1514 }
1515 self.invalidate_change_id_header_probe();
1516 }
1517
1518 fn invalidate_change_id_header_probe(&self) {
1519 if let Some(caches) = self.shared_config_state.as_ref() {
1520 crate::change_id::invalidate_stored_change_id_header_probe(caches, self.objects_dir());
1521 }
1522 }
1523
1524 fn note_commit_object_written(&self, kind: ObjectKind) {
1525 if kind == ObjectKind::Commit {
1526 self.invalidate_change_id_header_probe();
1527 }
1528 }
1529
1530 pub(crate) fn loosen_unreachable_from_superseded_packs(
1544 &self,
1545 pack_dir: &Path,
1546 keep_pack: &Path,
1547 reachable: &crate::gc::LazyReachable<'_>,
1548 retention: UnreachablePackRetention,
1549 retained_packs: &HashSet<PathBuf>,
1550 inventory: &RepackPackInventorySnapshot,
1551 ) -> Result<usize> {
1552 if matches!(retention, UnreachablePackRetention::DropAll) {
1553 return Ok(0);
1554 }
1555 self.require_files_primary("repack")?;
1556 let keep_idx = keep_pack.with_extension("idx");
1557 let keep_pack_oids = if keep_idx.is_file() {
1558 PackedObjects::object_ids_from_index_path(&keep_idx)?
1559 } else {
1560 HashSet::new()
1561 };
1562 let kept_pack_oids =
1563 oids_in_inventory_kept_packs(pack_dir, keep_pack, &inventory.kept_at_inventory)?;
1564 let mut loosened = 0usize;
1565 for (idx_path, pack_mtime) in
1566 superseded_pack_indexes(pack_dir, keep_pack, retained_packs, inventory)?
1567 {
1568 let oids = PackedObjects::object_ids_from_index_path(&idx_path)?;
1569 for oid in oids {
1570 let keep = match retention {
1571 UnreachablePackRetention::KeepAll => true,
1572 UnreachablePackRetention::KeepGraceProtected(cutoff) => pack_mtime >= cutoff,
1573 UnreachablePackRetention::DropAll => false,
1574 };
1575 if !keep {
1576 continue;
1577 }
1578 if keep_pack_oids.contains(&oid) || kept_pack_oids.contains(&oid) {
1579 continue;
1580 }
1581 if reachable.get()?.contains(&oid) {
1582 continue;
1583 }
1584 let loose_path = oid.loose_path_in(&self.objects_dir);
1585 let pack_ft = system_time_to_file_time(pack_mtime);
1586 match retention {
1587 UnreachablePackRetention::KeepGraceProtected(cutoff) => {
1588 if loose_path.is_file() {
1589 let loose_mtime = fs::metadata(&loose_path)
1590 .and_then(|m| m.modified())
1591 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1592 if loose_mtime >= cutoff {
1593 continue;
1594 }
1595 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1596 loosened += 1;
1597 continue;
1598 }
1599 }
1600 UnreachablePackRetention::KeepAll => {
1601 if loose_path.is_file() {
1602 continue;
1603 }
1604 }
1605 UnreachablePackRetention::DropAll => {}
1606 }
1607 let obj = self.read(&oid)?;
1608 self.write_loose_materialize(obj.kind, &obj.data)?;
1609 let primary = self.primary()?;
1610 let loose_path = primary.loose_store().object_path(&oid);
1611 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1612 loosened += 1;
1613 }
1614 }
1615 Ok(loosened)
1616 }
1617
1618 fn core_multi_pack_index_enabled(&self) -> bool {
1619 *self.core_multi_pack_index_cache.get_or_init(|| {
1623 if self.config_git_dir.is_none() {
1624 return false;
1625 }
1626 let cfg = self.load_config_cascade().unwrap_or_default();
1627 match cfg.get_bool("core.multiPackIndex") {
1628 Some(Ok(b)) => b,
1629 Some(Err(_)) => true,
1630 None => true,
1631 }
1632 })
1633 }
1634
1635 fn multi_pack_index_reads_enabled(&self) -> bool {
1636 if self.custom_primary.is_some() {
1637 return false;
1638 }
1639 if self.config_git_dir.is_none() {
1640 return false;
1641 }
1642 if !*self
1643 .local_pack_indexes_seen
1644 .get_or_init(|| objects_dir_has_pack_index_files(&self.objects_dir))
1645 {
1646 return false;
1647 }
1648 self.core_multi_pack_index_enabled()
1649 }
1650
1651 fn ensure_midx_prepared(&self) {
1652 if !self.multi_pack_index_reads_enabled() {
1653 return;
1654 }
1655 let _ = self.midx_packs_validated.get_or_init(|| {
1656 let diagnostics = self.diagnostics_handle();
1657 self.with_pack_store_for(&self.objects_dir, || {
1658 validate_midx_referenced_packs_with_diagnostics(
1659 &self.objects_dir,
1660 diagnostics.as_ref(),
1661 );
1662 });
1663 });
1664 }
1665
1666 fn try_read_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<Object>> {
1671 if self.custom_primary.is_some()
1672 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1673 || self.multi_pack_index_reads_enabled()
1674 {
1675 return Ok(None);
1676 }
1677 self.sync_delta_base_cache_limit();
1678 match pack::try_read_object_from_packs_with_options(
1679 &self.objects_dir,
1680 oid,
1681 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1682 ) {
1683 Ok(obj) => Ok(Some(obj)),
1684 Err(Error::ObjectNotFound(_)) => Ok(None),
1685 Err(err) => Err(err),
1686 }
1687 }
1688
1689 fn try_read_info_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1690 if self.custom_primary.is_some()
1691 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1692 || self.multi_pack_index_reads_enabled()
1693 {
1694 return Ok(None);
1695 }
1696 match pack::try_read_object_info_from_packs_with_options(
1697 &self.objects_dir,
1698 oid,
1699 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1700 ) {
1701 Ok(info) => Ok(Some(info)),
1702 Err(Error::ObjectNotFound(_)) => Ok(None),
1703 Err(err) => Err(err),
1704 }
1705 }
1706
1707 #[must_use]
1714 pub fn objects_dir(&self) -> &Path {
1715 &self.objects_dir
1716 }
1717
1718 pub(crate) fn commit_graph_chain(
1729 &self,
1730 ) -> Option<Arc<crate::commit_graph_file::CommitGraphChain>> {
1731 if let Some(chain) = &self.attached_commit_graph {
1732 return Some(Arc::clone(chain));
1733 }
1734 if self.custom_primary.is_some() {
1735 return None;
1736 }
1737 self.shared_config_state
1738 .as_ref()?
1739 .commit_graph_chain(self.objects_dir())
1740 }
1741
1742 fn try_read_extra_sources(&self, oid: &ObjectId) -> Result<Option<Object>> {
1743 for src in &self.extra_read_sources {
1744 if let Some(obj) = src.read(oid)? {
1745 return Ok(Some(obj));
1746 }
1747 }
1748 Ok(None)
1749 }
1750
1751 fn try_read_info_extra_sources(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1752 for src in &self.extra_read_sources {
1753 if let Some(info) = src.read_info(oid)? {
1754 return Ok(Some(info));
1755 }
1756 }
1757 Ok(None)
1758 }
1759
1760 fn try_open_stream_extra_sources(
1761 &self,
1762 oid: &ObjectId,
1763 ) -> Result<Option<store::ObjectStream<'_>>> {
1764 for src in &self.extra_read_sources {
1765 if let Some(stream) = src.open_stream(oid)? {
1766 return Ok(Some(stream));
1767 }
1768 }
1769 Ok(None)
1770 }
1771
1772 #[must_use]
1778 pub fn config_git_dir(&self) -> Option<&Path> {
1779 self.config_git_dir.as_deref()
1780 }
1781
1782 #[must_use]
1784 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
1785 oid.loose_path_in(&self.objects_dir)
1786 }
1787
1788 pub(crate) fn local_pack_indexes(&self) -> Result<Vec<Arc<pack::PackIndex>>> {
1795 self.with_primary_read_context(|| pack::read_local_pack_indexes_cached(&self.objects_dir))
1798 }
1799
1800 pub(crate) fn contained_in_alternate_pack(&self, oid: &ObjectId) -> Result<bool> {
1808 if !self.alternates_enabled {
1809 return Ok(false);
1810 }
1811 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
1812 if store::object_in_non_promisor_pack(alt_dir, oid) {
1813 return Ok(true);
1814 }
1815 }
1816 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1817 if store::object_in_non_promisor_pack(alt_dir, oid) {
1818 return Ok(true);
1819 }
1820 }
1821 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1822 for alt_dir in guard.iter() {
1823 if store::object_in_non_promisor_pack(alt_dir, oid) {
1824 return Ok(true);
1825 }
1826 }
1827 }
1828 Ok(false)
1829 }
1830
1831 pub(crate) fn remove_local_loose(&self, oid: &ObjectId) -> Result<bool> {
1840 match fs::remove_file(self.object_path(oid)) {
1841 Ok(()) => Ok(true),
1842 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
1843 Err(err) => Err(Error::Io(err)),
1844 }
1845 }
1846
1847 pub(crate) fn local_loose_file_size(&self, oid: &ObjectId) -> Option<u64> {
1850 fs::metadata(self.object_path(oid))
1851 .ok()
1852 .filter(fs::Metadata::is_file)
1853 .map(|meta| meta.len())
1854 }
1855
1856 #[must_use]
1872 pub fn exists_local(&self, oid: &ObjectId) -> bool {
1873 self.try_exists_local(oid).unwrap_or(false)
1874 }
1875
1876 pub(crate) fn try_exists_local(&self, oid: &ObjectId) -> Result<bool> {
1878 if self.is_empty_tree(oid) {
1879 return Ok(true);
1880 }
1881 if let Some(primary) = &self.custom_primary {
1882 return primary.contains(oid);
1883 }
1884 Ok(self.with_pack_store_for(&self.objects_dir, || {
1885 exists_materialized_in_objects_dir(&self.objects_dir, oid)
1886 }))
1887 }
1888
1889 #[must_use]
1897 pub fn exists_stored(&self, oid: &ObjectId) -> bool {
1898 self.try_exists_stored(oid).unwrap_or(false)
1899 }
1900
1901 pub(crate) fn try_exists_stored(&self, oid: &ObjectId) -> Result<bool> {
1903 if oid.is_canonical_empty_tree() || self.overlay_read(oid).is_some() {
1904 return Ok(true);
1905 }
1906 if let Some(primary) = &self.custom_primary {
1907 return primary.contains(oid);
1908 }
1909 Ok(self.with_pack_store_for(&self.objects_dir, || {
1910 store::local_object_stored(&self.objects_dir, oid)
1911 }))
1912 }
1913
1914 #[must_use]
1924 pub fn exists_materialized_local(&self, oid: &ObjectId) -> bool {
1925 if let Some(primary) = &self.custom_primary {
1926 return primary.contains(oid).unwrap_or(false);
1927 }
1928 self.with_pack_store_for(&self.objects_dir, || {
1929 exists_materialized_in_objects_dir(&self.objects_dir, oid)
1930 })
1931 }
1932
1933 pub(crate) fn reprepare_local_packs(&self) {
1938 if self.custom_primary.is_some() {
1939 return;
1940 }
1941 self.with_pack_store_for(&self.objects_dir, || {
1942 let _ = crate::pack::reprepare_pack_directory_on_miss(&self.objects_dir);
1943 });
1944 }
1945
1946 pub(crate) fn exists_materialized_local_without_rescan(&self, oid: &ObjectId) -> bool {
1954 if self.custom_primary.is_some() {
1955 return self.exists_materialized_local(oid);
1956 }
1957 self.with_pack_store_for(&self.objects_dir, || {
1958 store::local_object_materialized_without_rescan(&self.objects_dir, oid)
1959 })
1960 }
1961
1962 pub(crate) fn custom_primary_is_empty(&self) -> Result<bool> {
1971 use std::ops::ControlFlow;
1972 let Some(primary) = &self.custom_primary else {
1973 return Ok(false);
1974 };
1975 let mut empty = true;
1976 primary.for_each_object(&mut |oid| {
1977 let _ = oid;
1978 empty = false;
1979 ControlFlow::Break(())
1980 })?;
1981 Ok(empty)
1982 }
1983
1984 pub(crate) fn missing_materialized_many(&self, oids: &[ObjectId]) -> Result<Vec<ObjectId>> {
1995 let primary =
1996 self.custom_primary
1997 .as_ref()
1998 .ok_or_else(|| Error::UnsupportedObjectStore {
1999 operation: "missing_materialized_many",
2000 })?;
2001 if oids.is_empty() {
2002 return Ok(Vec::new());
2003 }
2004 let present = primary.contains_many(oids)?;
2005 Ok(oids
2006 .iter()
2007 .zip(present)
2008 .filter_map(|(oid, has)| (!has).then_some(*oid))
2009 .collect())
2010 }
2011
2012 pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
2024 self.probe_sources(oid, Err)
2025 }
2026
2027 fn probe_sources(
2030 &self,
2031 oid: &ObjectId,
2032 mut on_err: impl FnMut(Error) -> Result<()>,
2033 ) -> Result<bool> {
2034 #[cfg(test)]
2035 self.exists_probe.fetch_add(1, Ordering::Relaxed);
2036 if self.is_empty_tree(oid) || self.overlay_read(oid).is_some() {
2037 return Ok(true);
2038 }
2039 let mut found = |probe: Result<bool>| match probe {
2040 Ok(present) => Ok(present),
2041 Err(err) => on_err(err).map(|()| false),
2042 };
2043 if found(self.primary_writable().and_then(|p| p.contains(oid)))? {
2044 return Ok(true);
2045 }
2046 for src in &self.extra_read_sources {
2047 if found(src.contains(oid))? {
2048 return Ok(true);
2049 }
2050 }
2051 if !self.alternate_object_lookup_needed() {
2052 return Ok(false);
2053 }
2054 found(self.sources().and_then(|s| s.contains(oid)))
2055 }
2056
2057 pub(crate) fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
2067 #[cfg(test)]
2068 self.exists_probe.fetch_add(oids.len(), Ordering::Relaxed);
2069 let mut present: Vec<bool> = oids
2070 .iter()
2071 .map(|oid| self.is_empty_tree(oid) || self.overlay_read(oid).is_some())
2072 .collect();
2073 let mut pending: Vec<usize> = (0..oids.len()).filter(|&i| !present[i]).collect();
2074 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
2075 self.primary_writable()?.contains_many(batch)
2076 })?;
2077 for src in &self.extra_read_sources {
2078 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
2079 src.contains_many(batch)
2080 })?;
2081 }
2082 if !pending.is_empty() && self.alternate_object_lookup_needed() {
2083 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
2084 self.sources()?.contains_many(batch)
2085 })?;
2086 }
2087 Ok(present)
2088 }
2089
2090 pub(crate) fn present_oids(
2096 &self,
2097 oids: impl IntoIterator<Item = ObjectId>,
2098 ) -> Result<Vec<ObjectId>> {
2099 self.partition_oids(oids, true)
2100 }
2101
2102 pub(crate) fn missing_oids(
2108 &self,
2109 oids: impl IntoIterator<Item = ObjectId>,
2110 ) -> Result<Vec<ObjectId>> {
2111 self.partition_oids(oids, false)
2112 }
2113
2114 fn partition_oids(
2116 &self,
2117 oids: impl IntoIterator<Item = ObjectId>,
2118 want_present: bool,
2119 ) -> Result<Vec<ObjectId>> {
2120 let oids: Vec<ObjectId> = oids.into_iter().collect();
2121 let present = self.contains_many(&oids)?;
2122 Ok(oids
2123 .into_iter()
2124 .zip(present)
2125 .filter_map(|(oid, has)| (has == want_present).then_some(oid))
2126 .collect())
2127 }
2128
2129 pub(crate) fn missing_stored_oids(
2139 &self,
2140 oids: impl IntoIterator<Item = ObjectId>,
2141 ) -> Result<Vec<ObjectId>> {
2142 if self.custom_primary.is_some() {
2143 return filter_oids(oids, |oid| self.try_exists_stored(oid).map(|has| !has));
2144 }
2145 self.reprepare_local_packs();
2146 self.with_pack_store_for(&self.objects_dir, || {
2147 filter_oids(oids, |oid| {
2148 Ok(!(oid.is_canonical_empty_tree()
2149 || self.overlay_read(oid).is_some()
2150 || store::local_object_stored_without_rescan(&self.objects_dir, oid)))
2151 })
2152 })
2153 }
2154
2155 #[must_use]
2162 pub fn exists(&self, oid: &ObjectId) -> bool {
2163 self.probe_sources(oid, |_| Ok(())).unwrap_or(false)
2164 }
2165
2166 fn with_pack_store_for<R>(&self, objects_dir: &Path, f: impl FnOnce() -> R) -> R {
2168 let store = self.pack_store_for(objects_dir);
2169 PackStore::with_context(store, f)
2170 }
2171
2172 fn with_primary_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
2178 if self.custom_primary.is_some()
2179 || PackStore::read_context_matches_objects_dir(&self.objects_dir)
2180 {
2181 f()
2182 } else {
2183 self.with_pack_store_for(&self.objects_dir, f)
2184 }
2185 }
2186
2187 pub fn with_pack_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
2192 self.with_pack_store_for(&self.objects_dir, f)
2193 }
2194
2195 pub(crate) fn pack_read_context_guard(&self) -> crate::pack_store::PackReadContextGuard {
2198 crate::pack_store::PackReadContextGuard::install(self.pack_store_for(&self.objects_dir))
2199 }
2200
2201 pub(crate) fn open_session_pack_reader(&self) -> Result<Option<pack::SingleLocalPackReader>> {
2209 if self.custom_primary.is_some()
2210 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
2211 {
2212 return Ok(None);
2213 }
2214 self.sync_delta_base_cache_limit();
2215 pack::SingleLocalPackReader::open_allow_loose(&self.objects_dir)
2216 }
2217
2218 #[must_use]
2223 pub fn freshen_object(&self, oid: &ObjectId) -> bool {
2224 if self.is_empty_tree(oid) {
2225 return false;
2226 }
2227
2228 let loose = self.object_path(oid);
2229 if loose.is_file() {
2230 return self.touch_object_mtime(&loose).is_some();
2231 }
2232 self.freshen_packed_or_alternate(oid)
2233 }
2234
2235 fn freshen_packed_or_alternate(&self, oid: &ObjectId) -> bool {
2239 if self.is_empty_tree(oid) {
2240 return false;
2241 }
2242 if self.freshen_packed_in_objects_dir(&self.objects_dir, oid) {
2243 return true;
2244 }
2245
2246 let file_alts = self.file_alternate_dirs_snapshot();
2247 for alt_dir in file_alts.iter() {
2248 if self.freshen_object_in_objects_dir(alt_dir, oid) {
2249 return true;
2250 }
2251 }
2252
2253 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
2254 if self.freshen_object_in_objects_dir(alt_dir, oid) {
2255 return true;
2256 }
2257 }
2258
2259 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
2260 for alt_dir in guard.iter() {
2261 if self.freshen_object_in_objects_dir(alt_dir, oid) {
2262 return true;
2263 }
2264 }
2265 }
2266
2267 false
2268 }
2269
2270 fn freshen_object_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
2271 let loose = objects_dir
2272 .join(oid.loose_prefix())
2273 .join(oid.loose_suffix());
2274 if loose.is_file() {
2275 return self.touch_object_mtime(&loose).is_some();
2276 }
2277 self.freshen_packed_in_objects_dir(objects_dir, oid)
2278 }
2279
2280 fn freshen_packed_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
2281 self.with_pack_store_for(objects_dir, || {
2282 let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
2283 return false;
2284 };
2285 for idx in &indexes {
2286 if idx.contains(oid).unwrap_or(false) {
2287 return self.freshen_pack_once(&idx.pack_path, idx.is_cruft);
2288 }
2289 }
2290 false
2291 })
2292 }
2293
2294 fn freshen_pack_once(&self, pack_path: &Path, is_cruft: bool) -> bool {
2299 let Ok(mut guard) = self.freshened_packs.lock() else {
2300 return false;
2301 };
2302 if guard.contains(pack_path) {
2303 return true;
2304 }
2305 if is_cruft {
2306 return false;
2307 }
2308 let Some(touched_at) = self.touch_object_mtime(pack_path) else {
2309 return false;
2310 };
2311 pack::refresh_pack_bytes_signature(pack_path, touched_at);
2312 guard.insert(pack_path.to_path_buf());
2313 true
2314 }
2315
2316 #[must_use]
2325 pub fn bulk_stage_write_options(&self) -> WriteOptions {
2326 let stored_beyond_loose = self.custom_primary.is_none()
2327 && (self.alternate_object_lookup_needed()
2328 || self.with_pack_read_context(|| {
2329 pack::read_local_pack_indexes_cached(&self.objects_dir)
2330 .is_ok_and(|indexes| !indexes.is_empty())
2331 }));
2332 WriteOptions {
2333 freshen_existing: stored_beyond_loose,
2334 ..BULK_NEW_LOOSE_WRITE
2335 }
2336 }
2337
2338 fn try_freshen_existing(&self, oid: &ObjectId) -> Option<ObjectId> {
2341 if let Ok(primary) = self.primary() {
2342 if let Ok(Some(existing)) =
2343 primary.try_finish_duplicate_write(oid, WriteOptions::default())
2344 {
2345 return Some(existing);
2346 }
2347 }
2348 if !self.exists(oid) {
2349 return None;
2350 }
2351 if self.freshen_object(oid) {
2352 return Some(*oid);
2353 }
2354 None
2355 }
2356
2357 fn try_freshen_existing_local(&self, oid: &ObjectId) -> Option<ObjectId> {
2358 let primary = self.primary().ok()?;
2359 primary
2360 .try_finish_duplicate_write(
2361 oid,
2362 WriteOptions {
2363 assume_loose_only_existence: true,
2364 ..WriteOptions::default()
2365 },
2366 )
2367 .ok()
2368 .flatten()
2369 }
2370
2371 fn object_exists_for_write_skip(&self, oid: &ObjectId) -> bool {
2376 if self.is_empty_tree(oid) {
2377 self.exists_materialized_local(oid)
2378 } else {
2379 self.exists(oid)
2380 }
2381 }
2382
2383 pub fn read_loose_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
2393 LooseStore::read_verify_oid(path, expected_oid)
2394 }
2395
2396 fn local_loose(&self) -> Result<LooseStore> {
2397 self.require_files_primary("loose_write")?;
2398 Ok(LooseStore::new_with_negative_cache(
2399 self.objects_dir.clone(),
2400 self.hash_algo(),
2401 self.loose_compression()?,
2402 self.loose_negative_cache_for(&self.objects_dir),
2403 ))
2404 }
2405
2406 fn loose_at(&self, objects_dir: &Path) -> Result<LooseStore> {
2407 Ok(LooseStore::new_with_negative_cache(
2408 objects_dir.to_path_buf(),
2409 self.hash_algo(),
2410 self.loose_compression()?,
2411 self.loose_negative_cache_for(objects_dir),
2412 ))
2413 }
2414
2415 pub fn read(&self, oid: &ObjectId) -> Result<Object> {
2422 self.read_impl(oid, true)
2423 }
2424
2425 fn read_impl(&self, oid: &ObjectId, remember: bool) -> Result<Object> {
2428 if self.is_empty_tree(oid) {
2429 return Ok(Object {
2430 kind: ObjectKind::Tree,
2431 data: Vec::new(),
2432 });
2433 }
2434
2435 if let Some(obj) = self.session_cached_loose_read(oid) {
2436 return Ok(obj);
2437 }
2438
2439 if let Some(obj) = self.overlay_read(oid) {
2440 return Ok(obj);
2441 }
2442 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
2443 return Ok(obj);
2444 }
2445
2446 let use_midx = self.multi_pack_index_reads_enabled();
2447 if use_midx {
2448 self.ensure_midx_prepared();
2449 }
2450
2451 self.sync_delta_base_cache_limit();
2452
2453 let primary = self.primary_writable()?;
2454 let files_primary = self.primary().ok();
2455 let mut unreadable_local_loose = None;
2456 let read_primary = || primary.read(oid);
2457 match self.with_primary_read_context(read_primary) {
2458 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj, remember),
2459 Ok(None) => {}
2460 Err(err)
2461 if files_primary
2462 .as_ref()
2463 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2464 {
2465 unreadable_local_loose = Some(err);
2466 }
2467 Err(err) => return Err(err),
2468 }
2469
2470 if let Some(obj) = self.try_read_extra_sources(oid)? {
2471 return self.finish_session_loose_read(oid, obj, remember);
2472 }
2473
2474 if self.alternates_enabled {
2475 match self.sources()?.read(oid) {
2476 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj, remember),
2477 Ok(None) => {}
2478 Err(err) => return Err(err),
2479 }
2480 }
2481
2482 if let Some(err) = unreadable_local_loose {
2483 return Err(err);
2484 }
2485
2486 self.read_after_promisor_miss(oid)
2487 }
2488
2489 #[cold]
2490 #[inline(never)]
2491 fn read_after_promisor_miss(&self, oid: &ObjectId) -> Result<Object> {
2492 let mut lazy_retries = 0u32;
2493 loop {
2494 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2495 return Err(Error::ObjectNotFound(oid.to_hex()));
2496 };
2497 match res {
2498 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2499 lazy_retries += 1;
2500 if let Some(obj) = self.try_read_once(oid)? {
2501 return Ok(obj);
2502 }
2503 }
2504 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2505 return Err(Error::ObjectNotFound(oid.to_hex()));
2506 }
2507 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2508 return Err(err);
2509 }
2510 }
2511 }
2512 }
2513
2514 pub(crate) fn try_read_once(&self, oid: &ObjectId) -> Result<Option<Object>> {
2517 if self.is_empty_tree(oid) {
2518 return Ok(Some(Object {
2519 kind: ObjectKind::Tree,
2520 data: Vec::new(),
2521 }));
2522 }
2523
2524 if let Some(obj) = self.session_cached_loose_read(oid) {
2525 return Ok(Some(obj));
2526 }
2527
2528 if let Some(obj) = self.overlay_read(oid) {
2529 return Ok(Some(obj));
2530 }
2531 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
2532 return Ok(Some(self.finish_session_loose_read(oid, obj, true)?));
2533 }
2534
2535 let use_midx = self.multi_pack_index_reads_enabled();
2536 if use_midx {
2537 self.ensure_midx_prepared();
2538 }
2539
2540 self.sync_delta_base_cache_limit();
2541
2542 let primary = self.primary_writable()?;
2543 let files_primary = self.primary().ok();
2544 let mut unreadable_local_loose = None;
2545 let read_primary = || primary.read(oid);
2546 match self.with_primary_read_context(read_primary) {
2547 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj, true)?)),
2548 Ok(None) => {}
2549 Err(err)
2550 if files_primary
2551 .as_ref()
2552 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2553 {
2554 unreadable_local_loose = Some(err);
2555 }
2556 Err(err) => return Err(err),
2557 }
2558
2559 if let Some(obj) = self.try_read_extra_sources(oid)? {
2560 return Ok(Some(self.finish_session_loose_read(oid, obj, true)?));
2561 }
2562
2563 if self.alternates_enabled {
2564 match self.sources()?.read(oid) {
2565 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj, true)?)),
2566 Ok(None) => {}
2567 Err(err) => return Err(err),
2568 }
2569 }
2570
2571 if let Some(err) = unreadable_local_loose {
2572 return Err(err);
2573 }
2574
2575 Ok(None)
2576 }
2577
2578 fn resolve_promisor_miss(
2579 &self,
2580 oid: &ObjectId,
2581 lazy_retries: u32,
2582 ) -> Result<Option<crate::promisor_fetch::PromisorMissResolution>> {
2583 let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment)
2584 else {
2585 return Ok(None);
2586 };
2587 Ok(Some(crate::promisor_fetch::resolve_promisor_read_miss(
2588 caches,
2589 self,
2590 oid,
2591 env.as_ref(),
2592 lazy_retries,
2593 )?))
2594 }
2595
2596 pub(crate) fn prefetch_promisor_read_batch(
2604 &self,
2605 oids: impl IntoIterator<Item = ObjectId>,
2606 ) -> Result<()> {
2607 let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment)
2608 else {
2609 return Ok(());
2610 };
2611 crate::promisor_fetch::prefetch_promisor_read_batch(caches, self, env.as_ref(), oids)
2612 }
2613
2614 pub fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<ObjectKind> {
2623 if self.is_empty_tree(oid) {
2624 buf.clear();
2625 return Ok(ObjectKind::Tree);
2626 }
2627
2628 if let Some(obj) = self.session_cached_loose_read(oid) {
2629 buf.clear();
2630 buf.extend_from_slice(&obj.data);
2631 return Ok(obj.kind);
2632 }
2633
2634 if let Some(obj) = self.overlay_read(oid) {
2635 buf.clear();
2636 buf.extend_from_slice(&obj.data);
2637 return Ok(obj.kind);
2638 }
2639
2640 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2641 return Ok(kind);
2642 }
2643
2644 let use_midx = self.multi_pack_index_reads_enabled();
2645 if use_midx {
2646 self.ensure_midx_prepared();
2647 }
2648
2649 self.sync_delta_base_cache_limit();
2650
2651 let primary = self.primary_writable()?;
2652 let files_primary = self.primary().ok();
2653 let mut unreadable_local_loose = None;
2654 let read_primary = || primary.read_into(oid, buf);
2655 match self.with_primary_read_context(read_primary) {
2656 Ok(Some(kind)) => return Ok(kind),
2657 Ok(None) => {}
2658 Err(err)
2659 if files_primary
2660 .as_ref()
2661 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2662 {
2663 unreadable_local_loose = Some(err);
2664 }
2665 Err(err) => return Err(err),
2666 }
2667
2668 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2669 return Ok(kind);
2670 }
2671
2672 if self.alternates_enabled {
2673 match self.sources()?.read_into(oid, buf) {
2674 Ok(Some(kind)) => return Ok(kind),
2675 Ok(None) => {}
2676 Err(err) => return Err(err),
2677 }
2678 }
2679
2680 if let Some(err) = unreadable_local_loose {
2681 return Err(err);
2682 }
2683
2684 self.read_into_after_promisor_miss(oid, buf)
2685 }
2686
2687 pub(crate) fn read_into_caching_delta_bases(
2701 &self,
2702 oid: &ObjectId,
2703 buf: &mut Vec<u8>,
2704 ) -> Result<ObjectKind> {
2705 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, true, buf)? {
2706 return Ok(kind);
2707 }
2708 if self.custom_primary.is_none() && !self.is_empty_tree(oid) {
2709 if let Ok(files) = self.primary() {
2710 self.ensure_midx_prepared();
2711 self.sync_delta_base_cache_limit();
2712 let mut read = || files.read_into_with_delta_policy(oid, true, buf);
2713 let local = if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2714 read()
2715 } else {
2716 self.with_pack_store_for(&self.objects_dir, read)
2717 };
2718 if let Ok(Some(kind)) = local {
2719 return Ok(kind);
2720 }
2721 }
2722 }
2723 if self.custom_primary.is_some() {
2724 return self.read_into(oid, buf);
2725 }
2726 let object = self.read(oid)?;
2727 buf.clear();
2728 buf.extend_from_slice(&object.data);
2729 Ok(object.kind)
2730 }
2731
2732 pub(crate) fn read_packed_commit_header_block_into(
2745 &self,
2746 oid: &ObjectId,
2747 buf: &mut Vec<u8>,
2748 ) -> Result<bool> {
2749 if self.custom_primary.is_some() {
2750 return Ok(false);
2751 }
2752 let Ok(files) = self.primary() else {
2753 return Ok(false);
2754 };
2755 files.read_packed_commit_header_block_into(oid, buf)
2756 }
2757
2758 #[cold]
2759 #[inline(never)]
2760 fn read_into_after_promisor_miss(
2761 &self,
2762 oid: &ObjectId,
2763 buf: &mut Vec<u8>,
2764 ) -> Result<ObjectKind> {
2765 let mut lazy_retries = 0u32;
2766 loop {
2767 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2768 return Err(Error::ObjectNotFound(oid.to_hex()));
2769 };
2770 match res {
2771 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2772 lazy_retries += 1;
2773 if let Some(kind) = self.try_read_into_once(oid, buf)? {
2774 return Ok(kind);
2775 }
2776 }
2777 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2778 return Err(Error::ObjectNotFound(oid.to_hex()));
2779 }
2780 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2781 return Err(err);
2782 }
2783 }
2784 }
2785 }
2786
2787 fn try_read_into_once(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
2788 if oid.is_well_known_empty_tree() {
2789 buf.clear();
2790 return Ok(Some(ObjectKind::Tree));
2791 }
2792
2793 if let Some(obj) = self.session_cached_loose_read(oid) {
2794 buf.clear();
2795 buf.extend_from_slice(&obj.data);
2796 return Ok(Some(obj.kind));
2797 }
2798
2799 if let Some(obj) = self.overlay_read(oid) {
2800 buf.clear();
2801 buf.extend_from_slice(&obj.data);
2802 return Ok(Some(obj.kind));
2803 }
2804
2805 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2806 return Ok(Some(kind));
2807 }
2808
2809 let use_midx = self.multi_pack_index_reads_enabled();
2810 if use_midx {
2811 self.ensure_midx_prepared();
2812 }
2813
2814 self.sync_delta_base_cache_limit();
2815
2816 let primary = self.primary_writable()?;
2817 let files_primary = self.primary().ok();
2818 let mut unreadable_local_loose = None;
2819 let read_primary = || primary.read_into(oid, buf);
2820 match self.with_primary_read_context(read_primary) {
2821 Ok(Some(kind)) => return Ok(Some(kind)),
2822 Ok(None) => {}
2823 Err(err)
2824 if files_primary
2825 .as_ref()
2826 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2827 {
2828 unreadable_local_loose = Some(err);
2829 }
2830 Err(err) => return Err(err),
2831 }
2832
2833 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2834 return Ok(Some(kind));
2835 }
2836
2837 if self.alternates_enabled {
2838 match self.sources()?.read_into(oid, buf) {
2839 Ok(Some(kind)) => return Ok(Some(kind)),
2840 Ok(None) => {}
2841 Err(err) => return Err(err),
2842 }
2843 }
2844
2845 if let Some(err) = unreadable_local_loose {
2846 return Err(err);
2847 }
2848
2849 Ok(None)
2850 }
2851
2852 fn try_read_into_extra_sources(
2853 &self,
2854 oid: &ObjectId,
2855 buf: &mut Vec<u8>,
2856 ) -> Result<Option<ObjectKind>> {
2857 for src in &self.extra_read_sources {
2858 if let Some(kind) = src.read_into(oid, buf)? {
2859 return Ok(Some(kind));
2860 }
2861 }
2862 Ok(None)
2863 }
2864
2865 fn try_read_packed_in_batch_context_into(
2866 &self,
2867 oid: &ObjectId,
2868 populate_delta_lru: bool,
2869 buf: &mut Vec<u8>,
2870 ) -> Result<Option<ObjectKind>> {
2871 if self.custom_primary.is_some()
2872 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
2873 || self.multi_pack_index_reads_enabled()
2874 {
2875 return Ok(None);
2876 }
2877 self.sync_delta_base_cache_limit();
2878 match pack::try_read_object_from_packs_with_options_into(
2879 &self.objects_dir,
2880 oid,
2881 pack::PackLookupOptions::ALL_LOCAL_PACKS,
2882 populate_delta_lru,
2883 buf,
2884 ) {
2885 Ok(kind) => Ok(Some(kind)),
2886 Err(Error::ObjectNotFound(_)) => Ok(None),
2887 Err(err) => Err(err),
2888 }
2889 }
2890
2891 pub fn read_info(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2902 if self.is_empty_tree(oid) {
2903 return Ok(ObjectInfo {
2904 kind: ObjectKind::Tree,
2905 size: 0,
2906 });
2907 }
2908
2909 if let Some(obj) = self.overlay_read(oid) {
2910 return Ok(ObjectInfo {
2911 kind: obj.kind,
2912 size: u64::try_from(obj.data.len())
2913 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
2914 });
2915 }
2916
2917 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
2918 return Ok(info);
2919 }
2920
2921 let use_midx = self.multi_pack_index_reads_enabled();
2922 if use_midx {
2923 self.ensure_midx_prepared();
2924 }
2925
2926 let primary = self.primary_writable()?;
2927 let files_primary = self.primary().ok();
2928 let mut unreadable_local_loose = None;
2929 match primary.read_info(oid) {
2930 Ok(Some(info)) => return Ok(info),
2931 Ok(None) => {}
2932 Err(err)
2933 if files_primary
2934 .as_ref()
2935 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2936 {
2937 unreadable_local_loose = Some(err);
2938 }
2939 Err(err) => return Err(err),
2940 }
2941
2942 if let Some(info) = self.try_read_info_extra_sources(oid)? {
2943 return Ok(info);
2944 }
2945
2946 if self.alternates_enabled {
2947 match self.sources()?.read_info(oid) {
2948 Ok(Some(info)) => return Ok(info),
2949 Ok(None) => {}
2950 Err(err) => return Err(err),
2951 }
2952 }
2953
2954 if let Some(err) = unreadable_local_loose {
2955 return Err(err);
2956 }
2957
2958 self.read_info_after_promisor_miss(oid)
2959 }
2960
2961 pub(crate) fn read_info_without_fetch(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2966 self.try_read_info_once(oid)?
2967 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))
2968 }
2969
2970 pub(crate) fn stored_object_kind(&self, oid: &ObjectId) -> Result<Option<ObjectKind>> {
2982 if oid.is_well_known_empty_tree() && !self.empty_tree_stored(oid) {
2983 return Ok(None);
2984 }
2985 match self.try_read_info_once(oid) {
2986 Ok(info) => Ok(info.map(|info| info.kind)),
2987 Err(Error::ObjectNotFound(_)) => Ok(None),
2988 Err(err) => Err(err),
2989 }
2990 }
2991
2992 fn empty_tree_stored(&self, oid: &ObjectId) -> bool {
2993 if self.overlay_read(oid).is_some() || self.exists_materialized_local(oid) {
2994 return true;
2995 }
2996 if !self.alternates_enabled {
2997 return false;
2998 }
2999 let file_dirs = self.file_alternate_dirs_snapshot();
3000 let env_dirs = self.env_alternate_dirs_snapshot();
3001 file_dirs
3002 .iter()
3003 .chain(env_dirs.iter())
3004 .any(|dir| self.with_pack_store_for(dir, || store::local_object_materialized(dir, oid)))
3005 }
3006
3007 #[cold]
3008 #[inline(never)]
3009 fn read_info_after_promisor_miss(&self, oid: &ObjectId) -> Result<ObjectInfo> {
3010 let mut lazy_retries = 0u32;
3011 loop {
3012 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
3013 return Err(Error::ObjectNotFound(oid.to_hex()));
3014 };
3015 match res {
3016 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
3017 return Err(err);
3018 }
3019 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
3020 return Err(Error::ObjectNotFound(oid.to_hex()));
3021 }
3022 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
3023 lazy_retries += 1;
3024 if let Some(info) = self.try_read_info_once(oid)? {
3025 return Ok(info);
3026 }
3027 }
3028 }
3029 }
3030 }
3031
3032 pub(crate) fn try_read_info_once(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
3035 if self.is_empty_tree(oid) {
3036 return Ok(Some(ObjectInfo {
3037 kind: ObjectKind::Tree,
3038 size: 0,
3039 }));
3040 }
3041
3042 if let Some(obj) = self.overlay_read(oid) {
3043 return Ok(Some(ObjectInfo {
3044 kind: obj.kind,
3045 size: u64::try_from(obj.data.len())
3046 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
3047 }));
3048 }
3049
3050 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
3051 return Ok(Some(info));
3052 }
3053
3054 let use_midx = self.multi_pack_index_reads_enabled();
3055 if use_midx {
3056 self.ensure_midx_prepared();
3057 }
3058
3059 let primary = self.primary_writable()?;
3060 let files_primary = self.primary().ok();
3061 let mut unreadable_local_loose = None;
3062 match primary.read_info(oid) {
3063 Ok(Some(info)) => return Ok(Some(info)),
3064 Ok(None) => {}
3065 Err(err)
3066 if files_primary
3067 .as_ref()
3068 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
3069 {
3070 unreadable_local_loose = Some(err);
3071 }
3072 Err(err) => return Err(err),
3073 }
3074
3075 if let Some(info) = self.try_read_info_extra_sources(oid)? {
3076 return Ok(Some(info));
3077 }
3078
3079 if self.alternates_enabled {
3080 match self.sources()?.read_info(oid) {
3081 Ok(Some(info)) => return Ok(Some(info)),
3082 Ok(None) => {}
3083 Err(err) => return Err(err),
3084 }
3085 }
3086
3087 if let Some(err) = unreadable_local_loose {
3088 return Err(err);
3089 }
3090
3091 Ok(None)
3092 }
3093
3094 pub(crate) fn read_unless_larger(
3110 &self,
3111 oid: &ObjectId,
3112 limit: u64,
3113 cache_small: bool,
3114 ) -> Result<BoundedRead> {
3115 if cache_small {
3116 if let Some(read) = self.session_loose_reads.lock().ok().and_then(|cache| {
3117 cache.get(oid).map(|obj| {
3118 if obj.data.len() as u64 > limit {
3119 BoundedRead::Larger
3120 } else {
3121 BoundedRead::Object(obj.clone())
3122 }
3123 })
3124 }) {
3125 return Ok(read);
3126 }
3127 }
3128 if self.custom_primary.is_none() && !self.multi_pack_index_reads_enabled() {
3129 if let Some(read) =
3130 pack::pack_cache::try_read_object_via_single_pack_reader_unless_whole_larger(
3131 &self.objects_dir,
3132 oid,
3133 limit,
3134 )?
3135 {
3136 return Ok(read);
3137 }
3138 }
3139 if let Ok(files) = self.primary() {
3140 if self.multi_pack_index_reads_enabled() {
3141 self.ensure_midx_prepared();
3142 }
3143 match files.read_unless_larger(oid, limit)? {
3144 LocalSizedRead::Packed(Some(size)) if size > limit => {
3145 return Ok(BoundedRead::Larger);
3146 }
3147 LocalSizedRead::Packed(_) => return self.read(oid).map(BoundedRead::Object),
3148 LocalSizedRead::Loose(BoundedRead::Object(obj)) => {
3149 if cache_small && obj.data.len() <= SESSION_BOUNDED_LOOSE_CACHE_MAX {
3150 self.remember_session_loose_read(oid, &obj);
3151 }
3152 return Ok(BoundedRead::Object(obj));
3153 }
3154 LocalSizedRead::Loose(read) => return Ok(read),
3155 LocalSizedRead::Missing => {}
3156 }
3157 }
3158 let info = self.read_info(oid)?;
3159 if info.size > limit {
3160 return Ok(BoundedRead::Larger);
3161 }
3162 self.read(oid).map(BoundedRead::Object)
3163 }
3164
3165 pub fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'static>>> {
3174 self.with_stream(oid, materialize_stream)
3175 }
3176
3177 pub fn with_stream<R>(
3194 &self,
3195 oid: &ObjectId,
3196 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
3197 ) -> Result<Option<R>> {
3198 use std::io::Cursor;
3199 if self.is_empty_tree(oid) {
3200 return f(ObjectStream {
3201 kind: ObjectKind::Tree,
3202 size: 0,
3203 reader: Box::new(std::io::empty()),
3204 })
3205 .map(Some);
3206 }
3207 if let Some(obj) = self.overlay_read(oid) {
3208 let size = u64::try_from(obj.data.len()).map_err(|_| {
3209 Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
3210 })?;
3211 return f(ObjectStream {
3212 kind: obj.kind,
3213 size,
3214 reader: Box::new(Cursor::new(obj.data)),
3215 })
3216 .map(Some);
3217 }
3218 if self.multi_pack_index_reads_enabled() {
3219 self.ensure_midx_prepared();
3220 }
3221 self.sync_delta_base_cache_limit();
3222 let primary = self.primary_writable()?;
3223 let files_primary = self.primary().ok();
3224 let mut unreadable_local_loose = None;
3225 match primary.open_stream(oid) {
3226 Ok(Some(stream)) => return f(stream).map(Some),
3227 Ok(None) => {}
3228 Err(err)
3229 if files_primary
3230 .as_ref()
3231 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
3232 {
3233 unreadable_local_loose = Some(err);
3234 }
3235 Err(err) => return Err(err),
3236 }
3237 if let Some(stream) = self.try_open_stream_extra_sources(oid)? {
3238 return f(stream).map(Some);
3239 }
3240 if self.alternates_enabled {
3241 let sources = self.sources()?;
3242 let stream = sources.open_stream(oid)?;
3243 if let Some(stream) = stream {
3244 return f(stream).map(Some);
3245 }
3246 }
3247 if let Some(err) = unreadable_local_loose {
3248 return Err(err);
3249 }
3250 Ok(None)
3251 }
3252
3253 pub fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
3259 let mut seen = oid_hash_set();
3260 let mut visit = |store: &dyn ObjectStore| -> Result<bool> {
3261 store::for_each_propagate_break(store, &mut |oid| {
3262 if !seen.insert(*oid) {
3263 return ControlFlow::Continue(());
3264 }
3265 f(oid)
3266 })
3267 };
3268 if let Some(overlay) = self.overlay_store() {
3269 if visit(overlay.as_ref())? {
3270 return Ok(());
3271 }
3272 }
3273 if visit(self.primary_writable()?.as_ref())? {
3274 return Ok(());
3275 }
3276 for src in &self.extra_read_sources {
3277 if visit(src.as_ref())? {
3278 return Ok(());
3279 }
3280 }
3281 if self.alternates_enabled {
3282 let _ = visit(self.sources()?.as_ref())?;
3283 }
3284 Ok(())
3285 }
3286
3287 pub fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
3293 let mut seen: OidHashSet = out.iter().copied().collect();
3294 if let Some(overlay) = self.overlay_store() {
3295 append_lookup_prefix_layer(overlay.as_ref(), prefix, limit, out, &mut seen)?;
3296 if limit != 0 && out.len() >= limit {
3297 return Ok(());
3298 }
3299 }
3300 append_lookup_prefix_layer(
3301 self.primary_writable()?.as_ref(),
3302 prefix,
3303 limit,
3304 out,
3305 &mut seen,
3306 )?;
3307 if limit != 0 && out.len() >= limit {
3308 return Ok(());
3309 }
3310 for src in &self.extra_read_sources {
3311 append_lookup_prefix_layer(src.as_ref(), prefix, limit, out, &mut seen)?;
3312 if limit != 0 && out.len() >= limit {
3313 return Ok(());
3314 }
3315 }
3316 if self.alternates_enabled {
3317 append_lookup_prefix_layer(self.sources()?.as_ref(), prefix, limit, out, &mut seen)?;
3318 }
3319 Ok(())
3320 }
3321
3322 pub fn enumerate_local_loose_objects(&self) -> Result<Vec<(ObjectId, PathBuf)>> {
3330 let primary = self.primary()?;
3331 let loose = primary.loose_store();
3332 let mut out = Vec::new();
3333 loose.for_each_object(&mut |oid| {
3334 out.push((*oid, loose.object_path(oid)));
3335 ControlFlow::Continue(())
3336 })?;
3337 Ok(out)
3338 }
3339
3340 pub(crate) fn enumerate_loose_objects_at(
3342 &self,
3343 objects_dir: &Path,
3344 ) -> Result<Vec<(ObjectId, PathBuf)>> {
3345 let loose = self.loose_at(objects_dir)?;
3346 let mut out = Vec::new();
3347 loose.for_each_object(&mut |oid| {
3348 out.push((*oid, loose.object_path(oid)));
3349 ControlFlow::Continue(())
3350 })?;
3351 Ok(out)
3352 }
3353
3354 pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3362 hash_object_data_with(self.hash_algo(), kind, data)
3363 }
3364
3365 pub fn hash_stream(
3376 &self,
3377 kind: ObjectKind,
3378 size: u64,
3379 reader: &mut dyn std::io::Read,
3380 ) -> Result<ObjectId> {
3381 #[cfg(test)]
3382 self.hot_path_metrics().record_stream_hash();
3383 stream::hash_stream(self.hash_algo(), kind, size, reader)
3384 }
3385
3386 pub fn write_stream(
3408 &self,
3409 kind: ObjectKind,
3410 size: u64,
3411 reader: &mut dyn std::io::Read,
3412 options: WriteOptions,
3413 ) -> Result<ObjectId> {
3414 #[cfg(test)]
3415 self.hot_path_metrics().record_stream_write();
3416 if let Some(primary) = &self.custom_primary {
3417 if !self.overlay_active() {
3418 let oid = primary.write_stream(kind, size, reader, options)?;
3419 self.note_commit_object_written(kind);
3420 return Ok(oid);
3421 }
3422 }
3423 if self.overlay_active() || size < stream::STREAMED_WRITE_MIN_SIZE {
3424 let len = usize::try_from(size).map_err(|_| {
3425 Error::CorruptObject(format!("object size {size} exceeds address space"))
3426 })?;
3427 let mut data = Vec::with_capacity(len);
3428 let read =
3429 std::io::Read::read_to_end(&mut std::io::Read::take(reader, size), &mut data)?;
3430 if read != len {
3431 return Err(Error::ObjectStreamTruncated {
3432 expected: size,
3433 actual: read as u64,
3434 });
3435 }
3436 return self.write_with_options(kind, &data, options);
3437 }
3438 if size < self.big_file_threshold()? {
3439 let loose = self.local_loose()?;
3440 let oid = loose.write_payload_stream(kind, size, reader, options)?;
3441 self.note_commit_object_written(kind);
3442 return Ok(oid);
3443 }
3444 let staged = stream::StagedPack::write(
3445 &self.objects_dir.join("pack"),
3446 self.hash_algo(),
3447 kind,
3448 size,
3449 reader,
3450 self.pack_compression()?,
3451 )?;
3452 let oid = staged.oid();
3453 if self.object_exists_for_write_skip(&oid) {
3454 if !options.silent {
3455 let _ = self.freshen_object(&oid);
3456 }
3457 return Ok(oid);
3458 }
3459 staged.install()?;
3460 self.invalidate_packs();
3461 self.note_commit_object_written(kind);
3462 Ok(oid)
3463 }
3464
3465 pub fn empty_tree_oid(&self) -> Result<ObjectId> {
3473 self.hash(ObjectKind::Tree, b"")
3474 }
3475
3476 #[must_use]
3478 pub fn is_empty_tree(&self, oid: &ObjectId) -> bool {
3479 empty_tree_oid_matches(self.hash_algo(), oid)
3480 }
3481
3482 pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3495 self.write_with_options(kind, data, WriteOptions::default())
3496 }
3497
3498 pub fn flush(&self) -> Result<()> {
3506 if let Some(primary) = &self.custom_primary {
3507 primary.flush()
3508 } else {
3509 Ok(())
3510 }
3511 }
3512
3513 pub fn flush_for_publish(&self) -> Result<()> {
3522 if let Some(primary) = &self.custom_primary {
3523 primary.flush_for_publish()
3524 } else {
3525 Ok(())
3526 }
3527 }
3528
3529 pub fn rollback(&self) -> Result<()> {
3537 if let Some(primary) = &self.custom_primary {
3538 primary.rollback()
3539 } else {
3540 Ok(())
3541 }
3542 }
3543
3544 pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
3553 if self.custom_primary.is_some() {
3554 return Ok(());
3555 }
3556 self.local_loose()?.ensure_all_loose_prefix_dirs()
3557 }
3558
3559 #[cfg(test)]
3561 #[must_use]
3562 pub fn test_loose_object_path(&self, oid: &ObjectId) -> PathBuf {
3563 self.object_path(oid)
3564 }
3565
3566 #[cfg(test)]
3568 pub fn test_remove_loose_object(&self, oid: &ObjectId) {
3569 let _ = fs::remove_file(self.object_path(oid));
3570 }
3571
3572 pub fn count_loose_objects(&self) -> Result<usize> {
3578 let mut count = 0usize;
3579 for_each_loose_object_id(&self.objects_dir, self.hash_algo(), &mut |_| {
3580 count += 1;
3581 ControlFlow::Continue(())
3582 })?;
3583 Ok(count)
3584 }
3585
3586 pub fn approximate_object_count(&self) -> Result<usize> {
3596 let pack_objects: Result<usize> = self.with_pack_read_context(|| {
3597 let indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
3598 Ok(indexes.iter().map(|idx| idx.len()).sum())
3599 });
3600 let mut count = pack_objects?;
3601 count += self.count_loose_objects()?;
3602 Ok(count)
3603 }
3604
3605 pub fn zlib_compress_loose_store(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
3611 self.local_loose()?.zlib_compress_store_bytes(store_bytes)
3612 }
3613
3614 pub fn write_loose_zlib_prehashed(
3628 &self,
3629 oid: &ObjectId,
3630 zlib_store: &[u8],
3631 options: WriteOptions,
3632 ) -> Result<ObjectId> {
3633 if self.custom_primary.is_some() {
3634 let raw = decompress_zlib_loose_bytes(zlib_store)?;
3635 let obj = parse_object_bytes(&raw)?;
3636 return self.write_with_options(obj.kind, &obj.data, options);
3637 }
3638 let primary = self.primary()?;
3639 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
3640 return Ok(existing);
3641 }
3642 if options.freshen_existing && self.freshen_packed_or_alternate(oid) {
3643 return Ok(*oid);
3644 }
3645
3646 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3647 && self.object_exists_for_write_skip(oid);
3648
3649 if self.overlay_active() && !already_exists {
3650 if let Ok(raw) = decompress_zlib_loose_bytes(zlib_store) {
3651 if let Ok(obj) = parse_object_bytes(&raw) {
3652 if self.overlay_store_object(*oid, obj.kind, &obj.data) {
3653 return Ok(*oid);
3654 }
3655 }
3656 }
3657 }
3658
3659 if already_exists {
3660 if !options.silent {
3661 let _ = self.freshen_object(oid);
3662 }
3663 return Ok(*oid);
3664 }
3665
3666 primary.write_zlib_prehashed(oid, zlib_store, options)
3667 }
3668
3669 pub(crate) fn write_blob_prehashed(
3675 &self,
3676 oid: &ObjectId,
3677 data: &[u8],
3678 options: WriteOptions,
3679 ) -> Result<ObjectId> {
3680 if self.custom_primary.is_some() {
3681 return self.write_with_options(ObjectKind::Blob, data, options);
3682 }
3683 let primary = self.primary()?;
3684 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
3685 return Ok(existing);
3686 }
3687 if options.freshen_existing && self.freshen_packed_or_alternate(oid) {
3688 return Ok(*oid);
3689 }
3690 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3691 && self.object_exists_for_write_skip(oid);
3692 if self.overlay_active()
3693 && !already_exists
3694 && self.overlay_store_object(*oid, ObjectKind::Blob, data)
3695 {
3696 return Ok(*oid);
3697 }
3698 if already_exists {
3699 if !options.silent {
3700 let _ = self.freshen_object(oid);
3701 }
3702 return Ok(*oid);
3703 }
3704 primary
3705 .loose_store()
3706 .write_prehashed(oid, ObjectKind::Blob, data, options)
3707 }
3708 pub fn write_with_options(
3718 &self,
3719 kind: ObjectKind,
3720 data: &[u8],
3721 options: WriteOptions,
3722 ) -> Result<ObjectId> {
3723 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3724
3725 if let Some(primary) = &self.custom_primary {
3726 if self.overlay_active() && self.overlay_store_object(oid, kind, data) {
3727 return Ok(oid);
3728 }
3729 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3730 self.note_commit_object_written(kind);
3731 return Ok(oid);
3732 }
3733
3734 let primary = self.primary()?;
3735 if let Some(existing) = primary.try_finish_duplicate_write(&oid, options)? {
3736 return Ok(existing);
3737 }
3738 if options.freshen_existing && self.freshen_packed_or_alternate(&oid) {
3739 return Ok(oid);
3740 }
3741
3742 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3743 && self.object_exists_for_write_skip(&oid);
3744
3745 if self.overlay_active() && !already_exists && self.overlay_store_object(oid, kind, data) {
3748 return Ok(oid);
3749 }
3750
3751 if already_exists {
3752 if !options.silent {
3753 let _ = self.freshen_object(&oid);
3754 }
3755 return Ok(oid);
3756 }
3757
3758 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3759 self.note_commit_object_written(kind);
3760 Ok(oid)
3761 }
3762
3763 pub fn write_local(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3779 if self.custom_primary.is_some() {
3780 return Err(Error::UnsupportedObjectStore {
3781 operation: "write_local",
3782 });
3783 }
3784 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3785
3786 if let Some(existing) = self.try_freshen_existing_local(&oid) {
3787 return Ok(existing);
3788 }
3789
3790 let oid = self
3791 .local_loose()?
3792 .write(kind, data, WriteOptions::default())?;
3793 self.note_commit_object_written(kind);
3794 Ok(oid)
3795 }
3796
3797 fn write_into_primary(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3802 if let Some(primary) = &self.custom_primary {
3803 let oid =
3804 WritableObjectStore::write(primary.as_ref(), kind, data, WriteOptions::default())?;
3805 self.note_commit_object_written(kind);
3806 return Ok(oid);
3807 }
3808 self.write_local(kind, data)
3809 }
3810
3811 pub(crate) fn write_local_stream_with(
3813 &self,
3814 kind: ObjectKind,
3815 size: u64,
3816 supply: impl FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()>,
3817 ) -> Result<ObjectId> {
3818 if self.custom_primary.is_some() {
3819 return Err(Error::UnsupportedObjectStore {
3820 operation: "write_local_stream",
3821 });
3822 }
3823 let loose = self.local_loose()?;
3824 let oid = loose.write_payload_stream_with(
3825 kind,
3826 size,
3827 WriteOptions::default(),
3828 self.pack_compression()?,
3829 supply,
3830 )?;
3831 self.note_commit_object_written(kind);
3832 Ok(oid)
3833 }
3834
3835 pub fn write_loose_materialize(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3842 if self.custom_primary.is_some() {
3843 return Err(Error::UnsupportedObjectStore {
3844 operation: "write_loose_materialize",
3845 });
3846 }
3847 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3848 if let Ok(primary) = self.primary() {
3849 if let Ok(Some(existing)) = primary
3850 .loose_store()
3851 .try_finish_if_loose_present(&oid, WriteOptions::default())
3852 {
3853 return Ok(existing);
3854 }
3855 }
3856
3857 self.local_loose()?
3858 .write(kind, data, WriteOptions::default())
3859 }
3860
3861 pub fn write_raw(&self, store_bytes: &[u8]) -> Result<ObjectId> {
3871 let obj = parse_object_bytes(store_bytes)?;
3872 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
3873
3874 if let Some(primary) = &self.custom_primary {
3875 if self.exists(&oid) {
3876 return Ok(oid);
3877 }
3878 if self.overlay_active() && self.overlay_store_object(oid, obj.kind, &obj.data) {
3879 return Ok(oid);
3880 }
3881 return WritableObjectStore::write(
3882 primary.as_ref(),
3883 obj.kind,
3884 &obj.data,
3885 WriteOptions::default(),
3886 );
3887 }
3888
3889 if let Some(existing) = self.try_freshen_existing(&oid) {
3890 return Ok(existing);
3891 }
3892
3893 self.local_loose()?
3894 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
3895 }
3896
3897 pub fn write_raw_local(&self, store_bytes: &[u8]) -> Result<ObjectId> {
3905 if self.custom_primary.is_some() {
3906 return Err(Error::UnsupportedObjectStore {
3907 operation: "write_raw_local",
3908 });
3909 }
3910 let obj = parse_object_bytes(store_bytes)?;
3911 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
3912 if let Some(existing) = self.try_freshen_existing_local(&oid) {
3913 return Ok(existing);
3914 }
3915
3916 self.local_loose()?
3917 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
3918 }
3919
3920 fn touch_object_mtime(&self, path: &Path) -> Option<std::time::SystemTime> {
3922 #[cfg(test)]
3923 self.hot_path_test_metrics.record_freshen();
3924 crate::fs_readonly::set_file_times_now(path)
3925 }
3926}
3927
3928fn hash_object_data_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3931 hash::hash_object(algo, kind, data)
3932}
3933
3934#[must_use]
3936pub(crate) fn empty_tree_oid_matches(algo: HashAlgo, oid: &ObjectId) -> bool {
3937 oid.is_empty_tree_for(algo)
3938}
3939
3940pub(crate) fn zlib_compress_blob_loose(
3943 data: &[u8],
3944 compression: flate2::Compression,
3945) -> Result<Vec<u8>> {
3946 use std::io::Write as _;
3947 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), compression);
3948 let zlib_err = |e: std::io::Error| Error::Zlib(e.to_string());
3949 write!(encoder, "{} {}\0", ObjectKind::Blob, data.len()).map_err(zlib_err)?;
3950 encoder.write_all(data).map_err(zlib_err)?;
3951 encoder.finish().map_err(zlib_err)
3952}
3953
3954fn append_lookup_prefix_layer(
3955 store: &dyn ObjectStore,
3956 prefix: &str,
3957 limit: usize,
3958 out: &mut Vec<ObjectId>,
3959 seen: &mut OidHashSet,
3960) -> Result<()> {
3961 let mut scratch = Vec::new();
3962 store.lookup_prefix(prefix, 0, &mut scratch)?;
3963 for oid in scratch {
3964 if seen.insert(oid) {
3965 out.push(oid);
3966 if limit != 0 && out.len() >= limit {
3967 return Ok(());
3968 }
3969 }
3970 }
3971 Ok(())
3972}
3973
3974fn materialize_stream(stream: ObjectStream<'_>) -> Result<ObjectStream<'static>> {
3975 use std::io::{Cursor, Read};
3976 let ObjectStream {
3977 kind,
3978 size,
3979 mut reader,
3980 } = stream;
3981 let mut payload = Vec::new();
3982 reader.read_to_end(&mut payload).map_err(Error::Io)?;
3983 Ok(ObjectStream {
3984 kind,
3985 size,
3986 reader: Box::new(Cursor::new(payload)),
3987 })
3988}
3989
3990fn parse_alternate_env(val: &str) -> Vec<PathBuf> {
3993 let mut result = Vec::new();
3994 let mut chars = val.chars().peekable();
3995 while chars.peek().is_some() {
3996 if chars.peek() == Some(&':') {
3997 chars.next();
3998 continue;
3999 }
4000 if chars.peek() == Some(&'"') {
4001 chars.next(); let saved: Vec<char> = chars.clone().collect();
4005 let mut path = String::new();
4006 let mut properly_closed = false;
4007 loop {
4008 match chars.next() {
4009 None => break,
4010 Some('"') => {
4011 properly_closed = true;
4012 break;
4013 }
4014 Some('\\') => match chars.peek() {
4015 Some(c) if c.is_ascii_digit() => {
4016 let mut oct = String::new();
4017 for _ in 0..3 {
4018 if let Some(&c) = chars.peek() {
4019 if c.is_ascii_digit() {
4020 oct.push(c);
4021 chars.next();
4022 } else {
4023 break;
4024 }
4025 } else {
4026 break;
4027 }
4028 }
4029 if let Ok(byte) = u8::from_str_radix(&oct, 8) {
4030 path.push(byte as char);
4031 }
4032 }
4033 Some(_) => {
4034 if let Some(c) = chars.next() {
4035 match c {
4036 'n' => path.push('\n'),
4037 't' => path.push('\t'),
4038 'r' => path.push('\r'),
4039 _ => path.push(c),
4040 }
4041 }
4042 }
4043 None => {}
4044 },
4045 Some(c) => path.push(c),
4046 }
4047 }
4048 if !properly_closed {
4049 let raw: String = std::iter::once('"').chain(saved).collect();
4052 let raw_path = raw.split(':').next().unwrap_or(&raw);
4054 if !raw_path.is_empty() {
4055 result.push(PathBuf::from(raw_path));
4056 }
4057 let remainder = &raw[raw_path.len()..];
4061 if let Some(rest) = remainder.strip_prefix(':') {
4062 result.extend(parse_alternate_env(rest));
4064 }
4065 return result;
4066 } else if !path.is_empty() {
4067 result.push(PathBuf::from(path));
4068 }
4069 } else {
4070 let mut path = String::new();
4071 while let Some(&c) = chars.peek() {
4072 if c == ':' {
4073 break;
4074 }
4075 path.push(c);
4076 chars.next();
4077 }
4078 if !path.is_empty() {
4079 result.push(PathBuf::from(path));
4080 }
4081 }
4082 }
4083 result
4084}
4085
4086fn system_time_to_file_time(t: std::time::SystemTime) -> FileTime {
4087 let dur = t
4088 .duration_since(std::time::SystemTime::UNIX_EPOCH)
4089 .unwrap_or_default();
4090 FileTime::from_unix_time(
4091 i64::try_from(dur.as_secs()).unwrap_or(i64::MAX),
4092 dur.subsec_nanos(),
4093 )
4094}
4095
4096fn superseded_pack_indexes(
4097 pack_dir: &Path,
4098 keep_pack: &Path,
4099 retained_packs: &HashSet<PathBuf>,
4100 inventory: &RepackPackInventorySnapshot,
4101) -> Result<Vec<(PathBuf, std::time::SystemTime)>> {
4102 let keep_stem = keep_pack
4103 .file_stem()
4104 .and_then(|s| s.to_str())
4105 .ok_or_else(|| {
4106 Error::Io(std::io::Error::new(
4107 std::io::ErrorKind::InvalidInput,
4108 "bad pack path",
4109 ))
4110 })?;
4111 if !pack_dir.is_dir() {
4112 return Ok(Vec::new());
4113 }
4114 let mut out = Vec::new();
4115 for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
4116 let entry = entry.map_err(Error::Io)?;
4117 let name = entry.file_name();
4118 let name_str = name.to_string_lossy();
4119 if name_str == keep_stem || name_str.starts_with(&format!("{keep_stem}.")) {
4120 continue;
4121 }
4122 if !name_str.ends_with(".idx") {
4123 continue;
4124 }
4125 let idx_path = entry.path();
4126 let pack_path = idx_path.with_extension("pack");
4127 if !pack_path.is_file() {
4128 continue;
4129 }
4130 if inventory.kept_at_inventory.contains(&pack_path)
4131 || pack_path.with_extension("promisor").is_file()
4132 || retained_packs.contains(&pack_path)
4133 || !inventory.pack_paths.contains(&pack_path)
4134 {
4135 continue;
4136 }
4137 let pack_mtime = fs::metadata(&pack_path)
4138 .and_then(|m| m.modified())
4139 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
4140 out.push((idx_path, pack_mtime));
4141 }
4142 Ok(out)
4143}
4144
4145fn oids_in_inventory_kept_packs(
4146 pack_dir: &Path,
4147 keep_pack: &Path,
4148 inventory_kept_packs: &HashSet<PathBuf>,
4149) -> Result<OidHashSet> {
4150 let keep_stem = keep_pack.file_stem().and_then(|s| s.to_str()).unwrap_or("");
4151 let mut oids = oid_hash_set();
4152 for pack_path in inventory_kept_packs {
4153 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
4154 continue;
4155 };
4156 if stem == keep_stem {
4157 continue;
4158 }
4159 if pack_path.parent().is_some_and(|p| p != pack_dir) {
4160 continue;
4161 }
4162 let idx_path = pack_path.with_extension("idx");
4163 if idx_path.is_file() {
4164 oids.extend(PackedObjects::object_ids_from_index_path(&idx_path)?);
4165 }
4166 }
4167 Ok(oids)
4168}
4169
4170#[cfg(test)]
4171mod tests {
4172 #![allow(clippy::expect_used, clippy::unwrap_used)]
4173
4174 use super::*;
4175 use tempfile::TempDir;
4176
4177 #[test]
4178 fn round_trip_blob() {
4179 let dir = TempDir::new().unwrap();
4180 let odb = Odb::new(dir.path());
4181 let data = b"hello world";
4182 let oid = odb.write(ObjectKind::Blob, data).unwrap();
4183 let obj = odb.read(&oid).unwrap();
4184 assert_eq!(obj.kind, ObjectKind::Blob);
4185 assert_eq!(obj.data, data);
4186 }
4187
4188 #[test]
4189 fn known_blob_hash() {
4190 let oid = HashAlgo::Sha1
4193 .hash_object(ObjectKind::Blob, b"hello")
4194 .expect("hash");
4195 assert_eq!(oid.to_hex(), "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0");
4196 }
4197
4198 fn pack_objects(objects: &Path, oids: &[ObjectId]) {
4200 let odb = Odb::new(objects);
4201 let pack = crate::pack_objects::build_pack_from_send_list(
4202 &odb,
4203 oids,
4204 &HashSet::new(),
4205 &crate::pack_objects::PackBuildOptions::default(),
4206 &HashMap::new(),
4207 )
4208 .unwrap();
4209 for oid in oids {
4210 fs::remove_file(oid.loose_path_in(objects)).unwrap();
4211 }
4212 crate::index_pack::install_pack_bytes(
4213 pack,
4214 &odb,
4215 &crate::index_pack::IngestPackOptions::default(),
4216 )
4217 .unwrap();
4218 }
4219
4220 #[test]
4221 fn general_reads_cache_trees_and_tiny_blobs_but_not_larger_blobs() {
4222 let dir = TempDir::new().unwrap();
4223 let odb = Odb::new(dir.path());
4224 let blob = odb
4225 .write(
4226 ObjectKind::Blob,
4227 &vec![b'x'; SESSION_READ_BLOB_CACHE_MAX + 1],
4228 )
4229 .unwrap();
4230 let tiny = odb.write(ObjectKind::Blob, b"tiny\n").unwrap();
4231 let tree = odb
4232 .write(
4233 ObjectKind::Tree,
4234 &crate::objects::serialize_tree(&[crate::objects::TreeEntry {
4235 mode: 0o100644,
4236 name: b"f".to_vec(),
4237 oid: blob,
4238 }]),
4239 )
4240 .unwrap();
4241 assert_eq!(
4242 odb.read(&blob).unwrap().data.len(),
4243 SESSION_READ_BLOB_CACHE_MAX + 1
4244 );
4245 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
4246 let tree_bytes = odb.read(&tree).unwrap().data;
4247 assert_eq!(odb.read(&tiny).unwrap().data, b"tiny\n");
4248 for oid in [blob, tree, tiny] {
4249 let hex = oid.to_hex();
4250 fs::remove_file(dir.path().join(&hex[..2]).join(&hex[2..])).unwrap();
4251 }
4252 assert_eq!(odb.read(&tree).unwrap().data, tree_bytes, "tree reused");
4253 assert_eq!(odb.read(&tiny).unwrap().data, b"tiny\n", "tiny blob reused");
4254 assert!(odb.read(&blob).is_err(), "blob was not retained");
4255 }
4256
4257 #[test]
4258 fn session_read_cache_is_bounded_by_bytes_and_entries() {
4259 let mut cache = SessionLooseReads::default();
4260 let chunk = SESSION_LOOSE_CACHE_MAX_BYTES / 4;
4261 let object = |len: usize| Object {
4262 kind: ObjectKind::Tree,
4263 data: vec![0; len],
4264 };
4265 let oid = |n: u32| {
4266 let mut raw = [0u8; 20];
4267 raw[..4].copy_from_slice(&n.to_be_bytes());
4268 ObjectId::from_bytes(&raw).unwrap()
4269 };
4270 for n in 0..4 {
4271 cache.insert(oid(n), &object(chunk));
4272 }
4273 assert_eq!(cache.bytes, SESSION_LOOSE_CACHE_MAX_BYTES);
4274 cache.insert(oid(4), &object(1));
4275 assert_eq!(cache.bytes, 1, "exceeding the byte budget starts over");
4276 assert!(cache.get(&oid(0)).is_none());
4277 cache.insert(oid(4), &object(10));
4278 assert_eq!(cache.bytes, 10, "replacing an entry drops its old size");
4279 cache.insert(oid(5), &object(SESSION_LOOSE_CACHE_MAX_BYTES + 1));
4280 assert!(
4281 cache.get(&oid(5)).is_none(),
4282 "oversized objects are never kept"
4283 );
4284 assert!(cache.get(&oid(4)).is_some());
4285 for n in 10..(10 + SESSION_LOOSE_CACHE_MAX_ENTRIES as u32 + 2) {
4286 cache.insert(oid(n), &object(1));
4287 }
4288 assert!(cache.objects.len() <= SESSION_LOOSE_CACHE_MAX_ENTRIES + 1);
4289 assert!(cache.bytes <= SESSION_LOOSE_CACHE_MAX_BYTES);
4290 }
4291
4292 fn bounded_read_fixture() -> (TempDir, PathBuf, ObjectId, ObjectId) {
4293 let dir = TempDir::new().unwrap();
4294 let objects = dir.path().to_path_buf();
4295 let odb = Odb::new(&objects);
4296 let small = odb.write(ObjectKind::Blob, b"small blob\n").unwrap();
4297 let big: Vec<u8> = (0..300_000u32).flat_map(|i| i.to_le_bytes()).collect();
4298 let big = odb.write(ObjectKind::Blob, &big).unwrap();
4299 (dir, objects, small, big)
4300 }
4301
4302 fn assert_bounded_reads(odb: &Odb, small: &ObjectId, big: &ObjectId) {
4303 match odb.read_unless_larger(small, 1000, false).unwrap() {
4304 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
4305 BoundedRead::Larger => panic!("small blob is within the limit"),
4306 }
4307 assert!(matches!(
4308 odb.read_unless_larger(big, 1000, false).unwrap(),
4309 BoundedRead::Larger
4310 ));
4311 match odb.read_unless_larger(big, 1_200_000, false).unwrap() {
4312 BoundedRead::Object(obj) => assert_eq!(obj.data.len(), 1_200_000),
4313 BoundedRead::Larger => panic!("limit equal to the size reads the object"),
4314 }
4315 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
4316 assert!(matches!(
4317 odb.read_unless_larger(&missing, 1000, false),
4318 Err(Error::ObjectNotFound(_))
4319 ));
4320 }
4321
4322 #[test]
4323 fn read_unless_larger_sizes_loose_objects_from_their_header() {
4324 let (_dir, objects, small, big) = bounded_read_fixture();
4325 assert_bounded_reads(&Odb::new(&objects), &small, &big);
4326 }
4327
4328 #[test]
4329 fn read_unless_larger_sizes_packed_objects_in_and_out_of_read_context() {
4330 let (_dir, objects, small, big) = bounded_read_fixture();
4331 pack_objects(&objects, &[small, big]);
4332 let odb = Odb::new(&objects);
4333 assert_bounded_reads(&odb, &small, &big);
4334 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
4335 }
4336
4337 #[test]
4338 fn parsed_tree_reads_keep_no_raw_copy_in_the_session_cache() {
4339 let (_dir, objects, small, _big) = bounded_read_fixture();
4340 let odb = Odb::new(&objects);
4341 let entries = [crate::objects::TreeEntry {
4342 mode: 0o100644,
4343 name: b"small".to_vec(),
4344 oid: small,
4345 }];
4346 let tree = odb
4347 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
4348 .unwrap();
4349 for packed in [false, true] {
4350 if packed {
4351 pack_objects(&objects, &[tree]);
4352 }
4353 let odb = Odb::new(&objects);
4354 assert_eq!(*odb.cached_tree_entries(&tree).unwrap(), entries);
4355 assert!(odb.session_cached_loose_read(&tree).is_none());
4356 assert!(Arc::ptr_eq(
4357 &odb.cached_tree_entries(&tree).unwrap(),
4358 &odb.cached_tree_entries(&tree).unwrap()
4359 ));
4360 assert_eq!(odb.read(&tree).unwrap().kind, ObjectKind::Tree);
4361 assert!(
4362 odb.session_cached_loose_read(&tree).is_some(),
4363 "plain reads still use the session cache (packed: {packed})"
4364 );
4365 }
4366 }
4367
4368 #[test]
4369 fn read_unless_larger_reuses_small_loose_reads_but_never_caches_big_ones() {
4370 let (_dir, objects, small, big) = bounded_read_fixture();
4371 let odb = Odb::new(&objects);
4372 assert_bounded_reads(&odb, &small, &big);
4373 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
4374 assert!(matches!(
4375 odb.read_unless_larger(&small, 1000, true).unwrap(),
4376 BoundedRead::Object(_)
4377 ));
4378 assert!(matches!(
4379 odb.read_unless_larger(&big, 1_200_000, true).unwrap(),
4380 BoundedRead::Object(_)
4381 ));
4382 for oid in [small, big] {
4383 let hex = oid.to_hex();
4384 fs::remove_file(objects.join(&hex[..2]).join(&hex[2..])).unwrap();
4385 }
4386 match odb.read_unless_larger(&small, 1000, true).unwrap() {
4387 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
4388 BoundedRead::Larger => panic!("small blob is within the limit"),
4389 }
4390 assert!(matches!(
4391 odb.read_unless_larger(&small, 4, true),
4392 Ok(BoundedRead::Larger)
4393 ));
4394 assert!(matches!(
4395 odb.read_unless_larger(&big, 1_200_000, true),
4396 Err(Error::ObjectNotFound(_))
4397 ));
4398 }
4399
4400 #[test]
4401 fn read_unless_larger_finds_objects_across_several_packs() {
4402 let (_dir, objects, small, big) = bounded_read_fixture();
4403 pack_objects(&objects, &[small]);
4404 pack_objects(&objects, &[big]);
4405 let odb = Odb::new(&objects);
4406 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
4407 }
4408
4409 #[test]
4410 fn parse_object_header_prefix_unknown_kind_is_typed_error() {
4411 let err = crate::odb::store::loose::parse_object_header_prefix(b"not-a-git-kind 0\0")
4412 .unwrap_err();
4413 assert!(matches!(err, Error::UnknownObjectType(_)));
4414 }
4415
4416 fn git_dir_with_config(config: &str) -> (TempDir, PathBuf, PathBuf) {
4417 let dir = TempDir::new().unwrap();
4418 let git_dir = dir.path().join(".git");
4419 let objects = git_dir.join("objects");
4420 fs::create_dir_all(&objects).unwrap();
4421 fs::write(git_dir.join("config"), config).unwrap();
4422 (dir, git_dir, objects)
4423 }
4424
4425 fn zlib_flevel(compressed: &[u8]) -> u8 {
4427 assert!(compressed.len() >= 2);
4428 (compressed[1] >> 6) & 3
4429 }
4430
4431 fn tmp_loose_object_paths(objects: &Path) -> Vec<PathBuf> {
4432 let mut tmp = Vec::new();
4433 let Ok(shards) = fs::read_dir(objects) else {
4434 return tmp;
4435 };
4436 for shard in shards.flatten() {
4437 if !shard.file_type().is_ok_and(|t| t.is_dir()) {
4438 continue;
4439 }
4440 let Ok(files) = fs::read_dir(shard.path()) else {
4441 continue;
4442 };
4443 for file in files.flatten() {
4444 let name = file.file_name();
4445 if name.to_string_lossy().starts_with("tmp_") {
4446 tmp.push(file.path());
4447 }
4448 }
4449 }
4450 tmp
4451 }
4452
4453 #[test]
4454 fn loose_write_honours_level() {
4455 let payload: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
4456
4457 let (_dir, git_dir, objects) = git_dir_with_config("");
4458 let odb = Odb::new(&objects).with_config_git_dir(git_dir.clone());
4459 let oid_default = odb.write(ObjectKind::Blob, &payload).unwrap();
4460 let bytes_default = fs::read(odb.object_path(&oid_default)).unwrap();
4461 assert!(zlib_flevel(&bytes_default) <= 1);
4462 assert_eq!(odb.read(&oid_default).unwrap().data, payload);
4463
4464 for level in -1i32..=9 {
4465 let config = format!("[core]\n\tloosecompression = {level}\n");
4466 let (_dir, git_dir, objects) = git_dir_with_config(&config);
4467 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4468 let byte = u8::try_from(level.rem_euclid(256)).unwrap_or(0);
4469 let bytes: Vec<u8> = vec![byte; 512];
4470 let oid = odb.write(ObjectKind::Blob, &bytes).unwrap();
4471 let on_disk = fs::read(odb.object_path(&oid)).unwrap();
4472 assert!(on_disk.len() >= 2, "level {level}");
4473 assert_eq!(odb.read(&oid).unwrap().data, bytes.as_slice());
4474 }
4475
4476 let config = "[core]\n\tcompression = 9\n";
4477 let (_dir, git_dir, objects) = git_dir_with_config(config);
4478 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4479 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
4480 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 3);
4481
4482 let config = "[core]\n\tcompression = 0\n\tloosecompression = 1\n";
4483 let (_dir, git_dir, objects) = git_dir_with_config(config);
4484 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4485 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
4486 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 0);
4487
4488 let config = "[core]\n\tloosecompression = 99\n";
4489 let (_dir, git_dir, objects) = git_dir_with_config(config);
4490 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4491 assert!(odb.write(ObjectKind::Blob, b"x").is_err());
4492 assert!(tmp_loose_object_paths(&objects).is_empty());
4493 }
4494
4495 fn git_run(dir: &Path, args: &[&str]) -> std::process::Output {
4496 std::process::Command::new("git") .current_dir(dir)
4498 .args(args)
4499 .env("GIT_CONFIG_GLOBAL", "/dev/null")
4500 .env("GIT_CONFIG_SYSTEM", "/dev/null")
4501 .env("GIT_AUTHOR_NAME", "T")
4502 .env("GIT_AUTHOR_EMAIL", "t@example.com")
4503 .env("GIT_COMMITTER_NAME", "T")
4504 .env("GIT_COMMITTER_EMAIL", "t@example.com")
4505 .output()
4506 .unwrap()
4507 }
4508
4509 fn git_in(dir: &Path, args: &[&str]) {
4510 let out = git_run(dir, args);
4511 assert!(
4512 out.status.success(),
4513 "git {:?}: {}",
4514 args,
4515 String::from_utf8_lossy(&out.stderr)
4516 );
4517 }
4518
4519 fn git_rev_parse_oid(dir: &Path, spec: &str) -> ObjectId {
4520 let out = git_run(dir, &["rev-parse", spec]);
4521 assert!(
4522 out.status.success(),
4523 "git rev-parse {spec}: {}",
4524 String::from_utf8_lossy(&out.stderr)
4525 );
4526 ObjectId::from_hex(std::str::from_utf8(&out.stdout).unwrap().trim()).unwrap()
4527 }
4528
4529 #[test]
4530 fn write_loose_zlib_prehashed_matches_write() {
4531 let dir = TempDir::new().unwrap();
4532 let objects = dir.path().join("objects");
4533 let odb = Odb::new(&objects);
4534 let data = b"parallel prehash path";
4535 let oid_normal = odb.write(ObjectKind::Blob, data).unwrap();
4536
4537 let dir2 = TempDir::new().unwrap();
4538 let objects2 = dir2.path().join("objects");
4539 let odb2 = Odb::new(&objects2);
4540 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, data).expect("hash");
4541 assert_eq!(oid, oid_normal);
4542 let zlib =
4543 super::zlib_compress_blob_loose(data, odb2.loose_compression().unwrap()).unwrap();
4544 let oid2 = odb2
4545 .write_loose_zlib_prehashed(
4546 &oid,
4547 &zlib,
4548 WriteOptions {
4549 assume_loose_only_existence: true,
4550 ..WriteOptions::default()
4551 },
4552 )
4553 .unwrap();
4554 assert_eq!(oid2, oid_normal);
4555 assert_eq!(
4556 fs::read(odb2.object_path(&oid2)).unwrap(),
4557 fs::read(odb.object_path(&oid_normal)).unwrap()
4558 );
4559 }
4560
4561 #[test]
4562 fn write_blob_prehashed_matches_write_and_skips_existing_objects() {
4563 let dir = TempDir::new().unwrap();
4564 let odb = Odb::new(&dir.path().join("objects"));
4565 let data = vec![7u8; 100_000];
4566 let oid_normal = odb.write(ObjectKind::Blob, &data).unwrap();
4567
4568 let dir2 = TempDir::new().unwrap();
4569 let odb2 = Odb::new(&dir2.path().join("objects"));
4570 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, &data).unwrap();
4571 let options = WriteOptions {
4572 assume_loose_only_existence: true,
4573 ..WriteOptions::default()
4574 };
4575 assert_eq!(
4576 odb2.write_blob_prehashed(&oid, &data, options).unwrap(),
4577 oid
4578 );
4579 assert_eq!(oid, oid_normal);
4580 assert!(odb2.object_path(&oid).is_file());
4581 let obj = odb2.read(&oid).unwrap();
4582 assert_eq!(
4583 (obj.kind, obj.data.as_slice()),
4584 (ObjectKind::Blob, data.as_slice())
4585 );
4586
4587 assert_eq!(
4589 odb2.write_blob_prehashed(&oid, b"other", options).unwrap(),
4590 oid
4591 );
4592 assert_eq!(odb2.read(&oid).unwrap().data, data);
4593 }
4594
4595 #[test]
4596 fn write_loose_zlib_prehashed_routes_to_custom_primary() {
4597 use crate::objects::HashAlgo;
4598 use std::sync::Arc;
4599
4600 let dir = TempDir::new().unwrap();
4601 let objects = dir.path().join("objects");
4602 fs::create_dir_all(&objects).unwrap();
4603 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
4604 let odb = OdbBuilder::files(&objects)
4605 .primary(store.clone())
4606 .alternates(false)
4607 .build();
4608 let data = b"staged through a custom primary";
4609 let oid = hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, data).unwrap();
4610 let zlib = super::zlib_compress_blob_loose(data, Compression::fast()).unwrap();
4611
4612 odb.ensure_all_loose_prefix_dirs().unwrap();
4613 let written = odb
4614 .write_loose_zlib_prehashed(&oid, &zlib, WriteOptions::default())
4615 .unwrap();
4616 assert_eq!(written, oid);
4617 assert!(store.contains(&oid).unwrap());
4618 assert_eq!(odb.read(&oid).unwrap().data, data);
4619 assert_eq!(
4620 fs::read_dir(&objects).unwrap().count(),
4621 0,
4622 "no loose shard directories or files beside a custom primary"
4623 );
4624
4625 assert!(odb
4626 .write_loose_zlib_prehashed(&oid, b"not zlib", WriteOptions::default())
4627 .is_err());
4628 }
4629
4630 #[test]
4631 fn custom_primary_write_skips_odb_contains_probes() {
4632 use crate::objects::HashAlgo;
4633 use std::ops::ControlFlow;
4634 use std::sync::atomic::{AtomicUsize, Ordering};
4635 use std::sync::Arc;
4636
4637 #[derive(Debug)]
4638 struct Counting(MemoryStore, AtomicUsize, AtomicUsize);
4639
4640 impl ObjectStore for Counting {
4641 fn hash_algo(&self) -> HashAlgo {
4642 self.0.hash_algo()
4643 }
4644 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
4645 self.0.read(oid)
4646 }
4647 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
4648 self.0.read_info(oid)
4649 }
4650 fn contains(&self, oid: &ObjectId) -> Result<bool> {
4651 self.1.fetch_add(1, Ordering::Relaxed);
4652 self.0.contains(oid)
4653 }
4654 fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
4655 self.2.fetch_add(1, Ordering::Relaxed);
4656 self.0.contains_many(oids)
4657 }
4658 fn for_each_object(
4659 &self,
4660 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
4661 ) -> Result<()> {
4662 self.0.for_each_object(f)
4663 }
4664 }
4665
4666 impl WritableObjectStore for Counting {
4667 fn write(
4668 &self,
4669 kind: ObjectKind,
4670 data: &[u8],
4671 options: WriteOptions,
4672 ) -> Result<ObjectId> {
4673 self.0.write(kind, data, options)
4674 }
4675 fn freshen(&self, oid: &ObjectId) -> Result<bool> {
4676 self.0.freshen(oid)
4677 }
4678 }
4679
4680 let dir = TempDir::new().unwrap();
4681 let objects = dir.path().join("objects");
4682 fs::create_dir_all(&objects).unwrap();
4683 let primary = Arc::new(Counting(
4684 MemoryStore::new(HashAlgo::Sha1),
4685 AtomicUsize::new(0),
4686 AtomicUsize::new(0),
4687 ));
4688 let read_source = Arc::new(Counting(
4689 MemoryStore::new(HashAlgo::Sha1),
4690 AtomicUsize::new(0),
4691 AtomicUsize::new(0),
4692 ));
4693 read_source
4694 .0
4695 .write(ObjectKind::Blob, b"only upstream", WriteOptions::default())
4696 .unwrap();
4697 let odb = OdbBuilder::files(&objects)
4698 .primary(primary.clone())
4699 .push_read_source(read_source.clone())
4700 .alternates(false)
4701 .build();
4702
4703 odb.write(ObjectKind::Blob, b"new local").unwrap();
4704 assert_eq!(primary.1.load(Ordering::Relaxed), 0);
4705 assert_eq!(read_source.1.load(Ordering::Relaxed), 0);
4706
4707 odb.write(ObjectKind::Blob, b"only upstream").unwrap();
4708 assert_eq!(primary.1.load(Ordering::Relaxed), 0);
4709 assert!(primary
4710 .0
4711 .contains(
4712 &hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, b"only upstream").unwrap()
4713 )
4714 .unwrap());
4715 }
4716
4717 #[test]
4720 fn write_existing_loose_does_not_rewrite() {
4721 let dir = TempDir::new().unwrap();
4722 let objects = dir.path().join("objects");
4723 fs::create_dir_all(&objects).unwrap();
4724 let odb = Odb::new(&objects);
4725 let payload = b"already loose";
4726 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4727 let path = odb.object_path(&oid);
4728 let before_bytes = fs::read(&path).unwrap();
4729 let meta_before = fs::metadata(&path).unwrap();
4730 let before_mtime = filetime::FileTime::from_last_modification_time(&meta_before);
4731 #[cfg(unix)]
4732 let ino_before = {
4733 use std::os::unix::fs::MetadataExt;
4734 meta_before.ino()
4735 };
4736 std::thread::sleep(std::time::Duration::from_millis(20));
4737 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4738 assert_eq!(oid, oid2);
4739 assert_eq!(fs::read(&path).unwrap(), before_bytes);
4740 let meta_after = fs::metadata(&path).unwrap();
4741 let after_mtime = filetime::FileTime::from_last_modification_time(&meta_after);
4742 assert!(
4743 after_mtime >= before_mtime,
4744 "expected freshen to bump or preserve mtime"
4745 );
4746 #[cfg(unix)]
4747 {
4748 use std::os::unix::fs::MetadataExt;
4749 assert_eq!(
4750 meta_after.ino(),
4751 ino_before,
4752 "rewrite must not replace the loose object file"
4753 );
4754 }
4755 }
4756
4757 #[cfg_attr(
4759 not(feature = "full-tests"),
4760 ignore = "starts system git or is heavy; run with --features full-tests"
4761 )]
4762 #[test]
4763 fn write_skips_loose_when_object_in_pack_only() {
4764 let dir = TempDir::new().unwrap();
4765 git_in(dir.path(), &["init", "-q"]);
4766 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4767 git_in(dir.path(), &["config", "user.name", "T"]);
4768 fs::write(dir.path().join("f"), b"pack-only payload").unwrap();
4769 git_in(dir.path(), &["add", "f"]);
4770 git_in(dir.path(), &["commit", "-m", "c"]);
4771 git_in(dir.path(), &["repack", "-ad"]);
4772 let objects = dir.path().join(".git").join("objects");
4773 let odb = Odb::new(&objects);
4774 let oid = odb.write(ObjectKind::Blob, b"pack-only payload").unwrap();
4775 assert!(!odb.object_path(&oid).exists());
4776 assert!(odb.exists(&oid));
4777 }
4778
4779 #[cfg_attr(
4781 not(feature = "full-tests"),
4782 ignore = "starts system git or is heavy; run with --features full-tests"
4783 )]
4784 #[test]
4785 fn write_freshens_loose_object_in_alternate() {
4786 use filetime::FileTime;
4787 use std::thread;
4788 use std::time::Duration;
4789
4790 let alt = TempDir::new().unwrap();
4791 git_in(alt.path(), &["init", "-q", "--bare"]);
4792 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
4793 let alt_objects = alt.path().join("objects");
4794 let alt_odb = Odb::new(&alt_objects);
4795 let payload = b"alternate-loose";
4796 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
4797 let alt_loose = alt_odb.object_path(&oid);
4798
4799 let primary = TempDir::new().unwrap();
4800 let primary_objects = primary.path().join("objects");
4801 fs::create_dir_all(primary_objects.join("info")).unwrap();
4802 fs::write(
4803 primary_objects.join("info").join("alternates"),
4804 format!("{}\n", alt_objects.display()),
4805 )
4806 .unwrap();
4807 let odb = Odb::new(&primary_objects);
4808 thread::sleep(Duration::from_millis(50));
4809 let before = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
4810 filetime::set_file_mtime(&alt_loose, before).unwrap();
4811
4812 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4813 assert_eq!(oid, oid2);
4814 assert!(!odb.object_path(&oid).exists());
4815 let after = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
4816 assert!(after > before, "write must freshen alternate loose object");
4817 }
4818
4819 #[cfg_attr(
4820 not(feature = "full-tests"),
4821 ignore = "starts system git or is heavy; run with --features full-tests"
4822 )]
4823 #[test]
4824 fn write_freshens_packed_object_in_alternate() {
4825 use filetime::FileTime;
4826 use std::thread;
4827 use std::time::Duration;
4828
4829 let alt = TempDir::new().unwrap();
4830 git_in(alt.path(), &["init", "-q"]);
4831 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
4832 git_in(alt.path(), &["config", "user.name", "T"]);
4833 let payload = b"alternate-pack";
4834 fs::write(alt.path().join("f"), payload).unwrap();
4835 git_in(alt.path(), &["add", "f"]);
4836 git_in(alt.path(), &["commit", "-m", "c"]);
4837 git_in(alt.path(), &["repack", "-ad"]);
4838 let alt_objects = alt.path().join(".git").join("objects");
4839 let alt_odb = Odb::new(&alt_objects);
4840 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
4841 let pack_path = fs::read_dir(alt_objects.join("pack"))
4842 .unwrap()
4843 .flatten()
4844 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
4845 .unwrap()
4846 .path();
4847 thread::sleep(Duration::from_millis(50));
4848 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4849
4850 let primary = TempDir::new().unwrap();
4851 let primary_objects = primary.path().join("objects");
4852 fs::create_dir_all(primary_objects.join("info")).unwrap();
4853 fs::write(
4854 primary_objects.join("info").join("alternates"),
4855 format!("{}\n", alt_objects.display()),
4856 )
4857 .unwrap();
4858 pack::clear_pack_cache();
4859 let odb = Odb::new(&primary_objects);
4860 assert!(odb.exists(&oid));
4861 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4862 assert_eq!(oid, oid2);
4863 assert!(!odb.object_path(&oid).exists());
4864 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4865 assert!(after >= before, "write must freshen alternate pack");
4866 }
4867
4868 #[cfg_attr(
4869 not(feature = "full-tests"),
4870 ignore = "starts system git or is heavy; run with --features full-tests"
4871 )]
4872 #[test]
4873 fn write_skips_loose_when_object_only_in_alternate() {
4874 let alt = TempDir::new().unwrap();
4875 git_in(alt.path(), &["init", "-q", "--bare"]);
4876 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
4877 let alt_objects = alt.path().join("objects");
4878 let alt_odb = Odb::new(&alt_objects);
4879 let payload = b"alternate-only";
4880 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
4881
4882 let primary = TempDir::new().unwrap();
4883 let primary_objects = primary.path().join("objects");
4884 fs::create_dir_all(primary.path().join("objects").join("info")).unwrap();
4885 fs::write(
4886 primary
4887 .path()
4888 .join("objects")
4889 .join("info")
4890 .join("alternates"),
4891 format!("{}\n", alt_objects.display()),
4892 )
4893 .unwrap();
4894 let odb = Odb::new(&primary_objects);
4895 assert!(odb.exists(&oid));
4896 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4897 assert_eq!(oid, oid2);
4898 assert!(!odb.object_path(&oid).exists());
4899 }
4900
4901 #[cfg_attr(
4902 not(feature = "full-tests"),
4903 ignore = "starts system git or is heavy; run with --features full-tests"
4904 )]
4905 #[test]
4906 fn write_with_mem_overlay_stays_in_memory() {
4907 let dir = TempDir::new().unwrap();
4908 let objects = dir.path().join("objects");
4909 fs::create_dir_all(&objects).unwrap();
4910 let odb = Odb::new(&objects);
4911 odb.enable_mem_overlay();
4912 let payload = b"overlay blob";
4913 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4914 assert!(!odb.object_path(&oid).exists());
4915 assert_eq!(odb.read(&oid).unwrap().data, payload);
4916 odb.disable_mem_overlay();
4917 assert!(odb.read(&oid).is_err());
4918 }
4919
4920 #[test]
4921 fn persist_mem_overlay_writes_through_custom_primary() {
4922 use crate::objects::HashAlgo;
4923 use std::sync::Arc;
4924
4925 let dir = TempDir::new().unwrap();
4926 let objects = dir.path().join("objects");
4927 fs::create_dir_all(&objects).unwrap();
4928 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
4929 let odb = OdbBuilder::files(&objects)
4930 .primary(store.clone())
4931 .alternates(false)
4932 .build();
4933 odb.enable_mem_overlay();
4934 let payload = b"overlay blob";
4935 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4936 assert!(!store.contains(&oid).unwrap());
4937 odb.persist_mem_overlay().unwrap();
4938 assert!(store.contains(&oid).unwrap());
4939 assert_eq!(odb.read(&oid).unwrap().data, payload);
4940 assert!(!odb.object_path(&oid).exists());
4941 }
4942
4943 #[test]
4944 fn read_alternates_relative_path_and_symlinked_objects_dir() {
4945 use crate::pack::read_alternates_recursive;
4946
4947 let layout = TempDir::new().unwrap();
4948 let alt_objects = layout.path().join("alt-store").join("objects");
4949 fs::create_dir_all(&alt_objects).unwrap();
4950 let primary_objects = layout.path().join("repo").join("objects");
4951 fs::create_dir_all(primary_objects.join("info")).unwrap();
4952 fs::write(
4953 primary_objects.join("info").join("alternates"),
4954 "../../alt-store/objects\n",
4955 )
4956 .unwrap();
4957 let resolved = read_alternates_recursive(&primary_objects).unwrap();
4958 let alt_canonical = alt_objects
4959 .canonicalize_simplified()
4960 .unwrap_or(alt_objects.clone());
4961 assert!(
4962 resolved.iter().any(|p| {
4963 p.canonicalize_simplified().unwrap_or_else(|_| p.clone()) == alt_canonical
4964 }),
4965 "relative alternate entry should resolve: {resolved:?}"
4966 );
4967
4968 #[cfg(unix)]
4969 {
4970 let link_parent = TempDir::new().unwrap();
4971 std::os::unix::fs::symlink(&primary_objects, link_parent.path().join("objects-link"))
4972 .unwrap();
4973 let via_link =
4974 read_alternates_recursive(&link_parent.path().join("objects-link")).unwrap();
4975 assert!(
4976 !via_link.is_empty(),
4977 "alternates through symlinked objects dir: {via_link:?}"
4978 );
4979 }
4980 }
4981 #[test]
4982 fn empty_tree_exists_without_hex() {
4983 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
4984 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4985 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").unwrap();
4986 assert!(odb.exists(&canon));
4987 assert!(odb.exists(&legacy));
4988 assert!(odb.exists_local(&canon));
4989 assert!(!odb.freshen_object(&canon));
4990 }
4991
4992 #[test]
4993 fn sha256_empty_tree_virtual_read_without_loose_file() {
4994 use crate::objects::HashAlgo;
4995 use std::sync::Arc;
4996
4997 let dir = TempDir::new().unwrap();
4998 let objects = dir.path().join("objects");
4999 fs::create_dir_all(&objects).unwrap();
5000 let store = Arc::new(MemoryStore::new(HashAlgo::Sha256));
5001 let odb = OdbBuilder::files(&objects)
5002 .primary(store)
5003 .alternates(false)
5004 .build();
5005 let empty = odb.empty_tree_oid().expect("hash empty tree");
5006 assert!(!odb.exists_materialized_local(&empty));
5007 assert!(odb.exists_local(&empty));
5008 assert!(odb.is_empty_tree(&empty));
5009 let obj = odb.read(&empty).expect("virtual read empty tree");
5010 assert_eq!(obj.kind, ObjectKind::Tree);
5011 assert!(obj.data.is_empty());
5012 }
5013
5014 #[test]
5015 fn empty_tree_oid_and_object_id_empty_tree_match_sha1() {
5016 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
5017 let from_odb = odb.empty_tree_oid().expect("empty tree oid");
5018 assert_eq!(from_odb, ObjectId::empty_tree());
5019 assert!(odb.is_empty_tree(&ObjectId::empty_tree()));
5020 }
5021
5022 #[test]
5023 fn empty_tree_virtual_presence_skips_materialized_local() {
5024 let dir = TempDir::new().unwrap();
5025 let objects = dir.path().join("objects");
5026 fs::create_dir_all(&objects).unwrap();
5027 let odb = Odb::new(&objects);
5028 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
5029 assert!(odb.exists_local(&canon));
5030 assert!(!odb.exists_materialized_local(&canon));
5031 }
5032
5033 #[test]
5034 fn write_materializes_well_known_empty_tree() {
5035 let dir = TempDir::new().unwrap();
5036 let objects = dir.path().join("objects");
5037 fs::create_dir_all(&objects).unwrap();
5038 let odb = Odb::new(&objects);
5039 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
5040 assert!(!odb.exists_materialized_local(&canon));
5041 let oid = odb
5042 .write(crate::objects::ObjectKind::Tree, &[])
5043 .expect("write empty tree");
5044 assert_eq!(oid, canon);
5045 assert!(odb.exists_materialized_local(&canon));
5046 }
5047
5048 #[test]
5049 fn append_alternate_objects_line_errors_on_unreadable_existing_file() {
5050 let dir = TempDir::new().unwrap();
5051 let objects = dir.path().join("objects");
5052 fs::create_dir_all(objects.join("info")).unwrap();
5053 let alt_path = objects.join("info/alternates");
5054 fs::write(&alt_path, b"not valid utf-8 \xff\n").unwrap();
5055 let err = Odb::append_alternate_objects_line(&objects, Path::new("/tmp/other/objects"))
5056 .unwrap_err();
5057 assert!(matches!(err, Error::Io(_)));
5058 assert_eq!(fs::read(&alt_path).unwrap(), b"not valid utf-8 \xff\n");
5059 }
5060
5061 #[test]
5062 fn append_file_alternate_refreshes_cached_snapshot() {
5063 let primary = TempDir::new().unwrap();
5064 let alternate = TempDir::new().unwrap();
5065 let alt_objects = alternate.path().join("objects");
5066 std::fs::create_dir_all(&alt_objects).unwrap();
5067 let alt_odb = Odb::new(&alt_objects);
5068 let oid = alt_odb.write(ObjectKind::Blob, b"via-alternate").unwrap();
5069
5070 let primary_objects = primary.path().join("objects");
5071 std::fs::create_dir_all(primary_objects.join("info")).unwrap();
5072 let primary_odb = Odb::new(&primary_objects);
5073 assert!(!primary_odb.exists(&oid));
5074 let _ = primary_odb.file_alternate_dirs_snapshot();
5075 primary_odb.append_file_alternate(&alt_objects).unwrap();
5076 assert!(primary_odb.exists(&oid));
5077 }
5078
5079 #[test]
5080 fn exists_via_env_alternate_dirs_at_construction() {
5081 let primary = TempDir::new().unwrap();
5082 let alternate = TempDir::new().unwrap();
5083 let alt_objects = alternate.path().join("objects");
5084 std::fs::create_dir_all(&alt_objects).unwrap();
5085 let alt_odb = Odb::new(&alt_objects);
5086 let oid = alt_odb.write(ObjectKind::Blob, b"env-alt").unwrap();
5087
5088 let primary_objects = primary.path().join("objects");
5089 std::fs::create_dir_all(&primary_objects).unwrap();
5090 let primary_odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
5091 assert!(primary_odb.exists(&oid));
5092 }
5093
5094 fn flaky_odb(objects: &Path) -> (Odb, Arc<store::flaky::FlakyStore>) {
5095 let flaky = store::flaky::FlakyStore::new();
5096 let odb = OdbBuilder::files(objects)
5097 .primary(flaky.clone())
5098 .alternates(false)
5099 .build();
5100 (odb, flaky)
5101 }
5102
5103 #[test]
5104 fn contains_reports_primary_failure_instead_of_missing() {
5105 let dir = TempDir::new().unwrap();
5106 let (odb, flaky) = flaky_odb(dir.path());
5107 let oid = odb.write(ObjectKind::Blob, b"stored").unwrap();
5108 assert!(odb.contains(&oid).unwrap());
5109
5110 flaky.fail_all(true);
5111 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
5112 assert!(store::flaky::is_injected(&odb.contains(&oid).unwrap_err()));
5113 assert!(
5114 !odb.exists(&oid),
5115 "exists() keeps treating failures as absent"
5116 );
5117 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
5118
5119 let try_local = odb.try_exists_local(&oid).unwrap_err();
5120 assert!(store::flaky::is_injected(&try_local));
5121 assert!(!odb.exists_local(&oid));
5122 let try_stored = odb.try_exists_stored(&oid).unwrap_err();
5123 assert!(store::flaky::is_injected(&try_stored));
5124 assert!(!odb.exists_stored(&oid));
5125 }
5126
5127 #[test]
5128 fn contains_reports_extra_read_source_failure() {
5129 let dir = TempDir::new().unwrap();
5130 let flaky = store::flaky::FlakyStore::new();
5131 let odb = OdbBuilder::files(dir.path())
5132 .push_read_source(flaky.clone())
5133 .alternates(false)
5134 .build();
5135 let local = odb.write(ObjectKind::Blob, b"in primary").unwrap();
5136 let absent = HashAlgo::Sha1
5137 .hash_object(ObjectKind::Blob, b"nowhere")
5138 .unwrap();
5139 assert!(!odb.contains(&absent).unwrap());
5140
5141 flaky.fail_contains(true);
5142 assert!(
5143 odb.contains(&local).unwrap(),
5144 "primary hit wins over a failing source"
5145 );
5146 assert!(store::flaky::is_injected(
5147 &odb.contains(&absent).unwrap_err()
5148 ));
5149 assert!(!odb.exists(&absent));
5150 }
5151
5152 #[test]
5153 fn contains_reports_primary_failure_even_when_later_source_has_object() {
5154 let dir = TempDir::new().unwrap();
5155 let extra = Arc::new(store::MemoryStore::new(HashAlgo::Sha1));
5156 let flaky = store::flaky::FlakyStore::new();
5157 let odb = OdbBuilder::files(dir.path())
5158 .primary(flaky.clone())
5159 .push_read_source(extra.clone())
5160 .alternates(false)
5161 .build();
5162 let oid = extra
5163 .write(ObjectKind::Blob, b"extra", WriteOptions::default())
5164 .unwrap();
5165 assert!(odb.contains(&oid).unwrap());
5166
5167 flaky.fail_all(true);
5168 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
5169 assert!(
5170 store::flaky::is_injected(&odb.contains(&oid).unwrap_err()),
5171 "contains must agree with read"
5172 );
5173 assert!(odb.exists(&oid), "exists() skips the failing primary");
5174 }
5175
5176 #[test]
5177 fn contains_reports_alternate_failure() {
5178 let primary = TempDir::new().unwrap();
5179 let alternate = TempDir::new().unwrap();
5180 let alt_objects = alternate.path().join("objects");
5181 std::fs::create_dir_all(&alt_objects).unwrap();
5182 let primary_objects = primary.path().join("objects");
5183 std::fs::create_dir_all(&primary_objects).unwrap();
5184 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
5185 let local = odb.write(ObjectKind::Blob, b"local").unwrap();
5186 let absent = HashAlgo::Sha1
5187 .hash_object(ObjectKind::Blob, b"nowhere")
5188 .unwrap();
5189
5190 let flaky = store::flaky::FlakyStore::new();
5193 odb.sources().unwrap();
5194 odb.alternate_sources_cache.write().unwrap().sources = Some(Arc::new(CompositeStore::new(
5195 vec![flaky.clone()],
5196 HashAlgo::Sha1,
5197 )));
5198 assert!(!odb.contains(&absent).unwrap());
5199
5200 flaky.fail_contains(true);
5201 assert!(odb.contains(&local).unwrap());
5202 assert!(store::flaky::is_injected(
5203 &odb.contains(&absent).unwrap_err()
5204 ));
5205 assert!(!odb.exists(&absent));
5206 }
5207
5208 #[test]
5209 fn contains_on_files_backend_covers_present_absent_empty_tree_and_alternate() {
5210 let primary = TempDir::new().unwrap();
5211 let alternate = TempDir::new().unwrap();
5212 let alt_objects = alternate.path().join("objects");
5213 std::fs::create_dir_all(&alt_objects).unwrap();
5214 let alt_oid = Odb::new(&alt_objects)
5215 .write(ObjectKind::Blob, b"alternate")
5216 .unwrap();
5217 let primary_objects = primary.path().join("objects");
5218 std::fs::create_dir_all(&primary_objects).unwrap();
5219 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
5220 let present = odb.write(ObjectKind::Blob, b"present").unwrap();
5221 let absent = HashAlgo::Sha1
5222 .hash_object(ObjectKind::Blob, b"absent")
5223 .unwrap();
5224
5225 assert!(odb.contains(&present).unwrap());
5226 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
5227 assert!(odb.contains(&alt_oid).unwrap());
5228 assert!(!odb.contains(&absent).unwrap());
5229 let empty_tree = odb.empty_tree_oid().unwrap();
5230 assert_eq!(
5231 odb.contains_many(&[present, absent, alt_oid, empty_tree])
5232 .unwrap(),
5233 vec![true, false, true, true]
5234 );
5235 assert!(odb.contains_many(&[]).unwrap().is_empty());
5236 assert_eq!(
5237 odb.present_oids([present, absent, alt_oid]).unwrap(),
5238 vec![present, alt_oid]
5239 );
5240 assert_eq!(
5241 odb.missing_oids([present, absent, alt_oid]).unwrap(),
5242 vec![absent]
5243 );
5244 assert_eq!(
5245 odb.missing_stored_oids([present, absent, alt_oid]).unwrap(),
5246 vec![absent, alt_oid],
5247 "stored ignores alternates"
5248 );
5249 }
5250
5251 #[test]
5252 fn missing_stored_oids_sees_packs_written_since_the_last_probe() {
5253 let dir = TempDir::new().unwrap();
5254 let odb = Odb::new(dir.path());
5255 let other = TempDir::new().unwrap();
5256 let source = Odb::new(other.path());
5257 let blobs: Vec<ObjectId> = (0..3)
5258 .map(|i| {
5259 source
5260 .write(ObjectKind::Blob, format!("b{i}").as_bytes())
5261 .unwrap()
5262 })
5263 .collect();
5264 assert_eq!(odb.missing_stored_oids(blobs.clone()).unwrap(), blobs);
5265 let pack = crate::pack_objects::build_pack_from_send_list(
5267 &source,
5268 &blobs[..2],
5269 &std::collections::HashSet::new(),
5270 &crate::pack_objects::PackBuildOptions::default(),
5271 &std::collections::HashMap::new(),
5272 )
5273 .unwrap();
5274 crate::index_pack::install_pack_bytes(pack, &Odb::new(dir.path()), &Default::default())
5275 .unwrap();
5276 assert_eq!(
5277 odb.missing_stored_oids(blobs.clone()).unwrap(),
5278 vec![blobs[2]]
5279 );
5280 }
5281
5282 #[test]
5283 fn oid_filters_propagate_backend_failure() {
5284 let dir = TempDir::new().unwrap();
5285 let (odb, flaky) = flaky_odb(dir.path());
5286 let oid = odb.write(ObjectKind::Blob, b"x").unwrap();
5287 assert_eq!(odb.contains_many(&[oid]).unwrap(), vec![true]);
5288 flaky.fail_contains(true);
5289 assert!(store::flaky::is_injected(
5290 &odb.contains_many(&[oid]).unwrap_err()
5291 ));
5292 assert!(store::flaky::is_injected(
5293 &odb.present_oids([oid]).unwrap_err()
5294 ));
5295 assert!(store::flaky::is_injected(
5296 &odb.missing_oids([oid]).unwrap_err()
5297 ));
5298 assert!(store::flaky::is_injected(
5299 &odb.missing_stored_oids([oid]).unwrap_err()
5300 ));
5301 }
5302
5303 #[test]
5304 fn write_existing_loose_uses_path_stat_not_exists() {
5305 let dir = TempDir::new().unwrap();
5306 let odb = Odb::new(dir.path());
5307 let oid = odb.write(ObjectKind::Blob, b"once").unwrap();
5308 let path = odb.object_path(&oid);
5309 assert!(path.is_file());
5310 odb.reset_exists_probe();
5311 let oid2 = odb.write(ObjectKind::Blob, b"once").unwrap();
5312 assert_eq!(oid, oid2);
5313 assert_eq!(
5314 odb.exists_probe_count(),
5315 0,
5316 "loose fast path must not call exists()"
5317 );
5318 }
5319
5320 #[cfg_attr(
5321 not(feature = "full-tests"),
5322 ignore = "starts system git or is heavy; run with --features full-tests"
5323 )]
5324 #[test]
5325 fn exists_and_write_after_invalidate_packs_use_odb_store() {
5326 let dir = TempDir::new().unwrap();
5327 git_in(dir.path(), &["init", "-q"]);
5328 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5329 git_in(dir.path(), &["config", "user.name", "T"]);
5330 let payload = b"invalidate-pack-exists";
5331 fs::write(dir.path().join("f"), payload).unwrap();
5332 git_in(dir.path(), &["add", "f"]);
5333 git_in(dir.path(), &["commit", "-m", "c"]);
5334 git_in(dir.path(), &["repack", "-ad"]);
5335 let objects = dir.path().join(".git").join("objects");
5336 let odb = Odb::new(&objects);
5337 let oid = odb.hash(ObjectKind::Blob, payload).expect("hash");
5338 assert!(odb.exists(&oid), "blob must live in pack after repack");
5339 let pack_dir = objects.join("pack");
5340 for entry in fs::read_dir(&pack_dir).unwrap().flatten() {
5341 let path = entry.path();
5342 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
5343 fs::remove_file(path).unwrap();
5344 }
5345 }
5346 odb.invalidate_packs();
5347 assert!(
5348 !odb.exists(&oid),
5349 "exists must not read stale legacy pack listing after invalidate_packs"
5350 );
5351 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
5352 assert_eq!(oid, oid2);
5353 assert!(
5354 odb.object_path(&oid).is_file(),
5355 "write must materialize loose object when pack copy is gone"
5356 );
5357 odb.read(&oid).expect("read after write");
5358 }
5359
5360 #[cfg_attr(
5361 not(feature = "full-tests"),
5362 ignore = "starts system git or is heavy; run with --features full-tests"
5363 )]
5364 #[test]
5365 fn write_uses_single_exists_probe_when_not_loose() {
5366 let dir = TempDir::new().unwrap();
5367 git_in(dir.path(), &["init", "-q"]);
5368 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5369 git_in(dir.path(), &["config", "user.name", "T"]);
5370 fs::write(dir.path().join("f"), b"probe-once").unwrap();
5371 git_in(dir.path(), &["add", "f"]);
5372 git_in(dir.path(), &["commit", "-m", "c"]);
5373 git_in(dir.path(), &["repack", "-ad"]);
5374 let objects = dir.path().join(".git").join("objects");
5375 let odb = Odb::new(&objects);
5376 let oid = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
5377 assert!(!odb.object_path(&oid).exists());
5378 odb.reset_exists_probe();
5379 let oid2 = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
5380 assert_eq!(oid, oid2);
5381 assert_eq!(
5382 odb.exists_probe_count(),
5383 1,
5384 "pack/alternate write path must call exists() exactly once"
5385 );
5386 }
5387 #[test]
5388 fn write_existing_object_freshens() {
5389 use filetime::FileTime;
5390 use std::fs;
5391 use std::thread;
5392 use std::time::Duration;
5393
5394 let dir = TempDir::new().unwrap();
5395 let odb = Odb::new(dir.path());
5396 let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
5397 let path = odb.object_path(&oid);
5398 thread::sleep(Duration::from_millis(50));
5399 let before = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
5400 filetime::set_file_mtime(&path, before).unwrap();
5401
5402 let _ = odb.write(ObjectKind::Blob, b"hello").unwrap();
5403 let after = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
5404 assert!(
5405 after > before,
5406 "rewriting an existing loose object must freshen its mtime"
5407 );
5408 }
5409
5410 #[test]
5411 fn freshen_pack_once_per_odb() {
5412 use filetime::FileTime;
5413 use flate2::write::ZlibEncoder;
5414 use flate2::Compression;
5415 use std::fs;
5416 use std::io::Write;
5417 use std::thread;
5418 use std::time::Duration;
5419
5420 fn append_pack_object(buf: &mut Vec<u8>, type_code: u8, data: &[u8]) {
5421 let mut size = data.len();
5422 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
5423 size >>= 4;
5424 if size > 0 {
5425 buf.push(first | 0x80);
5426 while size > 0 {
5427 let b = (size & 0x7f) as u8;
5428 size >>= 7;
5429 buf.push(if size > 0 { b | 0x80 } else { b });
5430 }
5431 } else {
5432 buf.push(first);
5433 }
5434 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5435 enc.write_all(data).unwrap();
5436 buf.extend_from_slice(&enc.finish().unwrap());
5437 }
5438
5439 fn write_v2_idx(
5440 idx_path: &Path,
5441 pack_path: &Path,
5442 entries: &[(ObjectId, u64)],
5443 ) -> Result<()> {
5444 let mut sorted = entries.to_vec();
5445 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
5446 let n = sorted.len();
5447 let mut fanout = [0u32; 256];
5448 for byte in 0u32..256 {
5449 let count = sorted
5450 .iter()
5451 .filter(|(oid, _)| u32::from(oid.as_bytes()[0]) <= byte)
5452 .count();
5453 fanout[byte as usize] = u32::try_from(count).unwrap_or(u32::MAX);
5454 }
5455 let mut buf = Vec::new();
5456 buf.extend_from_slice(b"\xfftOc");
5457 buf.extend_from_slice(&2u32.to_be_bytes());
5458 for f in fanout {
5459 buf.extend_from_slice(&f.to_be_bytes());
5460 }
5461 for (oid, _) in &sorted {
5462 buf.extend_from_slice(oid.as_bytes());
5463 }
5464 for _ in 0..n {
5465 buf.extend_from_slice(&0u32.to_be_bytes());
5466 }
5467 for (_, off) in &sorted {
5468 buf.extend_from_slice(&u32::try_from(*off).unwrap_or(0x8000_0000).to_be_bytes());
5469 }
5470 let pack_bytes = fs::read(pack_path)?;
5471 buf.extend_from_slice(&pack_bytes[pack_bytes.len() - 20..]);
5472 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
5473 fs::write(idx_path, buf)?;
5474 Ok(())
5475 }
5476
5477 let dir = TempDir::new().unwrap();
5478 let objects_dir = dir.path();
5479 let odb = Odb::new(objects_dir);
5480
5481 let oid_a = odb.write(ObjectKind::Blob, b"alpha").unwrap();
5482 let oid_b = odb.write(ObjectKind::Blob, b"beta").unwrap();
5483 fs::remove_file(odb.object_path(&oid_a)).unwrap();
5484 fs::remove_file(odb.object_path(&oid_b)).unwrap();
5485
5486 let mut pack = Vec::new();
5487 pack.extend_from_slice(b"PACK");
5488 pack.extend_from_slice(&2u32.to_be_bytes());
5489 pack.extend_from_slice(&2u32.to_be_bytes());
5490 let off_a = pack.len() as u64;
5491 append_pack_object(&mut pack, 3, b"alpha");
5492 let off_b = pack.len() as u64;
5493 append_pack_object(&mut pack, 3, b"beta");
5494 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
5495
5496 let pack_dir = objects_dir.join("pack");
5497 fs::create_dir_all(&pack_dir).unwrap();
5498 let pack_path = pack_dir.join("test-177.pack");
5499 let idx_path = pack_dir.join("test-177.idx");
5500 fs::write(&pack_path, &pack).unwrap();
5501 write_v2_idx(&idx_path, &pack_path, &[(oid_a, off_a), (oid_b, off_b)]).unwrap();
5502 odb.invalidate_packs();
5503
5504 thread::sleep(Duration::from_millis(50));
5505 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5506
5507 assert!(odb.freshen_object(&oid_a));
5508 let after_first = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5509 assert!(after_first > before);
5510
5511 assert!(odb.freshen_object(&oid_b));
5512 let after_second =
5513 FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5514 assert_eq!(
5515 after_second, after_first,
5516 "second freshen of another object in the same pack must not touch the pack again"
5517 );
5518 }
5519
5520 #[test]
5521 fn write_existing_packed_object_freshens_pack() {
5522 use filetime::FileTime;
5523 use flate2::write::ZlibEncoder;
5524 use flate2::Compression;
5525 use std::fs;
5526 use std::io::Write;
5527 use std::thread;
5528 use std::time::Duration;
5529
5530 let dir = TempDir::new().unwrap();
5531 let objects_dir = dir.path();
5532 let odb = Odb::new(objects_dir);
5533 let oid = odb.write(ObjectKind::Blob, b"packed-only").unwrap();
5534 fs::remove_file(odb.object_path(&oid)).unwrap();
5535
5536 let mut pack = Vec::new();
5537 pack.extend_from_slice(b"PACK");
5538 pack.extend_from_slice(&2u32.to_be_bytes());
5539 pack.extend_from_slice(&1u32.to_be_bytes());
5540 let off = pack.len() as u64;
5541 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5542 enc.write_all(b"packed-only").unwrap();
5543 let compressed = enc.finish().unwrap();
5544 pack.push(0x30 | 0x03); pack.extend_from_slice(&compressed);
5546 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
5547
5548 let pack_dir = objects_dir.join("pack");
5549 fs::create_dir_all(&pack_dir).unwrap();
5550 let pack_path = pack_dir.join("single-177.pack");
5551 fs::write(&pack_path, &pack).unwrap();
5552 let idx_path = pack_dir.join("single-177.idx");
5554 let mut sorted = [(oid, off)];
5555 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
5556 let mut fanout = [0u32; 256];
5557 for byte in 0u32..256 {
5558 fanout[byte as usize] = if u32::from(oid.as_bytes()[0]) <= byte {
5559 1
5560 } else {
5561 0
5562 };
5563 }
5564 let mut buf = Vec::new();
5565 buf.extend_from_slice(b"\xfftOc");
5566 buf.extend_from_slice(&2u32.to_be_bytes());
5567 for f in fanout {
5568 buf.extend_from_slice(&f.to_be_bytes());
5569 }
5570 buf.extend_from_slice(oid.as_bytes());
5571 buf.extend_from_slice(&0u32.to_be_bytes());
5572 buf.extend_from_slice(&(off as u32).to_be_bytes());
5573 buf.extend_from_slice(&pack[pack.len() - 20..]);
5574 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
5575 fs::write(&idx_path, buf).unwrap();
5576 odb.invalidate_packs();
5577
5578 thread::sleep(Duration::from_millis(50));
5579 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5580
5581 odb.write(ObjectKind::Blob, b"packed-only").unwrap();
5582
5583 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5584 assert!(
5585 after > before,
5586 "write of an existing packed object must freshen the pack mtime"
5587 );
5588 }
5589
5590 #[cfg_attr(
5594 not(feature = "full-tests"),
5595 ignore = "starts system git or is heavy; run with --features full-tests"
5596 )]
5597 #[test]
5598 fn write_materializes_loose_when_pack_freshen_fails() {
5599 let dir = TempDir::new().unwrap();
5600 git_in(dir.path(), &["init", "-q"]);
5601 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5602 git_in(dir.path(), &["config", "user.name", "T"]);
5603 let payload = b"packed-fallback";
5604 fs::write(dir.path().join("f"), payload).unwrap();
5605 git_in(dir.path(), &["add", "f"]);
5606 git_in(dir.path(), &["commit", "-m", "c"]);
5607 git_in(dir.path(), &["repack", "-ad"]);
5608 let objects = dir.path().join(".git").join("objects");
5609 let odb = Odb::new(&objects);
5610 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
5611 assert!(!odb.object_path(&oid).exists());
5612 assert!(odb.exists(&oid));
5613
5614 let pack_path = fs::read_dir(objects.join("pack"))
5615 .unwrap()
5616 .flatten()
5617 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
5618 .unwrap()
5619 .path();
5620 fs::remove_file(&pack_path).unwrap();
5621 fs::create_dir(&pack_path).unwrap();
5622 odb.invalidate_packs();
5623 assert!(!odb.freshen_object(&oid));
5624
5625 odb.write(ObjectKind::Blob, payload).unwrap();
5626 assert!(odb.object_path(&oid).exists());
5627 }
5628 #[test]
5629 fn write_with_options_silent_skips_freshen_on_existing_loose_object() {
5630 let dir = TempDir::new().unwrap();
5631 let odb = Odb::new(dir.path());
5632 let oid = odb.write(ObjectKind::Blob, b"stable").unwrap();
5633 let path = odb.object_path(&oid);
5634 let before = std::fs::metadata(&path).unwrap().modified().unwrap();
5635 std::thread::sleep(std::time::Duration::from_millis(20));
5636 let again = odb
5637 .write_with_options(
5638 ObjectKind::Blob,
5639 b"stable",
5640 WriteOptions {
5641 silent: true,
5642 ..WriteOptions::default()
5643 },
5644 )
5645 .unwrap();
5646 assert_eq!(oid, again);
5647 let after = std::fs::metadata(&path).unwrap().modified().unwrap();
5648 assert_eq!(before, after);
5649
5650 let touched = odb
5651 .write_with_options(
5652 ObjectKind::Blob,
5653 b"stable",
5654 WriteOptions {
5655 silent: false,
5656 ..WriteOptions::default()
5657 },
5658 )
5659 .unwrap();
5660 assert_eq!(oid, touched);
5661 let freshened = std::fs::metadata(&path).unwrap().modified().unwrap();
5662 assert!(freshened >= before);
5663 }
5664
5665 #[cfg_attr(
5666 not(feature = "full-tests"),
5667 ignore = "starts system git or is heavy; run with --features full-tests"
5668 )]
5669 #[test]
5670 fn promisor_marker_checked_once_per_prepare() {
5671 use crate::pack::{
5672 clear_pack_cache, pack_cache_test_guard, test_pack_marker_stat_count,
5673 test_reset_pack_marker_stat_count,
5674 };
5675
5676 let _guard = pack_cache_test_guard();
5677 let dir = TempDir::new().unwrap();
5678 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5679 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5680 git_in(dir.path(), &["config", "user.name", "T"]);
5681 for i in 0..64 {
5682 fs::write(dir.path().join(format!("f{i}.txt")), format!("body {i}")).unwrap();
5683 git_in(dir.path(), &["add", "."]);
5684 git_in(dir.path(), &["commit", "-m", &format!("c{i}")]);
5685 }
5686 git_in(dir.path(), &["repack", "-a", "-d"]);
5687 let objects = dir.path().join(".git/objects");
5688 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5689
5690 clear_pack_cache();
5691 test_reset_pack_marker_stat_count();
5692 let pack_count = fs::read_dir(objects.join("pack"))
5693 .unwrap()
5694 .filter_map(|e| e.ok())
5695 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
5696 .count();
5697 let expected_prepare_stats = u64::try_from(pack_count * 2).unwrap();
5698 let odb = Odb::new(&objects);
5699 let before_prepare = test_pack_marker_stat_count();
5700 assert!(odb.exists_local(&oid));
5701 assert_eq!(
5702 test_pack_marker_stat_count().saturating_sub(before_prepare),
5703 expected_prepare_stats,
5704 "prepare should stat promisor and mtimes once per pack"
5705 );
5706
5707 test_reset_pack_marker_stat_count();
5708 for _ in 0..10_000 {
5709 let before_probe = test_pack_marker_stat_count();
5710 assert!(odb.exists_local(&oid));
5711 assert_eq!(
5712 test_pack_marker_stat_count(),
5713 before_probe,
5714 "cached sidecar flags must not stat markers on each exists_local probe"
5715 );
5716 }
5717 }
5718
5719 #[cfg_attr(
5720 not(feature = "full-tests"),
5721 ignore = "starts system git or is heavy; run with --features full-tests"
5722 )]
5723 #[test]
5724 fn promisor_marker_added_after_prepare_is_seen_after_reprepare() {
5725 use crate::pack::{
5726 clear_pack_cache, pack_cache_test_guard, reprepare_pack_directory_on_miss,
5727 };
5728
5729 let _guard = pack_cache_test_guard();
5730 let dir = TempDir::new().unwrap();
5731 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5732 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5733 git_in(dir.path(), &["config", "user.name", "T"]);
5734 fs::write(dir.path().join("one.txt"), b"x").unwrap();
5735 git_in(dir.path(), &["add", "one.txt"]);
5736 git_in(dir.path(), &["commit", "-m", "one"]);
5737 git_in(dir.path(), &["repack", "-a", "-d"]);
5738 let objects = dir.path().join(".git/objects");
5739 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5740
5741 clear_pack_cache();
5742 let odb = Odb::new(&objects);
5743 assert!(odb.exists_local(&oid));
5744
5745 let pack_path = fs::read_dir(objects.join("pack"))
5746 .unwrap()
5747 .filter_map(|e| e.ok())
5748 .map(|e| e.path())
5749 .find(|p| p.extension().is_some_and(|x| x == "pack"))
5750 .unwrap();
5751 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
5752 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now()).unwrap();
5753
5754 assert!(
5755 odb.with_pack_store_for(&objects, || {
5756 reprepare_pack_directory_on_miss(&objects).unwrap()
5757 }),
5758 "pack directory change must require reprepare"
5759 );
5760 assert!(
5761 !odb.exists_local(&oid),
5762 "promisor marker must hide pack objects after reprepare refreshed sidecar flags"
5763 );
5764 }
5765
5766 #[cfg_attr(
5767 not(feature = "full-tests"),
5768 ignore = "starts system git or is heavy; run with --features full-tests"
5769 )]
5770 #[test]
5771 fn git_promisor_marker_pack_not_materialized_for_exists_local() {
5772 use crate::pack::clear_pack_cache;
5773
5774 let dir = TempDir::new().unwrap();
5775 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5776 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5777 git_in(dir.path(), &["config", "user.name", "T"]);
5778 fs::write(dir.path().join("blob.txt"), b"git promisor compat").unwrap();
5779 git_in(dir.path(), &["add", "blob.txt"]);
5780 git_in(dir.path(), &["commit", "-m", "seed"]);
5781 git_in(dir.path(), &["repack", "-a", "-d"]);
5782 let objects = dir.path().join(".git/objects");
5783 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5784 let pack_path = fs::read_dir(objects.join("pack"))
5785 .unwrap()
5786 .filter_map(|e| e.ok())
5787 .map(|e| e.path())
5788 .find(|p| p.extension().is_some_and(|x| x == "pack"))
5789 .unwrap();
5790 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
5791
5792 clear_pack_cache();
5793 let odb = Odb::new(&objects);
5794 assert!(
5795 !odb.exists_local(&oid),
5796 "git-written pack with .promisor must not count as local materialization"
5797 );
5798
5799 let cat = git_run(dir.path(), &["cat-file", "-e", &oid.to_hex()]);
5800 assert!(
5801 cat.status.success(),
5802 "git still resolves the object in a promisor-marked pack"
5803 );
5804 }
5805
5806 #[cfg_attr(
5807 not(feature = "full-tests"),
5808 ignore = "starts system git or is heavy; run with --features full-tests"
5809 )]
5810 #[test]
5811 fn packed_object_read_avoids_loose_path_open() {
5812 use crate::hot_path_test_metrics::HotPathMetricsScope;
5813 use crate::pack::clear_pack_cache;
5814
5815 let dir = TempDir::new().unwrap();
5816 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5817 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5818 git_in(dir.path(), &["config", "user.name", "T"]);
5819 fs::write(dir.path().join("packed.txt"), b"packed-only payload").unwrap();
5820 git_in(dir.path(), &["add", "packed.txt"]);
5821 git_in(dir.path(), &["commit", "-m", "pack"]);
5822 git_in(dir.path(), &["repack", "-a", "-d"]);
5823 let objects = dir.path().join(".git/objects");
5824 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5825
5826 clear_pack_cache();
5827 let odb = Odb::new(&objects);
5828 odb.hot_path_metrics().set_loose_open_counting(true);
5829 odb.hot_path_metrics().reset_loose_path_open_attempts();
5830 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
5831
5832 let obj = odb.read(&oid).expect("packed read");
5833 assert_eq!(obj.kind, ObjectKind::Commit);
5834 assert_eq!(
5835 odb.hot_path_metrics().loose_path_open_attempts(),
5836 0,
5837 "packed objects must not attempt a loose-path open"
5838 );
5839 }
5840
5841 #[cfg_attr(
5842 not(feature = "full-tests"),
5843 ignore = "starts system git or is heavy; run with --features full-tests"
5844 )]
5845 #[test]
5846 fn alternate_packed_reads_do_not_rescan_local_pack_dir() {
5847 use crate::pack::{clear_pack_cache, pack_cache};
5848
5849 let upstream = TempDir::new().unwrap();
5850 git_in(upstream.path(), &["init", "-q", "-b", "main"]);
5851 git_in(upstream.path(), &["config", "user.email", "t@example.com"]);
5852 git_in(upstream.path(), &["config", "user.name", "T"]);
5853 fs::write(
5854 upstream.path().join("payload.txt"),
5855 b"alternate-only-in-upstream",
5856 )
5857 .unwrap();
5858 git_in(upstream.path(), &["add", "payload.txt"]);
5859 git_in(upstream.path(), &["commit", "-m", "seed"]);
5860 git_in(upstream.path(), &["repack", "-a", "-d"]);
5861 let oid = git_rev_parse_oid(upstream.path(), "HEAD");
5862
5863 let layout = TempDir::new().unwrap();
5864 git_in(
5865 layout.path(),
5866 &[
5867 "clone",
5868 "-q",
5869 "--shared",
5870 upstream.path().to_str().unwrap(),
5871 "shared",
5872 ],
5873 );
5874 let objects = layout.path().join("shared/.git/objects");
5875 let local_pack = objects.join("pack");
5876
5877 clear_pack_cache();
5878 let odb = Odb::new(&objects);
5879 pack_cache::test_reset_dir_rescan_count(&local_pack);
5880 odb.read(&oid).expect("alternate read");
5881 let after_first = pack_cache::test_dir_rescan_count(&local_pack);
5882 for _ in 0..512 {
5883 odb.read_info(&oid).expect("repeat read_info");
5884 }
5885 assert_eq!(
5886 pack_cache::test_dir_rescan_count(&local_pack),
5887 after_first,
5888 "alternate hits must not re-list the local pack directory"
5889 );
5890 }
5891
5892 #[test]
5893 fn loose_only_read_records_loose_path_open() {
5894 use crate::hot_path_test_metrics::HotPathMetricsScope;
5895
5896 let dir = TempDir::new().unwrap();
5897 let objects = dir.path().join("objects");
5898 fs::create_dir_all(&objects).unwrap();
5899 let odb = Odb::new(&objects);
5900 let oid = odb
5901 .write(ObjectKind::Blob, b"loose-only")
5902 .expect("write loose");
5903 odb.hot_path_metrics().set_loose_open_counting(true);
5904 odb.hot_path_metrics().reset_loose_path_open_attempts();
5905 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
5906
5907 odb.read(&oid).expect("loose read");
5908 assert_eq!(
5909 odb.hot_path_metrics().loose_path_open_attempts(),
5910 1,
5911 "loose fallback must record a path open attempt"
5912 );
5913 }
5914
5915 #[cfg_attr(
5916 not(feature = "full-tests"),
5917 ignore = "starts system git or is heavy; run with --features full-tests"
5918 )]
5919 #[test]
5920 fn loose_and_packed_copies_return_identical_bytes() {
5921 use crate::pack::clear_pack_cache;
5922
5923 let dir = TempDir::new().unwrap();
5924 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5925 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5926 git_in(dir.path(), &["config", "user.name", "T"]);
5927 fs::write(dir.path().join("dup.txt"), b"duplicate loose and packed\n").unwrap();
5928 git_in(dir.path(), &["add", "dup.txt"]);
5929 git_in(dir.path(), &["commit", "-m", "seed"]);
5930 let objects = dir.path().join(".git/objects");
5931 let tree_oid = git_rev_parse_oid(dir.path(), "HEAD^{tree}");
5932 git_in(dir.path(), &["repack", "-a"]);
5933 clear_pack_cache();
5934 let odb = Odb::new(&objects);
5935 let loose_path = tree_oid.loose_path_in(&objects);
5936 assert!(
5937 loose_path.is_file(),
5938 "loose copy must remain after repack -a"
5939 );
5940 let loose = Odb::read_loose_verify_oid(&loose_path, &tree_oid).expect("loose tree");
5941 let packed = odb.read(&tree_oid).expect("packed tree");
5942 assert_eq!(packed.data, loose.data);
5943 assert_eq!(packed.kind, loose.kind);
5944 }
5945
5946 #[cfg_attr(
5947 not(feature = "full-tests"),
5948 ignore = "starts system git or is heavy; run with --features full-tests"
5949 )]
5950 #[test]
5951 fn freshly_written_loose_object_visible_before_repack() {
5952 let dir = TempDir::new().unwrap();
5953 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5954 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5955 git_in(dir.path(), &["config", "user.name", "T"]);
5956 fs::write(dir.path().join("seed.txt"), b"seed").unwrap();
5957 git_in(dir.path(), &["add", "seed.txt"]);
5958 git_in(dir.path(), &["commit", "-m", "seed"]);
5959 git_in(dir.path(), &["repack", "-a", "-d"]);
5960 let objects = dir.path().join(".git/objects");
5961 let odb = Odb::new(&objects);
5962 let fresh = b"brand-new loose blob\n";
5963 let oid = odb.write(ObjectKind::Blob, fresh).expect("loose write");
5964 let obj = odb.read(&oid).expect("immediate loose read");
5965 assert_eq!(obj.data.as_slice(), fresh);
5966 }
5967
5968 #[cfg_attr(
5969 not(feature = "full-tests"),
5970 ignore = "starts system git or is heavy; run with --features full-tests"
5971 )]
5972 #[test]
5973 fn pack_added_after_first_read_found_via_odb_reprepare() {
5974 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
5975
5976 let _guard = pack_cache_test_guard();
5977 clear_pack_cache();
5978 let dir = TempDir::new().unwrap();
5979 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5980 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5981 git_in(dir.path(), &["config", "user.name", "T"]);
5982 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
5983 git_in(dir.path(), &["add", "a.txt"]);
5984 git_in(dir.path(), &["commit", "-m", "a"]);
5985 git_in(dir.path(), &["repack", "-a", "-d"]);
5986 let objects = dir.path().join(".git/objects");
5987 let pack_dir = objects.join("pack");
5988 let git_dir = dir.path().join(".git");
5989
5990 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
5991 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
5992 odb.read(&warm_oid).expect("warm read");
5993
5994 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
5995 git_in(dir.path(), &["add", "b.txt"]);
5996 git_in(dir.path(), &["commit", "-m", "b"]);
5997 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
5998
5999 std::thread::sleep(std::time::Duration::from_secs(1));
6000 filetime::set_file_mtime(&pack_dir, filetime::FileTime::now()).unwrap();
6001
6002 let obj = odb.read(&new_oid).expect("new pack via reprepare");
6003 assert_eq!(obj.kind, ObjectKind::Commit);
6004 }
6005
6006 #[cfg_attr(
6007 not(feature = "full-tests"),
6008 ignore = "starts system git or is heavy; run with --features full-tests"
6009 )]
6010 #[test]
6011 fn late_midx_after_warm_read_resolves_new_commit() {
6012 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
6013
6014 let _guard = pack_cache_test_guard();
6015 clear_pack_cache();
6016 let dir = TempDir::new().unwrap();
6017 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6018 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6019 git_in(dir.path(), &["config", "user.name", "T"]);
6020 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
6021 git_in(dir.path(), &["add", "a.txt"]);
6022 git_in(dir.path(), &["commit", "-m", "a"]);
6023 git_in(dir.path(), &["repack", "-a", "-d"]);
6024 let objects = dir.path().join(".git/objects");
6025 let git_dir = dir.path().join(".git");
6026 assert!(
6027 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_none(),
6028 "fixture starts without a MIDX"
6029 );
6030
6031 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
6032 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
6033 odb.read(&warm_oid).expect("warm read before MIDX exists");
6034
6035 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
6036 git_in(dir.path(), &["add", "b.txt"]);
6037 git_in(dir.path(), &["commit", "-m", "b"]);
6038 git_in(dir.path(), &["repack", "-a", "-d"]);
6039 git_in(dir.path(), &["multi-pack-index", "write"]);
6040 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
6041
6042 let cat = git_run(dir.path(), &["cat-file", "-e", &new_oid.to_hex()]);
6043 assert!(
6044 cat.status.success(),
6045 "system git must resolve the new commit"
6046 );
6047
6048 let obj = odb
6049 .read(&new_oid)
6050 .expect("MIDX-covered commit after late MIDX write");
6051 assert_eq!(obj.kind, ObjectKind::Commit);
6052 assert!(
6053 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_some(),
6054 "successful read after late MIDX must refresh the tip cache"
6055 );
6056 }
6057}