1pub mod builder;
25mod commit;
26mod peel;
27pub mod store;
28mod stream;
29
30#[doc(inline)]
31pub use builder::OdbBuilder;
32
33pub(crate) use store::loose::{
34 decompress_zlib_loose_bytes, for_each_loose_object_id, parse_object_bytes,
35 read_loose_object_info,
36};
37#[doc(inline)]
38pub use store::LooseStore;
39pub use store::{CompositeStore, FilesSource, ObjectStream};
40#[doc(inline)]
41pub use store::{MemoryStore, ObjectStore, WritableObjectStore};
42
43use std::collections::{HashMap, HashSet};
44use std::fs;
45use std::hash::{Hash, Hasher};
46use std::ops::ControlFlow;
47use std::path::{Path, PathBuf};
48#[cfg(test)]
49use std::sync::atomic::AtomicUsize;
50use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
51use std::sync::{Arc, Mutex, OnceLock, RwLock};
52
53use crate::config::ConfigSet;
54use crate::diagnostics::NullDiagnostics;
55use crate::error::{Error, Result};
56use crate::hash;
57use crate::midx::validate_midx_referenced_packs_with_diagnostics;
58use crate::objects::{
59 oid_hash_map, oid_hash_set, HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind, OidHashMap,
60 OidHashSet,
61};
62use crate::pack;
63use crate::pack_store::PackStore;
64use filetime::{set_file_mtime, FileTime};
65use flate2::Compression;
66
67pub(crate) use store::BoundedRead;
68use store::{LocalSizedRead, PackedObjects};
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub(crate) enum UnreachablePackRetention {
73 KeepAll,
75 KeepGraceProtected(std::time::SystemTime),
77 DropAll,
79}
80
81type MemOdbOverlayStore = Arc<RwLock<Option<Arc<MemoryStore>>>>;
82
83struct AlternateSourcesCache {
85 file_generation: u64,
86 env_fingerprint: u64,
87 submodule_generation: u64,
88 sources: Option<Arc<CompositeStore>>,
89}
90
91struct AlternateSourcesKey {
92 file_generation: u64,
93 env_fingerprint: u64,
94 submodule_generation: u64,
95}
96
97struct FileAlternatesCache {
99 generation: u64,
100 snapshot: Option<Arc<Vec<PathBuf>>>,
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105pub struct WriteOptions {
106 pub silent: bool,
108 pub assume_loose_only_existence: bool,
114 pub trust_new_loose: bool,
117 pub loose_prefix_dirs_ready: bool,
120}
121
122pub const BULK_NEW_LOOSE_WRITE: WriteOptions = WriteOptions {
124 assume_loose_only_existence: true,
125 trust_new_loose: true,
126 silent: false,
127 loose_prefix_dirs_ready: true,
128};
129
130pub const BULK_TRACKED_RESTAGE_LOOSE_WRITE: WriteOptions = WriteOptions {
132 assume_loose_only_existence: true,
133 trust_new_loose: false,
134 silent: false,
135 loose_prefix_dirs_ready: true,
136};
137
138fn exists_materialized_in_objects_dir(objects_dir: &Path, oid: &ObjectId) -> bool {
144 store::local_object_materialized(objects_dir, oid)
145}
146
147fn filter_oids(
149 oids: impl IntoIterator<Item = ObjectId>,
150 mut keep: impl FnMut(&ObjectId) -> Result<bool>,
151) -> Result<Vec<ObjectId>> {
152 oids.into_iter()
153 .filter_map(|oid| keep(&oid).map(|k| k.then_some(oid)).transpose())
154 .collect()
155}
156
157fn objects_dir_has_pack_index_files(objects_dir: &Path) -> bool {
158 let pack_dir = objects_dir.join("pack");
159 let Ok(entries) = fs::read_dir(pack_dir) else {
160 return false;
161 };
162 entries
163 .filter_map(|e| e.ok())
164 .any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
165}
166
167#[derive(Clone)]
187pub struct Odb {
188 objects_dir: PathBuf,
189 work_tree: Option<PathBuf>,
191 submodule_alternate_dirs: Arc<Mutex<Vec<PathBuf>>>,
193 config_git_dir: Option<PathBuf>,
195 core_multi_pack_index_cache: Arc<OnceLock<bool>>,
201 local_pack_indexes_seen: Arc<OnceLock<bool>>,
203 file_alternate_dirs_cache: Arc<RwLock<FileAlternatesCache>>,
205 #[cfg(test)]
206 exists_probe: Arc<AtomicUsize>,
207 #[cfg(test)]
208 pub(crate) hot_path_test_metrics: Arc<crate::hot_path_test_metrics::HotPathTestMetrics>,
209 env_alternate_dirs: Vec<PathBuf>,
211 env_alternate_lazy: Arc<OnceLock<Arc<Vec<PathBuf>>>>,
213 mem_overlay: MemOdbOverlayStore,
218 mem_overlay_active: Arc<AtomicBool>,
220 primary_source: Arc<OnceLock<Arc<FilesSource>>>,
222 alternate_sources_cache: Arc<RwLock<AlternateSourcesCache>>,
224 midx_packs_validated: Arc<OnceLock<()>>,
226 hash_algo_cache: Arc<OnceLock<HashAlgo>>,
230 loose_zlib_cache: Arc<OnceLock<Compression>>,
232 pack_zlib_cache: Arc<OnceLock<Compression>>,
234 big_file_threshold_cache: Arc<OnceLock<u64>>,
236 shared_config_state: Option<crate::repo::RepositoryConfigSnapshot>,
238 delta_base_cache_sync: Arc<OnceLock<()>>,
240 shared_environment: Option<Arc<crate::environment::Environment>>,
242 empty_config_env: Arc<OnceLock<crate::environment::Environment>>,
244 freshened_packs: Arc<Mutex<HashSet<PathBuf>>>,
247 pack_store: Arc<PackStore>,
249 alternate_pack_stores: Arc<Mutex<HashMap<PathBuf, Arc<PackStore>>>>,
251 submodule_sources_generation: Arc<AtomicU64>,
253 custom_primary: Option<Arc<dyn WritableObjectStore>>,
255 extra_read_sources: Vec<Arc<dyn ObjectStore>>,
257 alternates_enabled: bool,
259 session_loose_reads: Arc<std::sync::Mutex<OidHashMap<Object>>>,
261 session_tree_entries: Arc<Mutex<SessionTreeEntries>>,
263}
264
265const SESSION_BOUNDED_LOOSE_CACHE_MAX: usize = 64 * 1024;
269
270#[derive(Default)]
272struct SessionTreeEntries {
273 trees: OidHashMap<Arc<Vec<crate::objects::TreeEntry>>>,
274 entries: usize,
275}
276
277impl SessionTreeEntries {
278 const ENTRY_BUDGET: usize = 1 << 16;
283
284 fn insert(&mut self, oid: ObjectId, tree: Arc<Vec<crate::objects::TreeEntry>>) {
285 if tree.len() > Self::ENTRY_BUDGET {
286 return;
287 }
288 if self.entries + tree.len() > Self::ENTRY_BUDGET {
289 self.trees.clear();
290 self.entries = 0;
291 }
292 self.entries += tree.len();
293 self.trees.insert(oid, tree);
294 }
295}
296
297#[cfg(test)]
298mod session_tree_entries_tests {
299 use super::*;
300 use crate::objects::TreeEntry;
301
302 fn tree(entries: usize) -> Arc<Vec<TreeEntry>> {
303 let entry = TreeEntry {
304 mode: 0o100644,
305 name: b"f".to_vec(),
306 oid: ObjectId::zero(),
307 };
308 Arc::new(vec![entry; entries])
309 }
310
311 fn oid(n: u8) -> ObjectId {
312 ObjectId::from_bytes(&[n; 20]).expect("oid")
313 }
314
315 #[test]
316 fn cache_starts_over_when_the_entry_budget_is_exceeded() {
317 let mut cache = SessionTreeEntries::default();
318 let half = SessionTreeEntries::ENTRY_BUDGET / 2;
319 cache.insert(oid(1), tree(half));
320 cache.insert(oid(2), tree(half));
321 assert_eq!(cache.entries, SessionTreeEntries::ENTRY_BUDGET);
322 assert_eq!(cache.trees.len(), 2);
323
324 cache.insert(oid(3), tree(1));
325 assert_eq!(cache.trees.len(), 1);
326 assert!(cache.trees.contains_key(&oid(3)));
327 assert_eq!(cache.entries, 1);
328
329 cache.insert(oid(4), tree(SessionTreeEntries::ENTRY_BUDGET + 1));
330 assert!(
331 !cache.trees.contains_key(&oid(4)),
332 "an oversized tree is not cached"
333 );
334 assert!(cache.trees.contains_key(&oid(3)));
335 assert!(cache.entries <= SessionTreeEntries::ENTRY_BUDGET);
336 }
337}
338
339const _: () = {
340 fn assert_send_sync<T: Send + Sync>() {}
341 let _ = assert_send_sync::<Odb>;
342};
343
344impl std::fmt::Debug for Odb {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("Odb")
347 .field("objects_dir", &self.objects_dir)
348 .field("work_tree", &self.work_tree)
349 .field("submodule_alternate_dirs", &"<mutex>")
350 .field("config_git_dir", &self.config_git_dir)
351 .finish()
352 }
353}
354
355#[must_use]
360pub fn hash_algo_for_git_dir(env: &crate::environment::Environment, git_dir: &Path) -> HashAlgo {
361 ConfigSet::load(env, Some(git_dir), true)
362 .ok()
363 .and_then(|cfg| cfg.get("extensions.objectformat"))
364 .and_then(|v| HashAlgo::from_name(&v))
365 .unwrap_or(HashAlgo::Sha1)
366}
367
368#[must_use]
370pub fn hash_algo_for_objects_dir(
371 env: &crate::environment::Environment,
372 objects_dir: &Path,
373) -> HashAlgo {
374 objects_dir
375 .parent()
376 .map(|d| hash_algo_for_git_dir(env, d))
377 .unwrap_or(HashAlgo::Sha1)
378}
379
380impl Odb {
381 #[must_use]
388 pub fn new(objects_dir: &Path) -> Self {
389 Self {
390 objects_dir: objects_dir.to_path_buf(),
391 work_tree: None,
392 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
393 config_git_dir: None,
394 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
395 local_pack_indexes_seen: Arc::new(OnceLock::new()),
396 file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
397 generation: 0,
398 snapshot: None,
399 })),
400 env_alternate_dirs: Vec::new(),
401 env_alternate_lazy: Arc::new(OnceLock::new()),
402 mem_overlay: Arc::new(RwLock::new(None)),
403 mem_overlay_active: Arc::new(AtomicBool::new(false)),
404 primary_source: Arc::new(OnceLock::new()),
405 alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
406 file_generation: 0,
407 env_fingerprint: 0,
408 submodule_generation: 0,
409 sources: None,
410 })),
411 midx_packs_validated: Arc::new(OnceLock::new()),
412 hash_algo_cache: Arc::new(OnceLock::new()),
413 loose_zlib_cache: Arc::new(OnceLock::new()),
414 pack_zlib_cache: Arc::new(OnceLock::new()),
415 big_file_threshold_cache: Arc::new(OnceLock::new()),
416 shared_config_state: None,
417 delta_base_cache_sync: Arc::new(OnceLock::new()),
418 shared_environment: None,
419 empty_config_env: Arc::new(OnceLock::new()),
420 freshened_packs: Arc::new(Mutex::new(HashSet::new())),
421 pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
422 alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
423 submodule_sources_generation: Arc::new(AtomicU64::new(0)),
424 custom_primary: None,
425 extra_read_sources: Vec::new(),
426 alternates_enabled: true,
427 session_loose_reads: Arc::new(Mutex::new(oid_hash_map())),
428 session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
429 #[cfg(test)]
430 exists_probe: Arc::new(AtomicUsize::new(0)),
431 #[cfg(test)]
432 hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
433 }
434 }
435
436 #[must_use]
441 pub fn with_work_tree(objects_dir: &Path, work_tree: &Path) -> Self {
442 Self {
443 objects_dir: objects_dir.to_path_buf(),
444 work_tree: Some(work_tree.to_path_buf()),
445 submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
446 config_git_dir: None,
447 core_multi_pack_index_cache: Arc::new(OnceLock::new()),
448 local_pack_indexes_seen: Arc::new(OnceLock::new()),
449 file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
450 generation: 0,
451 snapshot: None,
452 })),
453 env_alternate_dirs: Vec::new(),
454 env_alternate_lazy: Arc::new(OnceLock::new()),
455 mem_overlay: Arc::new(RwLock::new(None)),
456 mem_overlay_active: Arc::new(AtomicBool::new(false)),
457 primary_source: Arc::new(OnceLock::new()),
458 alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
459 file_generation: 0,
460 env_fingerprint: 0,
461 submodule_generation: 0,
462 sources: None,
463 })),
464 midx_packs_validated: Arc::new(OnceLock::new()),
465 hash_algo_cache: Arc::new(OnceLock::new()),
466 loose_zlib_cache: Arc::new(OnceLock::new()),
467 pack_zlib_cache: Arc::new(OnceLock::new()),
468 big_file_threshold_cache: Arc::new(OnceLock::new()),
469 shared_config_state: None,
470 delta_base_cache_sync: Arc::new(OnceLock::new()),
471 shared_environment: None,
472 empty_config_env: Arc::new(OnceLock::new()),
473 freshened_packs: Arc::new(Mutex::new(HashSet::new())),
474 pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
475 alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
476 submodule_sources_generation: Arc::new(AtomicU64::new(0)),
477 custom_primary: None,
478 extra_read_sources: Vec::new(),
479 alternates_enabled: true,
480 session_loose_reads: Arc::new(Mutex::new(oid_hash_map())),
481 session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
482 #[cfg(test)]
483 exists_probe: Arc::new(AtomicUsize::new(0)),
484 #[cfg(test)]
485 hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
486 }
487 }
488
489 pub(crate) fn cached_tree_entries(
491 &self,
492 oid: &ObjectId,
493 ) -> Result<Arc<Vec<crate::objects::TreeEntry>>> {
494 if let Ok(guard) = self.session_tree_entries.lock() {
495 if let Some(entries) = guard.trees.get(oid) {
496 return Ok(Arc::clone(entries));
497 }
498 }
499 let obj = self.read(oid)?;
500 if obj.kind != ObjectKind::Tree {
501 return Err(Error::CorruptObject(format!(
502 "expected tree, got {}",
503 obj.kind.as_str()
504 )));
505 }
506 let parsed = crate::objects::parse_tree_with_oid_len(&obj.data, self.hash_algo().len())?;
507 let arc = Arc::new(parsed);
508 if let Ok(mut guard) = self.session_tree_entries.lock() {
509 guard.insert(*oid, Arc::clone(&arc));
510 }
511 Ok(arc)
512 }
513
514 fn session_cached_loose_read(&self, oid: &ObjectId) -> Option<Object> {
515 self.session_loose_reads.lock().ok()?.get(oid).cloned()
516 }
517
518 fn remember_session_loose_read(&self, oid: &ObjectId, obj: &Object) {
519 if let Ok(mut guard) = self.session_loose_reads.lock() {
520 if guard.len() > 256 {
521 guard.clear();
522 }
523 guard.insert(*oid, obj.clone());
524 }
525 }
526
527 fn finish_session_loose_read(&self, oid: &ObjectId, obj: Object) -> Result<Object> {
528 self.remember_session_loose_read(oid, &obj);
529 Ok(obj)
530 }
531
532 #[cfg(test)]
534 #[must_use]
535 pub(crate) fn hot_path_metrics(&self) -> &crate::hot_path_test_metrics::HotPathTestMetrics {
536 &self.hot_path_test_metrics
537 }
538
539 #[cfg(test)]
540 #[must_use]
541 pub(crate) fn hot_path_metrics_arc(
542 &self,
543 ) -> Arc<crate::hot_path_test_metrics::HotPathTestMetrics> {
544 Arc::clone(&self.hot_path_test_metrics)
545 }
546
547 #[must_use]
549 pub(crate) fn with_shared_config_state(
550 mut self,
551 state: crate::repo::RepositoryConfigSnapshot,
552 env: Arc<crate::environment::Environment>,
553 ) -> Self {
554 self.pack_store.set_repo_caches(Arc::clone(&state));
555 self.shared_config_state = Some(state);
556 self.shared_environment = Some(env);
557 self
558 }
559
560 fn diagnostics_handle(&self) -> crate::diagnostics::DiagnosticsHandle {
561 self.shared_config_state
562 .as_ref()
563 .map(|state| state.diagnostics_handle())
564 .unwrap_or_else(|| Arc::new(NullDiagnostics))
565 }
566
567 fn load_config_cascade(&self) -> Result<ConfigSet> {
568 if let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment) {
569 let git_dir = self
570 .config_git_dir
571 .as_deref()
572 .or_else(|| self.objects_dir.parent());
573 let cfg = caches.load_config(env.as_ref(), git_dir, true)?;
574 return Ok(cfg.as_ref().clone());
575 }
576 let git_dir = self
577 .config_git_dir
578 .as_deref()
579 .or_else(|| self.objects_dir.parent());
580 if let Some(git_dir) = git_dir {
581 ConfigSet::load(self.config_environment(), Some(git_dir), true)
582 } else {
583 Ok(ConfigSet::new())
584 }
585 }
586
587 fn config_environment(&self) -> &crate::environment::Environment {
588 if let Some(env) = self.shared_environment.as_deref() {
589 return env;
590 }
591 self.empty_config_env
592 .get_or_init(crate::environment::Environment::empty)
593 }
594
595 fn env_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
596 if !self.env_alternate_dirs.is_empty() {
597 return Arc::new(self.env_alternate_dirs.clone());
598 }
599 Arc::clone(self.env_alternate_lazy.get_or_init(|| {
600 Arc::new(Self::env_alternate_dirs_from_var(
601 self.config_environment(),
602 self.work_tree.as_deref(),
603 ))
604 }))
605 }
606
607 #[must_use]
611 pub fn env_alternate_dirs_from_var(
612 env: &crate::environment::Environment,
613 resolve_base: Option<&Path>,
614 ) -> Vec<PathBuf> {
615 match env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES") {
616 Some(val) if !val.is_empty() => {
617 let mut dirs = parse_alternate_env(&val);
618 if let Some(base) = resolve_base {
619 for dir in &mut dirs {
620 if dir.is_relative() {
621 *dir = base.join(&dir);
622 }
623 }
624 }
625 dirs
626 }
627 _ => Vec::new(),
628 }
629 }
630
631 #[must_use]
633 pub fn with_env_alternate_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
634 self.env_alternate_dirs = dirs;
635 self.clear_alternate_sources_cache();
636 self
637 }
638
639 fn clear_alternate_sources_cache(&self) {
640 if let Ok(mut guard) = self.alternate_sources_cache.write() {
641 guard.sources = None;
642 }
643 }
644
645 fn alternate_sources_key(&self) -> AlternateSourcesKey {
646 AlternateSourcesKey {
647 file_generation: self
648 .file_alternate_dirs_cache
649 .read()
650 .map(|g| g.generation)
651 .unwrap_or(0),
652 env_fingerprint: self.env_alternate_fingerprint(),
653 submodule_generation: self.submodule_sources_generation.load(Ordering::Relaxed),
654 }
655 }
656
657 fn env_alternate_fingerprint(&self) -> u64 {
658 let mut hasher = std::collections::hash_map::DefaultHasher::new();
659 for path in self.env_alternate_dirs_snapshot().iter() {
660 path.hash(&mut hasher);
661 }
662 hasher.finish()
663 }
664
665 pub fn invalidate_alternates_cache(&self) {
667 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
668 guard.generation = guard.generation.wrapping_add(1);
669 guard.snapshot = None;
670 }
671 self.clear_alternate_sources_cache();
672 }
673
674 pub fn refresh_file_alternates_from_disk(&self) -> Result<()> {
680 let snapshot = Arc::new(pack::read_alternates_recursive(&self.objects_dir)?);
681 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
682 guard.generation = guard.generation.wrapping_add(1);
683 guard.snapshot = Some(snapshot);
684 }
685 self.clear_alternate_sources_cache();
686 Ok(())
687 }
688
689 pub fn append_file_alternate(&self, alternate_objects_dir: &Path) -> Result<()> {
695 Self::append_alternate_objects_line(&self.objects_dir, alternate_objects_dir)?;
696 self.refresh_file_alternates_from_disk()?;
697 Ok(())
698 }
699
700 pub fn append_alternate_objects_line(
705 objects_dir: &Path,
706 alternate_objects_dir: &Path,
707 ) -> Result<()> {
708 let info = objects_dir.join("info");
709 fs::create_dir_all(&info)?;
710 let alt_path = info.join("alternates");
711 let alt_abs = alternate_objects_dir
712 .canonicalize()
713 .unwrap_or_else(|_| alternate_objects_dir.to_path_buf());
714 let line = format!("{}\n", alt_abs.display());
715 let existing = match fs::read_to_string(&alt_path) {
716 Ok(s) => s,
717 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
718 Err(e) => return Err(Error::Io(e)),
719 };
720 if existing
721 .lines()
722 .any(|l| l.trim() == alt_abs.to_string_lossy())
723 {
724 return Ok(());
725 }
726 let mut out = existing;
727 out.push_str(&line);
728 fs::write(alt_path, out).map_err(Error::Io)
729 }
730
731 fn file_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
732 loop {
733 let observed_gen = self
734 .file_alternate_dirs_cache
735 .read()
736 .map(|g| g.generation)
737 .unwrap_or(0);
738 if let Ok(guard) = self.file_alternate_dirs_cache.read() {
739 if guard.generation == observed_gen {
740 if let Some(snapshot) = &guard.snapshot {
741 return Arc::clone(snapshot);
742 }
743 }
744 }
745 if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
746 if guard.generation != observed_gen {
747 continue;
748 }
749 if let Some(snapshot) = &guard.snapshot {
750 return Arc::clone(snapshot);
751 }
752 let snapshot = Arc::new(
753 pack::read_alternates_recursive(&self.objects_dir).unwrap_or_default(),
754 );
755 guard.snapshot = Some(Arc::clone(&snapshot));
756 return snapshot;
757 }
758 break;
759 }
760 Arc::new(Vec::new())
761 }
762
763 #[cfg(test)]
764 pub(crate) fn exists_probe_count(&self) -> usize {
765 self.exists_probe.load(Ordering::Relaxed)
766 }
767
768 #[cfg(test)]
769 pub(crate) fn reset_exists_probe(&self) {
770 self.exists_probe.store(0, Ordering::Relaxed);
771 }
772
773 pub fn enable_mem_overlay(&self) {
778 self.mem_overlay_active.store(true, Ordering::Release);
779 if let Ok(mut guard) = self.mem_overlay.write() {
780 if guard.is_none() {
781 *guard = Some(Arc::new(MemoryStore::new(self.hash_algo())));
782 }
783 }
784 }
785
786 pub fn disable_mem_overlay(&self) {
788 self.mem_overlay_active.store(false, Ordering::Release);
789 if let Ok(mut guard) = self.mem_overlay.write() {
790 *guard = None;
791 }
792 }
793
794 #[must_use]
796 pub(crate) fn is_mem_overlay_active(&self) -> bool {
797 self.mem_overlay_active.load(Ordering::Acquire)
798 }
799
800 fn overlay_active(&self) -> bool {
801 self.mem_overlay
802 .read()
803 .ok()
804 .and_then(|g| g.as_ref().map(|_| ()))
805 .is_some()
806 }
807
808 fn overlay_store(&self) -> Option<Arc<MemoryStore>> {
809 if !self.mem_overlay_active.load(Ordering::Acquire) {
810 return None;
811 }
812 self.mem_overlay.read().ok().and_then(|g| g.clone())
813 }
814
815 #[cfg(test)]
817 pub(crate) fn mem_overlay_len_for_tests(&self) -> Option<usize> {
818 let store = self.overlay_store()?;
819 let mut count = 0usize;
820 store
821 .for_each_object(&mut |_| {
822 count += 1;
823 ControlFlow::Continue(())
824 })
825 .ok()?;
826 Some(count)
827 }
828
829 fn overlay_store_object(&self, oid: ObjectId, kind: ObjectKind, data: &[u8]) -> bool {
832 let Some(store) = self.overlay_store() else {
833 return false;
834 };
835 store.insert_object(oid, kind, Arc::from(data.to_owned()));
836 true
837 }
838
839 fn overlay_read(&self, oid: &ObjectId) -> Option<Object> {
841 let store = self.overlay_store()?;
842 store.read(oid).ok().flatten()
843 }
844
845 pub fn primary(&self) -> Result<Arc<FilesSource>> {
851 self.require_files_primary("primary")?;
852 if let Some(src) = self.primary_source.get() {
853 return Ok(Arc::clone(src));
854 }
855 let src = self.build_files_source(
856 &self.objects_dir,
857 self.multi_pack_index_reads_enabled(),
858 self.midx_tip_listing_enabled(),
859 )?;
860 match self.primary_source.set(Arc::clone(&src)) {
861 Ok(()) => Ok(src),
862 Err(existing) => Ok(existing),
863 }
864 }
865
866 #[must_use]
868 fn alternate_object_lookup_needed(&self) -> bool {
869 if !self.file_alternate_dirs_snapshot().is_empty() {
870 return true;
871 }
872 if !self.env_alternate_dirs_snapshot().is_empty() {
873 return true;
874 }
875 self.submodule_alternate_dirs
876 .lock()
877 .ok()
878 .is_some_and(|guard| !guard.is_empty())
879 }
880
881 pub fn sources(&self) -> Result<Arc<CompositeStore>> {
887 if !self.alternates_enabled {
888 return Ok(Arc::new(CompositeStore::new(Vec::new(), self.hash_algo())));
889 }
890 let key = self.alternate_sources_key();
891 if let Ok(guard) = self.alternate_sources_cache.read() {
892 if guard.file_generation == key.file_generation
893 && guard.env_fingerprint == key.env_fingerprint
894 && guard.submodule_generation == key.submodule_generation
895 {
896 if let Some(sources) = guard.sources.as_ref() {
897 return Ok(Arc::clone(sources));
898 }
899 }
900 }
901 let use_midx = self.multi_pack_index_reads_enabled();
902 let midx_tip = self.midx_tip_listing_enabled();
903 let mut stores: Vec<Arc<dyn ObjectStore>> = Vec::new();
904 for alt_dir in self.file_alternate_dirs_snapshot().iter() {
905 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
906 }
907 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
908 stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
909 }
910 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
911 for alt_dir in guard.iter() {
912 stores.push(self.build_files_source(alt_dir, false, false)?);
913 }
914 }
915 let composite = Arc::new(CompositeStore::new(stores, self.hash_algo()));
916 if let Ok(mut guard) = self.alternate_sources_cache.write() {
917 guard.file_generation = key.file_generation;
918 guard.env_fingerprint = key.env_fingerprint;
919 guard.submodule_generation = key.submodule_generation;
920 guard.sources = Some(Arc::clone(&composite));
921 }
922 Ok(composite)
923 }
924
925 fn midx_tip_listing_enabled(&self) -> bool {
926 self.config_git_dir.is_some() && self.core_multi_pack_index_enabled()
927 }
928
929 fn build_files_source(
930 &self,
931 objects_dir: &Path,
932 use_midx: bool,
933 midx_tip_listing_for_exists: bool,
934 ) -> Result<Arc<FilesSource>> {
935 let loose = self.loose_at(objects_dir)?;
936 let pack_store = self.pack_store_for(objects_dir);
937 let diagnostics = if objects_dir == self.objects_dir.as_path() {
938 self.diagnostics_handle()
939 } else {
940 Arc::new(NullDiagnostics)
941 };
942 FilesSource::open(
943 pack_store,
944 loose,
945 use_midx,
946 midx_tip_listing_for_exists,
947 diagnostics,
948 )
949 .map(Arc::new)
950 }
951
952 pub fn register_submodule_object_directories_from_index(
956 &self,
957 work_tree: &Path,
958 index: &crate::index::Index,
959 ) {
960 use crate::diff::submodule_embedded_git_dir;
961
962 let Ok(mut dirs) = self.submodule_alternate_dirs.lock() else {
963 return;
964 };
965 dirs.clear();
966 for e in &index.entries {
967 if e.stage() != 0 || e.mode != crate::index::MODE_GITLINK {
968 continue;
969 }
970 let path_str = String::from_utf8_lossy(&e.path);
971 let abs = work_tree.join(path_str.as_ref());
972 let Some(sub_git) = submodule_embedded_git_dir(&abs) else {
973 continue;
974 };
975 let objects = sub_git.join("objects");
976 if !objects.is_dir() {
977 continue;
978 }
979 let canon = objects.canonicalize().unwrap_or(objects);
980 if !dirs.iter().any(|p| p == &canon) {
981 dirs.push(canon);
982 }
983 }
984 self.submodule_sources_generation
985 .fetch_add(1, Ordering::Relaxed);
986 self.clear_alternate_sources_cache();
987 }
988
989 #[must_use]
991 pub fn with_config_git_dir(mut self, git_dir: PathBuf) -> Self {
992 self.config_git_dir = Some(git_dir);
993 self
994 }
995
996 #[must_use]
1003 pub fn hash_algo(&self) -> HashAlgo {
1004 if let Some(primary) = &self.custom_primary {
1005 return primary.hash_algo();
1006 }
1007 *self.hash_algo_cache.get_or_init(|| {
1008 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1009 return HashAlgo::Sha1;
1010 }
1011 let cfg = self.load_config_cascade().unwrap_or_default();
1012 cfg.get("extensions.objectformat")
1013 .and_then(|v| HashAlgo::from_name(&v))
1014 .unwrap_or(HashAlgo::Sha1)
1015 })
1016 }
1017
1018 fn resolve_loose_zlib_level(&self) -> Result<u32> {
1019 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1020 return Ok(ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT as u32);
1021 }
1022 let cfg = self.load_config_cascade()?;
1023 Ok(cfg.loose_objects_zlib_level()? as u32)
1024 }
1025
1026 pub(crate) fn loose_compression(&self) -> Result<Compression> {
1033 if let Some(cached) = self.loose_zlib_cache.get() {
1034 return Ok(*cached);
1035 }
1036 let level = self.resolve_loose_zlib_level()?;
1037 let compression = Compression::new(level);
1038 let _ = self.loose_zlib_cache.set(compression);
1039 Ok(compression)
1040 }
1041
1042 fn resolve_pack_zlib_level(&self) -> Result<i32> {
1043 if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1044 return Ok(6);
1045 }
1046 let cfg = self.load_config_cascade()?;
1047 cfg.pack_objects_zlib_level()
1048 }
1049
1050 pub(crate) fn pack_compression(&self) -> Result<Compression> {
1056 if let Some(cached) = self.pack_zlib_cache.get() {
1057 return Ok(*cached);
1058 }
1059 let level = self.resolve_pack_zlib_level()?;
1060 let compression = crate::pack_zlib::flate2_compression_for_pack_level(level);
1061 let _ = self.pack_zlib_cache.set(compression);
1062 Ok(compression)
1063 }
1064
1065 pub fn big_file_threshold(&self) -> Result<u64> {
1076 if let Some(cached) = self.big_file_threshold_cache.get() {
1077 return Ok(*cached);
1078 }
1079 let threshold = if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
1080 ConfigSet::BIG_FILE_THRESHOLD_DEFAULT
1081 } else {
1082 self.load_config_cascade()?.big_file_threshold()?
1083 };
1084 let _ = self.big_file_threshold_cache.set(threshold);
1085 Ok(threshold)
1086 }
1087
1088 fn sync_delta_base_cache_limit(&self) {
1089 self.delta_base_cache_sync.get_or_init(|| {
1090 if self.config_git_dir.is_some() {
1091 let cfg = self.load_config_cascade().unwrap_or_default();
1092 self.pack_store.configure_delta_base_from_config(Some(&cfg));
1093 }
1094 });
1095 }
1096
1097 pub(crate) fn pack_store_for(&self, objects_dir: &Path) -> Arc<PackStore> {
1098 if objects_dir == self.objects_dir.as_path() {
1099 return Arc::clone(&self.pack_store);
1100 }
1101 if let Ok(mut guard) = self.alternate_pack_stores.lock() {
1102 if let Some(store) = guard.get(objects_dir) {
1103 return Arc::clone(store);
1104 }
1105 let store = Arc::new(PackStore::new(objects_dir.to_path_buf()));
1106 guard.insert(objects_dir.to_path_buf(), Arc::clone(&store));
1107 return store;
1108 }
1109 Arc::new(PackStore::new(objects_dir.to_path_buf()))
1110 }
1111
1112 pub fn pack_store(&self) -> Result<&Arc<PackStore>> {
1114 self.require_files_primary("pack_store")?;
1115 Ok(&self.pack_store)
1116 }
1117
1118 pub fn largest_pack_index_path(&self) -> Result<Option<PathBuf>> {
1127 self.require_files_primary("largest_pack_index_path")?;
1128 let mut indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
1129 indexes.sort_by_key(|idx| std::cmp::Reverse(idx.len()));
1130 Ok(indexes.first().map(|idx| idx.idx_path.clone()))
1131 }
1132
1133 pub(crate) fn clear_delta_base_cache(&self) {
1137 self.pack_store.delta_bases().clear();
1138 }
1139
1140 pub fn invalidate_packs(&self) {
1147 self.pack_store.invalidate_all();
1148 PackStore::invalidate_legacy_for_objects_dir(&self.objects_dir);
1149 if let Ok(guard) = self.alternate_pack_stores.lock() {
1150 for store in guard.values() {
1151 store.invalidate_all();
1152 PackStore::invalidate_legacy_for_objects_dir(store.objects_dir());
1153 }
1154 }
1155 self.invalidate_change_id_header_probe();
1156 }
1157
1158 fn invalidate_change_id_header_probe(&self) {
1159 if let Some(caches) = self.shared_config_state.as_ref() {
1160 crate::change_id::invalidate_stored_change_id_header_probe(caches, self.objects_dir());
1161 }
1162 }
1163
1164 fn note_commit_object_written(&self, kind: ObjectKind) {
1165 if kind == ObjectKind::Commit {
1166 self.invalidate_change_id_header_probe();
1167 }
1168 }
1169
1170 pub(crate) fn loosen_unreachable_from_superseded_packs(
1183 &self,
1184 pack_dir: &Path,
1185 keep_pack: &Path,
1186 reachable: &crate::gc::LazyReachable<'_>,
1187 retention: UnreachablePackRetention,
1188 ) -> Result<usize> {
1189 if matches!(retention, UnreachablePackRetention::DropAll) {
1190 return Ok(0);
1191 }
1192 self.require_files_primary("repack")?;
1193 let keep_idx = keep_pack.with_extension("idx");
1194 let keep_pack_oids = if keep_idx.is_file() {
1195 PackedObjects::object_ids_from_index_path(&keep_idx)?
1196 } else {
1197 HashSet::new()
1198 };
1199 let kept_pack_oids = oids_in_marker_kept_packs(pack_dir, keep_pack)?;
1200 let mut loosened = 0usize;
1201 for (idx_path, pack_mtime) in superseded_pack_indexes(pack_dir, keep_pack)? {
1202 let oids = PackedObjects::object_ids_from_index_path(&idx_path)?;
1203 for oid in oids {
1204 let keep = match retention {
1205 UnreachablePackRetention::KeepAll => true,
1206 UnreachablePackRetention::KeepGraceProtected(cutoff) => pack_mtime >= cutoff,
1207 UnreachablePackRetention::DropAll => false,
1208 };
1209 if !keep {
1210 continue;
1211 }
1212 if keep_pack_oids.contains(&oid) || kept_pack_oids.contains(&oid) {
1213 continue;
1214 }
1215 if reachable.get()?.contains(&oid) {
1216 continue;
1217 }
1218 let loose_path = oid.loose_path_in(&self.objects_dir);
1219 let pack_ft = system_time_to_file_time(pack_mtime);
1220 match retention {
1221 UnreachablePackRetention::KeepGraceProtected(cutoff) => {
1222 if loose_path.is_file() {
1223 let loose_mtime = fs::metadata(&loose_path)
1224 .and_then(|m| m.modified())
1225 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1226 if loose_mtime >= cutoff {
1227 continue;
1228 }
1229 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1230 loosened += 1;
1231 continue;
1232 }
1233 }
1234 UnreachablePackRetention::KeepAll => {
1235 if loose_path.is_file() {
1236 continue;
1237 }
1238 }
1239 UnreachablePackRetention::DropAll => {}
1240 }
1241 let obj = self.read(&oid)?;
1242 self.write_loose_materialize(obj.kind, &obj.data)?;
1243 let primary = self.primary()?;
1244 let loose_path = primary.loose_store().object_path(&oid);
1245 set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
1246 loosened += 1;
1247 }
1248 }
1249 Ok(loosened)
1250 }
1251
1252 fn core_multi_pack_index_enabled(&self) -> bool {
1253 *self.core_multi_pack_index_cache.get_or_init(|| {
1257 if self.config_git_dir.is_none() {
1258 return false;
1259 }
1260 let cfg = self.load_config_cascade().unwrap_or_default();
1261 match cfg.get_bool("core.multiPackIndex") {
1262 Some(Ok(b)) => b,
1263 Some(Err(_)) => true,
1264 None => true,
1265 }
1266 })
1267 }
1268
1269 fn multi_pack_index_reads_enabled(&self) -> bool {
1270 if self.custom_primary.is_some() {
1271 return false;
1272 }
1273 if self.config_git_dir.is_none() {
1274 return false;
1275 }
1276 if !*self
1277 .local_pack_indexes_seen
1278 .get_or_init(|| objects_dir_has_pack_index_files(&self.objects_dir))
1279 {
1280 return false;
1281 }
1282 self.core_multi_pack_index_enabled()
1283 }
1284
1285 fn ensure_midx_prepared(&self) {
1286 if !self.multi_pack_index_reads_enabled() {
1287 return;
1288 }
1289 let _ = self.midx_packs_validated.get_or_init(|| {
1290 let diagnostics = self.diagnostics_handle();
1291 validate_midx_referenced_packs_with_diagnostics(
1292 &self.objects_dir,
1293 diagnostics.as_ref(),
1294 );
1295 });
1296 }
1297
1298 fn try_read_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<Object>> {
1303 if self.custom_primary.is_some()
1304 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1305 || self.multi_pack_index_reads_enabled()
1306 {
1307 return Ok(None);
1308 }
1309 self.sync_delta_base_cache_limit();
1310 match pack::try_read_object_from_packs_with_options(
1311 &self.objects_dir,
1312 oid,
1313 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1314 ) {
1315 Ok(obj) => Ok(Some(obj)),
1316 Err(Error::ObjectNotFound(_)) => Ok(None),
1317 Err(err) => Err(err),
1318 }
1319 }
1320
1321 fn try_read_info_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1322 if self.custom_primary.is_some()
1323 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1324 || self.multi_pack_index_reads_enabled()
1325 {
1326 return Ok(None);
1327 }
1328 match pack::try_read_object_info_from_packs_with_options(
1329 &self.objects_dir,
1330 oid,
1331 pack::PackLookupOptions::ALL_LOCAL_PACKS,
1332 ) {
1333 Ok(info) => Ok(Some(info)),
1334 Err(Error::ObjectNotFound(_)) => Ok(None),
1335 Err(err) => Err(err),
1336 }
1337 }
1338
1339 #[must_use]
1346 pub fn objects_dir(&self) -> &Path {
1347 &self.objects_dir
1348 }
1349
1350 fn try_read_extra_sources(&self, oid: &ObjectId) -> Result<Option<Object>> {
1351 for src in &self.extra_read_sources {
1352 if let Some(obj) = src.read(oid)? {
1353 return Ok(Some(obj));
1354 }
1355 }
1356 Ok(None)
1357 }
1358
1359 fn try_read_info_extra_sources(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
1360 for src in &self.extra_read_sources {
1361 if let Some(info) = src.read_info(oid)? {
1362 return Ok(Some(info));
1363 }
1364 }
1365 Ok(None)
1366 }
1367
1368 fn try_open_stream_extra_sources(
1369 &self,
1370 oid: &ObjectId,
1371 ) -> Result<Option<store::ObjectStream<'_>>> {
1372 for src in &self.extra_read_sources {
1373 if let Some(stream) = src.open_stream(oid)? {
1374 return Ok(Some(stream));
1375 }
1376 }
1377 Ok(None)
1378 }
1379
1380 #[must_use]
1386 pub fn config_git_dir(&self) -> Option<&Path> {
1387 self.config_git_dir.as_deref()
1388 }
1389
1390 #[must_use]
1392 pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
1393 oid.loose_path_in(&self.objects_dir)
1394 }
1395
1396 #[must_use]
1412 pub fn exists_local(&self, oid: &ObjectId) -> bool {
1413 self.try_exists_local(oid).unwrap_or(false)
1414 }
1415
1416 pub(crate) fn try_exists_local(&self, oid: &ObjectId) -> Result<bool> {
1418 if self.is_empty_tree(oid) {
1419 return Ok(true);
1420 }
1421 if let Some(primary) = &self.custom_primary {
1422 return primary.contains(oid);
1423 }
1424 Ok(self.with_pack_store_for(&self.objects_dir, || {
1425 exists_materialized_in_objects_dir(&self.objects_dir, oid)
1426 }))
1427 }
1428
1429 pub(crate) fn try_exists_local_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
1434 let mut present: Vec<bool> = oids.iter().map(|oid| self.is_empty_tree(oid)).collect();
1435 let mut pending: Vec<usize> = (0..oids.len()).filter(|&i| !present[i]).collect();
1436 if let Some(primary) = &self.custom_primary {
1437 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1438 primary.contains_many(batch)
1439 })?;
1440 return Ok(present);
1441 }
1442 self.with_pack_store_for(&self.objects_dir, || {
1443 for i in pending {
1444 present[i] = exists_materialized_in_objects_dir(&self.objects_dir, &oids[i]);
1445 }
1446 });
1447 Ok(present)
1448 }
1449
1450 #[must_use]
1458 pub fn exists_stored(&self, oid: &ObjectId) -> bool {
1459 self.try_exists_stored(oid).unwrap_or(false)
1460 }
1461
1462 pub(crate) fn try_exists_stored(&self, oid: &ObjectId) -> Result<bool> {
1464 if oid.is_canonical_empty_tree() || self.overlay_read(oid).is_some() {
1465 return Ok(true);
1466 }
1467 if let Some(primary) = &self.custom_primary {
1468 return primary.contains(oid);
1469 }
1470 Ok(self.with_pack_store_for(&self.objects_dir, || {
1471 store::local_object_stored(&self.objects_dir, oid)
1472 }))
1473 }
1474
1475 #[must_use]
1485 pub fn exists_materialized_local(&self, oid: &ObjectId) -> bool {
1486 if let Some(primary) = &self.custom_primary {
1487 return primary.contains(oid).unwrap_or(false);
1488 }
1489 self.with_pack_store_for(&self.objects_dir, || {
1490 exists_materialized_in_objects_dir(&self.objects_dir, oid)
1491 })
1492 }
1493
1494 pub(crate) fn reprepare_local_packs(&self) {
1499 if self.custom_primary.is_some() {
1500 return;
1501 }
1502 self.with_pack_store_for(&self.objects_dir, || {
1503 let _ = crate::pack::reprepare_pack_directory_on_miss(&self.objects_dir);
1504 });
1505 }
1506
1507 pub(crate) fn exists_materialized_local_without_rescan(&self, oid: &ObjectId) -> bool {
1515 if self.custom_primary.is_some() {
1516 return self.exists_materialized_local(oid);
1517 }
1518 self.with_pack_store_for(&self.objects_dir, || {
1519 store::local_object_materialized_without_rescan(&self.objects_dir, oid)
1520 })
1521 }
1522
1523 pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
1535 self.probe_sources(oid, Err)
1536 }
1537
1538 fn probe_sources(
1541 &self,
1542 oid: &ObjectId,
1543 mut on_err: impl FnMut(Error) -> Result<()>,
1544 ) -> Result<bool> {
1545 #[cfg(test)]
1546 self.exists_probe.fetch_add(1, Ordering::Relaxed);
1547 if self.is_empty_tree(oid) || self.overlay_read(oid).is_some() {
1548 return Ok(true);
1549 }
1550 let mut found = |probe: Result<bool>| match probe {
1551 Ok(present) => Ok(present),
1552 Err(err) => on_err(err).map(|()| false),
1553 };
1554 if found(self.primary_writable().and_then(|p| p.contains(oid)))? {
1555 return Ok(true);
1556 }
1557 for src in &self.extra_read_sources {
1558 if found(src.contains(oid))? {
1559 return Ok(true);
1560 }
1561 }
1562 if !self.alternate_object_lookup_needed() {
1563 return Ok(false);
1564 }
1565 found(self.sources().and_then(|s| s.contains(oid)))
1566 }
1567
1568 pub(crate) fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
1578 #[cfg(test)]
1579 self.exists_probe.fetch_add(oids.len(), Ordering::Relaxed);
1580 let mut present: Vec<bool> = oids
1581 .iter()
1582 .map(|oid| self.is_empty_tree(oid) || self.overlay_read(oid).is_some())
1583 .collect();
1584 let mut pending: Vec<usize> = (0..oids.len()).filter(|&i| !present[i]).collect();
1585 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1586 self.primary_writable()?.contains_many(batch)
1587 })?;
1588 for src in &self.extra_read_sources {
1589 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1590 src.contains_many(batch)
1591 })?;
1592 }
1593 if !pending.is_empty() && self.alternate_object_lookup_needed() {
1594 store::resolve_pending(oids, &mut present, &mut pending, |batch| {
1595 self.sources()?.contains_many(batch)
1596 })?;
1597 }
1598 Ok(present)
1599 }
1600
1601 pub(crate) fn present_oids(
1607 &self,
1608 oids: impl IntoIterator<Item = ObjectId>,
1609 ) -> Result<Vec<ObjectId>> {
1610 self.partition_oids(oids, true)
1611 }
1612
1613 pub(crate) fn missing_oids(
1619 &self,
1620 oids: impl IntoIterator<Item = ObjectId>,
1621 ) -> Result<Vec<ObjectId>> {
1622 self.partition_oids(oids, false)
1623 }
1624
1625 fn partition_oids(
1627 &self,
1628 oids: impl IntoIterator<Item = ObjectId>,
1629 want_present: bool,
1630 ) -> Result<Vec<ObjectId>> {
1631 let oids: Vec<ObjectId> = oids.into_iter().collect();
1632 let present = self.contains_many(&oids)?;
1633 Ok(oids
1634 .into_iter()
1635 .zip(present)
1636 .filter_map(|(oid, has)| (has == want_present).then_some(oid))
1637 .collect())
1638 }
1639
1640 pub(crate) fn missing_stored_oids(
1647 &self,
1648 oids: impl IntoIterator<Item = ObjectId>,
1649 ) -> Result<Vec<ObjectId>> {
1650 filter_oids(oids, |oid| self.try_exists_stored(oid).map(|has| !has))
1651 }
1652
1653 #[must_use]
1660 pub fn exists(&self, oid: &ObjectId) -> bool {
1661 self.probe_sources(oid, |_| Ok(())).unwrap_or(false)
1662 }
1663
1664 fn with_pack_store_for<R>(&self, objects_dir: &Path, f: impl FnOnce() -> R) -> R {
1666 let store = self.pack_store_for(objects_dir);
1667 PackStore::with_context(store, f)
1668 }
1669
1670 pub fn with_pack_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
1675 self.with_pack_store_for(&self.objects_dir, f)
1676 }
1677
1678 pub(crate) fn pack_read_context_guard(&self) -> crate::pack_store::PackReadContextGuard {
1681 crate::pack_store::PackReadContextGuard::install(self.pack_store_for(&self.objects_dir))
1682 }
1683
1684 pub(crate) fn open_session_pack_reader(&self) -> Result<Option<pack::SingleLocalPackReader>> {
1692 if self.custom_primary.is_some()
1693 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
1694 {
1695 return Ok(None);
1696 }
1697 self.sync_delta_base_cache_limit();
1698 pack::SingleLocalPackReader::open_allow_loose(&self.objects_dir)
1699 }
1700
1701 #[must_use]
1706 pub fn freshen_object(&self, oid: &ObjectId) -> bool {
1707 if self.is_empty_tree(oid) {
1708 return false;
1709 }
1710
1711 let loose = self.object_path(oid);
1712 if loose.is_file() {
1713 return self.touch_object_mtime(&loose).is_some();
1714 }
1715
1716 if self.freshen_object_in_objects_dir(&self.objects_dir, oid) {
1717 return true;
1718 }
1719
1720 let file_alts = self.file_alternate_dirs_snapshot();
1721 for alt_dir in file_alts.iter() {
1722 if self.freshen_object_in_objects_dir(alt_dir, oid) {
1723 return true;
1724 }
1725 }
1726
1727 for alt_dir in self.env_alternate_dirs_snapshot().iter() {
1728 if self.freshen_object_in_objects_dir(alt_dir, oid) {
1729 return true;
1730 }
1731 }
1732
1733 if let Ok(guard) = self.submodule_alternate_dirs.lock() {
1734 for alt_dir in guard.iter() {
1735 if self.freshen_object_in_objects_dir(alt_dir, oid) {
1736 return true;
1737 }
1738 }
1739 }
1740
1741 false
1742 }
1743
1744 fn freshen_object_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
1745 let loose = objects_dir
1746 .join(oid.loose_prefix())
1747 .join(oid.loose_suffix());
1748 if loose.is_file() {
1749 return self.touch_object_mtime(&loose).is_some();
1750 }
1751 self.with_pack_store_for(objects_dir, || {
1752 let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
1753 return false;
1754 };
1755 for idx in &indexes {
1756 if idx.contains(oid).unwrap_or(false) {
1757 return self.freshen_pack_once(&idx.pack_path, idx.is_cruft);
1758 }
1759 }
1760 false
1761 })
1762 }
1763
1764 fn freshen_pack_once(&self, pack_path: &Path, is_cruft: bool) -> bool {
1769 let Ok(mut guard) = self.freshened_packs.lock() else {
1770 return false;
1771 };
1772 if guard.contains(pack_path) {
1773 return true;
1774 }
1775 if is_cruft {
1776 return false;
1777 }
1778 let Some(touched_at) = self.touch_object_mtime(pack_path) else {
1779 return false;
1780 };
1781 pack::refresh_pack_bytes_signature(pack_path, touched_at);
1782 guard.insert(pack_path.to_path_buf());
1783 true
1784 }
1785
1786 fn try_freshen_existing(&self, oid: &ObjectId) -> Option<ObjectId> {
1789 if let Ok(primary) = self.primary() {
1790 if let Ok(Some(existing)) =
1791 primary.try_finish_duplicate_write(oid, WriteOptions::default())
1792 {
1793 return Some(existing);
1794 }
1795 }
1796 if !self.exists(oid) {
1797 return None;
1798 }
1799 if self.freshen_object(oid) {
1800 return Some(*oid);
1801 }
1802 None
1803 }
1804
1805 fn try_freshen_existing_local(&self, oid: &ObjectId) -> Option<ObjectId> {
1806 let primary = self.primary().ok()?;
1807 primary
1808 .try_finish_duplicate_write(
1809 oid,
1810 WriteOptions {
1811 assume_loose_only_existence: true,
1812 ..WriteOptions::default()
1813 },
1814 )
1815 .ok()
1816 .flatten()
1817 }
1818
1819 fn object_exists_for_write_skip(&self, oid: &ObjectId) -> bool {
1824 if self.is_empty_tree(oid) {
1825 self.exists_materialized_local(oid)
1826 } else {
1827 self.exists(oid)
1828 }
1829 }
1830
1831 pub fn read_loose_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
1842 LooseStore::read_verify_oid(path, expected_oid)
1843 }
1844
1845 fn local_loose(&self) -> Result<LooseStore> {
1846 self.require_files_primary("loose_write")?;
1847 Ok(LooseStore::new(
1848 self.objects_dir.clone(),
1849 self.hash_algo(),
1850 self.loose_compression()?,
1851 ))
1852 }
1853
1854 fn loose_at(&self, objects_dir: &Path) -> Result<LooseStore> {
1855 Ok(LooseStore::new(
1856 objects_dir.to_path_buf(),
1857 self.hash_algo(),
1858 self.loose_compression()?,
1859 ))
1860 }
1861
1862 pub fn read(&self, oid: &ObjectId) -> Result<Object> {
1870 if self.is_empty_tree(oid) {
1871 return Ok(Object {
1872 kind: ObjectKind::Tree,
1873 data: Vec::new(),
1874 });
1875 }
1876
1877 if let Some(obj) = self.session_cached_loose_read(oid) {
1878 return Ok(obj);
1879 }
1880
1881 if let Some(obj) = self.overlay_read(oid) {
1882 return Ok(obj);
1883 }
1884 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
1885 return Ok(obj);
1886 }
1887
1888 let use_midx = self.multi_pack_index_reads_enabled();
1889 if use_midx {
1890 self.ensure_midx_prepared();
1891 }
1892
1893 self.sync_delta_base_cache_limit();
1894
1895 let primary = self.primary_writable()?;
1896 let files_primary = self.primary().ok();
1897 let mut unreadable_local_loose = None;
1898 let read_primary = || primary.read(oid);
1899 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
1900 read_primary()
1901 } else {
1902 self.with_pack_store_for(&self.objects_dir, read_primary)
1903 } {
1904 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj),
1905 Ok(None) => {}
1906 Err(err)
1907 if files_primary
1908 .as_ref()
1909 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
1910 {
1911 unreadable_local_loose = Some(err);
1912 }
1913 Err(err) => return Err(err),
1914 }
1915
1916 if let Some(obj) = self.try_read_extra_sources(oid)? {
1917 return self.finish_session_loose_read(oid, obj);
1918 }
1919
1920 if self.alternates_enabled {
1921 match self.sources()?.read(oid) {
1922 Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj),
1923 Ok(None) => {}
1924 Err(err) => return Err(err),
1925 }
1926 }
1927
1928 if let Some(err) = unreadable_local_loose {
1929 return Err(err);
1930 }
1931
1932 self.read_after_promisor_miss(oid)
1933 }
1934
1935 #[cold]
1936 #[inline(never)]
1937 fn read_after_promisor_miss(&self, oid: &ObjectId) -> Result<Object> {
1938 let mut lazy_retries = 0u32;
1939 loop {
1940 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
1941 return Err(Error::ObjectNotFound(oid.to_hex()));
1942 };
1943 match res {
1944 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
1945 lazy_retries += 1;
1946 if let Some(obj) = self.try_read_once(oid)? {
1947 return Ok(obj);
1948 }
1949 }
1950 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
1951 return Err(Error::ObjectNotFound(oid.to_hex()));
1952 }
1953 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
1954 return Err(err);
1955 }
1956 }
1957 }
1958 }
1959
1960 fn try_read_once(&self, oid: &ObjectId) -> Result<Option<Object>> {
1961 if oid.is_well_known_empty_tree() {
1962 return Ok(Some(Object {
1963 kind: ObjectKind::Tree,
1964 data: Vec::new(),
1965 }));
1966 }
1967
1968 if let Some(obj) = self.session_cached_loose_read(oid) {
1969 return Ok(Some(obj));
1970 }
1971
1972 if let Some(obj) = self.overlay_read(oid) {
1973 return Ok(Some(obj));
1974 }
1975 if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
1976 return Ok(Some(self.finish_session_loose_read(oid, obj)?));
1977 }
1978
1979 let use_midx = self.multi_pack_index_reads_enabled();
1980 if use_midx {
1981 self.ensure_midx_prepared();
1982 }
1983
1984 self.sync_delta_base_cache_limit();
1985
1986 let primary = self.primary_writable()?;
1987 let files_primary = self.primary().ok();
1988 let mut unreadable_local_loose = None;
1989 let read_primary = || primary.read(oid);
1990 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
1991 read_primary()
1992 } else {
1993 self.with_pack_store_for(&self.objects_dir, read_primary)
1994 } {
1995 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj)?)),
1996 Ok(None) => {}
1997 Err(err)
1998 if files_primary
1999 .as_ref()
2000 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2001 {
2002 unreadable_local_loose = Some(err);
2003 }
2004 Err(err) => return Err(err),
2005 }
2006
2007 if let Some(obj) = self.try_read_extra_sources(oid)? {
2008 return Ok(Some(self.finish_session_loose_read(oid, obj)?));
2009 }
2010
2011 if self.alternates_enabled {
2012 match self.sources()?.read(oid) {
2013 Ok(Some(obj)) => return Ok(Some(self.finish_session_loose_read(oid, obj)?)),
2014 Ok(None) => {}
2015 Err(err) => return Err(err),
2016 }
2017 }
2018
2019 if let Some(err) = unreadable_local_loose {
2020 return Err(err);
2021 }
2022
2023 Ok(None)
2024 }
2025
2026 fn resolve_promisor_miss(
2027 &self,
2028 oid: &ObjectId,
2029 lazy_retries: u32,
2030 ) -> Result<Option<crate::promisor_fetch::PromisorMissResolution>> {
2031 let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment)
2032 else {
2033 return Ok(None);
2034 };
2035 Ok(Some(crate::promisor_fetch::resolve_promisor_read_miss(
2036 caches,
2037 self,
2038 oid,
2039 env.as_ref(),
2040 lazy_retries,
2041 )?))
2042 }
2043
2044 pub fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<ObjectKind> {
2053 if self.is_empty_tree(oid) {
2054 buf.clear();
2055 return Ok(ObjectKind::Tree);
2056 }
2057
2058 if let Some(obj) = self.session_cached_loose_read(oid) {
2059 buf.clear();
2060 buf.extend_from_slice(&obj.data);
2061 return Ok(obj.kind);
2062 }
2063
2064 if let Some(obj) = self.overlay_read(oid) {
2065 buf.clear();
2066 buf.extend_from_slice(&obj.data);
2067 return Ok(obj.kind);
2068 }
2069
2070 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2071 return Ok(kind);
2072 }
2073
2074 let use_midx = self.multi_pack_index_reads_enabled();
2075 if use_midx {
2076 self.ensure_midx_prepared();
2077 }
2078
2079 self.sync_delta_base_cache_limit();
2080
2081 let primary = self.primary_writable()?;
2082 let files_primary = self.primary().ok();
2083 let mut unreadable_local_loose = None;
2084 let mut read_primary = || primary.read_into(oid, buf);
2085 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2086 read_primary()
2087 } else {
2088 self.with_pack_store_for(&self.objects_dir, read_primary)
2089 } {
2090 Ok(Some(kind)) => return Ok(kind),
2091 Ok(None) => {}
2092 Err(err)
2093 if files_primary
2094 .as_ref()
2095 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2096 {
2097 unreadable_local_loose = Some(err);
2098 }
2099 Err(err) => return Err(err),
2100 }
2101
2102 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2103 return Ok(kind);
2104 }
2105
2106 if self.alternates_enabled {
2107 match self.sources()?.read_into(oid, buf) {
2108 Ok(Some(kind)) => return Ok(kind),
2109 Ok(None) => {}
2110 Err(err) => return Err(err),
2111 }
2112 }
2113
2114 if let Some(err) = unreadable_local_loose {
2115 return Err(err);
2116 }
2117
2118 self.read_into_after_promisor_miss(oid, buf)
2119 }
2120
2121 pub(crate) fn read_into_caching_delta_bases(
2133 &self,
2134 oid: &ObjectId,
2135 buf: &mut Vec<u8>,
2136 ) -> Result<ObjectKind> {
2137 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, true, buf)? {
2138 return Ok(kind);
2139 }
2140 if self.custom_primary.is_none() && !self.is_empty_tree(oid) {
2141 if let Ok(files) = self.primary() {
2142 self.ensure_midx_prepared();
2143 self.sync_delta_base_cache_limit();
2144 let mut read = || files.read_into_with_delta_policy(oid, true, buf);
2145 let local = if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2146 read()
2147 } else {
2148 self.with_pack_store_for(&self.objects_dir, read)
2149 };
2150 if let Ok(Some(kind)) = local {
2151 return Ok(kind);
2152 }
2153 }
2154 }
2155 let object = self.read(oid)?;
2156 buf.clear();
2157 buf.extend_from_slice(&object.data);
2158 Ok(object.kind)
2159 }
2160
2161 #[cold]
2162 #[inline(never)]
2163 fn read_into_after_promisor_miss(
2164 &self,
2165 oid: &ObjectId,
2166 buf: &mut Vec<u8>,
2167 ) -> Result<ObjectKind> {
2168 let mut lazy_retries = 0u32;
2169 loop {
2170 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2171 return Err(Error::ObjectNotFound(oid.to_hex()));
2172 };
2173 match res {
2174 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2175 lazy_retries += 1;
2176 if let Some(kind) = self.try_read_into_once(oid, buf)? {
2177 return Ok(kind);
2178 }
2179 }
2180 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2181 return Err(Error::ObjectNotFound(oid.to_hex()));
2182 }
2183 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2184 return Err(err);
2185 }
2186 }
2187 }
2188 }
2189
2190 fn try_read_into_once(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
2191 if oid.is_well_known_empty_tree() {
2192 buf.clear();
2193 return Ok(Some(ObjectKind::Tree));
2194 }
2195
2196 if let Some(obj) = self.session_cached_loose_read(oid) {
2197 buf.clear();
2198 buf.extend_from_slice(&obj.data);
2199 return Ok(Some(obj.kind));
2200 }
2201
2202 if let Some(obj) = self.overlay_read(oid) {
2203 buf.clear();
2204 buf.extend_from_slice(&obj.data);
2205 return Ok(Some(obj.kind));
2206 }
2207
2208 if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, false, buf)? {
2209 return Ok(Some(kind));
2210 }
2211
2212 let use_midx = self.multi_pack_index_reads_enabled();
2213 if use_midx {
2214 self.ensure_midx_prepared();
2215 }
2216
2217 self.sync_delta_base_cache_limit();
2218
2219 let primary = self.primary_writable()?;
2220 let files_primary = self.primary().ok();
2221 let mut unreadable_local_loose = None;
2222 let mut read_primary = || primary.read_into(oid, buf);
2223 match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
2224 read_primary()
2225 } else {
2226 self.with_pack_store_for(&self.objects_dir, read_primary)
2227 } {
2228 Ok(Some(kind)) => return Ok(Some(kind)),
2229 Ok(None) => {}
2230 Err(err)
2231 if files_primary
2232 .as_ref()
2233 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2234 {
2235 unreadable_local_loose = Some(err);
2236 }
2237 Err(err) => return Err(err),
2238 }
2239
2240 if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
2241 return Ok(Some(kind));
2242 }
2243
2244 if self.alternates_enabled {
2245 match self.sources()?.read_into(oid, buf) {
2246 Ok(Some(kind)) => return Ok(Some(kind)),
2247 Ok(None) => {}
2248 Err(err) => return Err(err),
2249 }
2250 }
2251
2252 if let Some(err) = unreadable_local_loose {
2253 return Err(err);
2254 }
2255
2256 Ok(None)
2257 }
2258
2259 fn try_read_into_extra_sources(
2260 &self,
2261 oid: &ObjectId,
2262 buf: &mut Vec<u8>,
2263 ) -> Result<Option<ObjectKind>> {
2264 for src in &self.extra_read_sources {
2265 if let Some(kind) = src.read_into(oid, buf)? {
2266 return Ok(Some(kind));
2267 }
2268 }
2269 Ok(None)
2270 }
2271
2272 fn try_read_packed_in_batch_context_into(
2273 &self,
2274 oid: &ObjectId,
2275 populate_delta_lru: bool,
2276 buf: &mut Vec<u8>,
2277 ) -> Result<Option<ObjectKind>> {
2278 if self.custom_primary.is_some()
2279 || !PackStore::read_context_matches_objects_dir(&self.objects_dir)
2280 || self.multi_pack_index_reads_enabled()
2281 {
2282 return Ok(None);
2283 }
2284 self.sync_delta_base_cache_limit();
2285 match pack::try_read_object_from_packs_with_options_into(
2286 &self.objects_dir,
2287 oid,
2288 pack::PackLookupOptions::ALL_LOCAL_PACKS,
2289 populate_delta_lru,
2290 buf,
2291 ) {
2292 Ok(kind) => Ok(Some(kind)),
2293 Err(Error::ObjectNotFound(_)) => Ok(None),
2294 Err(err) => Err(err),
2295 }
2296 }
2297
2298 pub fn read_info(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2310 if self.is_empty_tree(oid) {
2311 return Ok(ObjectInfo {
2312 kind: ObjectKind::Tree,
2313 size: 0,
2314 });
2315 }
2316
2317 if let Some(obj) = self.overlay_read(oid) {
2318 return Ok(ObjectInfo {
2319 kind: obj.kind,
2320 size: u64::try_from(obj.data.len())
2321 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
2322 });
2323 }
2324
2325 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
2326 return Ok(info);
2327 }
2328
2329 let use_midx = self.multi_pack_index_reads_enabled();
2330 if use_midx {
2331 self.ensure_midx_prepared();
2332 }
2333
2334 let primary = self.primary_writable()?;
2335 let files_primary = self.primary().ok();
2336 let mut unreadable_local_loose = None;
2337 match primary.read_info(oid) {
2338 Ok(Some(info)) => return Ok(info),
2339 Ok(None) => {}
2340 Err(err)
2341 if files_primary
2342 .as_ref()
2343 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2344 {
2345 unreadable_local_loose = Some(err);
2346 }
2347 Err(err) => return Err(err),
2348 }
2349
2350 if let Some(info) = self.try_read_info_extra_sources(oid)? {
2351 return Ok(info);
2352 }
2353
2354 if self.alternates_enabled {
2355 match self.sources()?.read_info(oid) {
2356 Ok(Some(info)) => return Ok(info),
2357 Ok(None) => {}
2358 Err(err) => return Err(err),
2359 }
2360 }
2361
2362 if let Some(err) = unreadable_local_loose {
2363 return Err(err);
2364 }
2365
2366 self.read_info_after_promisor_miss(oid)
2367 }
2368
2369 pub(crate) fn read_info_for_serve(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2374 self.try_read_info_once(oid)?
2375 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))
2376 }
2377
2378 #[cold]
2379 #[inline(never)]
2380 fn read_info_after_promisor_miss(&self, oid: &ObjectId) -> Result<ObjectInfo> {
2381 let mut lazy_retries = 0u32;
2382 loop {
2383 let Some(res) = self.resolve_promisor_miss(oid, lazy_retries)? else {
2384 return Err(Error::ObjectNotFound(oid.to_hex()));
2385 };
2386 match res {
2387 crate::promisor_fetch::PromisorMissResolution::PromisedMissing(err) => {
2388 return Err(err);
2389 }
2390 crate::promisor_fetch::PromisorMissResolution::NotPromisorRepo => {
2391 return Err(Error::ObjectNotFound(oid.to_hex()));
2392 }
2393 crate::promisor_fetch::PromisorMissResolution::RetryAfterFetch => {
2394 lazy_retries += 1;
2395 if let Some(info) = self.try_read_info_once(oid)? {
2396 return Ok(info);
2397 }
2398 }
2399 }
2400 }
2401 }
2402
2403 fn try_read_info_once(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
2404 if oid.is_well_known_empty_tree() {
2405 return Ok(Some(ObjectInfo {
2406 kind: ObjectKind::Tree,
2407 size: 0,
2408 }));
2409 }
2410
2411 if let Some(obj) = self.overlay_read(oid) {
2412 return Ok(Some(ObjectInfo {
2413 kind: obj.kind,
2414 size: u64::try_from(obj.data.len())
2415 .map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
2416 }));
2417 }
2418
2419 if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
2420 return Ok(Some(info));
2421 }
2422
2423 let use_midx = self.multi_pack_index_reads_enabled();
2424 if use_midx {
2425 self.ensure_midx_prepared();
2426 }
2427
2428 let primary = self.primary_writable()?;
2429 let files_primary = self.primary().ok();
2430 let mut unreadable_local_loose = None;
2431 match primary.read_info(oid) {
2432 Ok(Some(info)) => return Ok(Some(info)),
2433 Ok(None) => {}
2434 Err(err)
2435 if files_primary
2436 .as_ref()
2437 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2438 {
2439 unreadable_local_loose = Some(err);
2440 }
2441 Err(err) => return Err(err),
2442 }
2443
2444 if let Some(info) = self.try_read_info_extra_sources(oid)? {
2445 return Ok(Some(info));
2446 }
2447
2448 if self.alternates_enabled {
2449 match self.sources()?.read_info(oid) {
2450 Ok(Some(info)) => return Ok(Some(info)),
2451 Ok(None) => {}
2452 Err(err) => return Err(err),
2453 }
2454 }
2455
2456 if let Some(err) = unreadable_local_loose {
2457 return Err(err);
2458 }
2459
2460 Ok(None)
2461 }
2462
2463 pub(crate) fn read_unless_larger(
2479 &self,
2480 oid: &ObjectId,
2481 limit: u64,
2482 cache_small: bool,
2483 ) -> Result<BoundedRead> {
2484 if cache_small {
2485 if let Some(read) = self.session_loose_reads.lock().ok().and_then(|cache| {
2486 cache.get(oid).map(|obj| {
2487 if obj.data.len() as u64 > limit {
2488 BoundedRead::Larger
2489 } else {
2490 BoundedRead::Object(obj.clone())
2491 }
2492 })
2493 }) {
2494 return Ok(read);
2495 }
2496 }
2497 if self.custom_primary.is_none() && !self.multi_pack_index_reads_enabled() {
2498 if let Some(read) =
2499 pack::pack_cache::try_read_object_via_single_pack_reader_unless_whole_larger(
2500 &self.objects_dir,
2501 oid,
2502 limit,
2503 )?
2504 {
2505 return Ok(read);
2506 }
2507 }
2508 if let Ok(files) = self.primary() {
2509 if self.multi_pack_index_reads_enabled() {
2510 self.ensure_midx_prepared();
2511 }
2512 match files.read_unless_larger(oid, limit)? {
2513 LocalSizedRead::Packed(Some(size)) if size > limit => {
2514 return Ok(BoundedRead::Larger);
2515 }
2516 LocalSizedRead::Packed(_) => return self.read(oid).map(BoundedRead::Object),
2517 LocalSizedRead::Loose(BoundedRead::Object(obj)) => {
2518 if cache_small && obj.data.len() <= SESSION_BOUNDED_LOOSE_CACHE_MAX {
2519 self.remember_session_loose_read(oid, &obj);
2520 }
2521 return Ok(BoundedRead::Object(obj));
2522 }
2523 LocalSizedRead::Loose(read) => return Ok(read),
2524 LocalSizedRead::Missing => {}
2525 }
2526 }
2527 let info = self.read_info(oid)?;
2528 if info.size > limit {
2529 return Ok(BoundedRead::Larger);
2530 }
2531 self.read(oid).map(BoundedRead::Object)
2532 }
2533
2534 pub fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'static>>> {
2543 self.with_stream(oid, materialize_stream)
2544 }
2545
2546 pub fn with_stream<R>(
2563 &self,
2564 oid: &ObjectId,
2565 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
2566 ) -> Result<Option<R>> {
2567 use std::io::Cursor;
2568 if let Some(obj) = self.overlay_read(oid) {
2569 let size = u64::try_from(obj.data.len()).map_err(|_| {
2570 Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
2571 })?;
2572 return f(ObjectStream {
2573 kind: obj.kind,
2574 size,
2575 reader: Box::new(Cursor::new(obj.data)),
2576 })
2577 .map(Some);
2578 }
2579 if self.multi_pack_index_reads_enabled() {
2580 self.ensure_midx_prepared();
2581 }
2582 self.sync_delta_base_cache_limit();
2583 let primary = self.primary_writable()?;
2584 let files_primary = self.primary().ok();
2585 let mut unreadable_local_loose = None;
2586 match primary.open_stream(oid) {
2587 Ok(Some(stream)) => return f(stream).map(Some),
2588 Ok(None) => {}
2589 Err(err)
2590 if files_primary
2591 .as_ref()
2592 .is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
2593 {
2594 unreadable_local_loose = Some(err);
2595 }
2596 Err(err) => return Err(err),
2597 }
2598 if let Some(stream) = self.try_open_stream_extra_sources(oid)? {
2599 return f(stream).map(Some);
2600 }
2601 if self.alternates_enabled {
2602 let sources = self.sources()?;
2603 let stream = sources.open_stream(oid)?;
2604 if let Some(stream) = stream {
2605 return f(stream).map(Some);
2606 }
2607 }
2608 if let Some(err) = unreadable_local_loose {
2609 return Err(err);
2610 }
2611 Ok(None)
2612 }
2613
2614 pub fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
2620 let mut seen = oid_hash_set();
2621 let mut visit = |store: &dyn ObjectStore| -> Result<bool> {
2622 store::for_each_propagate_break(store, &mut |oid| {
2623 if !seen.insert(*oid) {
2624 return ControlFlow::Continue(());
2625 }
2626 f(oid)
2627 })
2628 };
2629 if let Some(overlay) = self.overlay_store() {
2630 if visit(overlay.as_ref())? {
2631 return Ok(());
2632 }
2633 }
2634 if visit(self.primary_writable()?.as_ref())? {
2635 return Ok(());
2636 }
2637 for src in &self.extra_read_sources {
2638 if visit(src.as_ref())? {
2639 return Ok(());
2640 }
2641 }
2642 if self.alternates_enabled {
2643 let _ = visit(self.sources()?.as_ref())?;
2644 }
2645 Ok(())
2646 }
2647
2648 pub fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
2654 let mut seen: OidHashSet = out.iter().copied().collect();
2655 if let Some(overlay) = self.overlay_store() {
2656 append_lookup_prefix_layer(overlay.as_ref(), prefix, limit, out, &mut seen)?;
2657 if limit != 0 && out.len() >= limit {
2658 return Ok(());
2659 }
2660 }
2661 append_lookup_prefix_layer(
2662 self.primary_writable()?.as_ref(),
2663 prefix,
2664 limit,
2665 out,
2666 &mut seen,
2667 )?;
2668 if limit != 0 && out.len() >= limit {
2669 return Ok(());
2670 }
2671 for src in &self.extra_read_sources {
2672 append_lookup_prefix_layer(src.as_ref(), prefix, limit, out, &mut seen)?;
2673 if limit != 0 && out.len() >= limit {
2674 return Ok(());
2675 }
2676 }
2677 if self.alternates_enabled {
2678 append_lookup_prefix_layer(self.sources()?.as_ref(), prefix, limit, out, &mut seen)?;
2679 }
2680 Ok(())
2681 }
2682
2683 pub fn enumerate_local_loose_objects(&self) -> Result<Vec<(ObjectId, PathBuf)>> {
2691 let primary = self.primary()?;
2692 let loose = primary.loose_store();
2693 let mut out = Vec::new();
2694 loose.for_each_object(&mut |oid| {
2695 out.push((*oid, loose.object_path(oid)));
2696 ControlFlow::Continue(())
2697 })?;
2698 Ok(out)
2699 }
2700
2701 pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
2709 hash_object_data_with(self.hash_algo(), kind, data)
2710 }
2711
2712 pub fn hash_stream(
2723 &self,
2724 kind: ObjectKind,
2725 size: u64,
2726 reader: &mut dyn std::io::Read,
2727 ) -> Result<ObjectId> {
2728 stream::hash_stream(self.hash_algo(), kind, size, reader)
2729 }
2730
2731 pub fn write_stream(
2752 &self,
2753 kind: ObjectKind,
2754 size: u64,
2755 reader: &mut dyn std::io::Read,
2756 options: WriteOptions,
2757 ) -> Result<ObjectId> {
2758 if let Some(primary) = &self.custom_primary {
2759 if !self.overlay_active() {
2760 let oid = primary.write_stream(kind, size, reader, options)?;
2761 self.note_commit_object_written(kind);
2762 return Ok(oid);
2763 }
2764 }
2765 if self.overlay_active() || size < self.big_file_threshold()? {
2766 let len = usize::try_from(size).map_err(|_| {
2767 Error::CorruptObject(format!("object size {size} exceeds address space"))
2768 })?;
2769 let mut data = Vec::with_capacity(len);
2770 let read =
2771 std::io::Read::read_to_end(&mut std::io::Read::take(reader, size), &mut data)?;
2772 if read != len {
2773 return Err(Error::ObjectStreamTruncated {
2774 expected: size,
2775 actual: read as u64,
2776 });
2777 }
2778 return self.write_with_options(kind, &data, options);
2779 }
2780 let staged = stream::StagedPack::write(
2781 &self.objects_dir.join("pack"),
2782 self.hash_algo(),
2783 kind,
2784 size,
2785 reader,
2786 self.pack_compression()?,
2787 )?;
2788 let oid = staged.oid();
2789 if self.object_exists_for_write_skip(&oid) {
2790 if !options.silent {
2791 let _ = self.freshen_object(&oid);
2792 }
2793 return Ok(oid);
2794 }
2795 staged.install()?;
2796 self.invalidate_packs();
2797 self.note_commit_object_written(kind);
2798 Ok(oid)
2799 }
2800
2801 pub fn empty_tree_oid(&self) -> Result<ObjectId> {
2809 self.hash(ObjectKind::Tree, b"")
2810 }
2811
2812 #[must_use]
2814 pub fn is_empty_tree(&self, oid: &ObjectId) -> bool {
2815 empty_tree_oid_matches(self.hash_algo(), oid)
2816 }
2817
2818 pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
2831 self.write_with_options(kind, data, WriteOptions::default())
2832 }
2833
2834 pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
2843 if self.custom_primary.is_some() {
2844 return Ok(());
2845 }
2846 self.local_loose()?.ensure_all_loose_prefix_dirs()
2847 }
2848
2849 #[cfg(test)]
2851 #[must_use]
2852 pub fn test_loose_object_path(&self, oid: &ObjectId) -> PathBuf {
2853 self.object_path(oid)
2854 }
2855
2856 #[cfg(test)]
2858 pub fn test_remove_loose_object(&self, oid: &ObjectId) {
2859 let _ = fs::remove_file(self.object_path(oid));
2860 }
2861
2862 pub fn count_loose_objects(&self) -> Result<usize> {
2868 let mut count = 0usize;
2869 for_each_loose_object_id(&self.objects_dir, self.hash_algo(), &mut |_| {
2870 count += 1;
2871 ControlFlow::Continue(())
2872 })?;
2873 Ok(count)
2874 }
2875
2876 pub fn approximate_object_count(&self) -> Result<usize> {
2886 let pack_objects: Result<usize> = self.with_pack_read_context(|| {
2887 let indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
2888 Ok(indexes.iter().map(|idx| idx.len()).sum())
2889 });
2890 let mut count = pack_objects?;
2891 count += self.count_loose_objects()?;
2892 Ok(count)
2893 }
2894
2895 pub fn zlib_compress_loose_store(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
2901 self.local_loose()?.zlib_compress_store_bytes(store_bytes)
2902 }
2903
2904 pub fn write_loose_zlib_prehashed(
2918 &self,
2919 oid: &ObjectId,
2920 zlib_store: &[u8],
2921 options: WriteOptions,
2922 ) -> Result<ObjectId> {
2923 if self.custom_primary.is_some() {
2924 let raw = decompress_zlib_loose_bytes(zlib_store)?;
2925 let obj = parse_object_bytes(&raw)?;
2926 return self.write_with_options(obj.kind, &obj.data, options);
2927 }
2928 let primary = self.primary()?;
2929 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
2930 return Ok(existing);
2931 }
2932
2933 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
2934 && self.object_exists_for_write_skip(oid);
2935
2936 if self.overlay_active() && !already_exists {
2937 if let Ok(raw) = decompress_zlib_loose_bytes(zlib_store) {
2938 if let Ok(obj) = parse_object_bytes(&raw) {
2939 if self.overlay_store_object(*oid, obj.kind, &obj.data) {
2940 return Ok(*oid);
2941 }
2942 }
2943 }
2944 }
2945
2946 if already_exists {
2947 if !options.silent {
2948 let _ = self.freshen_object(oid);
2949 }
2950 return Ok(*oid);
2951 }
2952
2953 primary.write_zlib_prehashed(oid, zlib_store, options)
2954 }
2955
2956 pub(crate) fn write_blob_prehashed(
2962 &self,
2963 oid: &ObjectId,
2964 data: &[u8],
2965 options: WriteOptions,
2966 ) -> Result<ObjectId> {
2967 if self.custom_primary.is_some() {
2968 return self.write_with_options(ObjectKind::Blob, data, options);
2969 }
2970 let primary = self.primary()?;
2971 if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
2972 return Ok(existing);
2973 }
2974 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
2975 && self.object_exists_for_write_skip(oid);
2976 if self.overlay_active()
2977 && !already_exists
2978 && self.overlay_store_object(*oid, ObjectKind::Blob, data)
2979 {
2980 return Ok(*oid);
2981 }
2982 if already_exists {
2983 if !options.silent {
2984 let _ = self.freshen_object(oid);
2985 }
2986 return Ok(*oid);
2987 }
2988 primary
2989 .loose_store()
2990 .write_prehashed(oid, ObjectKind::Blob, data, options)
2991 }
2992 pub fn write_with_options(
2994 &self,
2995 kind: ObjectKind,
2996 data: &[u8],
2997 options: WriteOptions,
2998 ) -> Result<ObjectId> {
2999 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3000
3001 if let Some(primary) = &self.custom_primary {
3002 let already_exists = if options.trust_new_loose {
3003 false
3004 } else if options.assume_loose_only_existence {
3005 primary.contains(&oid)?
3006 } else {
3007 self.object_exists_for_write_skip(&oid)
3008 };
3009 if self.overlay_active()
3010 && !already_exists
3011 && self.overlay_store_object(oid, kind, data)
3012 {
3013 return Ok(oid);
3014 }
3015 if already_exists {
3016 return Ok(oid);
3017 }
3018 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3019 self.note_commit_object_written(kind);
3020 return Ok(oid);
3021 }
3022
3023 let primary = self.primary()?;
3024 if let Some(existing) = primary.try_finish_duplicate_write(&oid, options)? {
3025 return Ok(existing);
3026 }
3027
3028 let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
3029 && self.object_exists_for_write_skip(&oid);
3030
3031 if self.overlay_active() && !already_exists && self.overlay_store_object(oid, kind, data) {
3034 return Ok(oid);
3035 }
3036
3037 if already_exists {
3038 if !options.silent {
3039 let _ = self.freshen_object(&oid);
3040 }
3041 return Ok(oid);
3042 }
3043
3044 let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
3045 self.note_commit_object_written(kind);
3046 Ok(oid)
3047 }
3048
3049 pub fn write_local(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3065 if self.custom_primary.is_some() {
3066 return Err(Error::UnsupportedObjectStore {
3067 operation: "write_local",
3068 });
3069 }
3070 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3071
3072 if let Some(existing) = self.try_freshen_existing_local(&oid) {
3073 return Ok(existing);
3074 }
3075
3076 let oid = self
3077 .local_loose()?
3078 .write(kind, data, WriteOptions::default())?;
3079 self.note_commit_object_written(kind);
3080 Ok(oid)
3081 }
3082
3083 pub(crate) fn write_local_stream_with(
3085 &self,
3086 kind: ObjectKind,
3087 size: u64,
3088 supply: impl FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()>,
3089 ) -> Result<ObjectId> {
3090 if self.custom_primary.is_some() {
3091 return Err(Error::UnsupportedObjectStore {
3092 operation: "write_local_stream",
3093 });
3094 }
3095 let loose = self.local_loose()?;
3096 let oid = loose.write_payload_stream_with(kind, size, WriteOptions::default(), supply)?;
3097 self.note_commit_object_written(kind);
3098 Ok(oid)
3099 }
3100
3101 pub fn write_loose_materialize(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3108 if self.custom_primary.is_some() {
3109 return Err(Error::UnsupportedObjectStore {
3110 operation: "write_loose_materialize",
3111 });
3112 }
3113 let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
3114 if let Ok(primary) = self.primary() {
3115 if let Ok(Some(existing)) = primary
3116 .loose_store()
3117 .try_finish_if_loose_present(&oid, WriteOptions::default())
3118 {
3119 return Ok(existing);
3120 }
3121 }
3122
3123 self.local_loose()?
3124 .write(kind, data, WriteOptions::default())
3125 }
3126
3127 pub fn write_raw(&self, store_bytes: &[u8]) -> Result<ObjectId> {
3137 let obj = parse_object_bytes(store_bytes)?;
3138 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
3139
3140 if let Some(primary) = &self.custom_primary {
3141 if self.exists(&oid) {
3142 return Ok(oid);
3143 }
3144 if self.overlay_active() && self.overlay_store_object(oid, obj.kind, &obj.data) {
3145 return Ok(oid);
3146 }
3147 return WritableObjectStore::write(
3148 primary.as_ref(),
3149 obj.kind,
3150 &obj.data,
3151 WriteOptions::default(),
3152 );
3153 }
3154
3155 if let Some(existing) = self.try_freshen_existing(&oid) {
3156 return Ok(existing);
3157 }
3158
3159 self.local_loose()?
3160 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
3161 }
3162
3163 pub fn write_raw_local(&self, store_bytes: &[u8]) -> Result<ObjectId> {
3171 if self.custom_primary.is_some() {
3172 return Err(Error::UnsupportedObjectStore {
3173 operation: "write_raw_local",
3174 });
3175 }
3176 let obj = parse_object_bytes(store_bytes)?;
3177 let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
3178 if let Some(existing) = self.try_freshen_existing_local(&oid) {
3179 return Ok(existing);
3180 }
3181
3182 self.local_loose()?
3183 .write_store_prehashed(&oid, store_bytes, WriteOptions::default())
3184 }
3185
3186 fn touch_object_mtime(&self, path: &Path) -> Option<std::time::SystemTime> {
3188 #[cfg(test)]
3189 self.hot_path_test_metrics.record_freshen();
3190 let now = filetime::FileTime::now();
3192 filetime::set_file_times(path, now, now).ok()?;
3193 Some(
3194 std::time::UNIX_EPOCH
3195 + std::time::Duration::new(now.unix_seconds() as u64, now.nanoseconds()),
3196 )
3197 }
3198}
3199
3200fn hash_object_data_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
3203 hash::hash_object(algo, kind, data)
3204}
3205
3206#[must_use]
3208pub(crate) fn empty_tree_oid_matches(algo: HashAlgo, oid: &ObjectId) -> bool {
3209 oid.is_empty_tree_for(algo)
3210}
3211
3212pub(crate) fn zlib_compress_blob_loose(
3215 data: &[u8],
3216 compression: flate2::Compression,
3217) -> Result<Vec<u8>> {
3218 use std::io::Write as _;
3219 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), compression);
3220 let zlib_err = |e: std::io::Error| Error::Zlib(e.to_string());
3221 write!(encoder, "{} {}\0", ObjectKind::Blob, data.len()).map_err(zlib_err)?;
3222 encoder.write_all(data).map_err(zlib_err)?;
3223 encoder.finish().map_err(zlib_err)
3224}
3225
3226fn append_lookup_prefix_layer(
3227 store: &dyn ObjectStore,
3228 prefix: &str,
3229 limit: usize,
3230 out: &mut Vec<ObjectId>,
3231 seen: &mut OidHashSet,
3232) -> Result<()> {
3233 let mut scratch = Vec::new();
3234 store.lookup_prefix(prefix, 0, &mut scratch)?;
3235 for oid in scratch {
3236 if seen.insert(oid) {
3237 out.push(oid);
3238 if limit != 0 && out.len() >= limit {
3239 return Ok(());
3240 }
3241 }
3242 }
3243 Ok(())
3244}
3245
3246fn materialize_stream(stream: ObjectStream<'_>) -> Result<ObjectStream<'static>> {
3247 use std::io::{Cursor, Read};
3248 let ObjectStream {
3249 kind,
3250 size,
3251 mut reader,
3252 } = stream;
3253 let mut payload = Vec::new();
3254 reader.read_to_end(&mut payload).map_err(Error::Io)?;
3255 Ok(ObjectStream {
3256 kind,
3257 size,
3258 reader: Box::new(Cursor::new(payload)),
3259 })
3260}
3261
3262fn parse_alternate_env(val: &str) -> Vec<PathBuf> {
3265 let mut result = Vec::new();
3266 let mut chars = val.chars().peekable();
3267 while chars.peek().is_some() {
3268 if chars.peek() == Some(&':') {
3269 chars.next();
3270 continue;
3271 }
3272 if chars.peek() == Some(&'"') {
3273 chars.next(); let saved: Vec<char> = chars.clone().collect();
3277 let mut path = String::new();
3278 let mut properly_closed = false;
3279 loop {
3280 match chars.next() {
3281 None => break,
3282 Some('"') => {
3283 properly_closed = true;
3284 break;
3285 }
3286 Some('\\') => match chars.peek() {
3287 Some(c) if c.is_ascii_digit() => {
3288 let mut oct = String::new();
3289 for _ in 0..3 {
3290 if let Some(&c) = chars.peek() {
3291 if c.is_ascii_digit() {
3292 oct.push(c);
3293 chars.next();
3294 } else {
3295 break;
3296 }
3297 } else {
3298 break;
3299 }
3300 }
3301 if let Ok(byte) = u8::from_str_radix(&oct, 8) {
3302 path.push(byte as char);
3303 }
3304 }
3305 Some(_) => {
3306 if let Some(c) = chars.next() {
3307 match c {
3308 'n' => path.push('\n'),
3309 't' => path.push('\t'),
3310 'r' => path.push('\r'),
3311 _ => path.push(c),
3312 }
3313 }
3314 }
3315 None => {}
3316 },
3317 Some(c) => path.push(c),
3318 }
3319 }
3320 if !properly_closed {
3321 let raw: String = std::iter::once('"').chain(saved).collect();
3324 let raw_path = raw.split(':').next().unwrap_or(&raw);
3326 if !raw_path.is_empty() {
3327 result.push(PathBuf::from(raw_path));
3328 }
3329 let remainder = &raw[raw_path.len()..];
3333 if let Some(rest) = remainder.strip_prefix(':') {
3334 result.extend(parse_alternate_env(rest));
3336 }
3337 return result;
3338 } else if !path.is_empty() {
3339 result.push(PathBuf::from(path));
3340 }
3341 } else {
3342 let mut path = String::new();
3343 while let Some(&c) = chars.peek() {
3344 if c == ':' {
3345 break;
3346 }
3347 path.push(c);
3348 chars.next();
3349 }
3350 if !path.is_empty() {
3351 result.push(PathBuf::from(path));
3352 }
3353 }
3354 }
3355 result
3356}
3357
3358fn system_time_to_file_time(t: std::time::SystemTime) -> FileTime {
3359 let dur = t
3360 .duration_since(std::time::SystemTime::UNIX_EPOCH)
3361 .unwrap_or_default();
3362 FileTime::from_unix_time(
3363 i64::try_from(dur.as_secs()).unwrap_or(i64::MAX),
3364 dur.subsec_nanos(),
3365 )
3366}
3367
3368fn superseded_pack_indexes(
3369 pack_dir: &Path,
3370 keep_pack: &Path,
3371) -> Result<Vec<(PathBuf, std::time::SystemTime)>> {
3372 let keep_stem = keep_pack
3373 .file_stem()
3374 .and_then(|s| s.to_str())
3375 .ok_or_else(|| {
3376 Error::Io(std::io::Error::new(
3377 std::io::ErrorKind::InvalidInput,
3378 "bad pack path",
3379 ))
3380 })?;
3381 if !pack_dir.is_dir() {
3382 return Ok(Vec::new());
3383 }
3384 let mut out = Vec::new();
3385 for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
3386 let entry = entry.map_err(Error::Io)?;
3387 let name = entry.file_name();
3388 let name_str = name.to_string_lossy();
3389 if name_str == keep_stem || name_str.starts_with(&format!("{keep_stem}.")) {
3390 continue;
3391 }
3392 if !name_str.ends_with(".idx") {
3393 continue;
3394 }
3395 let idx_path = entry.path();
3396 let pack_path = idx_path.with_extension("pack");
3397 if !pack_path.is_file() {
3398 continue;
3399 }
3400 if superseded_pack_has_keep_marker(pack_dir, &pack_path) {
3401 continue;
3402 }
3403 let pack_mtime = fs::metadata(&pack_path)
3404 .and_then(|m| m.modified())
3405 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
3406 out.push((idx_path, pack_mtime));
3407 }
3408 Ok(out)
3409}
3410
3411fn superseded_pack_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
3412 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
3413 return false;
3414 };
3415 pack_dir.join(format!("{stem}.keep")).is_file()
3416}
3417
3418fn oids_in_marker_kept_packs(pack_dir: &Path, keep_pack: &Path) -> Result<OidHashSet> {
3419 let keep_stem = keep_pack.file_stem().and_then(|s| s.to_str()).unwrap_or("");
3420 let mut oids = oid_hash_set();
3421 if !pack_dir.is_dir() {
3422 return Ok(oids);
3423 }
3424 for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
3425 let entry = entry.map_err(Error::Io)?;
3426 let pack_path = entry.path();
3427 if pack_path.extension().is_none_or(|e| e != "pack") {
3428 continue;
3429 }
3430 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
3431 continue;
3432 };
3433 if stem == keep_stem {
3434 continue;
3435 }
3436 if !superseded_pack_has_keep_marker(pack_dir, &pack_path) {
3437 continue;
3438 }
3439 let idx_path = pack_path.with_extension("idx");
3440 if idx_path.is_file() {
3441 oids.extend(PackedObjects::object_ids_from_index_path(&idx_path)?);
3442 }
3443 }
3444 Ok(oids)
3445}
3446
3447#[cfg(test)]
3448mod tests {
3449 #![allow(clippy::expect_used, clippy::unwrap_used)]
3450
3451 use super::*;
3452 use tempfile::TempDir;
3453
3454 #[test]
3455 fn round_trip_blob() {
3456 let dir = TempDir::new().unwrap();
3457 let odb = Odb::new(dir.path());
3458 let data = b"hello world";
3459 let oid = odb.write(ObjectKind::Blob, data).unwrap();
3460 let obj = odb.read(&oid).unwrap();
3461 assert_eq!(obj.kind, ObjectKind::Blob);
3462 assert_eq!(obj.data, data);
3463 }
3464
3465 #[test]
3466 fn known_blob_hash() {
3467 let oid = HashAlgo::Sha1
3470 .hash_object(ObjectKind::Blob, b"hello")
3471 .expect("hash");
3472 assert_eq!(oid.to_hex(), "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0");
3473 }
3474
3475 fn pack_objects(objects: &Path, oids: &[ObjectId]) {
3477 let odb = Odb::new(objects);
3478 let pack = crate::pack_objects::build_pack_from_send_list(
3479 &odb,
3480 oids,
3481 &HashSet::new(),
3482 &crate::pack_objects::PackBuildOptions::default(),
3483 &HashMap::new(),
3484 )
3485 .unwrap();
3486 for oid in oids {
3487 fs::remove_file(oid.loose_path_in(objects)).unwrap();
3488 }
3489 crate::index_pack::install_pack_bytes(
3490 pack,
3491 &odb,
3492 &crate::index_pack::IngestPackOptions::default(),
3493 )
3494 .unwrap();
3495 }
3496
3497 fn bounded_read_fixture() -> (TempDir, PathBuf, ObjectId, ObjectId) {
3498 let dir = TempDir::new().unwrap();
3499 let objects = dir.path().to_path_buf();
3500 let odb = Odb::new(&objects);
3501 let small = odb.write(ObjectKind::Blob, b"small blob\n").unwrap();
3502 let big: Vec<u8> = (0..300_000u32).flat_map(|i| i.to_le_bytes()).collect();
3503 let big = odb.write(ObjectKind::Blob, &big).unwrap();
3504 (dir, objects, small, big)
3505 }
3506
3507 fn assert_bounded_reads(odb: &Odb, small: &ObjectId, big: &ObjectId) {
3508 match odb.read_unless_larger(small, 1000, false).unwrap() {
3509 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
3510 BoundedRead::Larger => panic!("small blob is within the limit"),
3511 }
3512 assert!(matches!(
3513 odb.read_unless_larger(big, 1000, false).unwrap(),
3514 BoundedRead::Larger
3515 ));
3516 match odb.read_unless_larger(big, 1_200_000, false).unwrap() {
3517 BoundedRead::Object(obj) => assert_eq!(obj.data.len(), 1_200_000),
3518 BoundedRead::Larger => panic!("limit equal to the size reads the object"),
3519 }
3520 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").unwrap();
3521 assert!(matches!(
3522 odb.read_unless_larger(&missing, 1000, false),
3523 Err(Error::ObjectNotFound(_))
3524 ));
3525 }
3526
3527 #[test]
3528 fn read_unless_larger_sizes_loose_objects_from_their_header() {
3529 let (_dir, objects, small, big) = bounded_read_fixture();
3530 assert_bounded_reads(&Odb::new(&objects), &small, &big);
3531 }
3532
3533 #[test]
3534 fn read_unless_larger_sizes_packed_objects_in_and_out_of_read_context() {
3535 let (_dir, objects, small, big) = bounded_read_fixture();
3536 pack_objects(&objects, &[small, big]);
3537 let odb = Odb::new(&objects);
3538 assert_bounded_reads(&odb, &small, &big);
3539 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
3540 }
3541
3542 #[test]
3543 fn read_unless_larger_reuses_small_loose_reads_but_never_caches_big_ones() {
3544 let (_dir, objects, small, big) = bounded_read_fixture();
3545 let odb = Odb::new(&objects);
3546 assert_bounded_reads(&odb, &small, &big);
3547 assert!(odb.session_loose_reads.lock().unwrap().is_empty());
3548 assert!(matches!(
3549 odb.read_unless_larger(&small, 1000, true).unwrap(),
3550 BoundedRead::Object(_)
3551 ));
3552 assert!(matches!(
3553 odb.read_unless_larger(&big, 1_200_000, true).unwrap(),
3554 BoundedRead::Object(_)
3555 ));
3556 for oid in [small, big] {
3557 let hex = oid.to_hex();
3558 fs::remove_file(objects.join(&hex[..2]).join(&hex[2..])).unwrap();
3559 }
3560 match odb.read_unless_larger(&small, 1000, true).unwrap() {
3561 BoundedRead::Object(obj) => assert_eq!(obj.data, b"small blob\n"),
3562 BoundedRead::Larger => panic!("small blob is within the limit"),
3563 }
3564 assert!(matches!(
3565 odb.read_unless_larger(&small, 4, true),
3566 Ok(BoundedRead::Larger)
3567 ));
3568 assert!(matches!(
3569 odb.read_unless_larger(&big, 1_200_000, true),
3570 Err(Error::ObjectNotFound(_))
3571 ));
3572 }
3573
3574 #[test]
3575 fn read_unless_larger_finds_objects_across_several_packs() {
3576 let (_dir, objects, small, big) = bounded_read_fixture();
3577 pack_objects(&objects, &[small]);
3578 pack_objects(&objects, &[big]);
3579 let odb = Odb::new(&objects);
3580 odb.with_pack_read_context(|| assert_bounded_reads(&odb, &small, &big));
3581 }
3582
3583 #[test]
3584 fn parse_object_header_prefix_unknown_kind_is_typed_error() {
3585 let err = crate::odb::store::loose::parse_object_header_prefix(b"not-a-git-kind 0\0")
3586 .unwrap_err();
3587 assert!(matches!(err, Error::UnknownObjectType(_)));
3588 }
3589
3590 fn git_dir_with_config(config: &str) -> (TempDir, PathBuf, PathBuf) {
3591 let dir = TempDir::new().unwrap();
3592 let git_dir = dir.path().join(".git");
3593 let objects = git_dir.join("objects");
3594 fs::create_dir_all(&objects).unwrap();
3595 fs::write(git_dir.join("config"), config).unwrap();
3596 (dir, git_dir, objects)
3597 }
3598
3599 fn zlib_flevel(compressed: &[u8]) -> u8 {
3601 assert!(compressed.len() >= 2);
3602 (compressed[1] >> 6) & 3
3603 }
3604
3605 fn tmp_loose_object_paths(objects: &Path) -> Vec<PathBuf> {
3606 let mut tmp = Vec::new();
3607 let Ok(shards) = fs::read_dir(objects) else {
3608 return tmp;
3609 };
3610 for shard in shards.flatten() {
3611 if !shard.file_type().is_ok_and(|t| t.is_dir()) {
3612 continue;
3613 }
3614 let Ok(files) = fs::read_dir(shard.path()) else {
3615 continue;
3616 };
3617 for file in files.flatten() {
3618 let name = file.file_name();
3619 if name.to_string_lossy().starts_with("tmp_") {
3620 tmp.push(file.path());
3621 }
3622 }
3623 }
3624 tmp
3625 }
3626
3627 #[test]
3628 fn loose_write_honours_level() {
3629 let payload: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
3630
3631 let (_dir, git_dir, objects) = git_dir_with_config("");
3632 let odb = Odb::new(&objects).with_config_git_dir(git_dir.clone());
3633 let oid_default = odb.write(ObjectKind::Blob, &payload).unwrap();
3634 let bytes_default = fs::read(odb.object_path(&oid_default)).unwrap();
3635 assert!(zlib_flevel(&bytes_default) <= 1);
3636 assert_eq!(odb.read(&oid_default).unwrap().data, payload);
3637
3638 for level in -1i32..=9 {
3639 let config = format!("[core]\n\tloosecompression = {level}\n");
3640 let (_dir, git_dir, objects) = git_dir_with_config(&config);
3641 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3642 let byte = u8::try_from(level.rem_euclid(256)).unwrap_or(0);
3643 let bytes: Vec<u8> = vec![byte; 512];
3644 let oid = odb.write(ObjectKind::Blob, &bytes).unwrap();
3645 let on_disk = fs::read(odb.object_path(&oid)).unwrap();
3646 assert!(on_disk.len() >= 2, "level {level}");
3647 assert_eq!(odb.read(&oid).unwrap().data, bytes.as_slice());
3648 }
3649
3650 let config = "[core]\n\tcompression = 9\n";
3651 let (_dir, git_dir, objects) = git_dir_with_config(config);
3652 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3653 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
3654 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 3);
3655
3656 let config = "[core]\n\tcompression = 0\n\tloosecompression = 1\n";
3657 let (_dir, git_dir, objects) = git_dir_with_config(config);
3658 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3659 let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
3660 assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 0);
3661
3662 let config = "[core]\n\tloosecompression = 99\n";
3663 let (_dir, git_dir, objects) = git_dir_with_config(config);
3664 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3665 assert!(odb.write(ObjectKind::Blob, b"x").is_err());
3666 assert!(tmp_loose_object_paths(&objects).is_empty());
3667 }
3668
3669 fn git_run(dir: &Path, args: &[&str]) -> std::process::Output {
3670 std::process::Command::new("git") .current_dir(dir)
3672 .args(args)
3673 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3674 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3675 .env("GIT_AUTHOR_NAME", "T")
3676 .env("GIT_AUTHOR_EMAIL", "t@example.com")
3677 .env("GIT_COMMITTER_NAME", "T")
3678 .env("GIT_COMMITTER_EMAIL", "t@example.com")
3679 .output()
3680 .unwrap()
3681 }
3682
3683 fn git_in(dir: &Path, args: &[&str]) {
3684 let out = git_run(dir, args);
3685 assert!(
3686 out.status.success(),
3687 "git {:?}: {}",
3688 args,
3689 String::from_utf8_lossy(&out.stderr)
3690 );
3691 }
3692
3693 fn git_rev_parse_oid(dir: &Path, spec: &str) -> ObjectId {
3694 let out = git_run(dir, &["rev-parse", spec]);
3695 assert!(
3696 out.status.success(),
3697 "git rev-parse {spec}: {}",
3698 String::from_utf8_lossy(&out.stderr)
3699 );
3700 ObjectId::from_hex(std::str::from_utf8(&out.stdout).unwrap().trim()).unwrap()
3701 }
3702
3703 #[test]
3704 fn write_loose_zlib_prehashed_matches_write() {
3705 let dir = TempDir::new().unwrap();
3706 let objects = dir.path().join("objects");
3707 let odb = Odb::new(&objects);
3708 let data = b"parallel prehash path";
3709 let oid_normal = odb.write(ObjectKind::Blob, data).unwrap();
3710
3711 let dir2 = TempDir::new().unwrap();
3712 let objects2 = dir2.path().join("objects");
3713 let odb2 = Odb::new(&objects2);
3714 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, data).expect("hash");
3715 assert_eq!(oid, oid_normal);
3716 let zlib =
3717 super::zlib_compress_blob_loose(data, odb2.loose_compression().unwrap()).unwrap();
3718 let oid2 = odb2
3719 .write_loose_zlib_prehashed(
3720 &oid,
3721 &zlib,
3722 WriteOptions {
3723 assume_loose_only_existence: true,
3724 ..WriteOptions::default()
3725 },
3726 )
3727 .unwrap();
3728 assert_eq!(oid2, oid_normal);
3729 assert_eq!(
3730 fs::read(odb2.object_path(&oid2)).unwrap(),
3731 fs::read(odb.object_path(&oid_normal)).unwrap()
3732 );
3733 }
3734
3735 #[test]
3736 fn write_blob_prehashed_matches_write_and_skips_existing_objects() {
3737 let dir = TempDir::new().unwrap();
3738 let odb = Odb::new(&dir.path().join("objects"));
3739 let data = vec![7u8; 100_000];
3740 let oid_normal = odb.write(ObjectKind::Blob, &data).unwrap();
3741
3742 let dir2 = TempDir::new().unwrap();
3743 let odb2 = Odb::new(&dir2.path().join("objects"));
3744 let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, &data).unwrap();
3745 let options = WriteOptions {
3746 assume_loose_only_existence: true,
3747 ..WriteOptions::default()
3748 };
3749 assert_eq!(
3750 odb2.write_blob_prehashed(&oid, &data, options).unwrap(),
3751 oid
3752 );
3753 assert_eq!(oid, oid_normal);
3754 assert!(odb2.object_path(&oid).is_file());
3755 let obj = odb2.read(&oid).unwrap();
3756 assert_eq!(
3757 (obj.kind, obj.data.as_slice()),
3758 (ObjectKind::Blob, data.as_slice())
3759 );
3760
3761 assert_eq!(
3763 odb2.write_blob_prehashed(&oid, b"other", options).unwrap(),
3764 oid
3765 );
3766 assert_eq!(odb2.read(&oid).unwrap().data, data);
3767 }
3768
3769 #[test]
3770 fn write_loose_zlib_prehashed_routes_to_custom_primary() {
3771 use crate::objects::HashAlgo;
3772 use std::sync::Arc;
3773
3774 let dir = TempDir::new().unwrap();
3775 let objects = dir.path().join("objects");
3776 fs::create_dir_all(&objects).unwrap();
3777 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
3778 let odb = OdbBuilder::files(&objects)
3779 .primary(store.clone())
3780 .alternates(false)
3781 .build();
3782 let data = b"staged through a custom primary";
3783 let oid = hash::hash_object(HashAlgo::Sha1, ObjectKind::Blob, data).unwrap();
3784 let zlib = super::zlib_compress_blob_loose(data, Compression::fast()).unwrap();
3785
3786 odb.ensure_all_loose_prefix_dirs().unwrap();
3787 let written = odb
3788 .write_loose_zlib_prehashed(&oid, &zlib, WriteOptions::default())
3789 .unwrap();
3790 assert_eq!(written, oid);
3791 assert!(store.contains(&oid).unwrap());
3792 assert_eq!(odb.read(&oid).unwrap().data, data);
3793 assert_eq!(
3794 fs::read_dir(&objects).unwrap().count(),
3795 0,
3796 "no loose shard directories or files beside a custom primary"
3797 );
3798
3799 assert!(odb
3800 .write_loose_zlib_prehashed(&oid, b"not zlib", WriteOptions::default())
3801 .is_err());
3802 }
3803
3804 #[test]
3807 fn write_existing_loose_does_not_rewrite() {
3808 let dir = TempDir::new().unwrap();
3809 let objects = dir.path().join("objects");
3810 fs::create_dir_all(&objects).unwrap();
3811 let odb = Odb::new(&objects);
3812 let payload = b"already loose";
3813 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
3814 let path = odb.object_path(&oid);
3815 let before_bytes = fs::read(&path).unwrap();
3816 let meta_before = fs::metadata(&path).unwrap();
3817 let before_mtime = filetime::FileTime::from_last_modification_time(&meta_before);
3818 #[cfg(unix)]
3819 let ino_before = {
3820 use std::os::unix::fs::MetadataExt;
3821 meta_before.ino()
3822 };
3823 std::thread::sleep(std::time::Duration::from_millis(20));
3824 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
3825 assert_eq!(oid, oid2);
3826 assert_eq!(fs::read(&path).unwrap(), before_bytes);
3827 let meta_after = fs::metadata(&path).unwrap();
3828 let after_mtime = filetime::FileTime::from_last_modification_time(&meta_after);
3829 assert!(
3830 after_mtime >= before_mtime,
3831 "expected freshen to bump or preserve mtime"
3832 );
3833 #[cfg(unix)]
3834 {
3835 use std::os::unix::fs::MetadataExt;
3836 assert_eq!(
3837 meta_after.ino(),
3838 ino_before,
3839 "rewrite must not replace the loose object file"
3840 );
3841 }
3842 }
3843
3844 #[test]
3846 fn write_skips_loose_when_object_in_pack_only() {
3847 let dir = TempDir::new().unwrap();
3848 git_in(dir.path(), &["init", "-q"]);
3849 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
3850 git_in(dir.path(), &["config", "user.name", "T"]);
3851 fs::write(dir.path().join("f"), b"pack-only payload").unwrap();
3852 git_in(dir.path(), &["add", "f"]);
3853 git_in(dir.path(), &["commit", "-m", "c"]);
3854 git_in(dir.path(), &["repack", "-ad"]);
3855 let objects = dir.path().join(".git").join("objects");
3856 let odb = Odb::new(&objects);
3857 let oid = odb.write(ObjectKind::Blob, b"pack-only payload").unwrap();
3858 assert!(!odb.object_path(&oid).exists());
3859 assert!(odb.exists(&oid));
3860 }
3861
3862 #[test]
3864 fn write_freshens_loose_object_in_alternate() {
3865 use filetime::FileTime;
3866 use std::thread;
3867 use std::time::Duration;
3868
3869 let alt = TempDir::new().unwrap();
3870 git_in(alt.path(), &["init", "-q", "--bare"]);
3871 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
3872 let alt_objects = alt.path().join("objects");
3873 let alt_odb = Odb::new(&alt_objects);
3874 let payload = b"alternate-loose";
3875 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
3876 let alt_loose = alt_odb.object_path(&oid);
3877
3878 let primary = TempDir::new().unwrap();
3879 let primary_objects = primary.path().join("objects");
3880 fs::create_dir_all(primary_objects.join("info")).unwrap();
3881 fs::write(
3882 primary_objects.join("info").join("alternates"),
3883 format!("{}\n", alt_objects.display()),
3884 )
3885 .unwrap();
3886 let odb = Odb::new(&primary_objects);
3887 thread::sleep(Duration::from_millis(50));
3888 let before = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
3889 filetime::set_file_mtime(&alt_loose, before).unwrap();
3890
3891 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
3892 assert_eq!(oid, oid2);
3893 assert!(!odb.object_path(&oid).exists());
3894 let after = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
3895 assert!(after > before, "write must freshen alternate loose object");
3896 }
3897
3898 #[test]
3899 fn write_freshens_packed_object_in_alternate() {
3900 use filetime::FileTime;
3901 use std::thread;
3902 use std::time::Duration;
3903
3904 let alt = TempDir::new().unwrap();
3905 git_in(alt.path(), &["init", "-q"]);
3906 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
3907 git_in(alt.path(), &["config", "user.name", "T"]);
3908 let payload = b"alternate-pack";
3909 fs::write(alt.path().join("f"), payload).unwrap();
3910 git_in(alt.path(), &["add", "f"]);
3911 git_in(alt.path(), &["commit", "-m", "c"]);
3912 git_in(alt.path(), &["repack", "-ad"]);
3913 let alt_objects = alt.path().join(".git").join("objects");
3914 let alt_odb = Odb::new(&alt_objects);
3915 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
3916 let pack_path = fs::read_dir(alt_objects.join("pack"))
3917 .unwrap()
3918 .flatten()
3919 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
3920 .unwrap()
3921 .path();
3922 thread::sleep(Duration::from_millis(50));
3923 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
3924
3925 let primary = TempDir::new().unwrap();
3926 let primary_objects = primary.path().join("objects");
3927 fs::create_dir_all(primary_objects.join("info")).unwrap();
3928 fs::write(
3929 primary_objects.join("info").join("alternates"),
3930 format!("{}\n", alt_objects.display()),
3931 )
3932 .unwrap();
3933 pack::clear_pack_cache();
3934 let odb = Odb::new(&primary_objects);
3935 assert!(odb.exists(&oid));
3936 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
3937 assert_eq!(oid, oid2);
3938 assert!(!odb.object_path(&oid).exists());
3939 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
3940 assert!(after >= before, "write must freshen alternate pack");
3941 }
3942
3943 #[test]
3944 fn write_skips_loose_when_object_only_in_alternate() {
3945 let alt = TempDir::new().unwrap();
3946 git_in(alt.path(), &["init", "-q", "--bare"]);
3947 git_in(alt.path(), &["config", "user.email", "t@example.com"]);
3948 let alt_objects = alt.path().join("objects");
3949 let alt_odb = Odb::new(&alt_objects);
3950 let payload = b"alternate-only";
3951 let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
3952
3953 let primary = TempDir::new().unwrap();
3954 let primary_objects = primary.path().join("objects");
3955 fs::create_dir_all(primary.path().join("objects").join("info")).unwrap();
3956 fs::write(
3957 primary
3958 .path()
3959 .join("objects")
3960 .join("info")
3961 .join("alternates"),
3962 format!("{}\n", alt_objects.display()),
3963 )
3964 .unwrap();
3965 let odb = Odb::new(&primary_objects);
3966 assert!(odb.exists(&oid));
3967 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
3968 assert_eq!(oid, oid2);
3969 assert!(!odb.object_path(&oid).exists());
3970 }
3971
3972 #[test]
3973 fn write_with_mem_overlay_stays_in_memory() {
3974 let dir = TempDir::new().unwrap();
3975 let objects = dir.path().join("objects");
3976 fs::create_dir_all(&objects).unwrap();
3977 let odb = Odb::new(&objects);
3978 odb.enable_mem_overlay();
3979 let payload = b"overlay blob";
3980 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
3981 assert!(!odb.object_path(&oid).exists());
3982 assert_eq!(odb.read(&oid).unwrap().data, payload);
3983 odb.disable_mem_overlay();
3984 assert!(odb.read(&oid).is_err());
3985 }
3986
3987 #[test]
3988 fn read_alternates_relative_path_and_symlinked_objects_dir() {
3989 use crate::pack::read_alternates_recursive;
3990
3991 let layout = TempDir::new().unwrap();
3992 let alt_objects = layout.path().join("alt-store").join("objects");
3993 fs::create_dir_all(&alt_objects).unwrap();
3994 let primary_objects = layout.path().join("repo").join("objects");
3995 fs::create_dir_all(primary_objects.join("info")).unwrap();
3996 fs::write(
3997 primary_objects.join("info").join("alternates"),
3998 "../../alt-store/objects\n",
3999 )
4000 .unwrap();
4001 let resolved = read_alternates_recursive(&primary_objects).unwrap();
4002 let alt_canonical = fs::canonicalize(&alt_objects).unwrap_or(alt_objects.clone());
4003 assert!(
4004 resolved
4005 .iter()
4006 .any(|p| { fs::canonicalize(p).unwrap_or_else(|_| p.clone()) == alt_canonical }),
4007 "relative alternate entry should resolve: {resolved:?}"
4008 );
4009
4010 #[cfg(unix)]
4011 {
4012 let link_parent = TempDir::new().unwrap();
4013 std::os::unix::fs::symlink(&primary_objects, link_parent.path().join("objects-link"))
4014 .unwrap();
4015 let via_link =
4016 read_alternates_recursive(&link_parent.path().join("objects-link")).unwrap();
4017 assert!(
4018 !via_link.is_empty(),
4019 "alternates through symlinked objects dir: {via_link:?}"
4020 );
4021 }
4022 }
4023 #[test]
4024 fn empty_tree_exists_without_hex() {
4025 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
4026 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4027 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").unwrap();
4028 assert!(odb.exists(&canon));
4029 assert!(odb.exists(&legacy));
4030 assert!(odb.exists_local(&canon));
4031 assert!(!odb.freshen_object(&canon));
4032 }
4033
4034 #[test]
4035 fn sha256_empty_tree_virtual_read_without_loose_file() {
4036 use crate::objects::HashAlgo;
4037 use std::sync::Arc;
4038
4039 let dir = TempDir::new().unwrap();
4040 let objects = dir.path().join("objects");
4041 fs::create_dir_all(&objects).unwrap();
4042 let store = Arc::new(MemoryStore::new(HashAlgo::Sha256));
4043 let odb = OdbBuilder::files(&objects)
4044 .primary(store)
4045 .alternates(false)
4046 .build();
4047 let empty = odb.empty_tree_oid().expect("hash empty tree");
4048 assert!(!odb.exists_materialized_local(&empty));
4049 assert!(odb.exists_local(&empty));
4050 assert!(odb.is_empty_tree(&empty));
4051 let obj = odb.read(&empty).expect("virtual read empty tree");
4052 assert_eq!(obj.kind, ObjectKind::Tree);
4053 assert!(obj.data.is_empty());
4054 }
4055
4056 #[test]
4057 fn empty_tree_oid_and_object_id_empty_tree_match_sha1() {
4058 let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
4059 let from_odb = odb.empty_tree_oid().expect("empty tree oid");
4060 assert_eq!(from_odb, ObjectId::empty_tree());
4061 assert!(odb.is_empty_tree(&ObjectId::empty_tree()));
4062 }
4063
4064 #[test]
4065 fn empty_tree_virtual_presence_skips_materialized_local() {
4066 let dir = TempDir::new().unwrap();
4067 let objects = dir.path().join("objects");
4068 fs::create_dir_all(&objects).unwrap();
4069 let odb = Odb::new(&objects);
4070 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4071 assert!(odb.exists_local(&canon));
4072 assert!(!odb.exists_materialized_local(&canon));
4073 }
4074
4075 #[test]
4076 fn write_materializes_well_known_empty_tree() {
4077 let dir = TempDir::new().unwrap();
4078 let objects = dir.path().join("objects");
4079 fs::create_dir_all(&objects).unwrap();
4080 let odb = Odb::new(&objects);
4081 let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
4082 assert!(!odb.exists_materialized_local(&canon));
4083 let oid = odb
4084 .write(crate::objects::ObjectKind::Tree, &[])
4085 .expect("write empty tree");
4086 assert_eq!(oid, canon);
4087 assert!(odb.exists_materialized_local(&canon));
4088 }
4089
4090 #[test]
4091 fn append_alternate_objects_line_errors_on_unreadable_existing_file() {
4092 let dir = TempDir::new().unwrap();
4093 let objects = dir.path().join("objects");
4094 fs::create_dir_all(objects.join("info")).unwrap();
4095 let alt_path = objects.join("info/alternates");
4096 fs::write(&alt_path, b"not valid utf-8 \xff\n").unwrap();
4097 let err = Odb::append_alternate_objects_line(&objects, Path::new("/tmp/other/objects"))
4098 .unwrap_err();
4099 assert!(matches!(err, Error::Io(_)));
4100 assert_eq!(fs::read(&alt_path).unwrap(), b"not valid utf-8 \xff\n");
4101 }
4102
4103 #[test]
4104 fn append_file_alternate_refreshes_cached_snapshot() {
4105 let primary = TempDir::new().unwrap();
4106 let alternate = TempDir::new().unwrap();
4107 let alt_objects = alternate.path().join("objects");
4108 std::fs::create_dir_all(&alt_objects).unwrap();
4109 let alt_odb = Odb::new(&alt_objects);
4110 let oid = alt_odb.write(ObjectKind::Blob, b"via-alternate").unwrap();
4111
4112 let primary_objects = primary.path().join("objects");
4113 std::fs::create_dir_all(primary_objects.join("info")).unwrap();
4114 let primary_odb = Odb::new(&primary_objects);
4115 assert!(!primary_odb.exists(&oid));
4116 let _ = primary_odb.file_alternate_dirs_snapshot();
4117 primary_odb.append_file_alternate(&alt_objects).unwrap();
4118 assert!(primary_odb.exists(&oid));
4119 }
4120
4121 #[test]
4122 fn exists_via_env_alternate_dirs_at_construction() {
4123 let primary = TempDir::new().unwrap();
4124 let alternate = TempDir::new().unwrap();
4125 let alt_objects = alternate.path().join("objects");
4126 std::fs::create_dir_all(&alt_objects).unwrap();
4127 let alt_odb = Odb::new(&alt_objects);
4128 let oid = alt_odb.write(ObjectKind::Blob, b"env-alt").unwrap();
4129
4130 let primary_objects = primary.path().join("objects");
4131 std::fs::create_dir_all(&primary_objects).unwrap();
4132 let primary_odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
4133 assert!(primary_odb.exists(&oid));
4134 }
4135
4136 fn flaky_odb(objects: &Path) -> (Odb, Arc<store::flaky::FlakyStore>) {
4137 let flaky = store::flaky::FlakyStore::new();
4138 let odb = OdbBuilder::files(objects)
4139 .primary(flaky.clone())
4140 .alternates(false)
4141 .build();
4142 (odb, flaky)
4143 }
4144
4145 #[test]
4146 fn contains_reports_primary_failure_instead_of_missing() {
4147 let dir = TempDir::new().unwrap();
4148 let (odb, flaky) = flaky_odb(dir.path());
4149 let oid = odb.write(ObjectKind::Blob, b"stored").unwrap();
4150 assert!(odb.contains(&oid).unwrap());
4151
4152 flaky.fail_all(true);
4153 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
4154 assert!(store::flaky::is_injected(&odb.contains(&oid).unwrap_err()));
4155 assert!(
4156 !odb.exists(&oid),
4157 "exists() keeps treating failures as absent"
4158 );
4159 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
4160
4161 let try_local = odb.try_exists_local(&oid).unwrap_err();
4162 assert!(store::flaky::is_injected(&try_local));
4163 assert!(!odb.exists_local(&oid));
4164 let try_stored = odb.try_exists_stored(&oid).unwrap_err();
4165 assert!(store::flaky::is_injected(&try_stored));
4166 assert!(!odb.exists_stored(&oid));
4167 }
4168
4169 #[test]
4170 fn contains_reports_extra_read_source_failure() {
4171 let dir = TempDir::new().unwrap();
4172 let flaky = store::flaky::FlakyStore::new();
4173 let odb = OdbBuilder::files(dir.path())
4174 .push_read_source(flaky.clone())
4175 .alternates(false)
4176 .build();
4177 let local = odb.write(ObjectKind::Blob, b"in primary").unwrap();
4178 let absent = HashAlgo::Sha1
4179 .hash_object(ObjectKind::Blob, b"nowhere")
4180 .unwrap();
4181 assert!(!odb.contains(&absent).unwrap());
4182
4183 flaky.fail_contains(true);
4184 assert!(
4185 odb.contains(&local).unwrap(),
4186 "primary hit wins over a failing source"
4187 );
4188 assert!(store::flaky::is_injected(
4189 &odb.contains(&absent).unwrap_err()
4190 ));
4191 assert!(!odb.exists(&absent));
4192 }
4193
4194 #[test]
4195 fn contains_reports_primary_failure_even_when_later_source_has_object() {
4196 let dir = TempDir::new().unwrap();
4197 let extra = Arc::new(store::MemoryStore::new(HashAlgo::Sha1));
4198 let flaky = store::flaky::FlakyStore::new();
4199 let odb = OdbBuilder::files(dir.path())
4200 .primary(flaky.clone())
4201 .push_read_source(extra.clone())
4202 .alternates(false)
4203 .build();
4204 let oid = extra
4205 .write(ObjectKind::Blob, b"extra", WriteOptions::default())
4206 .unwrap();
4207 assert!(odb.contains(&oid).unwrap());
4208
4209 flaky.fail_all(true);
4210 assert!(store::flaky::is_injected(&odb.read(&oid).unwrap_err()));
4211 assert!(
4212 store::flaky::is_injected(&odb.contains(&oid).unwrap_err()),
4213 "contains must agree with read"
4214 );
4215 assert!(odb.exists(&oid), "exists() skips the failing primary");
4216 }
4217
4218 #[test]
4219 fn contains_reports_alternate_failure() {
4220 let primary = TempDir::new().unwrap();
4221 let alternate = TempDir::new().unwrap();
4222 let alt_objects = alternate.path().join("objects");
4223 std::fs::create_dir_all(&alt_objects).unwrap();
4224 let primary_objects = primary.path().join("objects");
4225 std::fs::create_dir_all(&primary_objects).unwrap();
4226 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
4227 let local = odb.write(ObjectKind::Blob, b"local").unwrap();
4228 let absent = HashAlgo::Sha1
4229 .hash_object(ObjectKind::Blob, b"nowhere")
4230 .unwrap();
4231
4232 let flaky = store::flaky::FlakyStore::new();
4235 odb.sources().unwrap();
4236 odb.alternate_sources_cache.write().unwrap().sources = Some(Arc::new(CompositeStore::new(
4237 vec![flaky.clone()],
4238 HashAlgo::Sha1,
4239 )));
4240 assert!(!odb.contains(&absent).unwrap());
4241
4242 flaky.fail_contains(true);
4243 assert!(odb.contains(&local).unwrap());
4244 assert!(store::flaky::is_injected(
4245 &odb.contains(&absent).unwrap_err()
4246 ));
4247 assert!(!odb.exists(&absent));
4248 }
4249
4250 #[test]
4251 fn contains_on_files_backend_covers_present_absent_empty_tree_and_alternate() {
4252 let primary = TempDir::new().unwrap();
4253 let alternate = TempDir::new().unwrap();
4254 let alt_objects = alternate.path().join("objects");
4255 std::fs::create_dir_all(&alt_objects).unwrap();
4256 let alt_oid = Odb::new(&alt_objects)
4257 .write(ObjectKind::Blob, b"alternate")
4258 .unwrap();
4259 let primary_objects = primary.path().join("objects");
4260 std::fs::create_dir_all(&primary_objects).unwrap();
4261 let odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
4262 let present = odb.write(ObjectKind::Blob, b"present").unwrap();
4263 let absent = HashAlgo::Sha1
4264 .hash_object(ObjectKind::Blob, b"absent")
4265 .unwrap();
4266
4267 assert!(odb.contains(&present).unwrap());
4268 assert!(odb.contains(&odb.empty_tree_oid().unwrap()).unwrap());
4269 assert!(odb.contains(&alt_oid).unwrap());
4270 assert!(!odb.contains(&absent).unwrap());
4271 let empty_tree = odb.empty_tree_oid().unwrap();
4272 assert_eq!(
4273 odb.contains_many(&[present, absent, alt_oid, empty_tree])
4274 .unwrap(),
4275 vec![true, false, true, true]
4276 );
4277 assert_eq!(
4278 odb.try_exists_local_many(&[present, absent, alt_oid, empty_tree])
4279 .unwrap(),
4280 vec![true, false, false, true],
4281 "local ignores alternates"
4282 );
4283 assert!(odb.contains_many(&[]).unwrap().is_empty());
4284 assert_eq!(
4285 odb.present_oids([present, absent, alt_oid]).unwrap(),
4286 vec![present, alt_oid]
4287 );
4288 assert_eq!(
4289 odb.missing_oids([present, absent, alt_oid]).unwrap(),
4290 vec![absent]
4291 );
4292 assert_eq!(
4293 odb.missing_stored_oids([present, absent, alt_oid]).unwrap(),
4294 vec![absent, alt_oid],
4295 "stored ignores alternates"
4296 );
4297 }
4298
4299 #[test]
4300 fn oid_filters_propagate_backend_failure() {
4301 let dir = TempDir::new().unwrap();
4302 let (odb, flaky) = flaky_odb(dir.path());
4303 let oid = odb.write(ObjectKind::Blob, b"x").unwrap();
4304 assert_eq!(odb.try_exists_local_many(&[oid]).unwrap(), vec![true]);
4305 flaky.fail_contains(true);
4306 assert!(store::flaky::is_injected(
4307 &odb.contains_many(&[oid]).unwrap_err()
4308 ));
4309 assert!(store::flaky::is_injected(
4310 &odb.try_exists_local_many(&[oid]).unwrap_err()
4311 ));
4312 assert!(store::flaky::is_injected(
4313 &odb.present_oids([oid]).unwrap_err()
4314 ));
4315 assert!(store::flaky::is_injected(
4316 &odb.missing_oids([oid]).unwrap_err()
4317 ));
4318 assert!(store::flaky::is_injected(
4319 &odb.missing_stored_oids([oid]).unwrap_err()
4320 ));
4321 }
4322
4323 #[test]
4324 fn write_existing_loose_uses_path_stat_not_exists() {
4325 let dir = TempDir::new().unwrap();
4326 let odb = Odb::new(dir.path());
4327 let oid = odb.write(ObjectKind::Blob, b"once").unwrap();
4328 let path = odb.object_path(&oid);
4329 assert!(path.is_file());
4330 odb.reset_exists_probe();
4331 let oid2 = odb.write(ObjectKind::Blob, b"once").unwrap();
4332 assert_eq!(oid, oid2);
4333 assert_eq!(
4334 odb.exists_probe_count(),
4335 0,
4336 "loose fast path must not call exists()"
4337 );
4338 }
4339
4340 #[test]
4341 fn exists_and_write_after_invalidate_packs_use_odb_store() {
4342 let dir = TempDir::new().unwrap();
4343 git_in(dir.path(), &["init", "-q"]);
4344 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4345 git_in(dir.path(), &["config", "user.name", "T"]);
4346 let payload = b"invalidate-pack-exists";
4347 fs::write(dir.path().join("f"), payload).unwrap();
4348 git_in(dir.path(), &["add", "f"]);
4349 git_in(dir.path(), &["commit", "-m", "c"]);
4350 git_in(dir.path(), &["repack", "-ad"]);
4351 let objects = dir.path().join(".git").join("objects");
4352 let odb = Odb::new(&objects);
4353 let oid = odb.hash(ObjectKind::Blob, payload).expect("hash");
4354 assert!(odb.exists(&oid), "blob must live in pack after repack");
4355 let pack_dir = objects.join("pack");
4356 for entry in fs::read_dir(&pack_dir).unwrap().flatten() {
4357 let path = entry.path();
4358 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
4359 fs::remove_file(path).unwrap();
4360 }
4361 }
4362 odb.invalidate_packs();
4363 assert!(
4364 !odb.exists(&oid),
4365 "exists must not read stale legacy pack listing after invalidate_packs"
4366 );
4367 let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
4368 assert_eq!(oid, oid2);
4369 assert!(
4370 odb.object_path(&oid).is_file(),
4371 "write must materialize loose object when pack copy is gone"
4372 );
4373 odb.read(&oid).expect("read after write");
4374 }
4375
4376 #[test]
4377 fn write_uses_single_exists_probe_when_not_loose() {
4378 let dir = TempDir::new().unwrap();
4379 git_in(dir.path(), &["init", "-q"]);
4380 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4381 git_in(dir.path(), &["config", "user.name", "T"]);
4382 fs::write(dir.path().join("f"), b"probe-once").unwrap();
4383 git_in(dir.path(), &["add", "f"]);
4384 git_in(dir.path(), &["commit", "-m", "c"]);
4385 git_in(dir.path(), &["repack", "-ad"]);
4386 let objects = dir.path().join(".git").join("objects");
4387 let odb = Odb::new(&objects);
4388 let oid = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
4389 assert!(!odb.object_path(&oid).exists());
4390 odb.reset_exists_probe();
4391 let oid2 = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
4392 assert_eq!(oid, oid2);
4393 assert_eq!(
4394 odb.exists_probe_count(),
4395 1,
4396 "pack/alternate write path must call exists() exactly once"
4397 );
4398 }
4399 #[test]
4400 fn write_existing_object_freshens() {
4401 use filetime::FileTime;
4402 use std::fs;
4403 use std::thread;
4404 use std::time::Duration;
4405
4406 let dir = TempDir::new().unwrap();
4407 let odb = Odb::new(dir.path());
4408 let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
4409 let path = odb.object_path(&oid);
4410 thread::sleep(Duration::from_millis(50));
4411 let before = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
4412 filetime::set_file_mtime(&path, before).unwrap();
4413
4414 let _ = odb.write(ObjectKind::Blob, b"hello").unwrap();
4415 let after = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
4416 assert!(
4417 after > before,
4418 "rewriting an existing loose object must freshen its mtime"
4419 );
4420 }
4421
4422 #[test]
4423 fn freshen_pack_once_per_odb() {
4424 use filetime::FileTime;
4425 use flate2::write::ZlibEncoder;
4426 use flate2::Compression;
4427 use std::fs;
4428 use std::io::Write;
4429 use std::thread;
4430 use std::time::Duration;
4431
4432 fn append_pack_object(buf: &mut Vec<u8>, type_code: u8, data: &[u8]) {
4433 let mut size = data.len();
4434 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
4435 size >>= 4;
4436 if size > 0 {
4437 buf.push(first | 0x80);
4438 while size > 0 {
4439 let b = (size & 0x7f) as u8;
4440 size >>= 7;
4441 buf.push(if size > 0 { b | 0x80 } else { b });
4442 }
4443 } else {
4444 buf.push(first);
4445 }
4446 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
4447 enc.write_all(data).unwrap();
4448 buf.extend_from_slice(&enc.finish().unwrap());
4449 }
4450
4451 fn write_v2_idx(
4452 idx_path: &Path,
4453 pack_path: &Path,
4454 entries: &[(ObjectId, u64)],
4455 ) -> Result<()> {
4456 let mut sorted = entries.to_vec();
4457 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
4458 let n = sorted.len();
4459 let mut fanout = [0u32; 256];
4460 for byte in 0u32..256 {
4461 let count = sorted
4462 .iter()
4463 .filter(|(oid, _)| u32::from(oid.as_bytes()[0]) <= byte)
4464 .count();
4465 fanout[byte as usize] = u32::try_from(count).unwrap_or(u32::MAX);
4466 }
4467 let mut buf = Vec::new();
4468 buf.extend_from_slice(b"\xfftOc");
4469 buf.extend_from_slice(&2u32.to_be_bytes());
4470 for f in fanout {
4471 buf.extend_from_slice(&f.to_be_bytes());
4472 }
4473 for (oid, _) in &sorted {
4474 buf.extend_from_slice(oid.as_bytes());
4475 }
4476 for _ in 0..n {
4477 buf.extend_from_slice(&0u32.to_be_bytes());
4478 }
4479 for (_, off) in &sorted {
4480 buf.extend_from_slice(&u32::try_from(*off).unwrap_or(0x8000_0000).to_be_bytes());
4481 }
4482 let pack_bytes = fs::read(pack_path)?;
4483 buf.extend_from_slice(&pack_bytes[pack_bytes.len() - 20..]);
4484 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
4485 fs::write(idx_path, buf)?;
4486 Ok(())
4487 }
4488
4489 let dir = TempDir::new().unwrap();
4490 let objects_dir = dir.path();
4491 let odb = Odb::new(objects_dir);
4492
4493 let oid_a = odb.write(ObjectKind::Blob, b"alpha").unwrap();
4494 let oid_b = odb.write(ObjectKind::Blob, b"beta").unwrap();
4495 fs::remove_file(odb.object_path(&oid_a)).unwrap();
4496 fs::remove_file(odb.object_path(&oid_b)).unwrap();
4497
4498 let mut pack = Vec::new();
4499 pack.extend_from_slice(b"PACK");
4500 pack.extend_from_slice(&2u32.to_be_bytes());
4501 pack.extend_from_slice(&2u32.to_be_bytes());
4502 let off_a = pack.len() as u64;
4503 append_pack_object(&mut pack, 3, b"alpha");
4504 let off_b = pack.len() as u64;
4505 append_pack_object(&mut pack, 3, b"beta");
4506 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
4507
4508 let pack_dir = objects_dir.join("pack");
4509 fs::create_dir_all(&pack_dir).unwrap();
4510 let pack_path = pack_dir.join("test-177.pack");
4511 let idx_path = pack_dir.join("test-177.idx");
4512 fs::write(&pack_path, &pack).unwrap();
4513 write_v2_idx(&idx_path, &pack_path, &[(oid_a, off_a), (oid_b, off_b)]).unwrap();
4514 odb.invalidate_packs();
4515
4516 thread::sleep(Duration::from_millis(50));
4517 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4518
4519 assert!(odb.freshen_object(&oid_a));
4520 let after_first = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4521 assert!(after_first > before);
4522
4523 assert!(odb.freshen_object(&oid_b));
4524 let after_second =
4525 FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4526 assert_eq!(
4527 after_second, after_first,
4528 "second freshen of another object in the same pack must not touch the pack again"
4529 );
4530 }
4531
4532 #[test]
4533 fn write_existing_packed_object_freshens_pack() {
4534 use filetime::FileTime;
4535 use flate2::write::ZlibEncoder;
4536 use flate2::Compression;
4537 use std::fs;
4538 use std::io::Write;
4539 use std::thread;
4540 use std::time::Duration;
4541
4542 let dir = TempDir::new().unwrap();
4543 let objects_dir = dir.path();
4544 let odb = Odb::new(objects_dir);
4545 let oid = odb.write(ObjectKind::Blob, b"packed-only").unwrap();
4546 fs::remove_file(odb.object_path(&oid)).unwrap();
4547
4548 let mut pack = Vec::new();
4549 pack.extend_from_slice(b"PACK");
4550 pack.extend_from_slice(&2u32.to_be_bytes());
4551 pack.extend_from_slice(&1u32.to_be_bytes());
4552 let off = pack.len() as u64;
4553 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
4554 enc.write_all(b"packed-only").unwrap();
4555 let compressed = enc.finish().unwrap();
4556 pack.push(0x30 | 0x03); pack.extend_from_slice(&compressed);
4558 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
4559
4560 let pack_dir = objects_dir.join("pack");
4561 fs::create_dir_all(&pack_dir).unwrap();
4562 let pack_path = pack_dir.join("single-177.pack");
4563 fs::write(&pack_path, &pack).unwrap();
4564 let idx_path = pack_dir.join("single-177.idx");
4566 let mut sorted = [(oid, off)];
4567 sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
4568 let mut fanout = [0u32; 256];
4569 for byte in 0u32..256 {
4570 fanout[byte as usize] = if u32::from(oid.as_bytes()[0]) <= byte {
4571 1
4572 } else {
4573 0
4574 };
4575 }
4576 let mut buf = Vec::new();
4577 buf.extend_from_slice(b"\xfftOc");
4578 buf.extend_from_slice(&2u32.to_be_bytes());
4579 for f in fanout {
4580 buf.extend_from_slice(&f.to_be_bytes());
4581 }
4582 buf.extend_from_slice(oid.as_bytes());
4583 buf.extend_from_slice(&0u32.to_be_bytes());
4584 buf.extend_from_slice(&(off as u32).to_be_bytes());
4585 buf.extend_from_slice(&pack[pack.len() - 20..]);
4586 buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
4587 fs::write(&idx_path, buf).unwrap();
4588 odb.invalidate_packs();
4589
4590 thread::sleep(Duration::from_millis(50));
4591 let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4592
4593 odb.write(ObjectKind::Blob, b"packed-only").unwrap();
4594
4595 let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
4596 assert!(
4597 after > before,
4598 "write of an existing packed object must freshen the pack mtime"
4599 );
4600 }
4601
4602 #[test]
4606 fn write_materializes_loose_when_pack_freshen_fails() {
4607 let dir = TempDir::new().unwrap();
4608 git_in(dir.path(), &["init", "-q"]);
4609 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4610 git_in(dir.path(), &["config", "user.name", "T"]);
4611 let payload = b"packed-fallback";
4612 fs::write(dir.path().join("f"), payload).unwrap();
4613 git_in(dir.path(), &["add", "f"]);
4614 git_in(dir.path(), &["commit", "-m", "c"]);
4615 git_in(dir.path(), &["repack", "-ad"]);
4616 let objects = dir.path().join(".git").join("objects");
4617 let odb = Odb::new(&objects);
4618 let oid = odb.write(ObjectKind::Blob, payload).unwrap();
4619 assert!(!odb.object_path(&oid).exists());
4620 assert!(odb.exists(&oid));
4621
4622 let pack_path = fs::read_dir(objects.join("pack"))
4623 .unwrap()
4624 .flatten()
4625 .find(|e| e.path().extension().is_some_and(|x| x == "pack"))
4626 .unwrap()
4627 .path();
4628 fs::remove_file(&pack_path).unwrap();
4629 fs::create_dir(&pack_path).unwrap();
4630 odb.invalidate_packs();
4631 assert!(!odb.freshen_object(&oid));
4632
4633 odb.write(ObjectKind::Blob, payload).unwrap();
4634 assert!(odb.object_path(&oid).exists());
4635 }
4636 #[test]
4637 fn write_with_options_silent_skips_freshen_on_existing_loose_object() {
4638 let dir = TempDir::new().unwrap();
4639 let odb = Odb::new(dir.path());
4640 let oid = odb.write(ObjectKind::Blob, b"stable").unwrap();
4641 let path = odb.object_path(&oid);
4642 let before = std::fs::metadata(&path).unwrap().modified().unwrap();
4643 std::thread::sleep(std::time::Duration::from_millis(20));
4644 let again = odb
4645 .write_with_options(
4646 ObjectKind::Blob,
4647 b"stable",
4648 WriteOptions {
4649 silent: true,
4650 ..WriteOptions::default()
4651 },
4652 )
4653 .unwrap();
4654 assert_eq!(oid, again);
4655 let after = std::fs::metadata(&path).unwrap().modified().unwrap();
4656 assert_eq!(before, after);
4657
4658 let touched = odb
4659 .write_with_options(
4660 ObjectKind::Blob,
4661 b"stable",
4662 WriteOptions {
4663 silent: false,
4664 ..WriteOptions::default()
4665 },
4666 )
4667 .unwrap();
4668 assert_eq!(oid, touched);
4669 let freshened = std::fs::metadata(&path).unwrap().modified().unwrap();
4670 assert!(freshened >= before);
4671 }
4672
4673 #[test]
4674 fn promisor_marker_checked_once_per_prepare() {
4675 use crate::pack::{
4676 clear_pack_cache, pack_cache_test_guard, test_pack_marker_stat_count,
4677 test_reset_pack_marker_stat_count,
4678 };
4679
4680 let _guard = pack_cache_test_guard();
4681 let dir = TempDir::new().unwrap();
4682 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4683 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4684 git_in(dir.path(), &["config", "user.name", "T"]);
4685 for i in 0..64 {
4686 fs::write(dir.path().join(format!("f{i}.txt")), format!("body {i}")).unwrap();
4687 git_in(dir.path(), &["add", "."]);
4688 git_in(dir.path(), &["commit", "-m", &format!("c{i}")]);
4689 }
4690 git_in(dir.path(), &["repack", "-a", "-d"]);
4691 let objects = dir.path().join(".git/objects");
4692 let oid = git_rev_parse_oid(dir.path(), "HEAD");
4693
4694 clear_pack_cache();
4695 test_reset_pack_marker_stat_count();
4696 let pack_count = fs::read_dir(objects.join("pack"))
4697 .unwrap()
4698 .filter_map(|e| e.ok())
4699 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
4700 .count();
4701 let expected_prepare_stats = u64::try_from(pack_count * 2).unwrap();
4702 let odb = Odb::new(&objects);
4703 let before_prepare = test_pack_marker_stat_count();
4704 assert!(odb.exists_local(&oid));
4705 assert_eq!(
4706 test_pack_marker_stat_count().saturating_sub(before_prepare),
4707 expected_prepare_stats,
4708 "prepare should stat promisor and mtimes once per pack"
4709 );
4710
4711 test_reset_pack_marker_stat_count();
4712 for _ in 0..10_000 {
4713 let before_probe = test_pack_marker_stat_count();
4714 assert!(odb.exists_local(&oid));
4715 assert_eq!(
4716 test_pack_marker_stat_count(),
4717 before_probe,
4718 "cached sidecar flags must not stat markers on each exists_local probe"
4719 );
4720 }
4721 }
4722
4723 #[test]
4724 fn promisor_marker_added_after_prepare_is_seen_after_reprepare() {
4725 use crate::pack::{
4726 clear_pack_cache, pack_cache_test_guard, reprepare_pack_directory_on_miss,
4727 };
4728
4729 let _guard = pack_cache_test_guard();
4730 let dir = TempDir::new().unwrap();
4731 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4732 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4733 git_in(dir.path(), &["config", "user.name", "T"]);
4734 fs::write(dir.path().join("one.txt"), b"x").unwrap();
4735 git_in(dir.path(), &["add", "one.txt"]);
4736 git_in(dir.path(), &["commit", "-m", "one"]);
4737 git_in(dir.path(), &["repack", "-a", "-d"]);
4738 let objects = dir.path().join(".git/objects");
4739 let oid = git_rev_parse_oid(dir.path(), "HEAD");
4740
4741 clear_pack_cache();
4742 let odb = Odb::new(&objects);
4743 assert!(odb.exists_local(&oid));
4744
4745 let pack_path = fs::read_dir(objects.join("pack"))
4746 .unwrap()
4747 .filter_map(|e| e.ok())
4748 .map(|e| e.path())
4749 .find(|p| p.extension().is_some_and(|x| x == "pack"))
4750 .unwrap();
4751 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
4752 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now()).unwrap();
4753
4754 assert!(
4755 odb.with_pack_store_for(&objects, || {
4756 reprepare_pack_directory_on_miss(&objects).unwrap()
4757 }),
4758 "pack directory change must require reprepare"
4759 );
4760 assert!(
4761 !odb.exists_local(&oid),
4762 "promisor marker must hide pack objects after reprepare refreshed sidecar flags"
4763 );
4764 }
4765
4766 #[test]
4767 fn git_promisor_marker_pack_not_materialized_for_exists_local() {
4768 use crate::pack::clear_pack_cache;
4769
4770 let dir = TempDir::new().unwrap();
4771 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4772 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4773 git_in(dir.path(), &["config", "user.name", "T"]);
4774 fs::write(dir.path().join("blob.txt"), b"git promisor compat").unwrap();
4775 git_in(dir.path(), &["add", "blob.txt"]);
4776 git_in(dir.path(), &["commit", "-m", "seed"]);
4777 git_in(dir.path(), &["repack", "-a", "-d"]);
4778 let objects = dir.path().join(".git/objects");
4779 let oid = git_rev_parse_oid(dir.path(), "HEAD");
4780 let pack_path = fs::read_dir(objects.join("pack"))
4781 .unwrap()
4782 .filter_map(|e| e.ok())
4783 .map(|e| e.path())
4784 .find(|p| p.extension().is_some_and(|x| x == "pack"))
4785 .unwrap();
4786 fs::write(pack_path.with_extension("promisor"), b"").unwrap();
4787
4788 clear_pack_cache();
4789 let odb = Odb::new(&objects);
4790 assert!(
4791 !odb.exists_local(&oid),
4792 "git-written pack with .promisor must not count as local materialization"
4793 );
4794
4795 let cat = git_run(dir.path(), &["cat-file", "-e", &oid.to_hex()]);
4796 assert!(
4797 cat.status.success(),
4798 "git still resolves the object in a promisor-marked pack"
4799 );
4800 }
4801
4802 #[test]
4803 fn packed_object_read_avoids_loose_path_open() {
4804 use crate::hot_path_test_metrics::HotPathMetricsScope;
4805 use crate::pack::clear_pack_cache;
4806
4807 let dir = TempDir::new().unwrap();
4808 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4809 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4810 git_in(dir.path(), &["config", "user.name", "T"]);
4811 fs::write(dir.path().join("packed.txt"), b"packed-only payload").unwrap();
4812 git_in(dir.path(), &["add", "packed.txt"]);
4813 git_in(dir.path(), &["commit", "-m", "pack"]);
4814 git_in(dir.path(), &["repack", "-a", "-d"]);
4815 let objects = dir.path().join(".git/objects");
4816 let oid = git_rev_parse_oid(dir.path(), "HEAD");
4817
4818 clear_pack_cache();
4819 let odb = Odb::new(&objects);
4820 odb.hot_path_metrics().set_loose_open_counting(true);
4821 odb.hot_path_metrics().reset_loose_path_open_attempts();
4822 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
4823
4824 let obj = odb.read(&oid).expect("packed read");
4825 assert_eq!(obj.kind, ObjectKind::Commit);
4826 assert_eq!(
4827 odb.hot_path_metrics().loose_path_open_attempts(),
4828 0,
4829 "packed objects must not attempt a loose-path open"
4830 );
4831 }
4832
4833 #[test]
4834 fn loose_only_read_records_loose_path_open() {
4835 use crate::hot_path_test_metrics::HotPathMetricsScope;
4836
4837 let dir = TempDir::new().unwrap();
4838 let objects = dir.path().join("objects");
4839 fs::create_dir_all(&objects).unwrap();
4840 let odb = Odb::new(&objects);
4841 let oid = odb
4842 .write(ObjectKind::Blob, b"loose-only")
4843 .expect("write loose");
4844 odb.hot_path_metrics().set_loose_open_counting(true);
4845 odb.hot_path_metrics().reset_loose_path_open_attempts();
4846 let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
4847
4848 odb.read(&oid).expect("loose read");
4849 assert_eq!(
4850 odb.hot_path_metrics().loose_path_open_attempts(),
4851 1,
4852 "loose fallback must record a path open attempt"
4853 );
4854 }
4855
4856 #[test]
4857 fn loose_and_packed_copies_return_identical_bytes() {
4858 use crate::pack::clear_pack_cache;
4859
4860 let dir = TempDir::new().unwrap();
4861 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4862 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4863 git_in(dir.path(), &["config", "user.name", "T"]);
4864 fs::write(dir.path().join("dup.txt"), b"duplicate loose and packed\n").unwrap();
4865 git_in(dir.path(), &["add", "dup.txt"]);
4866 git_in(dir.path(), &["commit", "-m", "seed"]);
4867 let objects = dir.path().join(".git/objects");
4868 let tree_oid = git_rev_parse_oid(dir.path(), "HEAD^{tree}");
4869 git_in(dir.path(), &["repack", "-a"]);
4870 clear_pack_cache();
4871 let odb = Odb::new(&objects);
4872 let loose_path = tree_oid.loose_path_in(&objects);
4873 assert!(
4874 loose_path.is_file(),
4875 "loose copy must remain after repack -a"
4876 );
4877 let loose = Odb::read_loose_verify_oid(&loose_path, &tree_oid).expect("loose tree");
4878 let packed = odb.read(&tree_oid).expect("packed tree");
4879 assert_eq!(packed.data, loose.data);
4880 assert_eq!(packed.kind, loose.kind);
4881 }
4882
4883 #[test]
4884 fn freshly_written_loose_object_visible_before_repack() {
4885 let dir = TempDir::new().unwrap();
4886 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4887 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4888 git_in(dir.path(), &["config", "user.name", "T"]);
4889 fs::write(dir.path().join("seed.txt"), b"seed").unwrap();
4890 git_in(dir.path(), &["add", "seed.txt"]);
4891 git_in(dir.path(), &["commit", "-m", "seed"]);
4892 git_in(dir.path(), &["repack", "-a", "-d"]);
4893 let objects = dir.path().join(".git/objects");
4894 let odb = Odb::new(&objects);
4895 let fresh = b"brand-new loose blob\n";
4896 let oid = odb.write(ObjectKind::Blob, fresh).expect("loose write");
4897 let obj = odb.read(&oid).expect("immediate loose read");
4898 assert_eq!(obj.data.as_slice(), fresh);
4899 }
4900
4901 #[test]
4902 fn pack_added_after_first_read_found_via_odb_reprepare() {
4903 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
4904
4905 let _guard = pack_cache_test_guard();
4906 clear_pack_cache();
4907 let dir = TempDir::new().unwrap();
4908 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4909 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4910 git_in(dir.path(), &["config", "user.name", "T"]);
4911 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
4912 git_in(dir.path(), &["add", "a.txt"]);
4913 git_in(dir.path(), &["commit", "-m", "a"]);
4914 git_in(dir.path(), &["repack", "-a", "-d"]);
4915 let objects = dir.path().join(".git/objects");
4916 let pack_dir = objects.join("pack");
4917 let git_dir = dir.path().join(".git");
4918
4919 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4920 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
4921 odb.read(&warm_oid).expect("warm read");
4922
4923 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
4924 git_in(dir.path(), &["add", "b.txt"]);
4925 git_in(dir.path(), &["commit", "-m", "b"]);
4926 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
4927
4928 std::thread::sleep(std::time::Duration::from_secs(1));
4929 filetime::set_file_mtime(&pack_dir, filetime::FileTime::now()).unwrap();
4930
4931 let obj = odb.read(&new_oid).expect("new pack via reprepare");
4932 assert_eq!(obj.kind, ObjectKind::Commit);
4933 }
4934
4935 #[test]
4936 fn late_midx_after_warm_read_resolves_new_commit() {
4937 use crate::pack::{clear_pack_cache, pack_cache_test_guard};
4938
4939 let _guard = pack_cache_test_guard();
4940 clear_pack_cache();
4941 let dir = TempDir::new().unwrap();
4942 git_in(dir.path(), &["init", "-q", "-b", "main"]);
4943 git_in(dir.path(), &["config", "user.email", "t@example.com"]);
4944 git_in(dir.path(), &["config", "user.name", "T"]);
4945 fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
4946 git_in(dir.path(), &["add", "a.txt"]);
4947 git_in(dir.path(), &["commit", "-m", "a"]);
4948 git_in(dir.path(), &["repack", "-a", "-d"]);
4949 let objects = dir.path().join(".git/objects");
4950 let git_dir = dir.path().join(".git");
4951 assert!(
4952 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_none(),
4953 "fixture starts without a MIDX"
4954 );
4955
4956 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4957 let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
4958 odb.read(&warm_oid).expect("warm read before MIDX exists");
4959
4960 fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
4961 git_in(dir.path(), &["add", "b.txt"]);
4962 git_in(dir.path(), &["commit", "-m", "b"]);
4963 git_in(dir.path(), &["repack", "-a", "-d"]);
4964 git_in(dir.path(), &["multi-pack-index", "write"]);
4965 let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
4966
4967 let cat = git_run(dir.path(), &["cat-file", "-e", &new_oid.to_hex()]);
4968 assert!(
4969 cat.status.success(),
4970 "system git must resolve the new commit"
4971 );
4972
4973 let obj = odb
4974 .read(&new_oid)
4975 .expect("MIDX-covered commit after late MIDX write");
4976 assert_eq!(obj.kind, ObjectKind::Commit);
4977 assert!(
4978 crate::midx::cached_tip_midx_path(&objects.join("pack")).is_some(),
4979 "successful read after late MIDX must refresh the tip cache"
4980 );
4981 }
4982}