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, for_each_object_id_in_fanout_dir,
39 parse_object_bytes, 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 pub(crate) fn read_once(&self, oid: &ObjectId) -> Result<Object> {
652 self.read_impl(oid, false)
653 }
654
655 fn read_tree_payload(&self, oid: &ObjectId) -> Result<Vec<u8>> {
656 let obj = self.read_impl(oid, false)?;
657 if obj.kind != ObjectKind::Tree {
658 return Err(Error::CorruptObject(format!(
659 "expected tree, got {}",
660 obj.kind.as_str()
661 )));
662 }
663 Ok(obj.data)
664 }
665
666 pub(crate) fn cached_tree_entries(
668 &self,
669 oid: &ObjectId,
670 ) -> Result<Arc<Vec<crate::objects::TreeEntry>>> {
671 let parsed = match self.session_tree(oid) {
672 Some(SessionTree::Parsed(entries)) => return Ok(entries),
673 Some(SessionTree::Raw(data)) => {
674 crate::objects::parse_tree_with_oid_len(&data, self.hash_algo().len())?
675 }
676 None => crate::objects::parse_tree_with_oid_len(
677 &self.read_tree_payload(oid)?,
678 self.hash_algo().len(),
679 )?,
680 };
681 let arc = Arc::new(parsed);
682 self.remember_session_tree(oid, SessionTree::Parsed(Arc::clone(&arc)));
683 Ok(arc)
684 }
685
686 pub(crate) fn cached_tree_for_walk(&self, oid: &ObjectId) -> Result<SessionTree> {
691 if let Some(tree) = self.session_tree(oid) {
692 return Ok(tree);
693 }
694 let tree = SessionTree::Raw(Arc::new(self.read_tree_payload(oid)?));
695 self.remember_session_tree(oid, tree.clone());
696 Ok(tree)
697 }
698
699 fn session_cached_loose_read(&self, oid: &ObjectId) -> Option<Object> {
700 self.session_loose_reads.lock().ok()?.get(oid).cloned()
701 }
702
703 #[cfg(test)]
705 pub(crate) fn session_read_is_cached(&self, oid: &ObjectId) -> bool {
706 self.session_cached_loose_read(oid).is_some()
707 }
708
709 fn remember_session_loose_read(&self, oid: &ObjectId, obj: &Object) {
712 if let Ok(mut guard) = self.session_loose_reads.lock() {
713 guard.insert(*oid, obj);
714 }
715 }
716
717 fn finish_session_loose_read(
725 &self,
726 oid: &ObjectId,
727 obj: Object,
728 remember: bool,
729 ) -> Result<Object> {
730 if remember
731 && (obj.kind != ObjectKind::Blob || obj.data.len() <= SESSION_READ_BLOB_CACHE_MAX)
732 {
733 self.remember_session_loose_read(oid, &obj);
734 }
735 Ok(obj)
736 }
737
738 #[cfg(test)]
740 #[must_use]
741 pub(crate) fn hot_path_metrics(&self) -> &crate::hot_path_test_metrics::HotPathTestMetrics {
742 &self.hot_path_test_metrics
743 }
744
745 #[cfg(test)]
746 #[must_use]
747 pub(crate) fn hot_path_metrics_arc(
748 &self,
749 ) -> Arc<crate::hot_path_test_metrics::HotPathTestMetrics> {
750 Arc::clone(&self.hot_path_test_metrics)
751 }
752
753 #[must_use]
755 pub(crate) fn with_shared_config_state(
756 mut self,
757 state: crate::repo::RepositoryConfigSnapshot,
758 env: Arc<crate::environment::Environment>,
759 ) -> Self {
760 self.pack_store.set_repo_caches(Arc::clone(&state));
761 self.shared_config_state = Some(state);
762 self.shared_environment = Some(env);
763 self
764 }
765
766 fn diagnostics_handle(&self) -> crate::diagnostics::DiagnosticsHandle {
767 self.shared_config_state
768 .as_ref()
769 .map(|state| state.diagnostics_handle())
770 .unwrap_or_else(|| Arc::new(NullDiagnostics))
771 }
772
773 pub(crate) fn load_config_cascade(&self) -> Result<Arc<ConfigSet>> {
779 let git_dir = self
780 .config_git_dir
781 .as_deref()
782 .or_else(|| self.objects_dir.parent());
783 if let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment) {
784 let opts = crate::config::LoadConfigOptions::for_repository(
785 env,
786 git_dir,
787 Some(Arc::clone(caches)),
788 );
789 return caches.load_config_with_options(env.as_ref(), git_dir, &opts);
790 }
791 match git_dir {
792 Some(git_dir) => {
793 ConfigSet::load(self.config_environment(), Some(git_dir), true).map(Arc::new)
794 }
795 None => Ok(Arc::new(ConfigSet::new())),
796 }
797 }
798
799 fn config_environment(&self) -> &crate::environment::Environment {
800 if let Some(env) = self.shared_environment.as_deref() {
801 return env;
802 }
803 self.empty_config_env
804 .get_or_init(crate::environment::Environment::empty)
805 }
806
807 fn env_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
808 if !self.env_alternate_dirs.is_empty() {
809 return Arc::new(self.env_alternate_dirs.clone());
810 }
811 Arc::clone(self.env_alternate_lazy.get_or_init(|| {
812 Arc::new(Self::env_alternate_dirs_from_var(
813 self.config_environment(),
814 self.work_tree.as_deref(),
815 ))
816 }))
817 }
818
819 #[must_use]
823 pub fn env_alternate_dirs_from_var(
824 env: &crate::environment::Environment,
825 resolve_base: Option<&Path>,
826 ) -> Vec<PathBuf> {
827 match env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES") {
828 Some(val) if !val.is_empty() => {
829 let mut dirs = parse_alternate_env(&val);
830 if let Some(base) = resolve_base {
831 for dir in &mut dirs {
832 if dir.is_relative() {
833 *dir = base.join(&dir);
834 }
835 }
836 }
837 dirs
838 }
839 _ => Vec::new(),
840 }
841 }
842
843 #[must_use]
845 pub fn with_env_alternate_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
846 self.env_alternate_dirs = dirs;
847 self.clear_alternate_sources_cache();
848 self
849 }
850
851 fn clear_alternate_sources_cache(&self) {
852 if let Ok(mut guard) = self.alternate_sources_cache.write() {
853 guard.sources = None;
854 }
855 }
856
857 fn alternate_sources_key(&self) -> AlternateSourcesKey {
858 AlternateSourcesKey {
859 file_generation: self
860 .file_alternate_dirs_cache
861 .read()
862 .map(|g| g.generation)
863 .unwrap_or(0),
864 env_fingerprint: self.env_alternate_fingerprint(),
865 submodule_generation: self.submodule_sources_generation.load(Ordering::Relaxed),
866 }
867 }
868
869 fn env_alternate_fingerprint(&self) -> u64 {
870 let mut hasher = std::collections::hash_map::DefaultHasher::new();
871 for path in self.env_alternate_dirs_snapshot().iter() {
872 path.hash(&mut hasher);
873 }
874 hasher.finish()
875 }
876
877 pub fn refresh_file_alternates_from_disk(&self) -> Result<()> {
883 let snapshot = Arc::new(pack::read_alternates_recursive(&self.objects_dir)?);
884 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
885 guard.generation = guard.generation.wrapping_add(1);
886 guard.snapshot = Some(snapshot);
887 }
888 self.clear_alternate_sources_cache();
889 Ok(())
890 }
891
892 pub fn append_file_alternate(&self, alternate_objects_dir: &Path) -> Result<()> {
898 Self::append_alternate_objects_line(&self.objects_dir, alternate_objects_dir)?;
899 self.refresh_file_alternates_from_disk()?;
900 Ok(())
901 }
902
903 pub fn append_alternate_objects_line(
908 objects_dir: &Path,
909 alternate_objects_dir: &Path,
910 ) -> Result<()> {
911 let info = objects_dir.join("info");
912 fs::create_dir_all(&info)?;
913 let alt_path = info.join("alternates");
914 let alt_abs = alternate_objects_dir
915 .canonicalize_simplified()
916 .unwrap_or_else(|_| alternate_objects_dir.to_path_buf());
917 let line = format!("{}\n", alt_abs.display());
918 let existing = match fs::read_to_string(&alt_path) {
919 Ok(s) => s,
920 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
921 Err(e) => return Err(Error::Io(e)),
922 };
923 if existing
924 .lines()
925 .any(|l| l.trim() == alt_abs.to_string_lossy())
926 {
927 return Ok(());
928 }
929 let mut out = existing;
930 out.push_str(&line);
931 fs::write(alt_path, out).map_err(Error::Io)
932 }
933
934 fn file_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
935 loop {
936 let observed_gen = self
937 .file_alternate_dirs_cache
938 .read()
939 .map(|g| g.generation)
940 .unwrap_or(0);
941 if let Ok(guard) = self.file_alternate_dirs_cache.read() {
942 if guard.generation == observed_gen {
943 if let Some(snapshot) = &guard.snapshot {
944 return Arc::clone(snapshot);
945 }
946 }
947 }
948 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
949 if guard.generation != observed_gen {
950 continue;
951 }
952 if let Some(snapshot) = &guard.snapshot {
953 return Arc::clone(snapshot);
954 }
955 let snapshot = Arc::new(
956 pack::read_alternates_recursive(&self.objects_dir).unwrap_or_default(),
957 );
958 guard.snapshot = Some(Arc::clone(&snapshot));
959 return snapshot;
960 }
961 break;
962 }
963 Arc::new(Vec::new())
964 }
965
966 pub(crate) fn local_store_validation_object_dirs(&self) -> Vec<PathBuf> {
970 if !self.uses_files_primary() {
971 return Vec::new();
972 }
973 let mut dirs = Vec::new();
974 if let Some(primary) = self.files_objects_dir() {
975 dirs.push(primary.to_path_buf());
976 }
977 if self.alternates_enabled {
978 for alt in self.file_alternate_dirs_snapshot().iter() {
979 if !dirs.iter().any(|d| d == alt) {
980 dirs.push(alt.clone());
981 }
982 }
983 for alt in self.env_alternate_dirs_snapshot().iter() {
984 if !dirs.iter().any(|d| d == alt) {
985 dirs.push(alt.clone());
986 }
987 }
988 }
989 dirs
990 }
991
992 fn loose_negative_cache_for(&self, objects_dir: &Path) -> store::SharedLooseNegativeCache {
993 let key = objects_dir.to_path_buf();
994 if let Ok(guard) = self.loose_negative_caches.lock() {
995 if let Some(hit) = guard.get(&key) {
996 return Arc::clone(hit);
997 }
998 }
999 let mut guard = self
1000 .loose_negative_caches
1001 .lock()
1002 .unwrap_or_else(|e| e.into_inner());
1003 guard
1004 .entry(key)
1005 .or_insert_with(|| Arc::new(Mutex::new(store::LooseNegativeCache::default())))
1006 .clone()
1007 }
1008
1009 fn invalidate_loose_negative_cache(&self, objects_dir: &Path) {
1010 let key = objects_dir.to_path_buf();
1011 let cache = self
1012 .loose_negative_caches
1013 .lock()
1014 .ok()
1015 .and_then(|guard| guard.get(&key).map(Arc::clone));
1016 let Some(cache) = cache else {
1017 return;
1018 };
1019 let mut inner = cache
1020 .lock()
1021 .unwrap_or_else(|poisoned| poisoned.into_inner());
1022 *inner = store::LooseNegativeCache::default();
1023 }
1024
1025 #[cold]
1026 #[inline(never)]
1027 fn try_read_primary_after_negative_revalidate(&self, oid: &ObjectId) -> Result<Option<Object>> {
1028 self.invalidate_loose_negative_cache(self.objects_dir.as_path());
1029 let primary = self.primary_writable()?;
1030 self.with_primary_read_context(|| primary.read(oid))
1031 }
1032
1033 #[cold]
1034 #[inline(never)]
1035 fn object_not_found_after_primary_revalidate(&self, oid: &ObjectId) -> Result<Object> {
1036 if let Some(obj) = self.try_read_primary_after_negative_revalidate(oid)? {
1037 return Ok(obj);
1038 }
1039 Err(Error::ObjectNotFound(oid.to_hex()))
1040 }
1041
1042 #[cold]
1043 #[inline(never)]
1044 fn try_read_primary_info_after_negative_revalidate(
1045 &self,
1046 oid: &ObjectId,
1047 ) -> Result<Option<ObjectInfo>> {
1048 self.invalidate_loose_negative_cache(self.objects_dir.as_path());
1049 let primary = self.primary_writable()?;
1050 self.with_primary_read_context(|| primary.read_info(oid))
1051 }
1052
1053 #[cold]
1054 #[inline(never)]
1055 fn try_read_primary_into_after_negative_revalidate(
1056 &self,
1057 oid: &ObjectId,
1058 buf: &mut Vec<u8>,
1059 ) -> Result<Option<ObjectKind>> {
1060 self.invalidate_loose_negative_cache(self.objects_dir.as_path());
1061 let primary = self.primary_writable()?;
1062 self.with_primary_read_context(|| primary.read_into(oid, buf))
1063 }
1064
1065 #[cold]
1066 #[inline(never)]
1067 fn object_info_not_found_after_primary_revalidate(&self, oid: &ObjectId) -> Result<ObjectInfo> {
1068 if let Some(info) = self.try_read_primary_info_after_negative_revalidate(oid)? {
1069 return Ok(info);
1070 }
1071 Err(Error::ObjectNotFound(oid.to_hex()))
1072 }
1073
1074 #[cold]
1075 #[inline(never)]
1076 fn object_kind_not_found_after_primary_revalidate(
1077 &self,
1078 oid: &ObjectId,
1079 buf: &mut Vec<u8>,
1080 ) -> Result<ObjectKind> {
1081 if let Some(kind) = self.try_read_primary_into_after_negative_revalidate(oid, buf)? {
1082 return Ok(kind);
1083 }
1084 Err(Error::ObjectNotFound(oid.to_hex()))
1085 }
1086
1087 #[cfg(test)]
1088 pub(crate) fn exists_probe_count(&self) -> usize {
1089 self.exists_probe.load(Ordering::Relaxed)
1090 }
1091
1092 #[cfg(test)]
1093 pub(crate) fn reset_exists_probe(&self) {
1094 self.exists_probe.store(0, Ordering::Relaxed);
1095 }
1096
1097 pub fn enable_mem_overlay(&self) {
1102 self.mem_overlay_active.store(true, Ordering::Release);
1103 if let Ok(mut guard) = self.mem_overlay.write() {
1104 if guard.is_none() {
1105 *guard = Some(Arc::new(MemoryStore::new(self.hash_algo())));
1106 }
1107 }
1108 }
1109
1110 pub fn disable_mem_overlay(&self) {
1112 self.mem_overlay_active.store(false, Ordering::Release);
1113 if let Ok(mut guard) = self.mem_overlay.write() {
1114 *guard = None;
1115 }
1116 }
1117
1118 pub(crate) fn persist_mem_overlay(&self) -> Result<()> {
1125 use std::ops::ControlFlow;
1126
1127 let store = match self.overlay_store() {
1128 Some(store) => store,
1129 None => return Ok(()),
1130 };
1131 let mut oids = Vec::new();
1132 store.for_each_object(&mut |oid| {
1133 oids.push(*oid);
1134 ControlFlow::Continue(())
1135 })?;
1136 let mut payloads = Vec::with_capacity(oids.len());
1137 for oid in oids {
1138 let obj = store
1139 .read(&oid)?
1140 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))?;
1141 payloads.push((obj.kind, obj.data));
1142 }
1143 self.mem_overlay_active.store(false, Ordering::Release);
1144 if let Ok(mut guard) = self.mem_overlay.write() {
1145 *guard = None;
1146 }
1147 let custom_primary = self.custom_primary.is_some();
1148 for (kind, data) in payloads {
1149 self.write_into_primary(kind, &data)?;
1150 }
1151 if custom_primary {
1152 self.flush()?;
1153 }
1154 Ok(())
1155 }
1156
1157 pub(crate) fn persist_mem_overlay_objects(
1165 &self,
1166 oids: impl IntoIterator<Item = ObjectId>,
1167 ) -> Result<()> {
1168 let store = match self.overlay_store() {
1169 Some(store) => store,
1170 None => return Ok(()),
1171 };
1172 for oid in oids {
1173 if self.exists_local(&oid) {
1174 continue;
1175 }
1176 let Some(obj) = store.read(&oid)? else {
1177 continue;
1178 };
1179 self.write_into_primary(obj.kind, &obj.data)?;
1180 }
1181 if self.custom_primary.is_some() {
1182 self.flush()?;
1183 }
1184 Ok(())
1185 }
1186
1187 #[must_use]
1189 pub(crate) fn is_mem_overlay_active(&self) -> bool {
1190 self.mem_overlay_active.load(Ordering::Acquire)
1191 }
1192
1193 fn overlay_active(&self) -> bool {
1194 self.mem_overlay
1195 .read()
1196 .ok()
1197 .and_then(|g| g.as_ref().map(|_| ()))
1198 .is_some()
1199 }
1200
1201 fn overlay_store(&self) -> Option<Arc<MemoryStore>> {
1202 if !self.mem_overlay_active.load(Ordering::Acquire) {
1203 return None;
1204 }
1205 self.mem_overlay.read().ok().and_then(|g| g.clone())
1206 }
1207
1208 #[cfg(test)]
1210 pub(crate) fn mem_overlay_len_for_tests(&self) -> Option<usize> {
1211 let store = self.overlay_store()?;
1212 let mut count = 0usize;
1213 store
1214 .for_each_object(&mut |_| {
1215 count += 1;
1216 ControlFlow::Continue(())
1217 })
1218 .ok()?;
1219 Some(count)
1220 }
1221
1222 fn overlay_store_object(&self, oid: ObjectId, kind: ObjectKind, data: &[u8]) -> bool {
1225 let Some(store) = self.overlay_store() else {
1226 return false;
1227 };
1228 store.insert_object(oid, kind, Arc::from(data.to_owned()));
1229 true
1230 }
1231
1232 fn overlay_read(&self, oid: &ObjectId) -> Option<Object> {
1234 let store = self.overlay_store()?;
1235 store.read(oid).ok().flatten()
1236 }
1237
1238 pub fn primary(&self) -> Result<Arc<FilesSource>> {
1250 self.require_files_primary("primary")?;
1251 if let Some(src) = self.primary_source.get() {
1252 return Ok(Arc::clone(src));
1253 }
1254 let src = self.build_files_source(
1255 &self.objects_dir,
1256 self.multi_pack_index_reads_enabled(),
1257 self.midx_tip_listing_enabled(),
1258 )?;
1259 match self.primary_source.set(Arc::clone(&src)) {
1260 Ok(()) => Ok(src),
1261 Err(existing) => Ok(existing),
1262 }
1263 }
1264
1265 #[must_use]
1271 pub(crate) fn has_secondary_object_stores(&self) -> bool {
1272 if !self.extra_read_sources.is_empty()
1273 || self.custom_primary.is_some()
1274 || self.overlay_store().is_some()
1275 {
1276 return true;
1277 }
1278 self.alternates_enabled && self.alternate_object_lookup_needed()
1279 }
1280
1281 #[must_use]
1283 fn alternate_object_lookup_needed(&self) -> bool {
1284 if !self.file_alternate_dirs_snapshot().is_empty() {
1285 return true;
1286 }
1287 if !self.env_alternate_dirs_snapshot().is_empty() {
1288 return true;
1289 }
1290 self.submodule_alternate_dirs
1291 .lock()
1292 .ok()
1293 .is_some_and(|guard| !guard.is_empty())
1294 }
1295
1296 pub fn sources(&self) -> Result<Arc<CompositeStore>> {
1306 if !self.alternates_enabled {
1307 return Ok(Arc::new(CompositeStore::new(Vec::new(), self.hash_algo())));
1308 }
1309 let key = self.alternate_sources_key();
1310 if let Ok(guard) = self.alternate_sources_cache.read() {
1311 if guard.file_generation == key.file_generation
1312 && guard.env_fingerprint == key.env_fingerprint
1313 && guard.submodule_generation == key.submodule_generation
1314 {
1315 if let Some(sources) = guard.sources.as_ref() {
1316 return Ok(Arc::clone(sources));
1317 }
1318 }
1319 }
1320 let use_midx = self.multi_pack_index_reads_enabled();
1321 let midx_tip = self.midx_tip_listing_enabled();
1322 let mut stores: Vec<Arc<dyn ObjectStore>> = Vec::new();
1323 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
1324 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
1325 }
1326 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1327 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
1328 }
1329 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1330 for alt_dir in guard.iter() {
1331 stores.push(self.build_files_source(alt_dir, false, false)?);
1332 }
1333 }
1334 let composite = Arc::new(CompositeStore::new(stores, self.hash_algo()));
1335 if let Ok(mut guard) = self.alternate_sources_cache.write() {
1336 guard.file_generation = key.file_generation;
1337 guard.env_fingerprint = key.env_fingerprint;
1338 guard.submodule_generation = key.submodule_generation;
1339 guard.sources = Some(Arc::clone(&composite));
1340 }
1341 Ok(composite)
1342 }
1343
1344 fn midx_tip_listing_enabled(&self) -> bool {
1345 self.config_git_dir.is_some() && self.core_multi_pack_index_enabled()
1346 }
1347
1348 fn build_files_source(
1349 &self,
1350 objects_dir: &Path,
1351 use_midx: bool,
1352 midx_tip_listing_for_exists: bool,
1353 ) -> Result<Arc<FilesSource>> {
1354 let loose = self.loose_at(objects_dir)?;
1355 let pack_store = self.pack_store_for(objects_dir);
1356 let diagnostics = if objects_dir == self.objects_dir.as_path() {
1357 self.diagnostics_handle()
1358 } else {
1359 Arc::new(NullDiagnostics)
1360 };
1361 FilesSource::open(
1362 pack_store,
1363 loose,
1364 use_midx,
1365 midx_tip_listing_for_exists,
1366 diagnostics,
1367 )
1368 .map(Arc::new)
1369 }
1370
1371 pub fn register_submodule_object_directories_from_index(
1375 &self,
1376 work_tree: &Path,
1377 index: &crate::index::Index,
1378 ) {
1379 use crate::diff::submodule_embedded_git_dir;
1380
1381 let Ok(mut dirs) = self.submodule_alternate_dirs.lock() else {
1382 return;
1383 };
1384 dirs.clear();
1385 for e in &index.entries {
1386 if e.stage() != 0 || e.mode != crate::index::MODE_GITLINK {
1387 continue;
1388 }
1389 let path_str = String::from_utf8_lossy(&e.path);
1390 let abs = work_tree.join(path_str.as_ref());
1391 let Some(sub_git) = submodule_embedded_git_dir(&abs) else {
1392 continue;
1393 };
1394 let objects = sub_git.join("objects");
1395 if !objects.is_dir() {
1396 continue;
1397 }
1398 let canon = objects.canonicalize_simplified().unwrap_or(objects);
1399 if !dirs.iter().any(|p| p == &canon) {
1400 dirs.push(canon);
1401 }
1402 }
1403 self.submodule_sources_generation
1404 .fetch_add(1, Ordering::Relaxed);
1405 self.clear_alternate_sources_cache();
1406 }
1407
1408 #[must_use]
1410 pub fn with_config_git_dir(mut self, git_dir: PathBuf) -> Self {
1411 self.config_git_dir = Some(git_dir);
1412 self
1413 }
1414
1415 #[must_use]
1427 pub fn hash_algo(&self) -> HashAlgo {
1428 if let Some(primary) = &self.custom_primary {
1429 return primary.hash_algo();
1430 }
1431 *self.hash_algo_cache.get_or_init(|| {
1432 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1433 return HashAlgo::Sha1;
1434 }
1435 let cfg = self.load_config_cascade().unwrap_or_default();
1436 cfg.get("extensions.objectformat")
1437 .and_then(|v| HashAlgo::from_name(&v))
1438 .unwrap_or(HashAlgo::Sha1)
1439 })
1440 }
1441
1442 fn resolve_loose_zlib_level(&self) -> Result<u32> {
1443 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1444 return Ok(ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT as u32);
1445 }
1446 let cfg = self.load_config_cascade()?;
1447 Ok(cfg.loose_objects_zlib_level()? as u32)
1448 }
1449
1450 pub(crate) fn loose_compression(&self) -> Result<Compression> {
1457 if let Some(cached) = self.loose_zlib_cache.get() {
1458 return Ok(*cached);
1459 }
1460 let level = self.resolve_loose_zlib_level()?;
1461 let compression = Compression::new(level);
1462 let _ = self.loose_zlib_cache.set(compression);
1463 Ok(compression)
1464 }
1465
1466 fn resolve_pack_zlib_level(&self) -> Result<i32> {
1467 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1468 return Ok(6);
1469 }
1470 let cfg = self.load_config_cascade()?;
1471 cfg.pack_objects_zlib_level()
1472 }
1473
1474 pub(crate) fn pack_compression(&self) -> Result<Compression> {
1480 if let Some(cached) = self.pack_zlib_cache.get() {
1481 return Ok(*cached);
1482 }
1483 let level = self.resolve_pack_zlib_level()?;
1484 let compression = crate::pack_zlib::flate2_compression_for_pack_level(level);
1485 let _ = self.pack_zlib_cache.set(compression);
1486 Ok(compression)
1487 }
1488
1489 pub fn big_file_threshold(&self) -> Result<u64> {
1500 if let Some(cached) = self.big_file_threshold_cache.get() {
1501 return Ok(*cached);
1502 }
1503 let threshold = if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1504 ConfigSet::BIG_FILE_THRESHOLD_DEFAULT
1505 } else {
1506 self.load_config_cascade()?.big_file_threshold()?
1507 };
1508 let _ = self.big_file_threshold_cache.set(threshold);
1509 Ok(threshold)
1510 }
1511
1512 pub(crate) fn with_bounded_pack_cache(&self) -> Self {
1516 let mut odb = self.clone();
1517 odb.pack_store = Arc::new(PackStore::for_bounded_reads(self.objects_dir.clone()));
1518 odb.primary_source = Arc::new(OnceLock::new());
1519 odb.session_loose_reads = Arc::new(Mutex::new(SessionLooseReads::default()));
1520 odb.alternate_pack_stores = Arc::new(Mutex::new(HashMap::new()));
1521 odb.alternate_sources_cache = Arc::new(RwLock::new(AlternateSourcesCache {
1522 file_generation: 0,
1523 env_fingerprint: 0,
1524 submodule_generation: 0,
1525 sources: None,
1526 }));
1527 odb
1528 }
1529
1530 fn sync_delta_base_cache_limit(&self) {
1531 if self.pack_store.bounded_reads() {
1532 return;
1533 }
1534 self.delta_base_cache_sync.get_or_init(|| {
1535 if self.config_git_dir.is_some() {
1536 let cfg = self.load_config_cascade().unwrap_or_default();
1537 self.pack_store
1538 .configure_delta_base_from_config(Some(cfg.as_ref()));
1539 }
1540 });
1541 }
1542
1543 pub(crate) fn pack_store_for(&self, objects_dir: &Path) -> Arc<PackStore> {
1544 if objects_dir == self.objects_dir.as_path() {
1545 return Arc::clone(&self.pack_store);
1546 }
1547 let new_store = || {
1548 Arc::new(if self.pack_store.bounded_reads() {
1549 PackStore::for_bounded_reads(objects_dir.to_path_buf())
1550 } else {
1551 PackStore::new(objects_dir.to_path_buf())
1552 })
1553 };
1554 if let Ok(mut guard) = self.alternate_pack_stores.lock() {
1555 return Arc::clone(
1556 guard
1557 .entry(objects_dir.to_path_buf())
1558 .or_insert_with(new_store),
1559 );
1560 }
1561 new_store()
1562 }
1563
1564 pub fn pack_store(&self) -> Result<&Arc<PackStore>> {
1566 self.require_files_primary("pack_store")?;
1567 Ok(&self.pack_store)
1568 }
1569
1570 pub fn largest_pack_index_path(&self) -> Result<Option<PathBuf>> {
1579 self.require_files_primary("largest_pack_index_path")?;
1580 let mut indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
1581 indexes.sort_by_key(|idx| std::cmp::Reverse(idx.len()));
1582 Ok(indexes.first().map(|idx| idx.idx_path.clone()))
1583 }
1584
1585 pub(crate) fn clear_delta_base_cache(&self) {
1589 self.pack_store.delta_bases().clear();
1590 }
1591
1592 pub fn invalidate_packs(&self) {
1599 self.pack_store.invalidate_all();
1600 PackStore::invalidate_legacy_for_objects_dir(&self.objects_dir);
1601 if let Ok(guard) = self.alternate_pack_stores.lock() {
1602 for store in guard.values() {
1603 store.invalidate_all();
1604 PackStore::invalidate_legacy_for_objects_dir(store.objects_dir());
1605 }
1606 }
1607 self.invalidate_change_id_header_probe();
1608 }
1609
1610 fn invalidate_change_id_header_probe(&self) {
1611 if let Some(caches) = self.shared_config_state.as_ref() {
1612 crate::change_id::invalidate_stored_change_id_header_probe(caches, self.objects_dir());
1613 }
1614 }
1615
1616 fn note_commit_object_written(&self, kind: ObjectKind) {
1617 if kind == ObjectKind::Commit {
1618 self.invalidate_change_id_header_probe();
1619 }
1620 }
1621
1622 pub(crate) fn loosen_unreachable_from_superseded_packs(
1636 &self,
1637 pack_dir: &Path,
1638 keep_pack: &Path,
1639 reachable: &crate::gc::LazyReachable<'_>,
1640 retention: UnreachablePackRetention,
1641 retained_packs: &HashSet<PathBuf>,
1642 inventory: &RepackPackInventorySnapshot,
1643 ) -> Result<usize> {
1644 if matches!(retention, UnreachablePackRetention::DropAll) {
1645 return Ok(0);
1646 }
1647 self.require_files_primary("repack")?;
1648 let keep_idx = keep_pack.with_extension("idx");
1649 let keep_pack_oids = if keep_idx.is_file() {
1650 PackedObjects::object_ids_from_index_path(&keep_idx)?
1651 } else {
1652 HashSet::new()
1653 };
1654 let kept_pack_oids =
1655 oids_in_inventory_kept_packs(pack_dir, keep_pack, &inventory.kept_at_inventory)?;
1656 let mut loosened = 0usize;
1657 for (idx_path, pack_mtime) in
1658 superseded_pack_indexes(pack_dir, keep_pack, retained_packs, inventory)?
1659 {
1660 let oids = PackedObjects::object_ids_from_index_path(&idx_path)?;
1661 for oid in oids {
1662 let keep = match retention {
1663 UnreachablePackRetention::KeepAll => true,
1664 UnreachablePackRetention::KeepGraceProtected(cutoff) => pack_mtime >= cutoff,
1665 UnreachablePackRetention::DropAll => false,
1666 };
1667 if !keep {
1668 continue;
1669 }
1670 if keep_pack_oids.contains(&oid) || kept_pack_oids.contains(&oid) {
1671 continue;
1672 }
1673 if reachable.get()?.contains(&oid) {
1674 continue;
1675 }
1676 let loose_path = oid.loose_path_in(&self.objects_dir);
1677 let pack_ft = system_time_to_file_time(pack_mtime);
1678 match retention {
1679 UnreachablePackRetention::KeepGraceProtected(cutoff) => {
1680 if loose_path.is_file() {
1681 let loose_mtime = fs::metadata(&loose_path)
1682 .and_then(|m| m.modified())
1683 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1684 if loose_mtime >= cutoff {
1685 continue;
1686 }
1687 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1688 loosened += 1;
1689 continue;
1690 }
1691 }
1692 UnreachablePackRetention::KeepAll => {
1693 if loose_path.is_file() {
1694 continue;
1695 }
1696 }
1697 UnreachablePackRetention::DropAll => {}
1698 }
1699 let obj = self.read(&oid)?;
1700 self.write_loose_materialize(obj.kind, &obj.data)?;
1701 let primary = self.primary()?;
1702 let loose_path = primary.loose_store().object_path(&oid);
1703 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1704 loosened += 1;
1705 }
1706 }
1707 Ok(loosened)
1708 }
1709
1710 fn core_multi_pack_index_enabled(&self) -> bool {
1711 *self.core_multi_pack_index_cache.get_or_init(|| {
1715 if self.config_git_dir.is_none() {
1716 return false;
1717 }
1718 let cfg = self.load_config_cascade().unwrap_or_default();
1719 cfg.bool_or("core.multiPackIndex", true)
1720 })
1721 }
1722
1723 fn multi_pack_index_reads_enabled(&self) -> bool {
1724 if self.custom_primary.is_some() {
1725 return false;
1726 }
1727 if self.config_git_dir.is_none() {
1728 return false;
1729 }
1730 if !*self
1731 .local_pack_indexes_seen
1732 .get_or_init(|| objects_dir_has_pack_index_files(&self.objects_dir))
1733 {
1734 return false;
1735 }
1736 self.core_multi_pack_index_enabled()
1737 }
1738
1739 fn skip_batch_packed_read_for_active_midx(&self) -> bool {
1741 self.multi_pack_index_reads_enabled()
1742 && self
1743 .primary()
1744 .ok()
1745 .is_some_and(|files| files.midx_reads_active())
1746 }
1747
1748 fn ensure_midx_prepared(&self) {
1749 if !self.multi_pack_index_reads_enabled() {
1750 return;
1751 }
1752 let _ = self.midx_packs_validated.get_or_init(|| {
1753 let diagnostics = self.diagnostics_handle();
1754 self.with_pack_store_for(&self.objects_dir, || {
1755 validate_midx_referenced_packs_with_diagnostics(
1756 &self.objects_dir,
1757 diagnostics.as_ref(),
1758 );
1759 });
1760 });
1761 }
1762
1763 fn try_read_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<Object>> {
1768 if self.custom_primary.is_some()
1769 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1770 || self.skip_batch_packed_read_for_active_midx()
1771 {
1772 return Ok(None);
1773 }
1774 self.sync_delta_base_cache_limit();
1775 match pack::try_read_object_from_packs_with_options(
1776 &self.objects_dir,
1777 oid,
1778 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1779 ) {
1780 Ok(obj) => Ok(Some(obj)),
1781 Err(Error::ObjectNotFound(_)) => Ok(None),
1782 Err(err) => Err(err),
1783 }
1784 }
1785
1786 fn try_read_info_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1787 if self.custom_primary.is_some()
1788 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1789 || self.skip_batch_packed_read_for_active_midx()
1790 {
1791 return Ok(None);
1792 }
1793 match pack::try_read_object_info_from_packs_with_options(
1794 &self.objects_dir,
1795 oid,
1796 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1797 ) {
1798 Ok(info) => Ok(Some(info)),
1799 Err(Error::ObjectNotFound(_)) => Ok(None),
1800 Err(err) => Err(err),
1801 }
1802 }
1803
1804 #[must_use]
1811 pub fn objects_dir(&self) -> &Path {
1812 &self.objects_dir
1813 }
1814
1815 pub(crate) fn commit_graph_chain(
1826 &self,
1827 ) -> Option<Arc<crate::commit_graph_file::CommitGraphChain>> {
1828 if let Some(chain) = &self.attached_commit_graph {
1829 return Some(Arc::clone(chain));
1830 }
1831 if self.custom_primary.is_some() {
1832 return None;
1833 }
1834 let caches = self.shared_config_state.as_ref()?;
1835 let mut alternate_object_dirs = Vec::new();
1836 if self.alternates_enabled {
1837 for alt in self.file_alternate_dirs_snapshot().iter() {
1838 if !alternate_object_dirs.iter().any(|d| d == alt) {
1839 alternate_object_dirs.push(alt.clone());
1840 }
1841 }
1842 for alt in self.env_alternate_dirs_snapshot().iter() {
1843 if !alternate_object_dirs.iter().any(|d| d == alt) {
1844 alternate_object_dirs.push(alt.clone());
1845 }
1846 }
1847 }
1848 caches.commit_graph_chain(self.objects_dir(), &alternate_object_dirs)
1849 }
1850
1851 fn try_read_extra_sources(&self, oid: &ObjectId) -> Result<Option<Object>> {
1852 for src in &self.extra_read_sources {
1853 if let Some(obj) = src.read(oid)? {
1854 return Ok(Some(obj));
1855 }
1856 }
1857 Ok(None)
1858 }
1859
1860 fn try_read_info_extra_sources(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1861 for src in &self.extra_read_sources {
1862 if let Some(info) = src.read_info(oid)? {
1863 return Ok(Some(info));
1864 }
1865 }
1866 Ok(None)
1867 }
1868
1869 fn try_open_stream_extra_sources(
1870 &self,
1871 oid: &ObjectId,
1872 ) -> Result<Option<store::ObjectStream<'_>>> {
1873 for src in &self.extra_read_sources {
1874 if let Some(stream) = src.open_stream(oid)? {
1875 return Ok(Some(stream));
1876 }
1877 }
1878 Ok(None)
1879 }
1880
1881 #[must_use]
1887 pub fn config_git_dir(&self) -> Option<&Path> {
1888 self.config_git_dir.as_deref()
1889 }
1890
1891 #[must_use]
1893 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
1894 oid.loose_path_in(&self.objects_dir)
1895 }
1896
1897 pub(crate) fn local_pack_indexes(&self) -> Result<Vec<Arc<pack::PackIndex>>> {
1904 self.with_primary_read_context(|| pack::read_local_pack_indexes_cached(&self.objects_dir))
1907 }
1908
1909 pub(crate) fn pack_indexes_for_display(&self) -> Result<Vec<Arc<pack::PackIndex>>> {
1918 let local = self.local_pack_indexes()?;
1919 if !local.is_empty() || !self.alternate_object_lookup_needed() {
1920 return Ok(local);
1921 }
1922 let alts = self.file_alternate_dirs_snapshot();
1923 let Some(alt) = alts.first() else {
1924 return Ok(local);
1925 };
1926 let dir = alt.as_path();
1927 if crate::pack_store::PackStore::read_context_matches_objects_dir(dir) {
1928 return pack::read_local_pack_indexes_cached(dir);
1929 }
1930 self.with_objects_dir_pack_read_access(dir, || pack::read_local_pack_indexes_cached(dir))
1931 }
1932
1933 #[must_use]
1938 pub(crate) fn alternates_may_hold_loose_objects(&self) -> bool {
1939 if !self.alternates_enabled {
1940 return false;
1941 }
1942 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
1943 if pack::objects_dir_has_loose_objects(alt_dir) {
1944 return true;
1945 }
1946 }
1947 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1948 if pack::objects_dir_has_loose_objects(alt_dir) {
1949 return true;
1950 }
1951 }
1952 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1953 for alt_dir in guard.iter() {
1954 if pack::objects_dir_has_loose_objects(alt_dir) {
1955 return true;
1956 }
1957 }
1958 }
1959 false
1960 }
1961
1962 pub(crate) fn contained_in_alternate_pack(&self, oid: &ObjectId) -> Result<bool> {
1970 if !self.alternates_enabled {
1971 return Ok(false);
1972 }
1973 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
1974 if store::object_in_non_promisor_pack(alt_dir, oid) {
1975 return Ok(true);
1976 }
1977 }
1978 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1979 if store::object_in_non_promisor_pack(alt_dir, oid) {
1980 return Ok(true);
1981 }
1982 }
1983 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1984 for alt_dir in guard.iter() {
1985 if store::object_in_non_promisor_pack(alt_dir, oid) {
1986 return Ok(true);
1987 }
1988 }
1989 }
1990 Ok(false)
1991 }
1992
1993 pub(crate) fn remove_local_loose(&self, oid: &ObjectId) -> Result<bool> {
2002 match fs::remove_file(self.object_path(oid)) {
2003 Ok(()) => Ok(true),
2004 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
2005 Err(err) => Err(Error::Io(err)),
2006 }
2007 }
2008
2009 pub(crate) fn local_loose_file_size(&self, oid: &ObjectId) -> Option<u64> {
2012 fs::metadata(self.object_path(oid))
2013 .ok()
2014 .filter(fs::Metadata::is_file)
2015 .map(|meta| meta.len())
2016 }
2017
2018 #[must_use]
2034 pub fn exists_local(&self, oid: &ObjectId) -> bool {
2035 self.try_exists_local(oid).unwrap_or(false)
2036 }
2037
2038 pub(crate) fn try_exists_local(&self, oid: &ObjectId) -> Result<bool> {
2040 if self.is_empty_tree(oid) {
2041 return Ok(true);
2042 }
2043 if let Some(primary) = &self.custom_primary {
2044 return primary.contains(oid);
2045 }
2046 Ok(self.with_pack_store_for(&self.objects_dir, || {
2047 exists_materialized_in_objects_dir(&self.objects_dir, oid)
2048 }))
2049 }
2050
2051 #[must_use]
2059 pub fn exists_stored(&self, oid: &ObjectId) -> bool {
2060 self.try_exists_stored(oid).unwrap_or(false)
2061 }
2062
2063 pub(crate) fn try_exists_stored(&self, oid: &ObjectId) -> Result<bool> {
2065 if oid.is_canonical_empty_tree() || self.overlay_read(oid).is_some() {
2066 return Ok(true);
2067 }
2068 if let Some(primary) = &self.custom_primary {
2069 return primary.contains(oid);
2070 }
2071 Ok(self.with_pack_store_for(&self.objects_dir, || {
2072 store::local_object_stored(&self.objects_dir, oid)
2073 }))
2074 }
2075
2076 #[must_use]
2086 pub fn exists_materialized_local(&self, oid: &ObjectId) -> bool {
2087 if let Some(primary) = &self.custom_primary {
2088 return primary.contains(oid).unwrap_or(false);
2089 }
2090 self.with_pack_store_for(&self.objects_dir, || {
2091 exists_materialized_in_objects_dir(&self.objects_dir, oid)
2092 })
2093 }
2094
2095 pub(crate) fn reprepare_local_packs(&self) {
2100 if self.custom_primary.is_some() {
2101 return;
2102 }
2103 self.with_pack_store_for(&self.objects_dir, || {
2104 let _ = crate::pack::reprepare_pack_directory_on_miss(&self.objects_dir);
2105 });
2106 }
2107
2108 pub(crate) fn exists_materialized_local_without_rescan(&self, oid: &ObjectId) -> bool {
2116 if self.custom_primary.is_some() {
2117 return self.exists_materialized_local(oid);
2118 }
2119 self.with_pack_store_for(&self.objects_dir, || {
2120 store::local_object_materialized_without_rescan(&self.objects_dir, oid)
2121 })
2122 }
2123
2124 pub(crate) fn custom_primary_is_empty(&self) -> Result<bool> {
2133 use std::ops::ControlFlow;
2134 let Some(primary) = &self.custom_primary else {
2135 return Ok(false);
2136 };
2137 let mut empty = true;
2138 primary.for_each_object(&mut |oid| {
2139 let _ = oid;
2140 empty = false;
2141 ControlFlow::Break(())
2142 })?;
2143 Ok(empty)
2144 }
2145
2146 pub(crate) fn missing_materialized_many(&self, oids: &[ObjectId]) -> Result<Vec<ObjectId>> {
2157 let primary =
2158 self.custom_primary
2159 .as_ref()
2160 .ok_or_else(|| Error::UnsupportedObjectStore {
2161 operation: "missing_materialized_many",
2162 })?;
2163 if oids.is_empty() {
2164 return Ok(Vec::new());
2165 }
2166 let present = primary.contains_many(oids)?;
2167 Ok(oids
2168 .iter()
2169 .zip(present)
2170 .filter_map(|(oid, has)| (!has).then_some(*oid))
2171 .collect())
2172 }
2173
2174 pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
2186 self.probe_sources(oid, Err)
2187 }
2188
2189 fn probe_sources(
2192 &self,
2193 oid: &ObjectId,
2194 mut on_err: impl FnMut(Error) -> Result<()>,
2195 ) -> Result<bool> {
2196 #[cfg(test)]
2197 self.exists_probe.fetch_add(1, Ordering::Relaxed);
2198 if self.is_empty_tree(oid) || self.overlay_read(oid).is_some() {
2199 return Ok(true);
2200 }
2201 let mut found = |probe: Result<bool>| match probe {
2202 Ok(present) => Ok(present),
2203 Err(err) => on_err(err).map(|()| false),
2204 };
2205 if found(self.primary_writable().and_then(|p| p.contains(oid)))? {
2206 return Ok(true);
2207 }
2208 for src in &self.extra_read_sources {
2209 if found(src.contains(oid))? {
2210 return Ok(true);
2211 }
2212 }
2213 if !self.alternate_object_lookup_needed() {
2214 return Ok(false);
2215 }
2216 found(self.sources().and_then(|s| s.contains(oid)))
2217 }
2218
2219 pub(crate) fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
2229 #[cfg(test)]
2230 self.exists_probe.fetch_add(oids.len(), Ordering::Relaxed);
2231 let mut present: Vec<bool> = oids
2232 .iter()
2233 .map(|oid| self.is_empty_tree(oid) || self.overlay_read(oid).is_some())
2234 .collect();
2235 let mut pending: Vec<usize> = (0..oids.len()).filter(|&i| !present[i]).collect();
2236 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
2237 self.primary_writable()?.contains_many(batch)
2238 })?;
2239 for src in &self.extra_read_sources {
2240 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
2241 src.contains_many(batch)
2242 })?;
2243 }
2244 if !pending.is_empty() && self.alternate_object_lookup_needed() {
2245 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
2246 self.sources()?.contains_many(batch)
2247 })?;
2248 }
2249 Ok(present)
2250 }
2251
2252 pub(crate) fn present_oids(
2258 &self,
2259 oids: impl IntoIterator<Item = ObjectId>,
2260 ) -> Result<Vec<ObjectId>> {
2261 self.partition_oids(oids, true)
2262 }
2263
2264 pub(crate) fn missing_oids(
2270 &self,
2271 oids: impl IntoIterator<Item = ObjectId>,
2272 ) -> Result<Vec<ObjectId>> {
2273 self.partition_oids(oids, false)
2274 }
2275
2276 fn partition_oids(
2278 &self,
2279 oids: impl IntoIterator<Item = ObjectId>,
2280 want_present: bool,
2281 ) -> Result<Vec<ObjectId>> {
2282 let oids: Vec<ObjectId> = oids.into_iter().collect();
2283 let present = self.contains_many(&oids)?;
2284 Ok(oids
2285 .into_iter()
2286 .zip(present)
2287 .filter_map(|(oid, has)| (has == want_present).then_some(oid))
2288 .collect())
2289 }
2290
2291 pub(crate) fn missing_stored_oids(
2301 &self,
2302 oids: impl IntoIterator<Item = ObjectId>,
2303 ) -> Result<Vec<ObjectId>> {
2304 if self.custom_primary.is_some() {
2305 return filter_oids(oids, |oid| self.try_exists_stored(oid).map(|has| !has));
2306 }
2307 self.reprepare_local_packs();
2308 self.with_pack_store_for(&self.objects_dir, || {
2309 filter_oids(oids, |oid| {
2310 Ok(!(oid.is_canonical_empty_tree()
2311 || self.overlay_read(oid).is_some()
2312 || store::local_object_stored_without_rescan(&self.objects_dir, oid)))
2313 })
2314 })
2315 }
2316
2317 #[must_use]
2324 pub fn exists(&self, oid: &ObjectId) -> bool {
2325 self.probe_sources(oid, |_| Ok(())).unwrap_or(false)
2326 }
2327
2328 fn with_pack_store_for<R>(&self, objects_dir: &Path, f: impl FnOnce() -> R) -> R {
2330 let store = self.pack_store_for(objects_dir);
2331 PackStore::with_context(store, f)
2332 }
2333
2334 pub(crate) fn with_objects_dir_pack_read_access<R>(
2337 &self,
2338 objects_dir: &Path,
2339 f: impl FnOnce() -> R,
2340 ) -> R {
2341 if PackStore::read_context_matches_objects_dir(objects_dir) {
2342 f()
2343 } else if PackStore::has_context() {
2344 pack::with_alternate_pack_store(self.pack_store_for(objects_dir), f)
2345 } else {
2346 PackStore::with_context(self.pack_store_for(objects_dir), f)
2347 }
2348 }
2349
2350 fn skip_primary_for_alternate_backed_reads(&self) -> bool {
2355 self.custom_primary.is_none()
2356 && self.alternates_enabled
2357 && self.alternate_object_lookup_needed()
2358 && crate::pack::pack_cache::primary_has_verified_empty_listing(&self.objects_dir)
2359 }
2360
2361 fn with_primary_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
2367 if self.custom_primary.is_some()
2368 || PackStore::read_context_matches_objects_dir(&self.objects_dir)
2369 {
2370 f()
2371 } else {
2372 self.with_pack_store_for(&self.objects_dir, f)
2373 }
2374 }
2375
2376 fn pack_read_context_store(&self) -> std::sync::Arc<crate::pack_store::PackStore> {
2382 if self.skip_primary_for_alternate_backed_reads() {
2383 if let Some(alt) = self.file_alternate_dirs_snapshot().first() {
2384 return self.pack_store_for(alt);
2385 }
2386 }
2387 self.pack_store_for(&self.objects_dir)
2388 }
2389
2390 pub fn with_pack_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
2395 let store = self.pack_read_context_store();
2396 let on_primary = store.objects_dir() == self.objects_dir.as_path();
2397 crate::pack_store::PackStore::with_context(store, || {
2398 if on_primary {
2399 self.pin_shared_alternate_pack_store_if_needed();
2400 }
2401 f()
2402 })
2403 }
2404
2405 fn pin_shared_alternate_pack_store_if_needed(&self) {
2406 if !self.skip_primary_for_alternate_backed_reads() {
2407 return;
2408 }
2409 let alts = self.file_alternate_dirs_snapshot();
2410 let Some(alt) = alts.first() else {
2411 return;
2412 };
2413 let store = self.pack_store_for(alt);
2414 pack::with_alternate_pack_store(store, || ());
2415 }
2416
2417 pub(crate) fn pack_read_context_guard(&self) -> crate::pack_store::PackReadContextGuard {
2420 let store = self.pack_read_context_store();
2421 let on_primary = store.objects_dir() == self.objects_dir.as_path();
2422 let guard = crate::pack_store::PackReadContextGuard::install(store);
2423 if on_primary {
2424 self.pin_shared_alternate_pack_store_if_needed();
2425 }
2426 guard
2427 }
2428
2429 pub(crate) fn open_session_pack_reader(&self) -> Result<Option<pack::SingleLocalPackReader>> {
2437 if self.custom_primary.is_some()
2438 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
2439 {
2440 return Ok(None);
2441 }
2442 self.sync_delta_base_cache_limit();
2443 pack::SingleLocalPackReader::open_allow_loose(&self.objects_dir)
2444 }
2445
2446 #[must_use]
2451 pub fn freshen_object(&self, oid: &ObjectId) -> bool {
2452 if self.is_empty_tree(oid) {
2453 return false;
2454 }
2455
2456 let loose = self.object_path(oid);
2457 if loose.is_file() {
2458 return self.touch_object_mtime(&loose).is_some();
2459 }
2460 self.freshen_packed_or_alternate(oid)
2461 }
2462
2463 fn freshen_packed_or_alternate(&self, oid: &ObjectId) -> bool {
2467 if self.is_empty_tree(oid) {
2468 return false;
2469 }
2470 if self.freshen_packed_in_objects_dir(&self.objects_dir, oid) {
2471 return true;
2472 }
2473
2474 let file_alts = self.file_alternate_dirs_snapshot();
2475 for alt_dir in file_alts.iter() {
2476 if self.freshen_object_in_objects_dir(alt_dir, oid) {
2477 return true;
2478 }
2479 }
2480
2481 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
2482 if self.freshen_object_in_objects_dir(alt_dir, oid) {
2483 return true;
2484 }
2485 }
2486
2487 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
2488 for alt_dir in guard.iter() {
2489 if self.freshen_object_in_objects_dir(alt_dir, oid) {
2490 return true;
2491 }
2492 }
2493 }
2494
2495 false
2496 }
2497
2498 fn freshen_object_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
2499 let loose = objects_dir
2500 .join(oid.loose_prefix())
2501 .join(oid.loose_suffix());
2502 if loose.is_file() {
2503 return self.touch_object_mtime(&loose).is_some();
2504 }
2505 self.freshen_packed_in_objects_dir(objects_dir, oid)
2506 }
2507
2508 fn freshen_packed_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
2509 self.with_pack_store_for(objects_dir, || {
2510 let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
2511 return false;
2512 };
2513 for idx in &indexes {
2514 if idx.contains(oid).unwrap_or(false) {
2515 return self.freshen_pack_once(&idx.pack_path, idx.is_cruft);
2516 }
2517 }
2518 false
2519 })
2520 }
2521
2522 fn freshen_pack_once(&self, pack_path: &Path, is_cruft: bool) -> bool {
2527 let Ok(mut guard) = self.freshened_packs.lock() else {
2528 return false;
2529 };
2530 if guard.contains(pack_path) {
2531 return true;
2532 }
2533 if is_cruft {
2534 return false;
2535 }
2536 let Some(touched_at) = self.touch_object_mtime(pack_path) else {
2537 return false;
2538 };
2539 pack::refresh_pack_bytes_signature(pack_path, touched_at);
2540 guard.insert(pack_path.to_path_buf());
2541 true
2542 }
2543
2544 #[must_use]
2553 pub fn bulk_stage_write_options(&self) -> WriteOptions {
2554 let stored_beyond_loose = self.custom_primary.is_none()
2555 && (self.alternate_object_lookup_needed()
2556 || self.with_pack_read_context(|| {
2557 pack::read_local_pack_indexes_cached(&self.objects_dir)
2558 .is_ok_and(|indexes| !indexes.is_empty())
2559 }));
2560 WriteOptions {
2561 freshen_existing: stored_beyond_loose,
2562 ..BULK_NEW_LOOSE_WRITE
2563 }
2564 }
2565
2566 fn try_freshen_existing(&self, oid: &ObjectId) -> Option<ObjectId> {
2569 if let Ok(primary) = self.primary() {
2570 if let Ok(Some(existing)) =
2571 primary.try_finish_duplicate_write(oid, WriteOptions::default())
2572 {
2573 return Some(existing);
2574 }
2575 }
2576 if !self.exists(oid) {
2577 return None;
2578 }
2579 if self.freshen_object(oid) {
2580 return Some(*oid);
2581 }
2582 None
2583 }
2584
2585 fn try_freshen_existing_local(&self, oid: &ObjectId) -> Option<ObjectId> {
2586 let primary = self.primary().ok()?;
2587 primary
2588 .try_finish_duplicate_write(
2589 oid,
2590 WriteOptions {
2591 assume_loose_only_existence: true,
2592 ..WriteOptions::default()
2593 },
2594 )
2595 .ok()
2596 .flatten()
2597 }
2598
2599 fn object_exists_for_write_skip(&self, oid: &ObjectId) -> bool {
2604 if self.is_empty_tree(oid) {
2605 self.exists_materialized_local(oid)
2606 } else {
2607 self.exists(oid)
2608 }
2609 }
2610
2611 pub fn read_loose_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
2621 LooseStore::read_verify_oid(path, expected_oid)
2622 }
2623
2624 fn local_loose(&self) -> Result<LooseStore> {
2625 self.require_files_primary("loose_write")?;
2626 Ok(LooseStore::new_with_negative_cache(
2627 self.objects_dir.clone(),
2628 self.hash_algo(),
2629 self.loose_compression()?,
2630 self.loose_negative_cache_for(&self.objects_dir),
2631 ))
2632 }
2633
2634 fn loose_at(&self, objects_dir: &Path) -> Result<LooseStore> {
2635 Ok(LooseStore::new_with_negative_cache(
2636 objects_dir.to_path_buf(),
2637 self.hash_algo(),
2638 self.loose_compression()?,
2639 self.loose_negative_cache_for(objects_dir),
2640 ))
2641 }
2642
2643 pub fn read(&self, oid: &ObjectId) -> Result<Object> {
2650 self.read_impl(oid, true)
2651 }
2652
2653 fn read_impl(&self, oid: &ObjectId, remember: bool) -> Result<Object> {
2656 if self.is_empty_tree(oid) {
2657 return Ok(Object {
2658 kind: ObjectKind::Tree,
2659 data: Vec::new(),
2660 });
2661 }
2662
2663 if let Some(obj) = self.session_cached_loose_read(oid) {
2664 return Ok(obj);
2665 }
2666
2667 if let Some(obj) = self.overlay_read(oid) {
2668 return Ok(obj);
2669 }
2670 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
2671 return Ok(obj);
2672 }
2673
2674 let use_midx = self.multi_pack_index_reads_enabled();
2675 if use_midx {
2676 self.ensure_midx_prepared();
2677 }
2678
2679 self.sync_delta_base_cache_limit();
2680
2681 let mut unreadable_local_loose = None;
2682 if !self.skip_primary_for_alternate_backed_reads() {
2683 let primary = self.primary_writable()?;
2684 let files_primary = self.primary().ok();
2685 let read_primary = || primary.read(oid);
2686 match self.with_primary_read_context(read_primary) {
2687 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj, remember),
2688 Ok(None) => {}
2689 Err(err)
2690 if files_primary
2691 .as_ref()
2692 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2693 {
2694 unreadable_local_loose = Some(err);
2695 }
2696 Err(err) => return Err(err),
2697 }
2698 }
2699
2700 if let Some(obj) = self.try_read_extra_sources(oid)? {
2701 return self.finish_session_loose_read(oid, obj, remember);
2702 }
2703
2704 if self.alternates_enabled {
2705 match self.sources()?.read(oid) {
2706 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj, remember),
2707 Ok(None) => {}
2708 Err(err) => return Err(err),
2709 }
2710 }
2711
2712 if let Some(err) = unreadable_local_loose {
2713 return Err(err);
2714 }
2715
2716 self.read_after_promisor_miss(oid)
2717 }
2718
2719 #[cold]
2720 #[inline(never)]
2721 fn read_after_promisor_miss(&self, oid: &ObjectId) -> Result<Object> {
2722 let mut lazy_retries = 0u32;
2723 loop {
2724 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2725 return self.object_not_found_after_primary_revalidate(oid);
2726 };
2727 match res {
2728 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2729 lazy_retries += 1;
2730 if let Some(obj) = self.try_read_once(oid)? {
2731 return Ok(obj);
2732 }
2733 }
2734 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2735 return self.object_not_found_after_primary_revalidate(oid);
2736 }
2737 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2738 return Err(err);
2739 }
2740 }
2741 }
2742 }
2743
2744 pub(crate) fn try_read_once(&self, oid: &ObjectId) -> Result<Option<Object>> {
2747 if self.is_empty_tree(oid) {
2748 return Ok(Some(Object {
2749 kind: ObjectKind::Tree,
2750 data: Vec::new(),
2751 }));
2752 }
2753
2754 if let Some(obj) = self.session_cached_loose_read(oid) {
2755 return Ok(Some(obj));
2756 }
2757
2758 if let Some(obj) = self.overlay_read(oid) {
2759 return Ok(Some(obj));
2760 }
2761 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
2762 return Ok(Some(self.finish_session_loose_read(oid, obj, true)?));
2763 }
2764
2765 let use_midx = self.multi_pack_index_reads_enabled();
2766 if use_midx {
2767 self.ensure_midx_prepared();
2768 }
2769
2770 self.sync_delta_base_cache_limit();
2771
2772 let mut unreadable_local_loose = None;
2773 if !self.skip_primary_for_alternate_backed_reads() {
2774 let primary = self.primary_writable()?;
2775 let files_primary = self.primary().ok();
2776 let read_primary = || primary.read(oid);
2777 match self.with_primary_read_context(read_primary) {
2778 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj, true)?)),
2779 Ok(None) => {}
2780 Err(err)
2781 if files_primary
2782 .as_ref()
2783 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2784 {
2785 unreadable_local_loose = Some(err);
2786 }
2787 Err(err) => return Err(err),
2788 }
2789 }
2790
2791 if let Some(obj) = self.try_read_extra_sources(oid)? {
2792 return Ok(Some(self.finish_session_loose_read(oid, obj, true)?));
2793 }
2794
2795 if self.alternates_enabled {
2796 match self.sources()?.read(oid) {
2797 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj, true)?)),
2798 Ok(None) => {}
2799 Err(err) => return Err(err),
2800 }
2801 }
2802
2803 if let Some(err) = unreadable_local_loose {
2804 return Err(err);
2805 }
2806
2807 Ok(None)
2808 }
2809
2810 fn resolve_promisor_miss(
2811 &self,
2812 oid: &ObjectId,
2813 lazy_retries: u32,
2814 ) -> Result<Option<crate::promisor_fetch::PromisorMissResolution>> {
2815 let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment)
2816 else {
2817 return Ok(None);
2818 };
2819 Ok(Some(crate::promisor_fetch::resolve_promisor_read_miss(
2820 caches,
2821 self,
2822 oid,
2823 env.as_ref(),
2824 lazy_retries,
2825 )?))
2826 }
2827
2828 pub(crate) fn prefetch_promisor_read_batch(
2836 &self,
2837 oids: impl IntoIterator<Item = ObjectId>,
2838 ) -> Result<()> {
2839 let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment)
2840 else {
2841 return Ok(());
2842 };
2843 crate::promisor_fetch::prefetch_promisor_read_batch(caches, self, env.as_ref(), oids)
2844 }
2845
2846 pub fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<ObjectKind> {
2855 if self.is_empty_tree(oid) {
2856 buf.clear();
2857 return Ok(ObjectKind::Tree);
2858 }
2859
2860 if let Some(obj) = self.session_cached_loose_read(oid) {
2861 buf.clear();
2862 buf.extend_from_slice(&obj.data);
2863 return Ok(obj.kind);
2864 }
2865
2866 if let Some(obj) = self.overlay_read(oid) {
2867 buf.clear();
2868 buf.extend_from_slice(&obj.data);
2869 return Ok(obj.kind);
2870 }
2871
2872 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2873 return Ok(kind);
2874 }
2875
2876 let use_midx = self.multi_pack_index_reads_enabled();
2877 if use_midx {
2878 self.ensure_midx_prepared();
2879 }
2880
2881 self.sync_delta_base_cache_limit();
2882
2883 let mut unreadable_local_loose = None;
2884 if !self.skip_primary_for_alternate_backed_reads() {
2885 let primary = self.primary_writable()?;
2886 let files_primary = self.primary().ok();
2887 let read_primary = || primary.read_into(oid, buf);
2888 match self.with_primary_read_context(read_primary) {
2889 Ok(Some(kind)) => return Ok(kind),
2890 Ok(None) => {}
2891 Err(err)
2892 if files_primary
2893 .as_ref()
2894 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2895 {
2896 unreadable_local_loose = Some(err);
2897 }
2898 Err(err) => return Err(err),
2899 }
2900 }
2901
2902 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2903 return Ok(kind);
2904 }
2905
2906 if self.alternates_enabled {
2907 match self.sources()?.read_into(oid, buf) {
2908 Ok(Some(kind)) => return Ok(kind),
2909 Ok(None) => {}
2910 Err(err) => return Err(err),
2911 }
2912 }
2913
2914 if let Some(err) = unreadable_local_loose {
2915 return Err(err);
2916 }
2917
2918 self.read_into_after_promisor_miss(oid, buf)
2919 }
2920
2921 pub(crate) fn read_into_caching_delta_bases(
2935 &self,
2936 oid: &ObjectId,
2937 buf: &mut Vec<u8>,
2938 ) -> Result<ObjectKind> {
2939 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, true, buf)? {
2940 return Ok(kind);
2941 }
2942 if self.custom_primary.is_none() && !self.is_empty_tree(oid) {
2943 if let Ok(files) = self.primary() {
2944 self.ensure_midx_prepared();
2945 self.sync_delta_base_cache_limit();
2946 let mut read = || files.read_into_with_delta_policy(oid, true, buf);
2947 let local = if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2948 read()
2949 } else {
2950 self.with_pack_store_for(&self.objects_dir, read)
2951 };
2952 if let Ok(Some(kind)) = local {
2953 return Ok(kind);
2954 }
2955 }
2956 }
2957 if self.custom_primary.is_some() {
2958 return self.read_into(oid, buf);
2959 }
2960 let object = self.read(oid)?;
2961 buf.clear();
2962 buf.extend_from_slice(&object.data);
2963 Ok(object.kind)
2964 }
2965
2966 pub(crate) fn read_packed_commit_header_block_into(
2979 &self,
2980 oid: &ObjectId,
2981 buf: &mut Vec<u8>,
2982 ) -> Result<bool> {
2983 if self.custom_primary.is_some() {
2984 return Ok(false);
2985 }
2986 let Ok(files) = self.primary() else {
2987 return Ok(false);
2988 };
2989 files.read_packed_commit_header_block_into(oid, buf)
2990 }
2991
2992 pub(crate) fn read_packed_commit_header_blocks(
3002 &self,
3003 oids: &[ObjectId],
3004 found: impl FnMut(usize, &[u8]),
3005 ) -> Result<()> {
3006 if self.custom_primary.is_some() {
3007 return Ok(());
3008 }
3009 let Ok(files) = self.primary() else {
3010 return Ok(());
3011 };
3012 files.read_packed_commit_header_blocks(oids, found)
3013 }
3014
3015 #[cold]
3016 #[inline(never)]
3017 fn read_into_after_promisor_miss(
3018 &self,
3019 oid: &ObjectId,
3020 buf: &mut Vec<u8>,
3021 ) -> Result<ObjectKind> {
3022 let mut lazy_retries = 0u32;
3023 loop {
3024 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
3025 return self.object_kind_not_found_after_primary_revalidate(oid, buf);
3026 };
3027 match res {
3028 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
3029 lazy_retries += 1;
3030 if let Some(kind) = self.try_read_into_once(oid, buf)? {
3031 return Ok(kind);
3032 }
3033 }
3034 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
3035 return self.object_kind_not_found_after_primary_revalidate(oid, buf);
3036 }
3037 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
3038 return Err(err);
3039 }
3040 }
3041 }
3042 }
3043
3044 pub(crate) fn try_read_into_once(
3047 &self,
3048 oid: &ObjectId,
3049 buf: &mut Vec<u8>,
3050 ) -> Result<Option<ObjectKind>> {
3051 if oid.is_well_known_empty_tree() {
3052 buf.clear();
3053 return Ok(Some(ObjectKind::Tree));
3054 }
3055
3056 if let Some(obj) = self.session_cached_loose_read(oid) {
3057 buf.clear();
3058 buf.extend_from_slice(&obj.data);
3059 return Ok(Some(obj.kind));
3060 }
3061
3062 if let Some(obj) = self.overlay_read(oid) {
3063 buf.clear();
3064 buf.extend_from_slice(&obj.data);
3065 return Ok(Some(obj.kind));
3066 }
3067
3068 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
3069 return Ok(Some(kind));
3070 }
3071
3072 let use_midx = self.multi_pack_index_reads_enabled();
3073 if use_midx {
3074 self.ensure_midx_prepared();
3075 }
3076
3077 self.sync_delta_base_cache_limit();
3078
3079 let mut unreadable_local_loose = None;
3080 if !self.skip_primary_for_alternate_backed_reads() {
3081 let primary = self.primary_writable()?;
3082 let files_primary = self.primary().ok();
3083 let read_primary = || primary.read_into(oid, buf);
3084 match self.with_primary_read_context(read_primary) {
3085 Ok(Some(kind)) => return Ok(Some(kind)),
3086 Ok(None) => {}
3087 Err(err)
3088 if files_primary
3089 .as_ref()
3090 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
3091 {
3092 unreadable_local_loose = Some(err);
3093 }
3094 Err(err) => return Err(err),
3095 }
3096 }
3097
3098 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
3099 return Ok(Some(kind));
3100 }
3101
3102 if self.alternates_enabled {
3103 match self.sources()?.read_into(oid, buf) {
3104 Ok(Some(kind)) => return Ok(Some(kind)),
3105 Ok(None) => {}
3106 Err(err) => return Err(err),
3107 }
3108 }
3109
3110 if let Some(err) = unreadable_local_loose {
3111 return Err(err);
3112 }
3113
3114 Ok(None)
3115 }
3116
3117 fn try_read_into_extra_sources(
3118 &self,
3119 oid: &ObjectId,
3120 buf: &mut Vec<u8>,
3121 ) -> Result<Option<ObjectKind>> {
3122 for src in &self.extra_read_sources {
3123 if let Some(kind) = src.read_into(oid, buf)? {
3124 return Ok(Some(kind));
3125 }
3126 }
3127 Ok(None)
3128 }
3129
3130 fn try_read_packed_in_batch_context_into(
3131 &self,
3132 oid: &ObjectId,
3133 populate_delta_lru: bool,
3134 buf: &mut Vec<u8>,
3135 ) -> Result<Option<ObjectKind>> {
3136 if self.custom_primary.is_some()
3137 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
3138 || self.skip_batch_packed_read_for_active_midx()
3139 {
3140 return Ok(None);
3141 }
3142 self.sync_delta_base_cache_limit();
3143 match pack::try_read_object_from_packs_with_options_into(
3144 &self.objects_dir,
3145 oid,
3146 pack::PackLookupOptions::ALL_LOCAL_PACKS,
3147 populate_delta_lru,
3148 buf,
3149 ) {
3150 Ok(kind) => Ok(Some(kind)),
3151 Err(Error::ObjectNotFound(_)) => Ok(None),
3152 Err(err) => Err(err),
3153 }
3154 }
3155
3156 pub fn read_info(&self, oid: &ObjectId) -> Result<ObjectInfo> {
3167 if self.is_empty_tree(oid) {
3168 return Ok(ObjectInfo {
3169 kind: ObjectKind::Tree,
3170 size: 0,
3171 });
3172 }
3173
3174 if let Some(obj) = self.overlay_read(oid) {
3175 return Ok(ObjectInfo {
3176 kind: obj.kind,
3177 size: u64::try_from(obj.data.len())
3178 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
3179 });
3180 }
3181
3182 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
3183 return Ok(info);
3184 }
3185
3186 let use_midx = self.multi_pack_index_reads_enabled();
3187 if use_midx {
3188 self.ensure_midx_prepared();
3189 }
3190
3191 let mut unreadable_local_loose = None;
3192 if !self.skip_primary_for_alternate_backed_reads() {
3193 let primary = self.primary_writable()?;
3194 let files_primary = self.primary().ok();
3195 match primary.read_info(oid) {
3196 Ok(Some(info)) => return Ok(info),
3197 Ok(None) => {}
3198 Err(err)
3199 if files_primary
3200 .as_ref()
3201 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
3202 {
3203 unreadable_local_loose = Some(err);
3204 }
3205 Err(err) => return Err(err),
3206 }
3207 }
3208
3209 if let Some(info) = self.try_read_info_extra_sources(oid)? {
3210 return Ok(info);
3211 }
3212
3213 if self.alternates_enabled {
3214 match self.sources()?.read_info(oid) {
3215 Ok(Some(info)) => return Ok(info),
3216 Ok(None) => {}
3217 Err(err) => return Err(err),
3218 }
3219 }
3220
3221 if let Some(err) = unreadable_local_loose {
3222 return Err(err);
3223 }
3224
3225 self.read_info_after_promisor_miss(oid)
3226 }
3227
3228 pub(crate) fn read_info_without_fetch(&self, oid: &ObjectId) -> Result<ObjectInfo> {
3233 match self.try_read_info_once(oid)? {
3234 Some(info) => Ok(info),
3235 None => self.object_info_not_found_after_primary_revalidate(oid),
3236 }
3237 }
3238
3239 pub(crate) fn stored_object_kind(&self, oid: &ObjectId) -> Result<Option<ObjectKind>> {
3251 if oid.is_well_known_empty_tree() && !self.empty_tree_stored(oid) {
3252 return Ok(None);
3253 }
3254 match self.try_read_info_once(oid) {
3255 Ok(info) => Ok(info.map(|info| info.kind)),
3256 Err(Error::ObjectNotFound(_)) => Ok(None),
3257 Err(err) => Err(err),
3258 }
3259 }
3260
3261 fn empty_tree_stored(&self, oid: &ObjectId) -> bool {
3262 if self.overlay_read(oid).is_some() || self.exists_materialized_local(oid) {
3263 return true;
3264 }
3265 if !self.alternates_enabled {
3266 return false;
3267 }
3268 let file_dirs = self.file_alternate_dirs_snapshot();
3269 let env_dirs = self.env_alternate_dirs_snapshot();
3270 file_dirs
3271 .iter()
3272 .chain(env_dirs.iter())
3273 .any(|dir| self.with_pack_store_for(dir, || store::local_object_materialized(dir, oid)))
3274 }
3275
3276 #[cold]
3277 #[inline(never)]
3278 fn read_info_after_promisor_miss(&self, oid: &ObjectId) -> Result<ObjectInfo> {
3279 let mut lazy_retries = 0u32;
3280 loop {
3281 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
3282 return self.object_info_not_found_after_primary_revalidate(oid);
3283 };
3284 match res {
3285 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
3286 return Err(err);
3287 }
3288 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
3289 return self.object_info_not_found_after_primary_revalidate(oid);
3290 }
3291 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
3292 lazy_retries += 1;
3293 if let Some(info) = self.try_read_info_once(oid)? {
3294 return Ok(info);
3295 }
3296 }
3297 }
3298 }
3299 }
3300
3301 pub(crate) fn try_read_info_once(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
3304 if self.is_empty_tree(oid) {
3305 return Ok(Some(ObjectInfo {
3306 kind: ObjectKind::Tree,
3307 size: 0,
3308 }));
3309 }
3310
3311 if let Some(obj) = self.overlay_read(oid) {
3312 return Ok(Some(ObjectInfo {
3313 kind: obj.kind,
3314 size: u64::try_from(obj.data.len())
3315 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
3316 }));
3317 }
3318
3319 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
3320 return Ok(Some(info));
3321 }
3322
3323 let use_midx = self.multi_pack_index_reads_enabled();
3324 if use_midx {
3325 self.ensure_midx_prepared();
3326 }
3327
3328 let mut unreadable_local_loose = None;
3329 if !self.skip_primary_for_alternate_backed_reads() {
3330 let primary = self.primary_writable()?;
3331 let files_primary = self.primary().ok();
3332 match primary.read_info(oid) {
3333 Ok(Some(info)) => return Ok(Some(info)),
3334 Ok(None) => {}
3335 Err(err)
3336 if files_primary
3337 .as_ref()
3338 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
3339 {
3340 unreadable_local_loose = Some(err);
3341 }
3342 Err(err) => return Err(err),
3343 }
3344 }
3345
3346 if let Some(info) = self.try_read_info_extra_sources(oid)? {
3347 return Ok(Some(info));
3348 }
3349
3350 if self.alternates_enabled {
3351 match self.sources()?.read_info(oid) {
3352 Ok(Some(info)) => return Ok(Some(info)),
3353 Ok(None) => {}
3354 Err(err) => return Err(err),
3355 }
3356 }
3357
3358 if let Some(err) = unreadable_local_loose {
3359 return Err(err);
3360 }
3361
3362 Ok(None)
3363 }
3364
3365 pub(crate) fn read_unless_larger(
3381 &self,
3382 oid: &ObjectId,
3383 limit: u64,
3384 cache_small: bool,
3385 ) -> Result<BoundedRead> {
3386 if cache_small {
3387 if let Some(read) = self.session_loose_reads.lock().ok().and_then(|cache| {
3388 cache.get(oid).map(|obj| {
3389 if obj.data.len() as u64 > limit {
3390 BoundedRead::Larger
3391 } else {
3392 BoundedRead::Object(obj.clone())
3393 }
3394 })
3395 }) {
3396 return Ok(read);
3397 }
3398 }
3399 if self.custom_primary.is_none() && !self.multi_pack_index_reads_enabled() {
3400 if let Some(read) =
3401 pack::pack_cache::try_read_object_via_single_pack_reader_unless_whole_larger(
3402 &self.objects_dir,
3403 oid,
3404 limit,
3405 )?
3406 {
3407 return Ok(read);
3408 }
3409 }
3410 if let Ok(files) = self.primary() {
3411 if self.multi_pack_index_reads_enabled() {
3412 self.ensure_midx_prepared();
3413 }
3414 match files.read_unless_larger(oid, limit)? {
3415 LocalSizedRead::Packed(Some(size)) if size > limit => {
3416 return Ok(BoundedRead::Larger);
3417 }
3418 LocalSizedRead::Packed(_) => return self.read(oid).map(BoundedRead::Object),
3419 LocalSizedRead::Loose(BoundedRead::Object(obj)) => {
3420 if cache_small && obj.data.len() <= SESSION_BOUNDED_LOOSE_CACHE_MAX {
3421 self.remember_session_loose_read(oid, &obj);
3422 }
3423 return Ok(BoundedRead::Object(obj));
3424 }
3425 LocalSizedRead::Loose(read) => return Ok(read),
3426 LocalSizedRead::Missing => {}
3427 }
3428 }
3429 let info = self.read_info(oid)?;
3430 if info.size > limit {
3431 return Ok(BoundedRead::Larger);
3432 }
3433 self.read(oid).map(BoundedRead::Object)
3434 }
3435
3436 pub fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'static>>> {
3445 self.with_stream(oid, materialize_stream)
3446 }
3447
3448 pub fn with_stream<R>(
3465 &self,
3466 oid: &ObjectId,
3467 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
3468 ) -> Result<Option<R>> {
3469 use std::io::Cursor;
3470 if self.is_empty_tree(oid) {
3471 return f(ObjectStream {
3472 kind: ObjectKind::Tree,
3473 size: 0,
3474 reader: Box::new(std::io::empty()),
3475 })
3476 .map(Some);
3477 }
3478 if let Some(obj) = self.overlay_read(oid) {
3479 let size = u64::try_from(obj.data.len()).map_err(|_| {
3480 Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
3481 })?;
3482 return f(ObjectStream {
3483 kind: obj.kind,
3484 size,
3485 reader: Box::new(Cursor::new(obj.data)),
3486 })
3487 .map(Some);
3488 }
3489 if self.multi_pack_index_reads_enabled() {
3490 self.ensure_midx_prepared();
3491 }
3492 self.sync_delta_base_cache_limit();
3493 let primary = self.primary_writable()?;
3494 let files_primary = self.primary().ok();
3495 let mut unreadable_local_loose = None;
3496 match primary.open_stream(oid) {
3497 Ok(Some(stream)) => return f(stream).map(Some),
3498 Ok(None) => {}
3499 Err(err)
3500 if files_primary
3501 .as_ref()
3502 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
3503 {
3504 unreadable_local_loose = Some(err);
3505 }
3506 Err(err) => return Err(err),
3507 }
3508 if let Some(stream) = self.try_open_stream_extra_sources(oid)? {
3509 return f(stream).map(Some);
3510 }
3511 if self.alternates_enabled {
3512 let sources = self.sources()?;
3513 let stream = sources.open_stream(oid)?;
3514 if let Some(stream) = stream {
3515 return f(stream).map(Some);
3516 }
3517 }
3518 if let Some(err) = unreadable_local_loose {
3519 return Err(err);
3520 }
3521 Ok(None)
3522 }
3523
3524 pub fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
3530 let mut seen = oid_hash_set();
3531 let mut visit = |store: &dyn ObjectStore| -> Result<bool> {
3532 store::for_each_propagate_break(store, &mut |oid| {
3533 if !seen.insert(*oid) {
3534 return ControlFlow::Continue(());
3535 }
3536 f(oid)
3537 })
3538 };
3539 if let Some(overlay) = self.overlay_store() {
3540 if visit(overlay.as_ref())? {
3541 return Ok(());
3542 }
3543 }
3544 if visit(self.primary_writable()?.as_ref())? {
3545 return Ok(());
3546 }
3547 for src in &self.extra_read_sources {
3548 if visit(src.as_ref())? {
3549 return Ok(());
3550 }
3551 }
3552 if self.alternates_enabled {
3553 let _ = visit(self.sources()?.as_ref())?;
3554 }
3555 Ok(())
3556 }
3557
3558 pub fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
3564 let mut seen: OidHashSet = out.iter().copied().collect();
3565 if let Some(overlay) = self.overlay_store() {
3566 append_lookup_prefix_layer(overlay.as_ref(), prefix, limit, out, &mut seen)?;
3567 if limit != 0 && out.len() >= limit {
3568 return Ok(());
3569 }
3570 }
3571 append_lookup_prefix_layer(
3572 self.primary_writable()?.as_ref(),
3573 prefix,
3574 limit,
3575 out,
3576 &mut seen,
3577 )?;
3578 if limit != 0 && out.len() >= limit {
3579 return Ok(());
3580 }
3581 for src in &self.extra_read_sources {
3582 append_lookup_prefix_layer(src.as_ref(), prefix, limit, out, &mut seen)?;
3583 if limit != 0 && out.len() >= limit {
3584 return Ok(());
3585 }
3586 }
3587 if self.alternates_enabled {
3588 append_lookup_prefix_layer(self.sources()?.as_ref(), prefix, limit, out, &mut seen)?;
3589 }
3590 Ok(())
3591 }
3592
3593 pub fn enumerate_local_loose_objects(&self) -> Result<Vec<(ObjectId, PathBuf)>> {
3601 let primary = self.primary()?;
3602 let loose = primary.loose_store();
3603 let mut out = Vec::new();
3604 loose.for_each_object(&mut |oid| {
3605 out.push((*oid, loose.object_path(oid)));
3606 ControlFlow::Continue(())
3607 })?;
3608 Ok(out)
3609 }
3610
3611 pub(crate) fn enumerate_loose_objects_at(
3613 &self,
3614 objects_dir: &Path,
3615 ) -> Result<Vec<(ObjectId, PathBuf)>> {
3616 let loose = self.loose_at(objects_dir)?;
3617 let mut out = Vec::new();
3618 loose.for_each_object(&mut |oid| {
3619 out.push((*oid, loose.object_path(oid)));
3620 ControlFlow::Continue(())
3621 })?;
3622 Ok(out)
3623 }
3624
3625 pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3633 hash_object_data_with(self.hash_algo(), kind, data)
3634 }
3635
3636 pub fn hash_stream(
3647 &self,
3648 kind: ObjectKind,
3649 size: u64,
3650 reader: &mut dyn std::io::Read,
3651 ) -> Result<ObjectId> {
3652 #[cfg(test)]
3653 self.hot_path_metrics().record_stream_hash();
3654 stream::hash_stream(self.hash_algo(), kind, size, reader)
3655 }
3656
3657 pub fn write_stream(
3679 &self,
3680 kind: ObjectKind,
3681 size: u64,
3682 reader: &mut dyn std::io::Read,
3683 options: WriteOptions,
3684 ) -> Result<ObjectId> {
3685 #[cfg(test)]
3686 self.hot_path_metrics().record_stream_write();
3687 if let Some(primary) = &self.custom_primary {
3688 if !self.overlay_active() {
3689 let oid = primary.write_stream(kind, size, reader, options)?;
3690 self.note_commit_object_written(kind);
3691 return Ok(oid);
3692 }
3693 }
3694 if self.overlay_active() || size < stream::STREAMED_WRITE_MIN_SIZE {
3695 let len = usize::try_from(size).map_err(|_| {
3696 Error::CorruptObject(format!("object size {size} exceeds address space"))
3697 })?;
3698 let mut data = Vec::with_capacity(len);
3699 let read =
3700 std::io::Read::read_to_end(&mut std::io::Read::take(reader, size), &mut data)?;
3701 if read != len {
3702 return Err(Error::ObjectStreamTruncated {
3703 expected: size,
3704 actual: read as u64,
3705 });
3706 }
3707 return self.write_with_options(kind, &data, options);
3708 }
3709 if size < self.big_file_threshold()? {
3710 let loose = self.local_loose()?;
3711 let oid = loose.write_payload_stream(kind, size, reader, options)?;
3712 self.note_commit_object_written(kind);
3713 return Ok(oid);
3714 }
3715 let staged = stream::StagedPack::write(
3716 &self.objects_dir.join("pack"),
3717 self.hash_algo(),
3718 kind,
3719 size,
3720 reader,
3721 self.pack_compression()?,
3722 )?;
3723 let oid = staged.oid();
3724 if self.object_exists_for_write_skip(&oid) {
3725 if !options.silent {
3726 let _ = self.freshen_object(&oid);
3727 }
3728 return Ok(oid);
3729 }
3730 staged.install()?;
3731 self.invalidate_packs();
3732 self.note_commit_object_written(kind);
3733 Ok(oid)
3734 }
3735
3736 pub fn empty_tree_oid(&self) -> Result<ObjectId> {
3744 self.hash(ObjectKind::Tree, b"")
3745 }
3746
3747 #[must_use]
3749 pub fn is_empty_tree(&self, oid: &ObjectId) -> bool {
3750 empty_tree_oid_matches(self.hash_algo(), oid)
3751 }
3752
3753 pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3766 self.write_with_options(kind, data, WriteOptions::default())
3767 }
3768
3769 pub fn flush(&self) -> Result<()> {
3777 if let Some(primary) = &self.custom_primary {
3778 primary.flush()
3779 } else {
3780 Ok(())
3781 }
3782 }
3783
3784 pub fn flush_for_publish(&self) -> Result<()> {
3793 if let Some(primary) = &self.custom_primary {
3794 primary.flush_for_publish()
3795 } else {
3796 Ok(())
3797 }
3798 }
3799
3800 pub fn rollback(&self) -> Result<()> {
3808 if let Some(primary) = &self.custom_primary {
3809 primary.rollback()
3810 } else {
3811 Ok(())
3812 }
3813 }
3814
3815 pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
3824 if self.custom_primary.is_some() {
3825 return Ok(());
3826 }
3827 self.local_loose()?.ensure_all_loose_prefix_dirs()
3828 }
3829
3830 #[cfg(test)]
3832 #[must_use]
3833 pub fn test_loose_object_path(&self, oid: &ObjectId) -> PathBuf {
3834 self.object_path(oid)
3835 }
3836
3837 #[cfg(test)]
3839 pub fn test_remove_loose_object(&self, oid: &ObjectId) {
3840 let _ = fs::remove_file(self.object_path(oid));
3841 }
3842
3843 pub fn count_loose_objects(&self) -> Result<usize> {
3849 let mut count = 0usize;
3850 for_each_loose_object_id(&self.objects_dir, self.hash_algo(), &mut |_| {
3851 count += 1;
3852 ControlFlow::Continue(())
3853 })?;
3854 Ok(count)
3855 }
3856
3857 pub fn approximate_object_count(&self) -> Result<usize> {
3868 self.with_pack_read_context(|| {
3869 let indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
3870 Ok(indexes.iter().map(|idx| idx.len()).sum())
3871 })
3872 }
3873
3874 pub fn zlib_compress_loose_store(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
3880 self.local_loose()?.zlib_compress_store_bytes(store_bytes)
3881 }
3882
3883 pub fn write_loose_zlib_prehashed(
3897 &self,
3898 oid: &ObjectId,
3899 zlib_store: &[u8],
3900 options: WriteOptions,
3901 ) -> Result<ObjectId> {
3902 if self.custom_primary.is_some() {
3903 let raw = decompress_zlib_loose_bytes(zlib_store)?;
3904 let obj = parse_object_bytes(&raw)?;
3905 return self.write_with_options(obj.kind, &obj.data, options);
3906 }
3907 let primary = self.primary()?;
3908 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
3909 return Ok(existing);
3910 }
3911 if options.freshen_existing && self.freshen_packed_or_alternate(oid) {
3912 return Ok(*oid);
3913 }
3914
3915 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3916 && self.object_exists_for_write_skip(oid);
3917
3918 if self.overlay_active() && !already_exists {
3919 if let Ok(raw) = decompress_zlib_loose_bytes(zlib_store) {
3920 if let Ok(obj) = parse_object_bytes(&raw) {
3921 if self.overlay_store_object(*oid, obj.kind, &obj.data) {
3922 return Ok(*oid);
3923 }
3924 }
3925 }
3926 }
3927
3928 if already_exists {
3929 if !options.silent {
3930 let _ = self.freshen_object(oid);
3931 }
3932 return Ok(*oid);
3933 }
3934
3935 primary.write_zlib_prehashed(oid, zlib_store, options)
3936 }
3937
3938 pub(crate) fn write_blob_prehashed(
3944 &self,
3945 oid: &ObjectId,
3946 data: &[u8],
3947 options: WriteOptions,
3948 ) -> Result<ObjectId> {
3949 if self.custom_primary.is_some() {
3950 return self.write_with_options(ObjectKind::Blob, data, options);
3951 }
3952 let primary = self.primary()?;
3953 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
3954 return Ok(existing);
3955 }
3956 if options.freshen_existing && self.freshen_packed_or_alternate(oid) {
3957 return Ok(*oid);
3958 }
3959 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3960 && self.object_exists_for_write_skip(oid);
3961 if self.overlay_active()
3962 && !already_exists
3963 && self.overlay_store_object(*oid, ObjectKind::Blob, data)
3964 {
3965 return Ok(*oid);
3966 }
3967 if already_exists {
3968 if !options.silent {
3969 let _ = self.freshen_object(oid);
3970 }
3971 return Ok(*oid);
3972 }
3973 primary
3974 .loose_store()
3975 .write_prehashed(oid, ObjectKind::Blob, data, options)
3976 }
3977 pub fn write_with_options(
3987 &self,
3988 kind: ObjectKind,
3989 data: &[u8],
3990 options: WriteOptions,
3991 ) -> Result<ObjectId> {
3992 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3993
3994 if let Some(primary) = &self.custom_primary {
3995 if self.overlay_active() && self.overlay_store_object(oid, kind, data) {
3996 return Ok(oid);
3997 }
3998 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3999 self.note_commit_object_written(kind);
4000 return Ok(oid);
4001 }
4002
4003 let primary = self.primary()?;
4004 if let Some(existing) = primary.try_finish_duplicate_write(&oid, options)? {
4005 return Ok(existing);
4006 }
4007 if options.freshen_existing && self.freshen_packed_or_alternate(&oid) {
4008 return Ok(oid);
4009 }
4010
4011 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
4012 && self.object_exists_for_write_skip(&oid);
4013
4014 if self.overlay_active() && !already_exists && self.overlay_store_object(oid, kind, data) {
4017 return Ok(oid);
4018 }
4019
4020 if already_exists {
4021 if !options.silent {
4022 let _ = self.freshen_object(&oid);
4023 }
4024 return Ok(oid);
4025 }
4026
4027 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
4028 self.note_commit_object_written(kind);
4029 Ok(oid)
4030 }
4031
4032 pub fn write_local(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
4048 if self.custom_primary.is_some() {
4049 return Err(Error::UnsupportedObjectStore {
4050 operation: "write_local",
4051 });
4052 }
4053 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
4054
4055 if let Some(existing) = self.try_freshen_existing_local(&oid) {
4056 return Ok(existing);
4057 }
4058
4059 let oid = self
4060 .local_loose()?
4061 .write(kind, data, WriteOptions::default())?;
4062 self.note_commit_object_written(kind);
4063 Ok(oid)
4064 }
4065
4066 fn write_into_primary(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
4071 if let Some(primary) = &self.custom_primary {
4072 let oid =
4073 WritableObjectStore::write(primary.as_ref(), kind, data, WriteOptions::default())?;
4074 self.note_commit_object_written(kind);
4075 return Ok(oid);
4076 }
4077 self.write_local(kind, data)
4078 }
4079
4080 pub(crate) fn write_local_stream_with(
4082 &self,
4083 kind: ObjectKind,
4084 size: u64,
4085 supply: impl FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()>,
4086 ) -> Result<ObjectId> {
4087 if self.custom_primary.is_some() {
4088 return Err(Error::UnsupportedObjectStore {
4089 operation: "write_local_stream",
4090 });
4091 }
4092 let loose = self.local_loose()?;
4093 let oid = loose.write_payload_stream_with(
4094 kind,
4095 size,
4096 WriteOptions::default(),
4097 self.pack_compression()?,
4098 supply,
4099 )?;
4100 self.note_commit_object_written(kind);
4101 Ok(oid)
4102 }
4103
4104 pub fn write_loose_materialize(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
4111 if self.custom_primary.is_some() {
4112 return Err(Error::UnsupportedObjectStore {
4113 operation: "write_loose_materialize",
4114 });
4115 }
4116 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
4117 if let Ok(primary) = self.primary() {
4118 if let Ok(Some(existing)) = primary
4119 .loose_store()
4120 .try_finish_if_loose_present(&oid, WriteOptions::default())
4121 {
4122 return Ok(existing);
4123 }
4124 }
4125
4126 self.local_loose()?
4127 .write(kind, data, WriteOptions::default())
4128 }
4129
4130 pub fn write_raw(&self, store_bytes: &[u8]) -> Result<ObjectId> {
4140 let obj = parse_object_bytes(store_bytes)?;
4141 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
4142
4143 if let Some(primary) = &self.custom_primary {
4144 if self.exists(&oid) {
4145 return Ok(oid);
4146 }
4147 if self.overlay_active() && self.overlay_store_object(oid, obj.kind, &obj.data) {
4148 return Ok(oid);
4149 }
4150 return WritableObjectStore::write(
4151 primary.as_ref(),
4152 obj.kind,
4153 &obj.data,
4154 WriteOptions::default(),
4155 );
4156 }
4157
4158 if let Some(existing) = self.try_freshen_existing(&oid) {
4159 return Ok(existing);
4160 }
4161
4162 self.local_loose()?
4163 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
4164 }
4165
4166 pub fn write_raw_local(&self, store_bytes: &[u8]) -> Result<ObjectId> {
4174 if self.custom_primary.is_some() {
4175 return Err(Error::UnsupportedObjectStore {
4176 operation: "write_raw_local",
4177 });
4178 }
4179 let obj = parse_object_bytes(store_bytes)?;
4180 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
4181 if let Some(existing) = self.try_freshen_existing_local(&oid) {
4182 return Ok(existing);
4183 }
4184
4185 self.local_loose()?
4186 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
4187 }
4188
4189 fn touch_object_mtime(&self, path: &Path) -> Option<std::time::SystemTime> {
4191 #[cfg(test)]
4192 self.hot_path_test_metrics.record_freshen();
4193 crate::fs_readonly::set_file_times_now(path)
4194 }
4195}
4196
4197fn hash_object_data_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
4200 hash::hash_object(algo, kind, data)
4201}
4202
4203#[must_use]
4205pub(crate) fn empty_tree_oid_matches(algo: HashAlgo, oid: &ObjectId) -> bool {
4206 oid.is_empty_tree_for(algo)
4207}
4208
4209pub(crate) fn zlib_compress_blob_loose(
4212 data: &[u8],
4213 compression: flate2::Compression,
4214) -> Result<Vec<u8>> {
4215 use std::io::Write as _;
4216 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), compression);
4217 let zlib_err = |e: std::io::Error| Error::Zlib(e.to_string());
4218 write!(encoder, "{} {}\0", ObjectKind::Blob, data.len()).map_err(zlib_err)?;
4219 encoder.write_all(data).map_err(zlib_err)?;
4220 encoder.finish().map_err(zlib_err)
4221}
4222
4223fn append_lookup_prefix_layer(
4224 store: &dyn ObjectStore,
4225 prefix: &str,
4226 limit: usize,
4227 out: &mut Vec<ObjectId>,
4228 seen: &mut OidHashSet,
4229) -> Result<()> {
4230 let mut scratch = Vec::new();
4231 store.lookup_prefix(prefix, 0, &mut scratch)?;
4232 for oid in scratch {
4233 if seen.insert(oid) {
4234 out.push(oid);
4235 if limit != 0 && out.len() >= limit {
4236 return Ok(());
4237 }
4238 }
4239 }
4240 Ok(())
4241}
4242
4243fn materialize_stream(stream: ObjectStream<'_>) -> Result<ObjectStream<'static>> {
4244 use std::io::{Cursor, Read};
4245 let ObjectStream {
4246 kind,
4247 size,
4248 mut reader,
4249 } = stream;
4250 let mut payload = Vec::new();
4251 reader.read_to_end(&mut payload).map_err(Error::Io)?;
4252 Ok(ObjectStream {
4253 kind,
4254 size,
4255 reader: Box::new(Cursor::new(payload)),
4256 })
4257}
4258
4259fn parse_alternate_env(val: &str) -> Vec<PathBuf> {
4262 let mut result = Vec::new();
4263 let mut chars = val.chars().peekable();
4264 while chars.peek().is_some() {
4265 if chars.peek() == Some(&':') {
4266 chars.next();
4267 continue;
4268 }
4269 if chars.peek() == Some(&'"') {
4270 chars.next(); let saved: Vec<char> = chars.clone().collect();
4274 let mut path = String::new();
4275 let mut properly_closed = false;
4276 loop {
4277 match chars.next() {
4278 None => break,
4279 Some('"') => {
4280 properly_closed = true;
4281 break;
4282 }
4283 Some('\\') => match chars.peek() {
4284 Some(c) if c.is_ascii_digit() => {
4285 let mut oct = String::new();
4286 for _ in 0..3 {
4287 if let Some(&c) = chars.peek() {
4288 if c.is_ascii_digit() {
4289 oct.push(c);
4290 chars.next();
4291 } else {
4292 break;
4293 }
4294 } else {
4295 break;
4296 }
4297 }
4298 if let Ok(byte) = u8::from_str_radix(&oct, 8) {
4299 path.push(byte as char);
4300 }
4301 }
4302 Some(_) => {
4303 if let Some(c) = chars.next() {
4304 match c {
4305 'n' => path.push('\n'),
4306 't' => path.push('\t'),
4307 'r' => path.push('\r'),
4308 _ => path.push(c),
4309 }
4310 }
4311 }
4312 None => {}
4313 },
4314 Some(c) => path.push(c),
4315 }
4316 }
4317 if !properly_closed {
4318 let raw: String = std::iter::once('"').chain(saved).collect();
4321 let raw_path = raw.split(':').next().unwrap_or(&raw);
4323 if !raw_path.is_empty() {
4324 result.push(PathBuf::from(raw_path));
4325 }
4326 let remainder = &raw[raw_path.len()..];
4330 if let Some(rest) = remainder.strip_prefix(':') {
4331 result.extend(parse_alternate_env(rest));
4333 }
4334 return result;
4335 } else if !path.is_empty() {
4336 result.push(PathBuf::from(path));
4337 }
4338 } else {
4339 let mut path = String::new();
4340 while let Some(&c) = chars.peek() {
4341 if c == ':' {
4342 break;
4343 }
4344 path.push(c);
4345 chars.next();
4346 }
4347 if !path.is_empty() {
4348 result.push(PathBuf::from(path));
4349 }
4350 }
4351 }
4352 result
4353}
4354
4355fn system_time_to_file_time(t: std::time::SystemTime) -> FileTime {
4356 let dur = t
4357 .duration_since(std::time::SystemTime::UNIX_EPOCH)
4358 .unwrap_or_default();
4359 FileTime::from_unix_time(
4360 i64::try_from(dur.as_secs()).unwrap_or(i64::MAX),
4361 dur.subsec_nanos(),
4362 )
4363}
4364
4365fn superseded_pack_indexes(
4366 pack_dir: &Path,
4367 keep_pack: &Path,
4368 retained_packs: &HashSet<PathBuf>,
4369 inventory: &RepackPackInventorySnapshot,
4370) -> Result<Vec<(PathBuf, std::time::SystemTime)>> {
4371 let keep_stem = keep_pack
4372 .file_stem()
4373 .and_then(|s| s.to_str())
4374 .ok_or_else(|| {
4375 Error::Io(std::io::Error::new(
4376 std::io::ErrorKind::InvalidInput,
4377 "bad pack path",
4378 ))
4379 })?;
4380 if !pack_dir.is_dir() {
4381 return Ok(Vec::new());
4382 }
4383 let mut out = Vec::new();
4384 for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
4385 let entry = entry.map_err(Error::Io)?;
4386 let name = entry.file_name();
4387 let name_str = name.to_string_lossy();
4388 if name_str == keep_stem || name_str.starts_with(&format!("{keep_stem}.")) {
4389 continue;
4390 }
4391 if !name_str.ends_with(".idx") {
4392 continue;
4393 }
4394 let idx_path = entry.path();
4395 let pack_path = idx_path.with_extension("pack");
4396 if !pack_path.is_file() {
4397 continue;
4398 }
4399 if inventory.kept_at_inventory.contains(&pack_path)
4400 || pack_path.with_extension("promisor").is_file()
4401 || retained_packs.contains(&pack_path)
4402 || !inventory.pack_paths.contains(&pack_path)
4403 {
4404 continue;
4405 }
4406 let pack_mtime = fs::metadata(&pack_path)
4407 .and_then(|m| m.modified())
4408 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
4409 out.push((idx_path, pack_mtime));
4410 }
4411 Ok(out)
4412}
4413
4414fn oids_in_inventory_kept_packs(
4415 pack_dir: &Path,
4416 keep_pack: &Path,
4417 inventory_kept_packs: &HashSet<PathBuf>,
4418) -> Result<OidHashSet> {
4419 let keep_stem = keep_pack.file_stem().and_then(|s| s.to_str()).unwrap_or("");
4420 let mut oids = oid_hash_set();
4421 for pack_path in inventory_kept_packs {
4422 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
4423 continue;
4424 };
4425 if stem == keep_stem {
4426 continue;
4427 }
4428 if pack_path.parent().is_some_and(|p| p != pack_dir) {
4429 continue;
4430 }
4431 let idx_path = pack_path.with_extension("idx");
4432 if idx_path.is_file() {
4433 oids.extend(PackedObjects::object_ids_from_index_path(&idx_path)?);
4434 }
4435 }
4436 Ok(oids)
4437}
4438
4439#[cfg(test)]
4440mod tests {
4441 #![allow(clippy::expect_used, clippy::unwrap_used)]
4442
4443 use super::*;
4444 use tempfile::TempDir;
4445
4446 #[test]
4447 fn round_trip_blob() {
4448 let dir = TempDir::new().unwrap();
4449 let odb = Odb::new(dir.path());
4450 let data = b"hello world";
4451 let oid = odb.write(ObjectKind::Blob, data).unwrap();
4452 let obj = odb.read(&oid).unwrap();
4453 assert_eq!(obj.kind, ObjectKind::Blob);
4454 assert_eq!(obj.data, data);
4455 }
4456
4457 #[test]
4458 fn known_blob_hash() {
4459 let oid = HashAlgo::Sha1
4462 .hash_object(ObjectKind::Blob, b"hello")
4463 .expect("hash");
4464 assert_eq!(oid.to_hex(), "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0");
4465 }
4466
4467 fn pack_objects(objects: &Path, oids: &[ObjectId]) {
4469 let odb = Odb::new(objects);
4470 let pack = crate::pack_objects::build_pack_from_send_list(
4471 &odb,
4472 oids,
4473 &HashSet::new(),
4474 &crate::pack_objects::PackBuildOptions::default(),
4475 &HashMap::new(),
4476 )
4477 .unwrap();
4478 for oid in oids {
4479 fs::remove_file(oid.loose_path_in(objects)).unwrap();
4480 }
4481 crate::index_pack::install_pack_bytes(
4482 pack,
4483 &odb,
4484 &crate::index_pack::IngestPackOptions::default(),
4485 )
4486 .unwrap();
4487 }
4488
4489 #[test]
4490 fn general_reads_cache_trees_and_tiny_blobs_but_not_larger_blobs() {
4491 let dir = TempDir::new().unwrap();
4492 let odb = Odb::new(dir.path());
4493 let blob = odb
4494 .write(
4495 ObjectKind::Blob,
4496 &vec![b'x'; SESSION_READ_BLOB_CACHE_MAX + 1],
4497 )
4498 .unwrap();
4499 let tiny = odb.write(ObjectKind::Blob, b"tiny\n").unwrap();
4500 let tree = odb
4501 .write(
4502 ObjectKind::Tree,
4503 &crate::objects::serialize_tree(&[crate::objects::TreeEntry {
4504 mode: 0o100644,
4505 name: b"f".to_vec(),
4506 oid: blob,
4507 }]),
4508 )
4509 .unwrap();
4510 assert_eq!(
4511 odb.read(&blob).unwrap().data.len(),
4512 SESSION_READ_BLOB_CACHE_MAX + 1
4513 );
4514 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
4515 let tree_bytes = odb.read(&tree).unwrap().data;
4516 assert_eq!(odb.read(&tiny).unwrap().data, b"tiny\n");
4517 for oid in [blob, tree, tiny] {
4518 let hex = oid.to_hex();
4519 fs::remove_file(dir.path().join(&hex[..2]).join(&hex[2..])).unwrap();
4520 }
4521 assert_eq!(odb.read(&tree).unwrap().data, tree_bytes, "tree reused");
4522 assert_eq!(odb.read(&tiny).unwrap().data, b"tiny\n", "tiny blob reused");
4523 assert!(odb.read(&blob).is_err(), "blob was not retained");
4524 }
4525
4526 #[test]
4527 fn session_read_cache_is_bounded_by_bytes_and_entries() {
4528 let mut cache = SessionLooseReads::default();
4529 let chunk = SESSION_LOOSE_CACHE_MAX_BYTES / 4;
4530 let object = |len: usize| Object {
4531 kind: ObjectKind::Tree,
4532 data: vec![0; len],
4533 };
4534 let oid = |n: u32| {
4535 let mut raw = [0u8; 20];
4536 raw[..4].copy_from_slice(&n.to_be_bytes());
4537 ObjectId::from_bytes(&raw).unwrap()
4538 };
4539 for n in 0..4 {
4540 cache.insert(oid(n), &object(chunk));
4541 }
4542 assert_eq!(cache.bytes, SESSION_LOOSE_CACHE_MAX_BYTES);
4543 cache.insert(oid(4), &object(1));
4544 assert_eq!(cache.bytes, 1, "exceeding the byte budget starts over");
4545 assert!(cache.get(&oid(0)).is_none());
4546 cache.insert(oid(4), &object(10));
4547 assert_eq!(cache.bytes, 10, "replacing an entry drops its old size");
4548 cache.insert(oid(5), &object(SESSION_LOOSE_CACHE_MAX_BYTES + 1));
4549 assert!(
4550 cache.get(&oid(5)).is_none(),
4551 "oversized objects are never kept"
4552 );
4553 assert!(cache.get(&oid(4)).is_some());
4554 for n in 10..(10 + SESSION_LOOSE_CACHE_MAX_ENTRIES as u32 + 2) {
4555 cache.insert(oid(n), &object(1));
4556 }
4557 assert!(cache.objects.len() <= SESSION_LOOSE_CACHE_MAX_ENTRIES + 1);
4558 assert!(cache.bytes <= SESSION_LOOSE_CACHE_MAX_BYTES);
4559 }
4560
4561 fn bounded_read_fixture() -> (TempDir, PathBuf, ObjectId, ObjectId) {
4562 let dir = TempDir::new().unwrap();
4563 let objects = dir.path().to_path_buf();
4564 let odb = Odb::new(&objects);
4565 let small = odb.write(ObjectKind::Blob, b"small blob\n").unwrap();
4566 let big: Vec<u8> = (0..300_000u32).flat_map(|i| i.to_le_bytes()).collect();
4567 let big = odb.write(ObjectKind::Blob, &big).unwrap();
4568 (dir, objects, small, big)
4569 }
4570
4571 fn assert_bounded_reads(odb: &Odb, small: &ObjectId, big: &ObjectId) {
4572 match odb.read_unless_larger(small, 1000, false).unwrap() {
4573 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
4574 BoundedRead::Larger => panic!("small blob is within the limit"),
4575 }
4576 assert!(matches!(
4577 odb.read_unless_larger(big, 1000, false).unwrap(),
4578 BoundedRead::Larger
4579 ));
4580 match odb.read_unless_larger(big, 1_200_000, false).unwrap() {
4581 BoundedRead::Object(obj) => assert_eq!(obj.data.len(), 1_200_000),
4582 BoundedRead::Larger => panic!("limit equal to the size reads the object"),
4583 }
4584 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
4585 assert!(matches!(
4586 odb.read_unless_larger(&missing, 1000, false),
4587 Err(Error::ObjectNotFound(_))
4588 ));
4589 }
4590
4591 #[test]
4592 fn read_unless_larger_sizes_loose_objects_from_their_header() {
4593 let (_dir, objects, small, big) = bounded_read_fixture();
4594 assert_bounded_reads(&Odb::new(&objects), &small, &big);
4595 }
4596
4597 #[test]
4598 fn read_unless_larger_sizes_packed_objects_in_and_out_of_read_context() {
4599 let (_dir, objects, small, big) = bounded_read_fixture();
4600 pack_objects(&objects, &[small, big]);
4601 let odb = Odb::new(&objects);
4602 assert_bounded_reads(&odb, &small, &big);
4603 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
4604 }
4605
4606 #[test]
4607 fn parsed_tree_reads_keep_no_raw_copy_in_the_session_cache() {
4608 let (_dir, objects, small, _big) = bounded_read_fixture();
4609 let odb = Odb::new(&objects);
4610 let entries = [crate::objects::TreeEntry {
4611 mode: 0o100644,
4612 name: b"small".to_vec(),
4613 oid: small,
4614 }];
4615 let tree = odb
4616 .write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
4617 .unwrap();
4618 for packed in [false, true] {
4619 if packed {
4620 pack_objects(&objects, &[tree]);
4621 }
4622 let odb = Odb::new(&objects);
4623 assert_eq!(*odb.cached_tree_entries(&tree).unwrap(), entries);
4624 assert!(odb.session_cached_loose_read(&tree).is_none());
4625 assert!(Arc::ptr_eq(
4626 &odb.cached_tree_entries(&tree).unwrap(),
4627 &odb.cached_tree_entries(&tree).unwrap()
4628 ));
4629 assert_eq!(odb.read(&tree).unwrap().kind, ObjectKind::Tree);
4630 assert!(
4631 odb.session_cached_loose_read(&tree).is_some(),
4632 "plain reads still use the session cache (packed: {packed})"
4633 );
4634 }
4635 }
4636
4637 #[test]
4638 fn read_unless_larger_reuses_small_loose_reads_but_never_caches_big_ones() {
4639 let (_dir, objects, small, big) = bounded_read_fixture();
4640 let odb = Odb::new(&objects);
4641 assert_bounded_reads(&odb, &small, &big);
4642 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
4643 assert!(matches!(
4644 odb.read_unless_larger(&small, 1000, true).unwrap(),
4645 BoundedRead::Object(_)
4646 ));
4647 assert!(matches!(
4648 odb.read_unless_larger(&big, 1_200_000, true).unwrap(),
4649 BoundedRead::Object(_)
4650 ));
4651 for oid in [small, big] {
4652 let hex = oid.to_hex();
4653 fs::remove_file(objects.join(&hex[..2]).join(&hex[2..])).unwrap();
4654 }
4655 match odb.read_unless_larger(&small, 1000, true).unwrap() {
4656 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
4657 BoundedRead::Larger => panic!("small blob is within the limit"),
4658 }
4659 assert!(matches!(
4660 odb.read_unless_larger(&small, 4, true),
4661 Ok(BoundedRead::Larger)
4662 ));
4663 assert!(matches!(
4664 odb.read_unless_larger(&big, 1_200_000, true),
4665 Err(Error::ObjectNotFound(_))
4666 ));
4667 }
4668
4669 #[test]
4670 fn read_unless_larger_finds_objects_across_several_packs() {
4671 let (_dir, objects, small, big) = bounded_read_fixture();
4672 pack_objects(&objects, &[small]);
4673 pack_objects(&objects, &[big]);
4674 let odb = Odb::new(&objects);
4675 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
4676 }
4677
4678 #[test]
4679 fn parse_object_header_prefix_unknown_kind_is_typed_error() {
4680 let err = crate::odb::store::loose::parse_object_header_prefix(b"not-a-git-kind 0\0")
4681 .unwrap_err();
4682 assert!(matches!(err, Error::UnknownObjectType(_)));
4683 }
4684
4685 fn git_dir_with_config(config: &str) -> (TempDir, PathBuf, PathBuf) {
4686 let dir = TempDir::new().unwrap();
4687 let git_dir = dir.path().join(".git");
4688 let objects = git_dir.join("objects");
4689 fs::create_dir_all(&objects).unwrap();
4690 fs::write(git_dir.join("config"), config).unwrap();
4691 (dir, git_dir, objects)
4692 }
4693
4694 fn zlib_flevel(compressed: &[u8]) -> u8 {
4696 assert!(compressed.len() >= 2);
4697 (compressed[1] >> 6) & 3
4698 }
4699
4700 fn tmp_loose_object_paths(objects: &Path) -> Vec<PathBuf> {
4701 let mut tmp = Vec::new();
4702 let Ok(shards) = fs::read_dir(objects) else {
4703 return tmp;
4704 };
4705 for shard in shards.flatten() {
4706 if !shard.file_type().is_ok_and(|t| t.is_dir()) {
4707 continue;
4708 }
4709 let Ok(files) = fs::read_dir(shard.path()) else {
4710 continue;
4711 };
4712 for file in files.flatten() {
4713 let name = file.file_name();
4714 if name.to_string_lossy().starts_with("tmp_") {
4715 tmp.push(file.path());
4716 }
4717 }
4718 }
4719 tmp
4720 }
4721
4722 #[test]
4723 fn loose_write_honours_level() {
4724 let payload: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
4725
4726 let (_dir, git_dir, objects) = git_dir_with_config("");
4727 let odb = Odb::new(&objects).with_config_git_dir(git_dir.clone());
4728 let oid_default = odb.write(ObjectKind::Blob, &payload).unwrap();
4729 let bytes_default = fs::read(odb.object_path(&oid_default)).unwrap();
4730 assert!(zlib_flevel(&bytes_default) <= 1);
4731 assert_eq!(odb.read(&oid_default).unwrap().data, payload);
4732
4733 for level in -1i32..=9 {
4734 let config = format!("[core]\n\tloosecompression = {level}\n");
4735 let (_dir, git_dir, objects) = git_dir_with_config(&config);
4736 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4737 let byte = u8::try_from(level.rem_euclid(256)).unwrap_or(0);
4738 let bytes: Vec<u8> = vec![byte; 512];
4739 let oid = odb.write(ObjectKind::Blob, &bytes).unwrap();
4740 let on_disk = fs::read(odb.object_path(&oid)).unwrap();
4741 assert!(on_disk.len() >= 2, "level {level}");
4742 assert_eq!(odb.read(&oid).unwrap().data, bytes.as_slice());
4743 }
4744
4745 let config = "[core]\n\tcompression = 9\n";
4746 let (_dir, git_dir, objects) = git_dir_with_config(config);
4747 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4748 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
4749 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 3);
4750
4751 let config = "[core]\n\tcompression = 0\n\tloosecompression = 1\n";
4752 let (_dir, git_dir, objects) = git_dir_with_config(config);
4753 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4754 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
4755 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 0);
4756
4757 let config = "[core]\n\tloosecompression = 99\n";
4758 let (_dir, git_dir, objects) = git_dir_with_config(config);
4759 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4760 assert!(odb.write(ObjectKind::Blob, b"x").is_err());
4761 assert!(tmp_loose_object_paths(&objects).is_empty());
4762 }
4763
4764 fn git_run(dir: &Path, args: &[&str]) -> std::process::Output {
4765 std::process::Command::new("git") .current_dir(dir)
4767 .args(args)
4768 .env("GIT_CONFIG_GLOBAL", "/dev/null")
4769 .env("GIT_CONFIG_SYSTEM", "/dev/null")
4770 .env("GIT_AUTHOR_NAME", "T")
4771 .env("GIT_AUTHOR_EMAIL", "t@example.com")
4772 .env("GIT_COMMITTER_NAME", "T")
4773 .env("GIT_COMMITTER_EMAIL", "t@example.com")
4774 .output()
4775 .unwrap()
4776 }
4777
4778 fn git_in(dir: &Path, args: &[&str]) {
4779 let out = git_run(dir, args);
4780 assert!(
4781 out.status.success(),
4782 "git {:?}: {}",
4783 args,
4784 String::from_utf8_lossy(&out.stderr)
4785 );
4786 }
4787
4788 fn git_rev_parse_oid(dir: &Path, spec: &str) -> ObjectId {
4789 let out = git_run(dir, &["rev-parse", spec]);
4790 assert!(
4791 out.status.success(),
4792 "git rev-parse {spec}: {}",
4793 String::from_utf8_lossy(&out.stderr)
4794 );
4795 ObjectId::from_hex(std::str::from_utf8(&out.stdout).unwrap().trim()).unwrap()
4796 }
4797
4798 #[test]
4799 fn write_loose_zlib_prehashed_matches_write() {
4800 let dir = TempDir::new().unwrap();
4801 let objects = dir.path().join("objects");
4802 let odb = Odb::new(&objects);
4803 let data = b"parallel prehash path";
4804 let oid_normal = odb.write(ObjectKind::Blob, data).unwrap();
4805
4806 let dir2 = TempDir::new().unwrap();
4807 let objects2 = dir2.path().join("objects");
4808 let odb2 = Odb::new(&objects2);
4809 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, data).expect("hash");
4810 assert_eq!(oid, oid_normal);
4811 let zlib =
4812 super::zlib_compress_blob_loose(data, odb2.loose_compression().unwrap()).unwrap();
4813 let oid2 = odb2
4814 .write_loose_zlib_prehashed(
4815 &oid,
4816 &zlib,
4817 WriteOptions {
4818 assume_loose_only_existence: true,
4819 ..WriteOptions::default()
4820 },
4821 )
4822 .unwrap();
4823 assert_eq!(oid2, oid_normal);
4824 assert_eq!(
4825 fs::read(odb2.object_path(&oid2)).unwrap(),
4826 fs::read(odb.object_path(&oid_normal)).unwrap()
4827 );
4828 }
4829
4830 #[test]
4831 fn write_blob_prehashed_matches_write_and_skips_existing_objects() {
4832 let dir = TempDir::new().unwrap();
4833 let odb = Odb::new(&dir.path().join("objects"));
4834 let data = vec![7u8; 100_000];
4835 let oid_normal = odb.write(ObjectKind::Blob, &data).unwrap();
4836
4837 let dir2 = TempDir::new().unwrap();
4838 let odb2 = Odb::new(&dir2.path().join("objects"));
4839 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, &data).unwrap();
4840 let options = WriteOptions {
4841 assume_loose_only_existence: true,
4842 ..WriteOptions::default()
4843 };
4844 assert_eq!(
4845 odb2.write_blob_prehashed(&oid, &data, options).unwrap(),
4846 oid
4847 );
4848 assert_eq!(oid, oid_normal);
4849 assert!(odb2.object_path(&oid).is_file());
4850 let obj = odb2.read(&oid).unwrap();
4851 assert_eq!(
4852 (obj.kind, obj.data.as_slice()),
4853 (ObjectKind::Blob, data.as_slice())
4854 );
4855
4856 assert_eq!(
4858 odb2.write_blob_prehashed(&oid, b"other", options).unwrap(),
4859 oid
4860 );
4861 assert_eq!(odb2.read(&oid).unwrap().data, data);
4862 }
4863
4864 #[test]
4865 fn write_loose_zlib_prehashed_routes_to_custom_primary() {
4866 use crate::objects::HashAlgo;
4867 use std::sync::Arc;
4868
4869 let dir = TempDir::new().unwrap();
4870 let objects = dir.path().join("objects");
4871 fs::create_dir_all(&objects).unwrap();
4872 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
4873 let odb = OdbBuilder::files(&objects)
4874 .primary(store.clone())
4875 .alternates(false)
4876 .build();
4877 let data = b"staged through a custom primary";
4878 let oid = hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, data).unwrap();
4879 let zlib = super::zlib_compress_blob_loose(data, Compression::fast()).unwrap();
4880
4881 odb.ensure_all_loose_prefix_dirs().unwrap();
4882 let written = odb
4883 .write_loose_zlib_prehashed(&oid, &zlib, WriteOptions::default())
4884 .unwrap();
4885 assert_eq!(written, oid);
4886 assert!(store.contains(&oid).unwrap());
4887 assert_eq!(odb.read(&oid).unwrap().data, data);
4888 assert_eq!(
4889 fs::read_dir(&objects).unwrap().count(),
4890 0,
4891 "no loose shard directories or files beside a custom primary"
4892 );
4893
4894 assert!(odb
4895 .write_loose_zlib_prehashed(&oid, b"not zlib", WriteOptions::default())
4896 .is_err());
4897 }
4898
4899 #[test]
4900 fn custom_primary_write_skips_odb_contains_probes() {
4901 use crate::objects::HashAlgo;
4902 use std::ops::ControlFlow;
4903 use std::sync::atomic::{AtomicUsize, Ordering};
4904 use std::sync::Arc;
4905
4906 #[derive(Debug)]
4907 struct Counting(MemoryStore, AtomicUsize, AtomicUsize);
4908
4909 impl ObjectStore for Counting {
4910 fn hash_algo(&self) -> HashAlgo {
4911 self.0.hash_algo()
4912 }
4913 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
4914 self.0.read(oid)
4915 }
4916 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
4917 self.0.read_info(oid)
4918 }
4919 fn contains(&self, oid: &ObjectId) -> Result<bool> {
4920 self.1.fetch_add(1, Ordering::Relaxed);
4921 self.0.contains(oid)
4922 }
4923 fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
4924 self.2.fetch_add(1, Ordering::Relaxed);
4925 self.0.contains_many(oids)
4926 }
4927 fn for_each_object(
4928 &self,
4929 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
4930 ) -> Result<()> {
4931 self.0.for_each_object(f)
4932 }
4933 }
4934
4935 impl WritableObjectStore for Counting {
4936 fn write(
4937 &self,
4938 kind: ObjectKind,
4939 data: &[u8],
4940 options: WriteOptions,
4941 ) -> Result<ObjectId> {
4942 self.0.write(kind, data, options)
4943 }
4944 fn freshen(&self, oid: &ObjectId) -> Result<bool> {
4945 self.0.freshen(oid)
4946 }
4947 }
4948
4949 let dir = TempDir::new().unwrap();
4950 let objects = dir.path().join("objects");
4951 fs::create_dir_all(&objects).unwrap();
4952 let primary = Arc::new(Counting(
4953 MemoryStore::new(HashAlgo::Sha1),
4954 AtomicUsize::new(0),
4955 AtomicUsize::new(0),
4956 ));
4957 let read_source = Arc::new(Counting(
4958 MemoryStore::new(HashAlgo::Sha1),
4959 AtomicUsize::new(0),
4960 AtomicUsize::new(0),
4961 ));
4962 read_source
4963 .0
4964 .write(ObjectKind::Blob, b"only upstream", WriteOptions::default())
4965 .unwrap();
4966 let odb = OdbBuilder::files(&objects)
4967 .primary(primary.clone())
4968 .push_read_source(read_source.clone())
4969 .alternates(false)
4970 .build();
4971
4972 odb.write(ObjectKind::Blob, b"new local").unwrap();
4973 assert_eq!(primary.1.load(Ordering::Relaxed), 0);
4974 assert_eq!(read_source.1.load(Ordering::Relaxed), 0);
4975
4976 odb.write(ObjectKind::Blob, b"only upstream").unwrap();
4977 assert_eq!(primary.1.load(Ordering::Relaxed), 0);
4978 assert!(primary
4979 .0
4980 .contains(
4981 &hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, b"only upstream").unwrap()
4982 )
4983 .unwrap());
4984 }
4985
4986 #[test]
4989 fn write_existing_loose_does_not_rewrite() {
4990 let dir = TempDir::new().unwrap();
4991 let objects = dir.path().join("objects");
4992 fs::create_dir_all(&objects).unwrap();
4993 let odb = Odb::new(&objects);
4994 let payload = b"already loose";
4995 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4996 let path = odb.object_path(&oid);
4997 let before_bytes = fs::read(&path).unwrap();
4998 let meta_before = fs::metadata(&path).unwrap();
4999 let before_mtime = filetime::FileTime::from_last_modification_time(&meta_before);
5000 #[cfg(unix)]
5001 let ino_before = {
5002 use std::os::unix::fs::MetadataExt;
5003 meta_before.ino()
5004 };
5005 std::thread::sleep(std::time::Duration::from_millis(20));
5006 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
5007 assert_eq!(oid, oid2);
5008 assert_eq!(fs::read(&path).unwrap(), before_bytes);
5009 let meta_after = fs::metadata(&path).unwrap();
5010 let after_mtime = filetime::FileTime::from_last_modification_time(&meta_after);
5011 assert!(
5012 after_mtime >= before_mtime,
5013 "expected freshen to bump or preserve mtime"
5014 );
5015 #[cfg(unix)]
5016 {
5017 use std::os::unix::fs::MetadataExt;
5018 assert_eq!(
5019 meta_after.ino(),
5020 ino_before,
5021 "rewrite must not replace the loose object file"
5022 );
5023 }
5024 }
5025
5026 #[cfg_attr(
5028 not(feature = "full-tests"),
5029 ignore = "starts system git or is heavy; run with --features full-tests"
5030 )]
5031 #[test]
5032 fn write_skips_loose_when_object_in_pack_only() {
5033 let dir = TempDir::new().unwrap();
5034 git_in(dir.path(), &["init", "-q"]);
5035 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5036 git_in(dir.path(), &["config", "user.name", "T"]);
5037 fs::write(dir.path().join("f"), b"pack-only payload").unwrap();
5038 git_in(dir.path(), &["add", "f"]);
5039 git_in(dir.path(), &["commit", "-m", "c"]);
5040 git_in(dir.path(), &["repack", "-ad"]);
5041 let objects = dir.path().join(".git").join("objects");
5042 let odb = Odb::new(&objects);
5043 let oid = odb.write(ObjectKind::Blob, b"pack-only payload").unwrap();
5044 assert!(!odb.object_path(&oid).exists());
5045 assert!(odb.exists(&oid));
5046 }
5047
5048 #[cfg_attr(
5050 not(feature = "full-tests"),
5051 ignore = "starts system git or is heavy; run with --features full-tests"
5052 )]
5053 #[test]
5054 fn write_freshens_loose_object_in_alternate() {
5055 use filetime::FileTime;
5056 use std::thread;
5057 use std::time::Duration;
5058
5059 let alt = TempDir::new().unwrap();
5060 git_in(alt.path(), &["init", "-q", "--bare"]);
5061 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
5062 let alt_objects = alt.path().join("objects");
5063 let alt_odb = Odb::new(&alt_objects);
5064 let payload = b"alternate-loose";
5065 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
5066 let alt_loose = alt_odb.object_path(&oid);
5067
5068 let primary = TempDir::new().unwrap();
5069 let primary_objects = primary.path().join("objects");
5070 fs::create_dir_all(primary_objects.join("info")).unwrap();
5071 fs::write(
5072 primary_objects.join("info").join("alternates"),
5073 format!("{}\n", alt_objects.display()),
5074 )
5075 .unwrap();
5076 let odb = Odb::new(&primary_objects);
5077 thread::sleep(Duration::from_millis(50));
5078 let before = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
5079 filetime::set_file_mtime(&alt_loose, before).unwrap();
5080
5081 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
5082 assert_eq!(oid, oid2);
5083 assert!(!odb.object_path(&oid).exists());
5084 let after = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
5085 assert!(after > before, "write must freshen alternate loose object");
5086 }
5087
5088 #[cfg_attr(
5089 not(feature = "full-tests"),
5090 ignore = "starts system git or is heavy; run with --features full-tests"
5091 )]
5092 #[test]
5093 fn write_freshens_packed_object_in_alternate() {
5094 use filetime::FileTime;
5095 use std::thread;
5096 use std::time::Duration;
5097
5098 let alt = TempDir::new().unwrap();
5099 git_in(alt.path(), &["init", "-q"]);
5100 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
5101 git_in(alt.path(), &["config", "user.name", "T"]);
5102 let payload = b"alternate-pack";
5103 fs::write(alt.path().join("f"), payload).unwrap();
5104 git_in(alt.path(), &["add", "f"]);
5105 git_in(alt.path(), &["commit", "-m", "c"]);
5106 git_in(alt.path(), &["repack", "-ad"]);
5107 let alt_objects = alt.path().join(".git").join("objects");
5108 let alt_odb = Odb::new(&alt_objects);
5109 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
5110 let pack_path = fs::read_dir(alt_objects.join("pack"))
5111 .unwrap()
5112 .flatten()
5113 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
5114 .unwrap()
5115 .path();
5116 thread::sleep(Duration::from_millis(50));
5117 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5118
5119 let primary = TempDir::new().unwrap();
5120 let primary_objects = primary.path().join("objects");
5121 fs::create_dir_all(primary_objects.join("info")).unwrap();
5122 fs::write(
5123 primary_objects.join("info").join("alternates"),
5124 format!("{}\n", alt_objects.display()),
5125 )
5126 .unwrap();
5127 pack::clear_pack_cache();
5128 let odb = Odb::new(&primary_objects);
5129 assert!(odb.exists(&oid));
5130 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
5131 assert_eq!(oid, oid2);
5132 assert!(!odb.object_path(&oid).exists());
5133 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5134 assert!(after >= before, "write must freshen alternate pack");
5135 }
5136
5137 #[cfg_attr(
5138 not(feature = "full-tests"),
5139 ignore = "starts system git or is heavy; run with --features full-tests"
5140 )]
5141 #[test]
5142 fn write_skips_loose_when_object_only_in_alternate() {
5143 let alt = TempDir::new().unwrap();
5144 git_in(alt.path(), &["init", "-q", "--bare"]);
5145 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
5146 let alt_objects = alt.path().join("objects");
5147 let alt_odb = Odb::new(&alt_objects);
5148 let payload = b"alternate-only";
5149 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
5150
5151 let primary = TempDir::new().unwrap();
5152 let primary_objects = primary.path().join("objects");
5153 fs::create_dir_all(primary.path().join("objects").join("info")).unwrap();
5154 fs::write(
5155 primary
5156 .path()
5157 .join("objects")
5158 .join("info")
5159 .join("alternates"),
5160 format!("{}\n", alt_objects.display()),
5161 )
5162 .unwrap();
5163 let odb = Odb::new(&primary_objects);
5164 assert!(odb.exists(&oid));
5165 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
5166 assert_eq!(oid, oid2);
5167 assert!(!odb.object_path(&oid).exists());
5168 }
5169
5170 #[cfg_attr(
5171 not(feature = "full-tests"),
5172 ignore = "starts system git or is heavy; run with --features full-tests"
5173 )]
5174 #[test]
5175 fn write_with_mem_overlay_stays_in_memory() {
5176 let dir = TempDir::new().unwrap();
5177 let objects = dir.path().join("objects");
5178 fs::create_dir_all(&objects).unwrap();
5179 let odb = Odb::new(&objects);
5180 odb.enable_mem_overlay();
5181 let payload = b"overlay blob";
5182 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
5183 assert!(!odb.object_path(&oid).exists());
5184 assert_eq!(odb.read(&oid).unwrap().data, payload);
5185 odb.disable_mem_overlay();
5186 assert!(odb.read(&oid).is_err());
5187 }
5188
5189 #[test]
5190 fn persist_mem_overlay_writes_through_custom_primary() {
5191 use crate::objects::HashAlgo;
5192 use std::sync::Arc;
5193
5194 let dir = TempDir::new().unwrap();
5195 let objects = dir.path().join("objects");
5196 fs::create_dir_all(&objects).unwrap();
5197 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
5198 let odb = OdbBuilder::files(&objects)
5199 .primary(store.clone())
5200 .alternates(false)
5201 .build();
5202 odb.enable_mem_overlay();
5203 let payload = b"overlay blob";
5204 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
5205 assert!(!store.contains(&oid).unwrap());
5206 odb.persist_mem_overlay().unwrap();
5207 assert!(store.contains(&oid).unwrap());
5208 assert_eq!(odb.read(&oid).unwrap().data, payload);
5209 assert!(!odb.object_path(&oid).exists());
5210 }
5211
5212 #[test]
5213 fn read_alternates_relative_path_and_symlinked_objects_dir() {
5214 use crate::pack::read_alternates_recursive;
5215
5216 let layout = TempDir::new().unwrap();
5217 let alt_objects = layout.path().join("alt-store").join("objects");
5218 fs::create_dir_all(&alt_objects).unwrap();
5219 let primary_objects = layout.path().join("repo").join("objects");
5220 fs::create_dir_all(primary_objects.join("info")).unwrap();
5221 fs::write(
5222 primary_objects.join("info").join("alternates"),
5223 "../../alt-store/objects\n",
5224 )
5225 .unwrap();
5226 let resolved = read_alternates_recursive(&primary_objects).unwrap();
5227 let alt_canonical = alt_objects
5228 .canonicalize_simplified()
5229 .unwrap_or(alt_objects.clone());
5230 assert!(
5231 resolved.iter().any(|p| {
5232 p.canonicalize_simplified().unwrap_or_else(|_| p.clone()) == alt_canonical
5233 }),
5234 "relative alternate entry should resolve: {resolved:?}"
5235 );
5236
5237 #[cfg(unix)]
5238 {
5239 let link_parent = TempDir::new().unwrap();
5240 std::os::unix::fs::symlink(&primary_objects, link_parent.path().join("objects-link"))
5241 .unwrap();
5242 let via_link =
5243 read_alternates_recursive(&link_parent.path().join("objects-link")).unwrap();
5244 assert!(
5245 !via_link.is_empty(),
5246 "alternates through symlinked objects dir: {via_link:?}"
5247 );
5248 }
5249 }
5250 #[test]
5251 fn empty_tree_exists_without_hex() {
5252 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
5253 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
5254 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").unwrap();
5255 assert!(odb.exists(&canon));
5256 assert!(odb.exists(&legacy));
5257 assert!(odb.exists_local(&canon));
5258 assert!(!odb.freshen_object(&canon));
5259 }
5260
5261 #[test]
5262 fn sha256_empty_tree_virtual_read_without_loose_file() {
5263 use crate::objects::HashAlgo;
5264 use std::sync::Arc;
5265
5266 let dir = TempDir::new().unwrap();
5267 let objects = dir.path().join("objects");
5268 fs::create_dir_all(&objects).unwrap();
5269 let store = Arc::new(MemoryStore::new(HashAlgo::Sha256));
5270 let odb = OdbBuilder::files(&objects)
5271 .primary(store)
5272 .alternates(false)
5273 .build();
5274 let empty = odb.empty_tree_oid().expect("hash empty tree");
5275 assert!(!odb.exists_materialized_local(&empty));
5276 assert!(odb.exists_local(&empty));
5277 assert!(odb.is_empty_tree(&empty));
5278 let obj = odb.read(&empty).expect("virtual read empty tree");
5279 assert_eq!(obj.kind, ObjectKind::Tree);
5280 assert!(obj.data.is_empty());
5281 }
5282
5283 #[test]
5284 fn empty_tree_oid_and_object_id_empty_tree_match_sha1() {
5285 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
5286 let from_odb = odb.empty_tree_oid().expect("empty tree oid");
5287 assert_eq!(from_odb, ObjectId::empty_tree());
5288 assert!(odb.is_empty_tree(&ObjectId::empty_tree()));
5289 }
5290
5291 #[test]
5292 fn empty_tree_virtual_presence_skips_materialized_local() {
5293 let dir = TempDir::new().unwrap();
5294 let objects = dir.path().join("objects");
5295 fs::create_dir_all(&objects).unwrap();
5296 let odb = Odb::new(&objects);
5297 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
5298 assert!(odb.exists_local(&canon));
5299 assert!(!odb.exists_materialized_local(&canon));
5300 }
5301
5302 #[test]
5303 fn write_materializes_well_known_empty_tree() {
5304 let dir = TempDir::new().unwrap();
5305 let objects = dir.path().join("objects");
5306 fs::create_dir_all(&objects).unwrap();
5307 let odb = Odb::new(&objects);
5308 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
5309 assert!(!odb.exists_materialized_local(&canon));
5310 let oid = odb
5311 .write(crate::objects::ObjectKind::Tree, &[])
5312 .expect("write empty tree");
5313 assert_eq!(oid, canon);
5314 assert!(odb.exists_materialized_local(&canon));
5315 }
5316
5317 #[test]
5318 fn append_alternate_objects_line_errors_on_unreadable_existing_file() {
5319 let dir = TempDir::new().unwrap();
5320 let objects = dir.path().join("objects");
5321 fs::create_dir_all(objects.join("info")).unwrap();
5322 let alt_path = objects.join("info/alternates");
5323 fs::write(&alt_path, b"not valid utf-8 \xff\n").unwrap();
5324 let err = Odb::append_alternate_objects_line(&objects, Path::new("/tmp/other/objects"))
5325 .unwrap_err();
5326 assert!(matches!(err, Error::Io(_)));
5327 assert_eq!(fs::read(&alt_path).unwrap(), b"not valid utf-8 \xff\n");
5328 }
5329
5330 #[test]
5331 fn append_file_alternate_refreshes_cached_snapshot() {
5332 let primary = TempDir::new().unwrap();
5333 let alternate = TempDir::new().unwrap();
5334 let alt_objects = alternate.path().join("objects");
5335 std::fs::create_dir_all(&alt_objects).unwrap();
5336 let alt_odb = Odb::new(&alt_objects);
5337 let oid = alt_odb.write(ObjectKind::Blob, b"via-alternate").unwrap();
5338
5339 let primary_objects = primary.path().join("objects");
5340 std::fs::create_dir_all(primary_objects.join("info")).unwrap();
5341 let primary_odb = Odb::new(&primary_objects);
5342 assert!(!primary_odb.exists(&oid));
5343 let _ = primary_odb.file_alternate_dirs_snapshot();
5344 primary_odb.append_file_alternate(&alt_objects).unwrap();
5345 assert!(primary_odb.exists(&oid));
5346 }
5347
5348 #[test]
5349 fn exists_via_env_alternate_dirs_at_construction() {
5350 let primary = TempDir::new().unwrap();
5351 let alternate = TempDir::new().unwrap();
5352 let alt_objects = alternate.path().join("objects");
5353 std::fs::create_dir_all(&alt_objects).unwrap();
5354 let alt_odb = Odb::new(&alt_objects);
5355 let oid = alt_odb.write(ObjectKind::Blob, b"env-alt").unwrap();
5356
5357 let primary_objects = primary.path().join("objects");
5358 std::fs::create_dir_all(&primary_objects).unwrap();
5359 let primary_odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
5360 assert!(primary_odb.exists(&oid));
5361 }
5362
5363 fn flaky_odb(objects: &Path) -> (Odb, Arc<store::flaky::FlakyStore>) {
5364 let flaky = store::flaky::FlakyStore::new();
5365 let odb = OdbBuilder::files(objects)
5366 .primary(flaky.clone())
5367 .alternates(false)
5368 .build();
5369 (odb, flaky)
5370 }
5371
5372 #[test]
5373 fn contains_reports_primary_failure_instead_of_missing() {
5374 let dir = TempDir::new().unwrap();
5375 let (odb, flaky) = flaky_odb(dir.path());
5376 let oid = odb.write(ObjectKind::Blob, b"stored").unwrap();
5377 assert!(odb.contains(&oid).unwrap());
5378
5379 flaky.fail_all(true);
5380 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
5381 assert!(store::flaky::is_injected(&odb.contains(&oid).unwrap_err()));
5382 assert!(
5383 !odb.exists(&oid),
5384 "exists() keeps treating failures as absent"
5385 );
5386 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
5387
5388 let try_local = odb.try_exists_local(&oid).unwrap_err();
5389 assert!(store::flaky::is_injected(&try_local));
5390 assert!(!odb.exists_local(&oid));
5391 let try_stored = odb.try_exists_stored(&oid).unwrap_err();
5392 assert!(store::flaky::is_injected(&try_stored));
5393 assert!(!odb.exists_stored(&oid));
5394 }
5395
5396 #[test]
5397 fn contains_reports_extra_read_source_failure() {
5398 let dir = TempDir::new().unwrap();
5399 let flaky = store::flaky::FlakyStore::new();
5400 let odb = OdbBuilder::files(dir.path())
5401 .push_read_source(flaky.clone())
5402 .alternates(false)
5403 .build();
5404 let local = odb.write(ObjectKind::Blob, b"in primary").unwrap();
5405 let absent = HashAlgo::Sha1
5406 .hash_object(ObjectKind::Blob, b"nowhere")
5407 .unwrap();
5408 assert!(!odb.contains(&absent).unwrap());
5409
5410 flaky.fail_contains(true);
5411 assert!(
5412 odb.contains(&local).unwrap(),
5413 "primary hit wins over a failing source"
5414 );
5415 assert!(store::flaky::is_injected(
5416 &odb.contains(&absent).unwrap_err()
5417 ));
5418 assert!(!odb.exists(&absent));
5419 }
5420
5421 #[test]
5422 fn contains_reports_primary_failure_even_when_later_source_has_object() {
5423 let dir = TempDir::new().unwrap();
5424 let extra = Arc::new(store::MemoryStore::new(HashAlgo::Sha1));
5425 let flaky = store::flaky::FlakyStore::new();
5426 let odb = OdbBuilder::files(dir.path())
5427 .primary(flaky.clone())
5428 .push_read_source(extra.clone())
5429 .alternates(false)
5430 .build();
5431 let oid = extra
5432 .write(ObjectKind::Blob, b"extra", WriteOptions::default())
5433 .unwrap();
5434 assert!(odb.contains(&oid).unwrap());
5435
5436 flaky.fail_all(true);
5437 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
5438 assert!(
5439 store::flaky::is_injected(&odb.contains(&oid).unwrap_err()),
5440 "contains must agree with read"
5441 );
5442 assert!(odb.exists(&oid), "exists() skips the failing primary");
5443 }
5444
5445 #[test]
5446 fn contains_reports_alternate_failure() {
5447 let primary = TempDir::new().unwrap();
5448 let alternate = TempDir::new().unwrap();
5449 let alt_objects = alternate.path().join("objects");
5450 std::fs::create_dir_all(&alt_objects).unwrap();
5451 let primary_objects = primary.path().join("objects");
5452 std::fs::create_dir_all(&primary_objects).unwrap();
5453 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
5454 let local = odb.write(ObjectKind::Blob, b"local").unwrap();
5455 let absent = HashAlgo::Sha1
5456 .hash_object(ObjectKind::Blob, b"nowhere")
5457 .unwrap();
5458
5459 let flaky = store::flaky::FlakyStore::new();
5462 odb.sources().unwrap();
5463 odb.alternate_sources_cache.write().unwrap().sources = Some(Arc::new(CompositeStore::new(
5464 vec![flaky.clone()],
5465 HashAlgo::Sha1,
5466 )));
5467 assert!(!odb.contains(&absent).unwrap());
5468
5469 flaky.fail_contains(true);
5470 assert!(odb.contains(&local).unwrap());
5471 assert!(store::flaky::is_injected(
5472 &odb.contains(&absent).unwrap_err()
5473 ));
5474 assert!(!odb.exists(&absent));
5475 }
5476
5477 #[test]
5478 fn contains_on_files_backend_covers_present_absent_empty_tree_and_alternate() {
5479 let primary = TempDir::new().unwrap();
5480 let alternate = TempDir::new().unwrap();
5481 let alt_objects = alternate.path().join("objects");
5482 std::fs::create_dir_all(&alt_objects).unwrap();
5483 let alt_oid = Odb::new(&alt_objects)
5484 .write(ObjectKind::Blob, b"alternate")
5485 .unwrap();
5486 let primary_objects = primary.path().join("objects");
5487 std::fs::create_dir_all(&primary_objects).unwrap();
5488 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
5489 let present = odb.write(ObjectKind::Blob, b"present").unwrap();
5490 let absent = HashAlgo::Sha1
5491 .hash_object(ObjectKind::Blob, b"absent")
5492 .unwrap();
5493
5494 assert!(odb.contains(&present).unwrap());
5495 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
5496 assert!(odb.contains(&alt_oid).unwrap());
5497 assert!(!odb.contains(&absent).unwrap());
5498 let empty_tree = odb.empty_tree_oid().unwrap();
5499 assert_eq!(
5500 odb.contains_many(&[present, absent, alt_oid, empty_tree])
5501 .unwrap(),
5502 vec![true, false, true, true]
5503 );
5504 assert!(odb.contains_many(&[]).unwrap().is_empty());
5505 assert_eq!(
5506 odb.present_oids([present, absent, alt_oid]).unwrap(),
5507 vec![present, alt_oid]
5508 );
5509 assert_eq!(
5510 odb.missing_oids([present, absent, alt_oid]).unwrap(),
5511 vec![absent]
5512 );
5513 assert_eq!(
5514 odb.missing_stored_oids([present, absent, alt_oid]).unwrap(),
5515 vec![absent, alt_oid],
5516 "stored ignores alternates"
5517 );
5518 }
5519
5520 #[test]
5521 fn missing_stored_oids_sees_packs_written_since_the_last_probe() {
5522 let dir = TempDir::new().unwrap();
5523 let odb = Odb::new(dir.path());
5524 let other = TempDir::new().unwrap();
5525 let source = Odb::new(other.path());
5526 let blobs: Vec<ObjectId> = (0..3)
5527 .map(|i| {
5528 source
5529 .write(ObjectKind::Blob, format!("b{i}").as_bytes())
5530 .unwrap()
5531 })
5532 .collect();
5533 assert_eq!(odb.missing_stored_oids(blobs.clone()).unwrap(), blobs);
5534 let pack = crate::pack_objects::build_pack_from_send_list(
5536 &source,
5537 &blobs[..2],
5538 &std::collections::HashSet::new(),
5539 &crate::pack_objects::PackBuildOptions::default(),
5540 &std::collections::HashMap::new(),
5541 )
5542 .unwrap();
5543 crate::index_pack::install_pack_bytes(pack, &Odb::new(dir.path()), &Default::default())
5544 .unwrap();
5545 assert_eq!(
5546 odb.missing_stored_oids(blobs.clone()).unwrap(),
5547 vec![blobs[2]]
5548 );
5549 }
5550
5551 #[test]
5552 fn oid_filters_propagate_backend_failure() {
5553 let dir = TempDir::new().unwrap();
5554 let (odb, flaky) = flaky_odb(dir.path());
5555 let oid = odb.write(ObjectKind::Blob, b"x").unwrap();
5556 assert_eq!(odb.contains_many(&[oid]).unwrap(), vec![true]);
5557 flaky.fail_contains(true);
5558 assert!(store::flaky::is_injected(
5559 &odb.contains_many(&[oid]).unwrap_err()
5560 ));
5561 assert!(store::flaky::is_injected(
5562 &odb.present_oids([oid]).unwrap_err()
5563 ));
5564 assert!(store::flaky::is_injected(
5565 &odb.missing_oids([oid]).unwrap_err()
5566 ));
5567 assert!(store::flaky::is_injected(
5568 &odb.missing_stored_oids([oid]).unwrap_err()
5569 ));
5570 }
5571
5572 #[test]
5573 fn write_existing_loose_uses_path_stat_not_exists() {
5574 let dir = TempDir::new().unwrap();
5575 let odb = Odb::new(dir.path());
5576 let oid = odb.write(ObjectKind::Blob, b"once").unwrap();
5577 let path = odb.object_path(&oid);
5578 assert!(path.is_file());
5579 odb.reset_exists_probe();
5580 let oid2 = odb.write(ObjectKind::Blob, b"once").unwrap();
5581 assert_eq!(oid, oid2);
5582 assert_eq!(
5583 odb.exists_probe_count(),
5584 0,
5585 "loose fast path must not call exists()"
5586 );
5587 }
5588
5589 #[cfg_attr(
5590 not(feature = "full-tests"),
5591 ignore = "starts system git or is heavy; run with --features full-tests"
5592 )]
5593 #[test]
5594 fn exists_and_write_after_invalidate_packs_use_odb_store() {
5595 let dir = TempDir::new().unwrap();
5596 git_in(dir.path(), &["init", "-q"]);
5597 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5598 git_in(dir.path(), &["config", "user.name", "T"]);
5599 let payload = b"invalidate-pack-exists";
5600 fs::write(dir.path().join("f"), payload).unwrap();
5601 git_in(dir.path(), &["add", "f"]);
5602 git_in(dir.path(), &["commit", "-m", "c"]);
5603 git_in(dir.path(), &["repack", "-ad"]);
5604 let objects = dir.path().join(".git").join("objects");
5605 let odb = Odb::new(&objects);
5606 let oid = odb.hash(ObjectKind::Blob, payload).expect("hash");
5607 assert!(odb.exists(&oid), "blob must live in pack after repack");
5608 let pack_dir = objects.join("pack");
5609 for entry in fs::read_dir(&pack_dir).unwrap().flatten() {
5610 let path = entry.path();
5611 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
5612 fs::remove_file(path).unwrap();
5613 }
5614 }
5615 odb.invalidate_packs();
5616 assert!(
5617 !odb.exists(&oid),
5618 "exists must not read stale legacy pack listing after invalidate_packs"
5619 );
5620 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
5621 assert_eq!(oid, oid2);
5622 assert!(
5623 odb.object_path(&oid).is_file(),
5624 "write must materialize loose object when pack copy is gone"
5625 );
5626 odb.read(&oid).expect("read after write");
5627 }
5628
5629 #[cfg_attr(
5630 not(feature = "full-tests"),
5631 ignore = "starts system git or is heavy; run with --features full-tests"
5632 )]
5633 #[test]
5634 fn write_uses_single_exists_probe_when_not_loose() {
5635 let dir = TempDir::new().unwrap();
5636 git_in(dir.path(), &["init", "-q"]);
5637 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5638 git_in(dir.path(), &["config", "user.name", "T"]);
5639 fs::write(dir.path().join("f"), b"probe-once").unwrap();
5640 git_in(dir.path(), &["add", "f"]);
5641 git_in(dir.path(), &["commit", "-m", "c"]);
5642 git_in(dir.path(), &["repack", "-ad"]);
5643 let objects = dir.path().join(".git").join("objects");
5644 let odb = Odb::new(&objects);
5645 let oid = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
5646 assert!(!odb.object_path(&oid).exists());
5647 odb.reset_exists_probe();
5648 let oid2 = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
5649 assert_eq!(oid, oid2);
5650 assert_eq!(
5651 odb.exists_probe_count(),
5652 1,
5653 "pack/alternate write path must call exists() exactly once"
5654 );
5655 }
5656 #[test]
5657 fn write_existing_object_freshens() {
5658 use filetime::FileTime;
5659 use std::fs;
5660 use std::thread;
5661 use std::time::Duration;
5662
5663 let dir = TempDir::new().unwrap();
5664 let odb = Odb::new(dir.path());
5665 let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
5666 let path = odb.object_path(&oid);
5667 thread::sleep(Duration::from_millis(50));
5668 let before = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
5669 filetime::set_file_mtime(&path, before).unwrap();
5670
5671 let _ = odb.write(ObjectKind::Blob, b"hello").unwrap();
5672 let after = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
5673 assert!(
5674 after > before,
5675 "rewriting an existing loose object must freshen its mtime"
5676 );
5677 }
5678
5679 #[test]
5680 fn freshen_pack_once_per_odb() {
5681 use filetime::FileTime;
5682 use flate2::write::ZlibEncoder;
5683 use flate2::Compression;
5684 use std::fs;
5685 use std::io::Write;
5686 use std::thread;
5687 use std::time::Duration;
5688
5689 fn append_pack_object(buf: &mut Vec<u8>, type_code: u8, data: &[u8]) {
5690 let mut size = data.len();
5691 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
5692 size >>= 4;
5693 if size > 0 {
5694 buf.push(first | 0x80);
5695 while size > 0 {
5696 let b = (size & 0x7f) as u8;
5697 size >>= 7;
5698 buf.push(if size > 0 { b | 0x80 } else { b });
5699 }
5700 } else {
5701 buf.push(first);
5702 }
5703 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5704 enc.write_all(data).unwrap();
5705 buf.extend_from_slice(&enc.finish().unwrap());
5706 }
5707
5708 fn write_v2_idx(
5709 idx_path: &Path,
5710 pack_path: &Path,
5711 entries: &[(ObjectId, u64)],
5712 ) -> Result<()> {
5713 let mut sorted = entries.to_vec();
5714 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
5715 let n = sorted.len();
5716 let mut fanout = [0u32; 256];
5717 for byte in 0u32..256 {
5718 let count = sorted
5719 .iter()
5720 .filter(|(oid, _)| u32::from(oid.as_bytes()[0]) <= byte)
5721 .count();
5722 fanout[byte as usize] = u32::try_from(count).unwrap_or(u32::MAX);
5723 }
5724 let mut buf = Vec::new();
5725 buf.extend_from_slice(b"\xfftOc");
5726 buf.extend_from_slice(&2u32.to_be_bytes());
5727 for f in fanout {
5728 buf.extend_from_slice(&f.to_be_bytes());
5729 }
5730 for (oid, _) in &sorted {
5731 buf.extend_from_slice(oid.as_bytes());
5732 }
5733 for _ in 0..n {
5734 buf.extend_from_slice(&0u32.to_be_bytes());
5735 }
5736 for (_, off) in &sorted {
5737 buf.extend_from_slice(&u32::try_from(*off).unwrap_or(0x8000_0000).to_be_bytes());
5738 }
5739 let pack_bytes = fs::read(pack_path)?;
5740 buf.extend_from_slice(&pack_bytes[pack_bytes.len() - 20..]);
5741 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
5742 fs::write(idx_path, buf)?;
5743 Ok(())
5744 }
5745
5746 let dir = TempDir::new().unwrap();
5747 let objects_dir = dir.path();
5748 let odb = Odb::new(objects_dir);
5749
5750 let oid_a = odb.write(ObjectKind::Blob, b"alpha").unwrap();
5751 let oid_b = odb.write(ObjectKind::Blob, b"beta").unwrap();
5752 fs::remove_file(odb.object_path(&oid_a)).unwrap();
5753 fs::remove_file(odb.object_path(&oid_b)).unwrap();
5754
5755 let mut pack = Vec::new();
5756 pack.extend_from_slice(b"PACK");
5757 pack.extend_from_slice(&2u32.to_be_bytes());
5758 pack.extend_from_slice(&2u32.to_be_bytes());
5759 let off_a = pack.len() as u64;
5760 append_pack_object(&mut pack, 3, b"alpha");
5761 let off_b = pack.len() as u64;
5762 append_pack_object(&mut pack, 3, b"beta");
5763 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
5764
5765 let pack_dir = objects_dir.join("pack");
5766 fs::create_dir_all(&pack_dir).unwrap();
5767 let pack_path = pack_dir.join("test-177.pack");
5768 let idx_path = pack_dir.join("test-177.idx");
5769 fs::write(&pack_path, &pack).unwrap();
5770 write_v2_idx(&idx_path, &pack_path, &[(oid_a, off_a), (oid_b, off_b)]).unwrap();
5771 odb.invalidate_packs();
5772
5773 thread::sleep(Duration::from_millis(50));
5774 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5775
5776 assert!(odb.freshen_object(&oid_a));
5777 let after_first = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5778 assert!(after_first > before);
5779
5780 assert!(odb.freshen_object(&oid_b));
5781 let after_second =
5782 FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5783 assert_eq!(
5784 after_second, after_first,
5785 "second freshen of another object in the same pack must not touch the pack again"
5786 );
5787 }
5788
5789 #[test]
5790 fn write_existing_packed_object_freshens_pack() {
5791 use filetime::FileTime;
5792 use flate2::write::ZlibEncoder;
5793 use flate2::Compression;
5794 use std::fs;
5795 use std::io::Write;
5796 use std::thread;
5797 use std::time::Duration;
5798
5799 let dir = TempDir::new().unwrap();
5800 let objects_dir = dir.path();
5801 let odb = Odb::new(objects_dir);
5802 let oid = odb.write(ObjectKind::Blob, b"packed-only").unwrap();
5803 fs::remove_file(odb.object_path(&oid)).unwrap();
5804
5805 let mut pack = Vec::new();
5806 pack.extend_from_slice(b"PACK");
5807 pack.extend_from_slice(&2u32.to_be_bytes());
5808 pack.extend_from_slice(&1u32.to_be_bytes());
5809 let off = pack.len() as u64;
5810 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5811 enc.write_all(b"packed-only").unwrap();
5812 let compressed = enc.finish().unwrap();
5813 pack.push(0x30 | 0x03); pack.extend_from_slice(&compressed);
5815 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
5816
5817 let pack_dir = objects_dir.join("pack");
5818 fs::create_dir_all(&pack_dir).unwrap();
5819 let pack_path = pack_dir.join("single-177.pack");
5820 fs::write(&pack_path, &pack).unwrap();
5821 let idx_path = pack_dir.join("single-177.idx");
5823 let mut sorted = [(oid, off)];
5824 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
5825 let mut fanout = [0u32; 256];
5826 for byte in 0u32..256 {
5827 fanout[byte as usize] = if u32::from(oid.as_bytes()[0]) <= byte {
5828 1
5829 } else {
5830 0
5831 };
5832 }
5833 let mut buf = Vec::new();
5834 buf.extend_from_slice(b"\xfftOc");
5835 buf.extend_from_slice(&2u32.to_be_bytes());
5836 for f in fanout {
5837 buf.extend_from_slice(&f.to_be_bytes());
5838 }
5839 buf.extend_from_slice(oid.as_bytes());
5840 buf.extend_from_slice(&0u32.to_be_bytes());
5841 buf.extend_from_slice(&(off as u32).to_be_bytes());
5842 buf.extend_from_slice(&pack[pack.len() - 20..]);
5843 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
5844 fs::write(&idx_path, buf).unwrap();
5845 odb.invalidate_packs();
5846
5847 thread::sleep(Duration::from_millis(50));
5848 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5849
5850 odb.write(ObjectKind::Blob, b"packed-only").unwrap();
5851
5852 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
5853 assert!(
5854 after > before,
5855 "write of an existing packed object must freshen the pack mtime"
5856 );
5857 }
5858
5859 #[cfg_attr(
5863 not(feature = "full-tests"),
5864 ignore = "starts system git or is heavy; run with --features full-tests"
5865 )]
5866 #[test]
5867 fn write_materializes_loose_when_pack_freshen_fails() {
5868 let dir = TempDir::new().unwrap();
5869 git_in(dir.path(), &["init", "-q"]);
5870 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5871 git_in(dir.path(), &["config", "user.name", "T"]);
5872 let payload = b"packed-fallback";
5873 fs::write(dir.path().join("f"), payload).unwrap();
5874 git_in(dir.path(), &["add", "f"]);
5875 git_in(dir.path(), &["commit", "-m", "c"]);
5876 git_in(dir.path(), &["repack", "-ad"]);
5877 let objects = dir.path().join(".git").join("objects");
5878 let odb = Odb::new(&objects);
5879 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
5880 assert!(!odb.object_path(&oid).exists());
5881 assert!(odb.exists(&oid));
5882
5883 let pack_path = fs::read_dir(objects.join("pack"))
5884 .unwrap()
5885 .flatten()
5886 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
5887 .unwrap()
5888 .path();
5889 fs::remove_file(&pack_path).unwrap();
5890 fs::create_dir(&pack_path).unwrap();
5891 odb.invalidate_packs();
5892 assert!(!odb.freshen_object(&oid));
5893
5894 odb.write(ObjectKind::Blob, payload).unwrap();
5895 assert!(odb.object_path(&oid).exists());
5896 }
5897 #[test]
5898 fn write_with_options_silent_skips_freshen_on_existing_loose_object() {
5899 let dir = TempDir::new().unwrap();
5900 let odb = Odb::new(dir.path());
5901 let oid = odb.write(ObjectKind::Blob, b"stable").unwrap();
5902 let path = odb.object_path(&oid);
5903 let before = std::fs::metadata(&path).unwrap().modified().unwrap();
5904 std::thread::sleep(std::time::Duration::from_millis(20));
5905 let again = odb
5906 .write_with_options(
5907 ObjectKind::Blob,
5908 b"stable",
5909 WriteOptions {
5910 silent: true,
5911 ..WriteOptions::default()
5912 },
5913 )
5914 .unwrap();
5915 assert_eq!(oid, again);
5916 let after = std::fs::metadata(&path).unwrap().modified().unwrap();
5917 assert_eq!(before, after);
5918
5919 let touched = odb
5920 .write_with_options(
5921 ObjectKind::Blob,
5922 b"stable",
5923 WriteOptions {
5924 silent: false,
5925 ..WriteOptions::default()
5926 },
5927 )
5928 .unwrap();
5929 assert_eq!(oid, touched);
5930 let freshened = std::fs::metadata(&path).unwrap().modified().unwrap();
5931 assert!(freshened >= before);
5932 }
5933
5934 #[cfg_attr(
5935 not(feature = "full-tests"),
5936 ignore = "starts system git or is heavy; run with --features full-tests"
5937 )]
5938 #[test]
5939 fn promisor_marker_checked_once_per_prepare() {
5940 use crate::pack::{
5941 clear_pack_cache, pack_cache_test_guard, test_pack_marker_stat_count,
5942 test_reset_pack_marker_stat_count,
5943 };
5944
5945 let _guard = pack_cache_test_guard();
5946 let dir = TempDir::new().unwrap();
5947 git_in(dir.path(), &["init", "-q", "-b", "main"]);
5948 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
5949 git_in(dir.path(), &["config", "user.name", "T"]);
5950 for i in 0..64 {
5951 fs::write(dir.path().join(format!("f{i}.txt")), format!("body {i}")).unwrap();
5952 git_in(dir.path(), &["add", "."]);
5953 git_in(dir.path(), &["commit", "-m", &format!("c{i}")]);
5954 }
5955 git_in(dir.path(), &["repack", "-a", "-d"]);
5956 let objects = dir.path().join(".git/objects");
5957 let oid = git_rev_parse_oid(dir.path(), "HEAD");
5958
5959 clear_pack_cache();
5960 test_reset_pack_marker_stat_count();
5961 let pack_count = fs::read_dir(objects.join("pack"))
5962 .unwrap()
5963 .filter_map(|e| e.ok())
5964 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
5965 .count();
5966 let expected_prepare_stats = u64::try_from(pack_count * 2).unwrap();
5967 let odb = Odb::new(&objects);
5968 let before_prepare = test_pack_marker_stat_count();
5969 assert!(odb.exists_local(&oid));
5970 assert_eq!(
5971 test_pack_marker_stat_count().saturating_sub(before_prepare),
5972 expected_prepare_stats,
5973 "prepare should stat promisor and mtimes once per pack"
5974 );
5975
5976 test_reset_pack_marker_stat_count();
5977 for _ in 0..10_000 {
5978 let before_probe = test_pack_marker_stat_count();
5979 assert!(odb.exists_local(&oid));
5980 assert_eq!(
5981 test_pack_marker_stat_count(),
5982 before_probe,
5983 "cached sidecar flags must not stat markers on each exists_local probe"
5984 );
5985 }
5986 }
5987
5988 #[cfg_attr(
5989 not(feature = "full-tests"),
5990 ignore = "starts system git or is heavy; run with --features full-tests"
5991 )]
5992 #[test]
5993 fn promisor_marker_added_after_prepare_is_seen_after_reprepare() {
5994 use crate::pack::{
5995 clear_pack_cache, pack_cache_test_guard, reprepare_pack_directory_on_miss,
5996 };
5997
5998 let _guard = pack_cache_test_guard();
5999 let dir = TempDir::new().unwrap();
6000 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6001 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6002 git_in(dir.path(), &["config", "user.name", "T"]);
6003 fs::write(dir.path().join("one.txt"), b"x").unwrap();
6004 git_in(dir.path(), &["add", "one.txt"]);
6005 git_in(dir.path(), &["commit", "-m", "one"]);
6006 git_in(dir.path(), &["repack", "-a", "-d"]);
6007 let objects = dir.path().join(".git/objects");
6008 let oid = git_rev_parse_oid(dir.path(), "HEAD");
6009
6010 clear_pack_cache();
6011 let odb = Odb::new(&objects);
6012 assert!(odb.exists_local(&oid));
6013
6014 let pack_path = fs::read_dir(objects.join("pack"))
6015 .unwrap()
6016 .filter_map(|e| e.ok())
6017 .map(|e| e.path())
6018 .find(|p| p.extension().is_some_and(|x| x == "pack"))
6019 .unwrap();
6020 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
6021 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now()).unwrap();
6022
6023 assert!(
6024 odb.with_pack_store_for(&objects, || {
6025 reprepare_pack_directory_on_miss(&objects).unwrap()
6026 }),
6027 "pack directory change must require reprepare"
6028 );
6029 assert!(
6030 !odb.exists_local(&oid),
6031 "promisor marker must hide pack objects after reprepare refreshed sidecar flags"
6032 );
6033 }
6034
6035 #[cfg_attr(
6036 not(feature = "full-tests"),
6037 ignore = "starts system git or is heavy; run with --features full-tests"
6038 )]
6039 #[test]
6040 fn git_promisor_marker_pack_not_materialized_for_exists_local() {
6041 use crate::pack::clear_pack_cache;
6042
6043 let dir = TempDir::new().unwrap();
6044 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6045 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6046 git_in(dir.path(), &["config", "user.name", "T"]);
6047 fs::write(dir.path().join("blob.txt"), b"git promisor compat").unwrap();
6048 git_in(dir.path(), &["add", "blob.txt"]);
6049 git_in(dir.path(), &["commit", "-m", "seed"]);
6050 git_in(dir.path(), &["repack", "-a", "-d"]);
6051 let objects = dir.path().join(".git/objects");
6052 let oid = git_rev_parse_oid(dir.path(), "HEAD");
6053 let pack_path = fs::read_dir(objects.join("pack"))
6054 .unwrap()
6055 .filter_map(|e| e.ok())
6056 .map(|e| e.path())
6057 .find(|p| p.extension().is_some_and(|x| x == "pack"))
6058 .unwrap();
6059 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
6060
6061 clear_pack_cache();
6062 let odb = Odb::new(&objects);
6063 assert!(
6064 !odb.exists_local(&oid),
6065 "git-written pack with .promisor must not count as local materialization"
6066 );
6067
6068 let cat = git_run(dir.path(), &["cat-file", "-e", &oid.to_hex()]);
6069 assert!(
6070 cat.status.success(),
6071 "git still resolves the object in a promisor-marked pack"
6072 );
6073 }
6074
6075 #[cfg_attr(
6076 not(feature = "full-tests"),
6077 ignore = "starts system git or is heavy; run with --features full-tests"
6078 )]
6079 #[test]
6080 fn packed_object_read_avoids_loose_path_open() {
6081 use crate::hot_path_test_metrics::HotPathMetricsScope;
6082 use crate::pack::clear_pack_cache;
6083
6084 let dir = TempDir::new().unwrap();
6085 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6086 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6087 git_in(dir.path(), &["config", "user.name", "T"]);
6088 fs::write(dir.path().join("packed.txt"), b"packed-only payload").unwrap();
6089 git_in(dir.path(), &["add", "packed.txt"]);
6090 git_in(dir.path(), &["commit", "-m", "pack"]);
6091 git_in(dir.path(), &["repack", "-a", "-d"]);
6092 let objects = dir.path().join(".git/objects");
6093 let oid = git_rev_parse_oid(dir.path(), "HEAD");
6094
6095 clear_pack_cache();
6096 let odb = Odb::new(&objects);
6097 odb.hot_path_metrics().set_loose_open_counting(true);
6098 odb.hot_path_metrics().reset_loose_path_open_attempts();
6099 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
6100
6101 let obj = odb.read(&oid).expect("packed read");
6102 assert_eq!(obj.kind, ObjectKind::Commit);
6103 assert_eq!(
6104 odb.hot_path_metrics().loose_path_open_attempts(),
6105 0,
6106 "packed objects must not attempt a loose-path open"
6107 );
6108 }
6109
6110 #[cfg_attr(
6111 not(feature = "full-tests"),
6112 ignore = "starts system git or is heavy; run with --features full-tests"
6113 )]
6114 #[test]
6115 fn initial_clone_ref_batch_hits_packed_commit_once_for_many_refs() {
6116 use std::sync::Arc;
6117
6118 use crate::hot_path_test_metrics::HotPathMetricsScope;
6119 use crate::pack::clear_pack_cache;
6120 use crate::refs::commit_initial_clone_ref_updates_on_store;
6121 use crate::refs::store::{Expected, ObjectCheckedRefStore, RawRef, RefUpdate};
6122
6123 const N: usize = 4096;
6124 let dir = TempDir::new().unwrap();
6125 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6126 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6127 git_in(dir.path(), &["config", "user.name", "T"]);
6128 fs::write(dir.path().join("packed.txt"), b"payload").unwrap();
6129 git_in(dir.path(), &["add", "packed.txt"]);
6130 git_in(dir.path(), &["commit", "-m", "pack"]);
6131 git_in(dir.path(), &["repack", "-a", "-d"]);
6132 let git_dir = dir.path().join(".git");
6133 let objects = git_dir.join("objects");
6134 let head = git_rev_parse_oid(dir.path(), "HEAD");
6135
6136 clear_pack_cache();
6137 let odb = Odb::new(&objects).with_config_git_dir(git_dir.clone());
6138 odb.hot_path_metrics().set_loose_open_counting(true);
6139 odb.hot_path_metrics().reset_loose_path_open_attempts();
6140 let metrics = odb.hot_path_metrics_arc();
6141 let _scope = HotPathMetricsScope::install(Arc::clone(&metrics));
6142 let inner = crate::refs::store::open_ref_store(&git_dir).expect("refs");
6143 let store = ObjectCheckedRefStore::new(inner, odb);
6144 let updates: Vec<RefUpdate> = (0..N)
6145 .map(|i| RefUpdate {
6146 name: format!("refs/remotes/origin/b/{i}"),
6147 new_value: Some(RawRef::Direct(head)),
6148 expected: Expected::Any,
6149 reflog: None,
6150 flags: Default::default(),
6151 })
6152 .collect();
6153 commit_initial_clone_ref_updates_on_store(&store, updates).expect("initial refs");
6154 assert!(
6155 metrics.loose_path_open_attempts() <= 1,
6156 "many refs at one packed oid must not open a loose path per ref (got {})",
6157 metrics.loose_path_open_attempts()
6158 );
6159 git_in(dir.path(), &["fsck"]);
6160 }
6161
6162 #[cfg_attr(
6163 not(feature = "full-tests"),
6164 ignore = "starts system git or is heavy; run with --features full-tests"
6165 )]
6166 #[test]
6167 fn packed_reads_after_first_pack_load_do_not_stat_pack_dir() {
6168 use crate::hot_path_test_metrics::HotPathMetricsScope;
6169 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
6170
6171 let _guard = pack_cache_test_guard();
6172 let dir = TempDir::new().unwrap();
6173 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6174 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6175 git_in(dir.path(), &["config", "user.name", "T"]);
6176 for i in 0..32 {
6177 fs::write(dir.path().join("f.txt"), format!("body {i}")).unwrap();
6178 git_in(dir.path(), &["add", "f.txt"]);
6179 git_in(dir.path(), &["commit", "-q", "-m", &format!("c{i}")]);
6180 }
6181 git_in(dir.path(), &["repack", "-a", "-d"]);
6182 let objects = dir.path().join(".git/objects");
6183 let commits: Vec<ObjectId> = (0..32)
6184 .map(|n| git_rev_parse_oid(dir.path(), &format!("HEAD~{n}")))
6185 .collect();
6186
6187 clear_pack_cache();
6188 let odb = Odb::new(&objects);
6189 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
6190 odb.with_pack_read_context(|| {
6191 let first = odb.read(&commits[0]).expect("first packed read");
6193 assert_eq!(first.kind, ObjectKind::Commit);
6194
6195 odb.hot_path_metrics().reset_pack_dir_stats();
6196 odb.hot_path_metrics().set_stamp_counting(true);
6197 for oid in &commits[1..] {
6198 let obj = odb.read(oid).expect("packed read");
6199 assert_eq!(obj.kind, ObjectKind::Commit);
6200 odb.read_info(oid).expect("packed read_info");
6201 }
6202 odb.hot_path_metrics().set_stamp_counting(false);
6203 });
6204 assert_eq!(
6205 odb.hot_path_metrics().pack_dir_stat_calls(),
6206 0,
6207 "loading a pack for the first time must keep the trusted pack listing"
6208 );
6209 }
6210
6211 #[cfg_attr(
6212 not(feature = "full-tests"),
6213 ignore = "starts system git or is heavy; run with --features full-tests"
6214 )]
6215 #[test]
6216 fn alternate_packed_reads_do_not_rescan_local_pack_dir() {
6217 use crate::hot_path_test_metrics::HotPathMetricsScope;
6218 use crate::pack::{clear_pack_cache, pack_cache};
6219
6220 let upstream = TempDir::new().unwrap();
6221 git_in(upstream.path(), &["init", "-q", "-b", "main"]);
6222 git_in(upstream.path(), &["config", "user.email", "t@example.com"]);
6223 git_in(upstream.path(), &["config", "user.name", "T"]);
6224 fs::write(
6225 upstream.path().join("payload.txt"),
6226 b"alternate-only-in-upstream",
6227 )
6228 .unwrap();
6229 git_in(upstream.path(), &["add", "payload.txt"]);
6230 git_in(upstream.path(), &["commit", "-m", "seed"]);
6231 for n in 0..48 {
6232 let name = format!("blob-{n}.txt");
6233 fs::write(
6234 upstream.path().join(&name),
6235 format!("alternate-only-payload-{n}").as_bytes(),
6236 )
6237 .unwrap();
6238 git_in(upstream.path(), &["add", name.as_str()]);
6239 git_in(
6240 upstream.path(),
6241 &["commit", "-m", &format!("packed commit {n}")],
6242 );
6243 }
6244 git_in(upstream.path(), &["repack", "-a", "-d"]);
6245 let rev_out = git_run(upstream.path(), &["rev-list", "HEAD"]);
6246 assert!(rev_out.status.success());
6247 let oids: Vec<ObjectId> = std::str::from_utf8(&rev_out.stdout)
6248 .unwrap()
6249 .lines()
6250 .filter(|line| !line.is_empty())
6251 .map(|line| ObjectId::from_hex(line.trim()).expect("rev-list oid"))
6252 .collect();
6253 assert!(
6254 oids.len() >= 32,
6255 "need many distinct commit oids for metadata regression"
6256 );
6257
6258 let layout = TempDir::new().unwrap();
6259 git_in(
6260 layout.path(),
6261 &[
6262 "clone",
6263 "-q",
6264 "--shared",
6265 upstream.path().to_str().unwrap(),
6266 "shared",
6267 ],
6268 );
6269 let objects = layout.path().join("shared/.git/objects");
6270 let local_pack = objects.join("pack");
6271
6272 clear_pack_cache();
6273 let odb = Odb::new(&objects);
6274 odb.hot_path_metrics().set_loose_open_counting(true);
6275 odb.hot_path_metrics().set_loose_metadata_counting(true);
6276 odb.hot_path_metrics().set_stamp_counting(true);
6277 odb.hot_path_metrics().reset_loose_path_open_attempts();
6278 odb.hot_path_metrics().reset_loose_metadata_probes();
6279 odb.hot_path_metrics().reset_pack_dir_metadata_probes();
6280 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
6281 pack_cache::test_reset_dir_rescan_count(&local_pack);
6282 let upstream_pack = upstream.path().join(".git/objects/pack");
6283 let upstream_objects = upstream.path().join(".git/objects");
6284 let upstream_store = odb.pack_store_for(&upstream_objects);
6285 pack_cache::test_reset_dir_rescan_count_on_store(&upstream_store, &upstream_pack);
6286 odb.with_pack_read_context(|| {
6287 for oid in &oids {
6288 odb.read(oid).expect("alternate warmup read");
6289 }
6290 let after_first = pack_cache::test_dir_rescan_count(&local_pack);
6291 let after_first_alt =
6292 pack_cache::test_dir_rescan_count_on_store(&upstream_store, &upstream_pack);
6293 let loose_after_first = odb.hot_path_metrics().loose_path_open_attempts();
6294 let root_meta_after_first = odb.hot_path_metrics().loose_objects_root_metadata_probes();
6295 let fanout_meta_after_first = odb.hot_path_metrics().loose_fanout_metadata_probes();
6296 let exists_meta_after_first = odb.hot_path_metrics().loose_path_exists_probes();
6297 let loose_meta_after_first = odb.hot_path_metrics().loose_metadata_probe_total();
6298 let pack_dir_meta_after_first = odb.hot_path_metrics().pack_dir_metadata_probes();
6299 for _ in 0..2 {
6300 for oid in &oids {
6301 odb.read(oid).expect("repeat alternate read");
6302 }
6303 }
6304 assert_eq!(
6305 odb.hot_path_metrics().loose_path_open_attempts(),
6306 loose_after_first,
6307 "alternate hits must not stat loose paths on the empty primary after warmup"
6308 );
6309 assert_eq!(
6310 odb.hot_path_metrics().loose_objects_root_metadata_probes(),
6311 root_meta_after_first,
6312 "known-absent fanout must not re-stat the objects root after warmup"
6313 );
6314 assert_eq!(
6315 odb.hot_path_metrics().loose_fanout_metadata_probes(),
6316 fanout_meta_after_first,
6317 "known-absent fanout must not re-stat fanout directories after warmup"
6318 );
6319 assert_eq!(
6320 odb.hot_path_metrics().loose_path_exists_probes(),
6321 exists_meta_after_first,
6322 "known-absent fanout must not is_file/is_dir loose paths after warmup"
6323 );
6324 assert_eq!(
6325 odb.hot_path_metrics().loose_metadata_probe_total(),
6326 loose_meta_after_first,
6327 "no loose metadata probes after warmup across distinct alternate oids"
6328 );
6329 assert_eq!(
6330 odb.hot_path_metrics().pack_dir_metadata_probes(),
6331 pack_dir_meta_after_first,
6332 "alternate catalog hits must not stat pack directories after warmup"
6333 );
6334 assert_eq!(
6335 pack_cache::test_dir_rescan_count(&local_pack),
6336 after_first,
6337 "alternate hits must not re-list the local pack directory"
6338 );
6339 assert_eq!(
6340 pack_cache::test_dir_rescan_count_on_store(&upstream_store, &upstream_pack),
6341 after_first_alt,
6342 "alternate hits must not re-list the alternate pack directory"
6343 );
6344
6345 let fresh = b"loose in primary after warmup\n";
6346 let local_oid = odb.write(ObjectKind::Blob, fresh).expect("local loose");
6347 let obj = odb.read(&local_oid).expect("read local loose after warmup");
6348 assert_eq!(obj.data.as_slice(), fresh);
6349 });
6350 }
6351
6352 #[cfg_attr(
6353 not(feature = "full-tests"),
6354 ignore = "starts system git or is heavy; run with --features full-tests"
6355 )]
6356 #[test]
6357 fn external_loose_write_visible_on_first_read_after_alternate_warmup() {
6358 use std::process::{Command, Stdio};
6359
6360 use crate::pack::clear_pack_cache;
6361
6362 let upstream = TempDir::new().unwrap();
6363 git_in(upstream.path(), &["init", "-q", "-b", "main"]);
6364 git_in(upstream.path(), &["config", "user.email", "t@example.com"]);
6365 git_in(upstream.path(), &["config", "user.name", "T"]);
6366 fs::write(upstream.path().join("seed.txt"), b"upstream seed").unwrap();
6367 git_in(upstream.path(), &["add", "seed.txt"]);
6368 git_in(upstream.path(), &["commit", "-m", "seed"]);
6369 git_in(upstream.path(), &["repack", "-a", "-d"]);
6370 let warm_oid = git_rev_parse_oid(upstream.path(), "HEAD");
6371
6372 let layout = TempDir::new().unwrap();
6373 git_in(
6374 layout.path(),
6375 &[
6376 "clone",
6377 "-q",
6378 "--shared",
6379 upstream.path().to_str().unwrap(),
6380 "shared",
6381 ],
6382 );
6383 let objects = layout.path().join("shared/.git/objects");
6384
6385 clear_pack_cache();
6386 let odb = Odb::new(&objects);
6387 odb.with_pack_read_context(|| {
6388 odb.read(&warm_oid).expect("warm alternate-packed commit");
6389 });
6390
6391 let payload = b"external loose after alternate warmup\n";
6392 let local_oid = odb.hash(ObjectKind::Blob, payload).unwrap();
6393 let mut child = Command::new("git")
6394 .args(["hash-object", "-w", "--stdin"])
6395 .stdin(Stdio::piped())
6396 .stdout(Stdio::piped())
6397 .env("GIT_OBJECT_DIRECTORY", &objects)
6398 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6399 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6400 .spawn()
6401 .expect("git hash-object");
6402 if let Some(mut stdin) = child.stdin.take() {
6403 use std::io::Write;
6404 stdin.write_all(payload).unwrap();
6405 }
6406 let out = child.wait_with_output().expect("git wait");
6407 assert!(
6408 out.status.success(),
6409 "git hash-object: {}",
6410 String::from_utf8_lossy(&out.stderr)
6411 );
6412 assert_eq!(
6413 String::from_utf8_lossy(&out.stdout).trim(),
6414 local_oid.to_hex()
6415 );
6416
6417 let obj = odb
6418 .read(&local_oid)
6419 .expect("first read after external primary loose write must succeed");
6420 assert_eq!(obj.data.as_slice(), payload);
6421 }
6422
6423 #[test]
6424 fn loose_only_read_records_loose_path_open() {
6425 use crate::hot_path_test_metrics::HotPathMetricsScope;
6426
6427 let dir = TempDir::new().unwrap();
6428 let objects = dir.path().join("objects");
6429 fs::create_dir_all(&objects).unwrap();
6430 let odb = Odb::new(&objects);
6431 let oid = odb
6432 .write(ObjectKind::Blob, b"loose-only")
6433 .expect("write loose");
6434 odb.hot_path_metrics().set_loose_open_counting(true);
6435 odb.hot_path_metrics().reset_loose_path_open_attempts();
6436 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
6437
6438 odb.read(&oid).expect("loose read");
6439 assert_eq!(
6440 odb.hot_path_metrics().loose_path_open_attempts(),
6441 1,
6442 "loose fallback must record a path open attempt"
6443 );
6444 }
6445
6446 #[cfg_attr(
6447 not(feature = "full-tests"),
6448 ignore = "starts system git or is heavy; run with --features full-tests"
6449 )]
6450 #[test]
6451 fn loose_and_packed_copies_return_identical_bytes() {
6452 use crate::pack::clear_pack_cache;
6453
6454 let dir = TempDir::new().unwrap();
6455 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6456 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6457 git_in(dir.path(), &["config", "user.name", "T"]);
6458 fs::write(dir.path().join("dup.txt"), b"duplicate loose and packed\n").unwrap();
6459 git_in(dir.path(), &["add", "dup.txt"]);
6460 git_in(dir.path(), &["commit", "-m", "seed"]);
6461 let objects = dir.path().join(".git/objects");
6462 let tree_oid = git_rev_parse_oid(dir.path(), "HEAD^{tree}");
6463 git_in(dir.path(), &["repack", "-a"]);
6464 clear_pack_cache();
6465 let odb = Odb::new(&objects);
6466 let loose_path = tree_oid.loose_path_in(&objects);
6467 assert!(
6468 loose_path.is_file(),
6469 "loose copy must remain after repack -a"
6470 );
6471 let loose = Odb::read_loose_verify_oid(&loose_path, &tree_oid).expect("loose tree");
6472 let packed = odb.read(&tree_oid).expect("packed tree");
6473 assert_eq!(packed.data, loose.data);
6474 assert_eq!(packed.kind, loose.kind);
6475 }
6476
6477 #[cfg_attr(
6478 not(feature = "full-tests"),
6479 ignore = "starts system git or is heavy; run with --features full-tests"
6480 )]
6481 #[test]
6482 fn freshly_written_loose_object_visible_before_repack() {
6483 let dir = TempDir::new().unwrap();
6484 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6485 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6486 git_in(dir.path(), &["config", "user.name", "T"]);
6487 fs::write(dir.path().join("seed.txt"), b"seed").unwrap();
6488 git_in(dir.path(), &["add", "seed.txt"]);
6489 git_in(dir.path(), &["commit", "-m", "seed"]);
6490 git_in(dir.path(), &["repack", "-a", "-d"]);
6491 let objects = dir.path().join(".git/objects");
6492 let odb = Odb::new(&objects);
6493 let fresh = b"brand-new loose blob\n";
6494 let oid = odb.write(ObjectKind::Blob, fresh).expect("loose write");
6495 let obj = odb.read(&oid).expect("immediate loose read");
6496 assert_eq!(obj.data.as_slice(), fresh);
6497 }
6498
6499 #[cfg_attr(
6500 not(feature = "full-tests"),
6501 ignore = "starts system git or is heavy; run with --features full-tests"
6502 )]
6503 #[test]
6504 fn pack_added_after_first_read_found_via_odb_reprepare() {
6505 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
6506
6507 let _guard = pack_cache_test_guard();
6508 clear_pack_cache();
6509 let dir = TempDir::new().unwrap();
6510 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6511 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6512 git_in(dir.path(), &["config", "user.name", "T"]);
6513 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
6514 git_in(dir.path(), &["add", "a.txt"]);
6515 git_in(dir.path(), &["commit", "-m", "a"]);
6516 git_in(dir.path(), &["repack", "-a", "-d"]);
6517 let objects = dir.path().join(".git/objects");
6518 let pack_dir = objects.join("pack");
6519 let git_dir = dir.path().join(".git");
6520
6521 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
6522 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
6523 odb.read(&warm_oid).expect("warm read");
6524
6525 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
6526 git_in(dir.path(), &["add", "b.txt"]);
6527 git_in(dir.path(), &["commit", "-m", "b"]);
6528 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
6529
6530 std::thread::sleep(std::time::Duration::from_secs(1));
6531 filetime::set_file_mtime(&pack_dir, filetime::FileTime::now()).unwrap();
6532
6533 let obj = odb.read(&new_oid).expect("new pack via reprepare");
6534 assert_eq!(obj.kind, ObjectKind::Commit);
6535 }
6536
6537 #[cfg_attr(
6538 not(feature = "full-tests"),
6539 ignore = "starts system git or is heavy; run with --features full-tests"
6540 )]
6541 #[test]
6542 fn external_pack_drop_in_visible_after_empty_primary_miss() {
6543 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
6544
6545 let _guard = pack_cache_test_guard();
6546 clear_pack_cache();
6547 let source = TempDir::new().unwrap();
6548 git_in(source.path(), &["init", "-q", "-b", "main"]);
6549 git_in(source.path(), &["config", "user.email", "t@example.com"]);
6550 git_in(source.path(), &["config", "user.name", "T"]);
6551 fs::write(source.path().join("drop-in.txt"), b"external pack payload").unwrap();
6552 git_in(source.path(), &["add", "drop-in.txt"]);
6553 git_in(source.path(), &["commit", "-m", "drop-in"]);
6554 git_in(source.path(), &["repack", "-a", "-d"]);
6555 let commit_oid = git_rev_parse_oid(source.path(), "HEAD");
6556 let source_pack = source.path().join(".git/objects/pack");
6557 let (pack_path, idx_path) = fs::read_dir(&source_pack)
6558 .unwrap()
6559 .filter_map(|e| e.ok())
6560 .map(|e| e.path())
6561 .find_map(|path| {
6562 path.extension()
6563 .is_some_and(|ext| ext == "pack")
6564 .then(|| (path.clone(), path.with_extension("idx")))
6565 })
6566 .expect("source pack");
6567
6568 let bare = TempDir::new().unwrap();
6569 let objects = bare.path().join("objects");
6570 fs::create_dir_all(objects.join("pack")).unwrap();
6571 let odb = Odb::new(&objects);
6572 assert!(
6573 odb.read(&commit_oid).is_err(),
6574 "empty primary must miss before the pack exists"
6575 );
6576
6577 std::thread::sleep(std::time::Duration::from_secs(1));
6578 let dest_pack = objects
6579 .join("pack")
6580 .join(pack_path.file_name().expect("pack basename"));
6581 let dest_idx = objects
6582 .join("pack")
6583 .join(idx_path.file_name().expect("idx basename"));
6584 fs::copy(&pack_path, &dest_pack).unwrap();
6585 fs::copy(&idx_path, &dest_idx).unwrap();
6586
6587 let obj = odb
6588 .read(&commit_oid)
6589 .expect("external pack drop-in must be visible after reprepare");
6590 assert_eq!(obj.kind, ObjectKind::Commit);
6591 }
6592
6593 #[cfg_attr(
6594 not(feature = "full-tests"),
6595 ignore = "starts system git or is heavy; run with --features full-tests"
6596 )]
6597 #[test]
6598 fn late_midx_after_warm_read_resolves_new_commit() {
6599 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
6600
6601 let _guard = pack_cache_test_guard();
6602 clear_pack_cache();
6603 let dir = TempDir::new().unwrap();
6604 git_in(dir.path(), &["init", "-q", "-b", "main"]);
6605 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
6606 git_in(dir.path(), &["config", "user.name", "T"]);
6607 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
6608 git_in(dir.path(), &["add", "a.txt"]);
6609 git_in(dir.path(), &["commit", "-m", "a"]);
6610 git_in(dir.path(), &["repack", "-a", "-d"]);
6611 let objects = dir.path().join(".git/objects");
6612 let git_dir = dir.path().join(".git");
6613 assert!(
6614 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_none(),
6615 "fixture starts without a MIDX"
6616 );
6617
6618 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
6619 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
6620 odb.read(&warm_oid).expect("warm read before MIDX exists");
6621
6622 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
6623 git_in(dir.path(), &["add", "b.txt"]);
6624 git_in(dir.path(), &["commit", "-m", "b"]);
6625 git_in(dir.path(), &["repack", "-a", "-d"]);
6626 git_in(dir.path(), &["multi-pack-index", "write"]);
6627 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
6628
6629 let cat = git_run(dir.path(), &["cat-file", "-e", &new_oid.to_hex()]);
6630 assert!(
6631 cat.status.success(),
6632 "system git must resolve the new commit"
6633 );
6634
6635 let obj = odb
6636 .read(&new_oid)
6637 .expect("MIDX-covered commit after late MIDX write");
6638 assert_eq!(obj.kind, ObjectKind::Commit);
6639 assert!(
6640 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_some(),
6641 "successful read after late MIDX must refresh the tip cache"
6642 );
6643 }
6644}