Skip to main content

sley_odb/
pack.rs

1//! Pack-backed object storage: [`FileObjectDatabase`], in-memory [`ObjectDatabase`],
2//! and the shared decode/index caches that back packed reads.
3//!
4//! ## Cache invariants (thread safety)
5//!
6//! A [`FileObjectDatabase`] is [`Clone`] via `Arc` on every cache map. Cloned handles
7//! share the same caches and may be used from multiple threads concurrently.
8//!
9//! * **Read-mostly maps** (`pack_indexes`, `pack_bytes`, `pack_reverse_indexes`,
10//!   `multi_pack_indexes`, `multi_pack_oid_lookups`) use [`RwLock`]: concurrent readers
11//!   take shared locks; writers take exclusive locks only on insert/clear. Lookups never
12//!   hold a lock across decode or I/O.
13//! * **Mutation-heavy maps** (`decoded`, `pack_deltas`, `pack_header_types`, `pack_registry`)
14//!   stay behind [`Mutex`] because inserts and LRU eviction interleave reads and writes on
15//!   the same critical section.
16//! * **Per-pack state** on [`RegisteredPack`](crate::registry::RegisteredPack) mirrors the
17//!   split: parsed indexes are [`RwLock`]-cached; delta-base LRU caches stay [`Mutex`]-backed.
18//! * **`refresh_read_cache`** clears every shared map so the next read sees packs installed
19//!   out-of-band; callers must not hold a cache guard across it.
20
21use parking_lot::RwLock;
22use sley_core::{GitError, MissingObjectContext, ObjectFormat, ObjectId, Result};
23use sley_object::{Commit, EncodedObject, ObjectType, Tag, TreeEntries};
24use sley_pack::{
25    MultiPackIndex, MultiPackIndexOidLookup, PackIndex, PackIndexByteSource, PackIndexViewData,
26    PackReverseIndex,
27};
28use std::collections::{HashMap, HashSet};
29use std::path::{Path, PathBuf};
30use std::sync::Mutex;
31use std::sync::{Arc, OnceLock};
32use std::{env, fs};
33
34use crate::{ObjectReader, ObjectWriter, implied_empty_tree_object};
35
36use crate::install::{ObjectPrefixResolution, ObjectStorageInfo};
37use crate::loose::LooseObjectStore;
38use crate::reachability::{
39    PackDeltaBase, PackIndexOffsetInfo, pack_entry_delta_base, scan_pack_index_offsets,
40    scan_pack_offsets_without_index, zero_oid,
41};
42use crate::registry::{
43    ObjectPresenceChecker, PackLookup, PackRegistryCache, PackRegistrySnapshot,
44    alternate_object_dirs, object_ids_in_objects_dir, object_ids_with_prefix_in_objects_dir,
45    read_incremental_midx_chain, repository_objects_dir, same_registered_pack_set,
46    scan_pack_registry, validate_object_id_prefix,
47};
48
49pub struct ObjectDatabase {
50    pub(crate) format: ObjectFormat,
51    // Behind a `Mutex` so `write_object` can take `&self` (matching the
52    // `ObjectWriter` trait) and a single handle can interleave reads and writes
53    // without a `&mut` borrow — the same shared-by-`&` shape the file-backed
54    // database uses for its caches. Removes the need for callers to wrap this in
55    // a `RefCell`/`&mut` just to write (see sley-fetch's former `RefCell` dance).
56    pub(crate) objects: Mutex<HashMap<ObjectId, Arc<EncodedObject>>>,
57    pub(crate) promisor: bool,
58}
59
60impl ObjectDatabase {
61    pub fn new(format: ObjectFormat) -> Self {
62        Self {
63            format,
64            objects: Mutex::new(HashMap::new()),
65            promisor: false,
66        }
67    }
68
69    pub fn with_promisor(mut self, promisor: bool) -> Self {
70        self.promisor = promisor;
71        self
72    }
73
74    pub fn contains(&self, oid: &ObjectId) -> bool {
75        self.objects
76            .lock()
77            .map(|objects| objects.contains_key(oid))
78            .unwrap_or(false)
79    }
80
81    pub fn validate(&self, oid: &ObjectId) -> Result<()> {
82        let object = self.read_object(oid)?;
83        let actual = object.object_id(self.format)?;
84        if &actual == oid {
85            Ok(())
86        } else {
87            Err(GitError::InvalidObject(format!(
88                "object id mismatch: expected {oid}, got {actual}"
89            )))
90        }
91    }
92}
93
94impl ObjectReader for ObjectDatabase {
95    fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
96        self.objects
97            .lock()
98            .map_err(|_| GitError::object_not_found_in(*oid, MissingObjectContext::Read))?
99            .get(oid)
100            .map(Arc::clone)
101            .or_else(|| implied_empty_tree_object(self.format, oid))
102            .ok_or_else(|| GitError::object_not_found_in(*oid, MissingObjectContext::Read))
103    }
104}
105
106impl ObjectWriter for ObjectDatabase {
107    fn write_object(&self, object: EncodedObject) -> Result<ObjectId> {
108        let oid = object.object_id(self.format)?;
109        self.objects
110            .lock()
111            .map_err(|_| GitError::Io("object cache lock poisoned".into()))?
112            .entry(oid)
113            .or_insert_with(|| Arc::new(object));
114        Ok(oid)
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct Alternate {
120    pub path: std::path::PathBuf,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct PartialClonePolicy {
125    pub promisor_remote: Option<String>,
126    pub allow_missing_promised_objects: bool,
127}
128
129/// Raw pack-file bytes keyed by pack path, shared across cloned handles. Loaded
130/// once so individual objects can be decoded at their offsets (see
131/// [`sley_pack::read_object_at`]) without re-reading the whole file per read.
132pub(crate) type PackBytesCache = Arc<RwLock<HashMap<PathBuf, Arc<PackData>>>>;
133
134/// Backing bytes of a pack file: either memory-mapped (under the `mmap` feature)
135/// or read into the heap. Both deref to `&[u8]`, so the decode path is identical.
136#[derive(Debug)]
137pub(crate) enum PackData {
138    #[cfg(feature = "mmap")]
139    Mapped(sley_mmap::MappedFile),
140    Heap(Vec<u8>),
141}
142
143impl std::ops::Deref for PackData {
144    type Target = [u8];
145
146    fn deref(&self) -> &[u8] {
147        match self {
148            #[cfg(feature = "mmap")]
149            Self::Mapped(mapped) => mapped,
150            Self::Heap(bytes) => bytes,
151        }
152    }
153}
154
155/// Load a pack file's bytes: memory-mapped when the `mmap` feature is on (falling
156/// back to a heap read if the map fails), otherwise read into the heap.
157#[cfg(feature = "mmap")]
158pub(crate) fn load_pack_data(pack_path: &Path) -> Result<PackData> {
159    match sley_mmap::MappedFile::open_pack(pack_path) {
160        Ok(mapped) => Ok(PackData::Mapped(mapped)),
161        Err(_) => Ok(PackData::Heap(fs::read(pack_path)?)),
162    }
163}
164
165#[cfg(not(feature = "mmap"))]
166pub(crate) fn load_pack_data(pack_path: &Path) -> Result<PackData> {
167    Ok(PackData::Heap(fs::read(pack_path)?))
168}
169
170#[cfg(feature = "mmap")]
171pub(crate) fn load_pack_index_data(index_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
172    match sley_mmap::MappedFile::open_pack(index_path) {
173        Ok(mapped) => Ok(Arc::new(mapped)),
174        Err(_) => Ok(Arc::new(fs::read(index_path)?)),
175    }
176}
177
178#[cfg(not(feature = "mmap"))]
179pub(crate) fn load_pack_index_data(index_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
180    Ok(Arc::new(fs::read(index_path)?))
181}
182
183#[cfg(feature = "mmap")]
184fn load_multi_pack_index_lookup_data(midx_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
185    match sley_mmap::MappedFile::open_multi_pack_index(midx_path) {
186        Ok(mapped) => Ok(Arc::new(mapped)),
187        Err(_) => Ok(Arc::new(fs::read(midx_path)?)),
188    }
189}
190
191#[cfg(not(feature = "mmap"))]
192fn load_multi_pack_index_lookup_data(midx_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
193    Ok(Arc::new(fs::read(midx_path)?))
194}
195
196/// Memory-capped LRU of recently decoded objects, shared across cloned handles,
197/// so hot delta bases and repeated reads during a walk aren't re-decoded. The
198/// cache is bounded by an approximate byte budget (not a fixed object count) so
199/// it neither thrashes on bulk reads of small objects nor blows up on a few
200/// large ones.
201pub(crate) type DecodedObjectCache = Arc<Mutex<LruObjectCache>>;
202
203/// Per-pack caches of objects decoded from a pack, keyed by pack path and then by
204/// the in-pack byte offset of each object's entry. Shared across cloned handles.
205/// This is the delta-base cache: resolving a delta chain by offset reuses already
206/// decoded bases instead of re-inflating the whole chain on every read.
207pub(crate) type PackDeltaCaches = Arc<Mutex<HashMap<PathBuf, Arc<Mutex<LruOffsetCache>>>>>;
208
209/// Per-pack memo of `in-pack offset -> end-of-chain object type` for the
210/// `cat-file --batch-check` header fast path. Resolving a packed delta's *type*
211/// walks the delta chain to its base; without this memo every header read
212/// re-walks (and re-inflates) the whole chain, so reading every object in a
213/// deeply-deltified pack is super-linear (sley#26). The type only depends on the
214/// chain base, so memoizing `offset -> type` lets each chain be walked at most
215/// once across a batch. Keyed by pack path so an offset key is never applied to
216/// the wrong pack's bytes; shared across cloned handles.
217/// One pack's offset-keyed header memo (see [`PackHeaderTypeCaches`]).
218pub(crate) type PackHeaderTypeCache = Arc<Mutex<HashMap<u64, (ObjectType, u64)>>>;
219
220pub(crate) type PackHeaderTypeCaches = Arc<Mutex<HashMap<PathBuf, PackHeaderTypeCache>>>;
221
222/// Default approximate byte budget for the decoded-object LRU. Sized to comfortably
223/// hold the working set of a history walk (commits/trees/blobs and their delta
224/// bases) without growing without bound on large repositories. Overridable via the
225/// `SLEY_OBJECT_CACHE_BYTES` environment variable; there is currently no git-config
226/// hook threaded into the object database, so this constant is the default.
227const DEFAULT_OBJECT_CACHE_BYTES: usize = 96 * 1024 * 1024;
228
229/// Default approximate byte budget for each per-pack delta-base cache. Holds the
230/// decoded bases of the delta chains being walked so neighboring reads stay warm.
231/// Overridable via `SLEY_DELTA_BASE_CACHE_BYTES`.
232const DEFAULT_DELTA_BASE_CACHE_BYTES: usize = 96 * 1024 * 1024;
233
234/// Approximate heap cost of caching one [`EncodedObject`]: its body plus a fixed
235/// allowance for the key, enum/`Vec` headers, and per-entry map overhead. Used
236/// only to drive eviction, so an estimate is fine.
237pub(crate) fn cached_object_cost(object: &EncodedObject) -> usize {
238    object.body.len().saturating_add(64)
239}
240
241/// Read an approximate byte budget from `var`, falling back to `default` when the
242/// variable is unset or unparseable. A value of `0` disables the cache.
243fn cache_budget_from_env(var: &str, default: usize) -> usize {
244    match env::var(var) {
245        Ok(value) => value.trim().parse::<usize>().unwrap_or(default),
246        Err(_) => default,
247    }
248}
249
250/// Approximate byte budget for the decoded-object LRU (see
251/// [`DEFAULT_OBJECT_CACHE_BYTES`], `SLEY_OBJECT_CACHE_BYTES`).
252///
253/// Resolved once per process: the environment does not change under us, and a new
254/// `FileObjectDatabase` is built often enough (e.g. once per revision resolved)
255/// that re-reading the variable each time showed up as per-object overhead.
256pub(crate) fn object_cache_budget() -> usize {
257    static BUDGET: OnceLock<usize> = OnceLock::new();
258    *BUDGET.get_or_init(|| {
259        cache_budget_from_env("SLEY_OBJECT_CACHE_BYTES", DEFAULT_OBJECT_CACHE_BYTES)
260    })
261}
262
263/// Approximate byte budget for each per-pack delta-base cache (see
264/// [`DEFAULT_DELTA_BASE_CACHE_BYTES`], `SLEY_DELTA_BASE_CACHE_BYTES`). Resolved
265/// once per process for the same reason as [`object_cache_budget`].
266pub(crate) fn delta_base_cache_budget() -> usize {
267    static BUDGET: OnceLock<usize> = OnceLock::new();
268    *BUDGET.get_or_init(|| {
269        cache_budget_from_env(
270            "SLEY_DELTA_BASE_CACHE_BYTES",
271            DEFAULT_DELTA_BASE_CACHE_BYTES,
272        )
273    })
274}
275
276/// Whether to re-hash every object on read and compare it to the requested id.
277///
278/// Off by default, matching git: reads trust the pack index → offset mapping and
279/// the loose object's on-disk name, and object ids are verified where git verifies
280/// them — when a pack is received (the index build re-hashes every object) and on
281/// demand via [`FileObjectDatabase`]'s `validate`/fsck. Re-hashing on *every* read
282/// dominated bulk-read cost (a scalar pure-Rust SHA-1 over each object's full
283/// body), so it is opt-in via `SLEY_VERIFY_READS` (any value other than unset, ``,
284/// or `0`) for callers that want the paranoid check back. Read once and cached, so
285/// the default path pays only a single relaxed atomic load per read.
286pub(crate) fn verify_reads_enabled() -> bool {
287    static VERIFY: OnceLock<bool> = OnceLock::new();
288    *VERIFY.get_or_init(|| match env::var("SLEY_VERIFY_READS") {
289        Ok(value) => !matches!(value.trim(), "" | "0"),
290        Err(_) => false,
291    })
292}
293
294/// A memory-capped LRU map from a key `K` to a decoded [`EncodedObject`].
295///
296/// Eviction is by approximate byte budget (gix-style), not object count, so the
297/// cache adapts to object size. On access an entry is moved to most-recently-used;
298/// on insert, least-recently-used entries are dropped until the budget holds. A
299/// budget of `0` makes the cache inert. Generic over the key so it backs both the
300/// oid-keyed decoded-object cache and the offset-keyed delta-base cache.
301#[derive(Debug)]
302pub(crate) struct LruCache<K: std::hash::Hash + Eq + Clone> {
303    budget: usize,
304    used: usize,
305    map: HashMap<K, LruEntry<K>>,
306    head: Option<K>,
307    tail: Option<K>,
308}
309
310#[derive(Debug)]
311struct LruEntry<K> {
312    object: Arc<EncodedObject>,
313    prev: Option<K>,
314    next: Option<K>,
315}
316
317impl<K: std::hash::Hash + Eq + Clone> LruCache<K> {
318    pub(crate) fn new(budget: usize) -> Self {
319        Self {
320            budget,
321            used: 0,
322            map: HashMap::new(),
323            head: None,
324            tail: None,
325        }
326    }
327
328    pub(crate) fn get(&mut self, key: &K) -> Option<Arc<EncodedObject>> {
329        let object = Arc::clone(&self.map.get(key)?.object);
330        self.touch(key);
331        Some(object)
332    }
333
334    /// Move `key` to the most-recently-used end in O(1).
335    pub(crate) fn touch(&mut self, key: &K) {
336        if self.tail.as_ref() == Some(key) {
337            return;
338        }
339        if self.map.contains_key(key) {
340            self.detach(key);
341            self.attach_back(key.clone());
342        }
343    }
344
345    /// Drop `key` from both the map and the recency queue, releasing its budget.
346    pub(crate) fn remove(&mut self, key: &K) {
347        if let Some(entry) = self.map.get(key) {
348            self.used = self.used.saturating_sub(cached_object_cost(&entry.object));
349        }
350        self.detach(key);
351        self.map.remove(key);
352    }
353
354    pub(crate) fn detach(&mut self, key: &K) {
355        let Some((prev, next)) = self.map.get_mut(key).map(|entry| {
356            let prev = entry.prev.take();
357            let next = entry.next.take();
358            (prev, next)
359        }) else {
360            return;
361        };
362
363        match &prev {
364            Some(prev_key) => {
365                if let Some(prev_entry) = self.map.get_mut(prev_key) {
366                    prev_entry.next = next.clone();
367                }
368            }
369            None => self.head = next.clone(),
370        }
371        match &next {
372            Some(next_key) => {
373                if let Some(next_entry) = self.map.get_mut(next_key) {
374                    next_entry.prev = prev.clone();
375                }
376            }
377            None => self.tail = prev.clone(),
378        }
379    }
380
381    pub(crate) fn attach_back(&mut self, key: K) {
382        let previous_tail = self.tail.replace(key.clone());
383        match previous_tail {
384            Some(tail_key) => {
385                if let Some(tail_entry) = self.map.get_mut(&tail_key) {
386                    tail_entry.next = Some(key.clone());
387                }
388                if let Some(entry) = self.map.get_mut(&key) {
389                    entry.prev = Some(tail_key);
390                    entry.next = None;
391                }
392            }
393            None => {
394                self.head = Some(key.clone());
395                if let Some(entry) = self.map.get_mut(&key) {
396                    entry.prev = None;
397                    entry.next = None;
398                }
399            }
400        }
401    }
402
403    pub(crate) fn clear(&mut self) {
404        self.map.clear();
405        self.head = None;
406        self.tail = None;
407        self.used = 0;
408    }
409
410    pub(crate) fn put(&mut self, key: K, object: Arc<EncodedObject>) {
411        if self.budget == 0 {
412            return;
413        }
414        let cost = cached_object_cost(&object);
415        // A single object larger than the whole budget is not worth caching; it
416        // would immediately evict everything including itself. Drop any stale
417        // smaller entry stored under the same key so accounting stays exact.
418        if cost > self.budget {
419            self.remove(&key);
420            return;
421        }
422        if let Some(entry) = self.map.get_mut(&key) {
423            let previous = std::mem::replace(&mut entry.object, object);
424            // Replacing an existing entry: adjust accounting and refresh recency.
425            self.used = self
426                .used
427                .saturating_sub(cached_object_cost(&previous))
428                .saturating_add(cost);
429            self.touch(&key);
430        } else {
431            self.used = self.used.saturating_add(cost);
432            self.map.insert(
433                key.clone(),
434                LruEntry {
435                    object,
436                    prev: None,
437                    next: None,
438                },
439            );
440            self.attach_back(key);
441        }
442        while self.used > self.budget {
443            let Some(evicted) = self.head.clone() else {
444                break;
445            };
446            self.remove(&evicted);
447        }
448    }
449}
450
451/// Decoded-object cache keyed by object id (loose + packed reads share it).
452type LruObjectCache = LruCache<ObjectId>;
453/// Delta-base cache keyed by in-pack byte offset, scoped to one pack.
454pub(crate) type LruOffsetCache = LruCache<u64>;
455
456/// Bridges the offset-keyed [`LruOffsetCache`] to [`sley_pack::PackDeltaCache`]
457/// so the pack decoder can reuse decoded delta bases. Holds the shared cache
458/// behind its mutex; a poisoned lock simply behaves as a cache miss/no-op, so a
459/// decode still completes correctly (just without reuse).
460struct PackDeltaCacheAdapter<'a>(&'a Arc<Mutex<LruOffsetCache>>);
461
462impl sley_pack::PackDeltaCache for PackDeltaCacheAdapter<'_> {
463    fn get(&self, offset: u64) -> Option<Arc<EncodedObject>> {
464        self.0.lock().ok()?.get(&offset)
465    }
466
467    fn insert(&self, offset: u64, object: Arc<EncodedObject>) {
468        if let Ok(mut cache) = self.0.lock() {
469            cache.put(offset, object);
470        }
471    }
472}
473
474/// Bridges a per-pack `offset -> ObjectType` memo into the header fast path so
475/// the ofs-delta chain walk is performed at most once per chain across a batch
476/// of `read_object_header` calls (sley#26).
477struct PackHeaderTypeCacheAdapter<'a>(&'a PackHeaderTypeCache);
478
479impl sley_pack::HeaderTypeCache for PackHeaderTypeCacheAdapter<'_> {
480    fn get(&self, pack_offset: u64) -> Option<(ObjectType, u64)> {
481        self.0.lock().ok()?.get(&pack_offset).copied()
482    }
483
484    fn put(&mut self, pack_offset: u64, header: (ObjectType, u64)) {
485        if let Ok(mut cache) = self.0.lock() {
486            cache.insert(pack_offset, header);
487        }
488    }
489}
490
491/// Parsed pack indexes keyed by `.idx` path, shared across cloned handles. This
492/// remains for MIDX and path-only fallback lookups; normal pack-directory scans
493/// use [`PackRegistrySnapshot`] so the lookup hot path can walk already-parsed
494/// pack records directly.
495pub(crate) type PackIndexCache = Arc<RwLock<HashMap<PathBuf, Arc<PackIndexViewData>>>>;
496
497/// Optional `.rev` reverse indexes keyed by `.idx` path, shared across cloned
498/// handles. A cached `None` means no usable reverse index was found.
499pub(crate) type PackReverseIndexCache =
500    Arc<RwLock<HashMap<PathBuf, Option<Arc<PackReverseIndex>>>>>;
501
502/// Parsed multi-pack-index files keyed by path, shared across cloned handles.
503/// Caches the MIDX parse so object lookups in repositories with a MIDX avoid
504/// reparsing the same fanout/object tables for every read.
505pub(crate) type MultiPackIndexCache = Arc<RwLock<HashMap<PathBuf, Arc<MultiPackIndex>>>>;
506
507/// Explicit replacement-object policy injected by repository setup.
508///
509/// The map is deliberately independent of refs/config/environment: embedders
510/// decide whether replacement refs are enabled and pass the already-resolved
511/// old-to-new object ids into the ODB.
512#[derive(Debug, Clone, Default, PartialEq, Eq)]
513pub struct ObjectReplacements {
514    replacements: HashMap<ObjectId, ObjectId>,
515}
516
517impl ObjectReplacements {
518    pub fn new(replacements: impl IntoIterator<Item = (ObjectId, ObjectId)>) -> Self {
519        Self {
520            replacements: replacements.into_iter().collect(),
521        }
522    }
523
524    pub fn is_empty(&self) -> bool {
525        self.replacements.is_empty()
526    }
527
528    /// Follow a replacement chain, rejecting cycles or chains beyond Git's
529    /// five-link recursion guard.
530    pub fn resolve(&self, oid: &ObjectId) -> Result<ObjectId> {
531        let mut current = *oid;
532        let mut seen = HashSet::new();
533        for _ in 0..5 {
534            if !seen.insert(current) {
535                return Err(GitError::InvalidObject(format!(
536                    "replace depth too high for object {oid}"
537                )));
538            }
539            let Some(next) = self.replacements.get(&current) else {
540                return Ok(current);
541            };
542            // `git replace --graft` may materialize an unchanged commit and
543            // consequently write an identity replace ref. Git treats that as
544            // a terminal no-op rather than replacement recursion.
545            if *next == current {
546                return Ok(current);
547            }
548            current = *next;
549        }
550        if self.replacements.contains_key(&current) {
551            return Err(GitError::InvalidObject(format!(
552                "replace depth too high for object {oid}"
553            )));
554        }
555        Ok(current)
556    }
557}
558
559/// Raw multi-pack-index OID lookup tables keyed by path, shared across cloned
560/// handles. These avoid hashing and materializing every MIDX object when a
561/// command only needs point lookups.
562type MultiPackIndexOidLookupCache = Arc<RwLock<HashMap<PathBuf, Arc<MultiPackIndexOidLookup>>>>;
563
564/// One registered `.idx`/`.pack` pair from a pack directory. The index is parsed
565/// when the registry snapshot is built; pack bytes and per-pack decode/header
566/// caches hang directly off this record so repeated object lookups do not bounce
567/// through path-keyed maps.
568#[derive(Debug, Clone)]
569pub struct FileObjectDatabase {
570    pub(crate) loose: LooseObjectStore,
571    pub(crate) objects_dir: PathBuf,
572    pub(crate) alternates: Vec<PathBuf>,
573    pub(crate) format: ObjectFormat,
574    pub(crate) pack_bytes: PackBytesCache,
575    pub(crate) pack_indexes: PackIndexCache,
576    pub(crate) pack_reverse_indexes: PackReverseIndexCache,
577    pub(crate) multi_pack_indexes: MultiPackIndexCache,
578    pub(crate) multi_pack_oid_lookups: MultiPackIndexOidLookupCache,
579    pub(crate) pack_registry: PackRegistryCache,
580    pub(crate) decoded: DecodedObjectCache,
581    pub(crate) pack_deltas: PackDeltaCaches,
582    pub(crate) pack_header_types: PackHeaderTypeCaches,
583    pub(crate) promisor_objects: Arc<OnceLock<HashSet<ObjectId>>>,
584    /// Whether the owning repository actually has a promisor remote configured
585    /// (`extensions.partialclone` is set, or some `remote.<name>.promisor` is
586    /// true). Mirrors git's `is_promisor_object`, which only treats objects in
587    /// `.promisor` packs as "promised" when `repo_has_promisor_remote()` holds:
588    /// a stray `.promisor` sidecar in a non-partial repo must NOT excuse missing
589    /// objects from fsck. Defaults to `false`; the fsck driver opts in after
590    /// reading the repo config.
591    pub(crate) promisor_remote_present: bool,
592    /// Graft points (`$GIT_DIR/shallow`), loaded lazily on the first
593    /// [`ObjectReader::is_shallow_graft`] query. `$GIT_DIR` is taken to be
594    /// the parent of `objects_dir`, matching the standard layout.
595    pub(crate) shallow_grafts: Arc<std::sync::OnceLock<HashSet<ObjectId>>>,
596    pub(crate) replacements: Arc<ObjectReplacements>,
597}
598
599fn read_shallow_grafts(shallow_file: &Path, format: ObjectFormat) -> HashSet<ObjectId> {
600    let Ok(contents) = std::fs::read_to_string(shallow_file) else {
601        return HashSet::new();
602    };
603    contents
604        .lines()
605        .filter_map(|line| ObjectId::from_hex(format, line.trim()).ok())
606        .collect()
607}
608
609impl FileObjectDatabase {
610    /// The object-id format (hash algorithm) this database was opened with.
611    pub fn object_format(&self) -> ObjectFormat {
612        self.format
613    }
614
615    /// The repository object directory this database reads from.
616    pub fn objects_dir(&self) -> &Path {
617        &self.objects_dir
618    }
619
620    pub fn new(objects_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
621        let objects_dir = objects_dir.into();
622        Self {
623            loose: LooseObjectStore::new(objects_dir.clone(), format),
624            alternates: alternate_object_dirs(&objects_dir),
625            objects_dir,
626            format,
627            pack_bytes: Arc::new(RwLock::new(HashMap::new())),
628            pack_indexes: Arc::new(RwLock::new(HashMap::new())),
629            pack_reverse_indexes: Arc::new(RwLock::new(HashMap::new())),
630            multi_pack_indexes: Arc::new(RwLock::new(HashMap::new())),
631            multi_pack_oid_lookups: Arc::new(RwLock::new(HashMap::new())),
632            pack_registry: Arc::new(Mutex::new(None)),
633            decoded: Arc::new(Mutex::new(LruObjectCache::new(object_cache_budget()))),
634            pack_deltas: Arc::new(Mutex::new(HashMap::new())),
635            pack_header_types: Arc::new(Mutex::new(HashMap::new())),
636            promisor_objects: Arc::new(OnceLock::new()),
637            promisor_remote_present: false,
638            shallow_grafts: Arc::new(std::sync::OnceLock::new()),
639            replacements: Arc::new(ObjectReplacements::default()),
640        }
641    }
642
643    pub(crate) fn without_alternates(
644        objects_dir: impl Into<PathBuf>,
645        format: ObjectFormat,
646    ) -> Self {
647        let objects_dir = objects_dir.into();
648        Self {
649            loose: LooseObjectStore::new(objects_dir.clone(), format),
650            alternates: Vec::new(),
651            objects_dir,
652            format,
653            pack_bytes: Arc::new(RwLock::new(HashMap::new())),
654            pack_indexes: Arc::new(RwLock::new(HashMap::new())),
655            pack_reverse_indexes: Arc::new(RwLock::new(HashMap::new())),
656            multi_pack_indexes: Arc::new(RwLock::new(HashMap::new())),
657            multi_pack_oid_lookups: Arc::new(RwLock::new(HashMap::new())),
658            pack_registry: Arc::new(Mutex::new(None)),
659            decoded: Arc::new(Mutex::new(LruObjectCache::new(object_cache_budget()))),
660            pack_deltas: Arc::new(Mutex::new(HashMap::new())),
661            pack_header_types: Arc::new(Mutex::new(HashMap::new())),
662            promisor_objects: Arc::new(OnceLock::new()),
663            promisor_remote_present: false,
664            shallow_grafts: Arc::new(std::sync::OnceLock::new()),
665            replacements: Arc::new(ObjectReplacements::default()),
666        }
667    }
668
669    pub fn from_git_dir(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Self {
670        let git_dir = git_dir.as_ref();
671        let shared_repository = sley_formats::SharedRepositoryPermissions::from_git_dir(git_dir);
672        let mut database = Self::new(repository_objects_dir(git_dir), format);
673        database.loose = database.loose.with_shared_repository(shared_repository);
674        database
675    }
676
677    /// Attach an explicit replacement map. Object enumeration/storage queries
678    /// remain raw; object content and header reads dereference the map.
679    pub fn with_replacements(mut self, replacements: ObjectReplacements) -> Self {
680        self.replacements = Arc::new(replacements);
681        self
682    }
683
684    pub fn replacements(&self) -> &ObjectReplacements {
685        &self.replacements
686    }
687
688    pub fn replacement_oid(&self, oid: &ObjectId) -> Result<ObjectId> {
689        self.replacements.resolve(oid)
690    }
691
692    /// Read object contents without consulting the injected replacement map.
693    /// Used by raw enumeration surfaces such as `cat-file --batch-all-objects`.
694    pub fn read_object_without_replacement(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
695        self.read_object_raw(oid)
696    }
697
698    /// Read an object header without consulting the injected replacement map.
699    pub fn read_object_header_without_replacement(
700        &self,
701        oid: &ObjectId,
702    ) -> Result<Option<(ObjectType, u64)>> {
703        self.read_object_header_raw(oid)
704    }
705
706    /// Declare whether the owning repository has a promisor remote configured.
707    /// Only when this holds does [`ObjectReader::is_promised_object`] treat
708    /// objects in `.promisor` packs (and their transitive references) as
709    /// promised — matching git's `is_promisor_object`, which is gated on
710    /// `repo_has_promisor_remote()`. Callers that know the repo config (e.g. the
711    /// fsck driver) opt in; readers built without config keep the safe default
712    /// of `false`, so a stray `.promisor` sidecar never silently excuses a
713    /// genuinely missing object.
714    pub fn with_promisor_remote_present(mut self, present: bool) -> Self {
715        self.promisor_remote_present = present;
716        self
717    }
718
719    /// Drop cached pack registries, indexes, and decoded objects so the next read
720    /// sees packs/objects installed after this handle was created (e.g. after
721    /// `fetch` or `install_pack`). Long-lived `Repository` sessions call this
722    /// via the owning repository's `refresh_objects` hook.
723    pub fn refresh_read_cache(&self) {
724        if let Ok(mut cache) = self.pack_registry.lock() {
725            *cache = None;
726        }
727        self.pack_indexes.write().clear();
728        self.multi_pack_indexes.write().clear();
729        self.multi_pack_oid_lookups.write().clear();
730        self.pack_bytes.write().clear();
731        if let Ok(mut cache) = self.pack_deltas.lock() {
732            cache.clear();
733        }
734        if let Ok(mut cache) = self.pack_header_types.lock() {
735            cache.clear();
736        }
737        if let Ok(mut cache) = self.decoded.lock() {
738            cache.clear();
739        }
740        self.loose.invalidate_cache();
741    }
742
743    pub fn loose(&self) -> &LooseObjectStore {
744        &self.loose
745    }
746
747    pub fn presence_checker(&self) -> ObjectPresenceChecker {
748        ObjectPresenceChecker::new(self.clone())
749    }
750
751    pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
752        if self.loose.exists(oid)? {
753            return Ok(true);
754        }
755        if self.find_pack_containing(oid)?.is_some() {
756            return Ok(true);
757        }
758        for alternate in &self.alternates {
759            if Self::without_alternates(alternate, self.format).contains(oid)? {
760                return Ok(true);
761            }
762        }
763        // Reprepare-on-miss: a cached negative loose verdict may predate a
764        // sibling write. Drop it and exact-probe once before reporting absence.
765        self.loose.invalidate_cache();
766        self.loose.exists(oid)
767    }
768
769    pub fn object_ids(&self) -> Result<Vec<ObjectId>> {
770        let mut oids = object_ids_in_objects_dir(&self.objects_dir, self.format)?
771            .into_iter()
772            .collect::<HashSet<_>>();
773        for alternate in &self.alternates {
774            oids.extend(Self::without_alternates(alternate, self.format).object_ids()?);
775        }
776        let mut oids = oids.into_iter().collect::<Vec<_>>();
777        oids.sort_by_key(ObjectId::to_hex);
778        Ok(oids)
779    }
780
781    pub fn object_storage_info(&self, oid: &ObjectId) -> Result<Option<ObjectStorageInfo>> {
782        if let Some(disk_size) = self.loose.disk_size(oid)? {
783            return Ok(Some(ObjectStorageInfo {
784                disk_size,
785                deltabase: zero_oid(self.format)?,
786            }));
787        }
788        if let Some(info) = self.packed_object_storage_info(oid)? {
789            return Ok(Some(info));
790        }
791        for alternate in &self.alternates {
792            if let Some(info) =
793                Self::without_alternates(alternate, self.format).object_storage_info(oid)?
794            {
795                return Ok(Some(info));
796            }
797        }
798        // Reprepare-on-miss: drop any stale negative loose cache and exact-probe
799        // once before reporting absence (see `read_object`).
800        self.loose.invalidate_cache();
801        if let Some(disk_size) = self.loose.disk_size(oid)? {
802            return Ok(Some(ObjectStorageInfo {
803                disk_size,
804                deltabase: zero_oid(self.format)?,
805            }));
806        }
807        Ok(None)
808    }
809
810    pub fn resolve_prefix(&self, prefix: &str) -> Result<ObjectPrefixResolution> {
811        let mut matches = self.object_ids_with_prefix(prefix)?;
812        Ok(match matches.len() {
813            0 => ObjectPrefixResolution::Missing,
814            1 => ObjectPrefixResolution::Unique(matches.remove(0)),
815            _ => ObjectPrefixResolution::Ambiguous(matches),
816        })
817    }
818
819    pub fn object_ids_with_prefix(&self, prefix: &str) -> Result<Vec<ObjectId>> {
820        validate_object_id_prefix(self.format, prefix)?;
821        let prefix_bytes = prefix.as_bytes();
822        let mut matches =
823            object_ids_with_prefix_in_objects_dir(&self.objects_dir, self.format, prefix_bytes)?
824                .into_iter()
825                .collect::<HashSet<_>>();
826        for alternate in &self.alternates {
827            for oid in
828                Self::without_alternates(alternate, self.format).object_ids_with_prefix(prefix)?
829            {
830                matches.insert(oid);
831            }
832        }
833        let mut matches = matches.into_iter().collect::<Vec<_>>();
834        matches.sort_by_key(ObjectId::to_hex);
835        Ok(matches)
836    }
837
838    /// The object type and content size of `oid` without decoding its full body —
839    /// git's `cat-file --batch-check` fast path. Tries the decoded-object cache,
840    /// then loose storage (inflating only the framing header), then packs (reading
841    /// the entry header and, for deltas, only the delta's leading varints), then
842    /// alternates. Returns `Ok(None)` if the object is not present.
843    ///
844    /// Unlike [`ObjectReader::read_object`], this never materializes the body, so it
845    /// stays cheap on huge blobs and deep delta chains. It does not populate the
846    /// decoded-object cache (nothing is decoded).
847    pub fn read_object_header(&self, oid: &ObjectId) -> Result<Option<(ObjectType, u64)>> {
848        let read_oid = self.replacements.resolve(oid)?;
849        self.read_object_header_raw(&read_oid)
850    }
851
852    fn read_object_header_raw(&self, oid: &ObjectId) -> Result<Option<(ObjectType, u64)>> {
853        if implied_empty_tree_object(self.format, oid).is_some() {
854            return Ok(Some((ObjectType::Tree, 0)));
855        }
856        if let Ok(mut cache) = self.decoded.lock()
857            && let Some(object) = cache.get(oid)
858        {
859            return Ok(Some((object.object_type, object.body.len() as u64)));
860        }
861        if let Some(header) = self.loose.read_header(oid)? {
862            return Ok(Some(header));
863        }
864        if let Some(pack_lookup) = self.find_pack_containing(oid)? {
865            let bytes = pack_lookup.pack_bytes(self)?;
866            // Per-pack offset->type memo so the ofs-delta chain walk that resolves
867            // a packed object's type runs at most once per chain across the batch,
868            // instead of re-walking (and re-inflating each link's leading varints)
869            // on every header read — the sley#26 super-linear cat-file --batch-check.
870            let type_cache = pack_lookup.header_type_cache(self);
871            let resolve_ref_base = |base: &ObjectId| {
872                self.read_object_header_raw(base)
873                    .map(|header| header.map(|(t, _)| t))
874            };
875            let header = match &type_cache {
876                Some(cache) => {
877                    let mut adapter = PackHeaderTypeCacheAdapter(cache);
878                    sley_pack::read_object_header_at_with_cache(
879                        &bytes,
880                        pack_lookup.offset,
881                        self.format,
882                        resolve_ref_base,
883                        &mut adapter,
884                    )?
885                }
886                None => sley_pack::read_object_header_at(
887                    &bytes,
888                    pack_lookup.offset,
889                    self.format,
890                    resolve_ref_base,
891                )?,
892            };
893            return Ok(Some(header));
894        }
895        for alternate in &self.alternates {
896            if let Some(header) =
897                Self::without_alternates(alternate, self.format).read_object_header_raw(oid)?
898            {
899                return Ok(Some(header));
900            }
901        }
902        // Reprepare-on-miss: discard any stale negative loose cache and retry an
903        // exact path probe once before reporting absence (see `read_object`).
904        self.loose.invalidate_cache();
905        if let Some(header) = self.loose.read_header(oid)? {
906            return Ok(Some(header));
907        }
908        Ok(None)
909    }
910
911    pub(crate) fn read_packed_object(&self, oid: &ObjectId) -> Result<Option<Arc<EncodedObject>>> {
912        // Memory-capped decoded-object cache first (delta-base reuse for ref-delta
913        // bases that resolve back through the store + repeated whole-object reads).
914        if let Ok(mut cache) = self.decoded.lock()
915            && let Some(object) = cache.get(oid)
916        {
917            return Ok(Some(object));
918        }
919        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
920            return Ok(None);
921        };
922        self.read_packed_object_at_lookup(oid, &pack_lookup)
923            .map(Some)
924    }
925
926    pub(crate) fn read_packed_object_at_lookup(
927        &self,
928        oid: &ObjectId,
929        pack_lookup: &PackLookup,
930    ) -> Result<Arc<EncodedObject>> {
931        if let Ok(mut cache) = self.decoded.lock()
932            && let Some(object) = cache.get(oid)
933        {
934            return Ok(object);
935        }
936        let bytes = pack_lookup.pack_bytes(self)?;
937        // Per-pack delta-base cache (keyed by in-pack offset). Resolving an
938        // ofs-delta chain reuses already-decoded bases instead of re-inflating the
939        // whole chain on every read. Scoped to this pack's path so an offset key is
940        // never applied to the wrong pack's bytes.
941        let delta_cache = pack_lookup.delta_cache(self);
942        let delta_adapter = delta_cache.as_ref().map(PackDeltaCacheAdapter);
943        // Decode only this object at its offset (plus its delta-base chain). A
944        // ref-delta base resolves through the full store (loose / other packs) and
945        // reuses the decoded-object cache. No cache lock is held across the decode,
946        // so the recursive resolver re-entry (which may re-enter read_object) is
947        // safe.
948        let resolve_ref_base = |base: &ObjectId| self.read_object_raw(base).map(Some);
949        let resolve_ofs_base =
950            |base_offset| self.read_ofs_delta_base_from_other_sources(pack_lookup, base_offset);
951        let object = match &delta_adapter {
952            Some(adapter) => sley_pack::read_object_at_with_cache_and_ofs_base_arc(
953                &bytes,
954                pack_lookup.offset,
955                self.format,
956                resolve_ref_base,
957                resolve_ofs_base,
958                adapter,
959            )?,
960            None => sley_pack::read_object_at_with_ofs_base_arc(
961                &bytes,
962                pack_lookup.offset,
963                self.format,
964                resolve_ref_base,
965                resolve_ofs_base,
966            )?,
967        };
968        // Trust the index → offset mapping rather than re-hashing every decoded
969        // object on read (see `verify_reads_enabled`); this re-hash dominated
970        // bulk-read cost. Opt back in with `SLEY_VERIFY_READS` for a paranoid check.
971        if verify_reads_enabled() {
972            let actual = object.object_id(self.format)?;
973            if actual != *oid {
974                return Err(GitError::InvalidObject(format!(
975                    "pack object id mismatch: index says {oid}, decoded {actual}"
976                )));
977            }
978        }
979        if let Ok(mut cache) = self.decoded.lock() {
980            cache.put(*oid, Arc::clone(&object));
981        }
982        Ok(object)
983    }
984
985    /// The per-pack delta-base cache for `pack_path`, creating it on first use.
986    /// Returns `None` only if the shared map's lock is poisoned, in which case the
987    /// caller falls back to an uncached decode (correctness preserved).
988    pub(crate) fn pack_delta_cache(&self, pack_path: &Path) -> Option<Arc<Mutex<LruOffsetCache>>> {
989        let mut caches = self.pack_deltas.lock().ok()?;
990        let cache = caches.entry(pack_path.to_path_buf()).or_insert_with(|| {
991            Arc::new(Mutex::new(LruOffsetCache::new(delta_base_cache_budget())))
992        });
993        Some(Arc::clone(cache))
994    }
995
996    /// The per-pack header-type memo for `pack_path`, creating it on first use.
997    /// Returns `None` only if the shared map's lock is poisoned, in which case the
998    /// caller falls back to an unmemoized header walk (correctness preserved).
999    pub(crate) fn pack_header_type_cache(&self, pack_path: &Path) -> Option<PackHeaderTypeCache> {
1000        let mut caches = self.pack_header_types.lock().ok()?;
1001        let cache = caches
1002            .entry(pack_path.to_path_buf())
1003            .or_insert_with(|| Arc::new(Mutex::new(HashMap::new())));
1004        Some(Arc::clone(cache))
1005    }
1006
1007    /// Backing bytes of the pack at `pack_path`, loaded at most once per database
1008    /// handle (cached, shared across clones). Memory-mapped under the `mmap` feature,
1009    /// otherwise read into the heap. On a poisoned lock it falls back to loading
1010    /// without caching, preserving correctness.
1011    pub(crate) fn cached_pack_bytes(&self, pack_path: &Path) -> Result<Arc<PackData>> {
1012        if let Some(bytes) = self.pack_bytes.read().get(pack_path) {
1013            return Ok(Arc::clone(bytes));
1014        }
1015        let bytes = Arc::new(load_pack_data(pack_path)?);
1016        self.pack_bytes
1017            .write()
1018            .insert(pack_path.to_path_buf(), Arc::clone(&bytes));
1019        Ok(bytes)
1020    }
1021
1022    /// Parsed index for the `.idx` at `index_path`, parsed at most once per
1023    /// database handle. On a poisoned lock it falls back to parsing without
1024    /// caching, preserving correctness.
1025    pub(crate) fn cached_pack_index(&self, index_path: &Path) -> Result<Arc<PackIndexViewData>> {
1026        if let Some(index) = self.pack_indexes.read().get(index_path) {
1027            return Ok(Arc::clone(index));
1028        }
1029        let index_bytes = load_pack_index_data(index_path)?;
1030        let index = Arc::new(PackIndexViewData::parse_trusted_source_without_checksum(
1031            index_bytes,
1032            self.format,
1033        )?);
1034        self.pack_indexes
1035            .write()
1036            .insert(index_path.to_path_buf(), Arc::clone(&index));
1037        Ok(index)
1038    }
1039
1040    /// Optional reverse index for the `.idx` at `index_path`, loaded at most once
1041    /// per database handle when a matching `.rev` sidecar is present.
1042    pub(crate) fn cached_pack_reverse_index(
1043        &self,
1044        index_path: &Path,
1045        index: &PackIndexViewData,
1046    ) -> Result<Option<Arc<PackReverseIndex>>> {
1047        if let Some(cached) = self.pack_reverse_indexes.read().get(index_path) {
1048            return Ok(cached.as_ref().map(Arc::clone));
1049        }
1050        let rev_path = index_path.with_extension("rev");
1051        let reverse = if rev_path.exists() {
1052            let bytes = fs::read(&rev_path)?;
1053            match PackReverseIndex::parse(&bytes, self.format, index.count) {
1054                Ok(parsed) if parsed.pack_checksum == index.pack_checksum => Some(Arc::new(parsed)),
1055                _ => None,
1056            }
1057        } else {
1058            None
1059        };
1060        self.pack_reverse_indexes
1061            .write()
1062            .insert(index_path.to_path_buf(), reverse.as_ref().map(Arc::clone));
1063        Ok(reverse)
1064    }
1065
1066    pub(crate) fn cached_multi_pack_index_oid_lookup(
1067        &self,
1068        midx_path: &Path,
1069    ) -> Result<Option<Arc<MultiPackIndexOidLookup>>> {
1070        if !midx_path.exists() {
1071            return Ok(None);
1072        }
1073        if let Some(midx) = self.multi_pack_oid_lookups.read().get(midx_path) {
1074            return Ok(Some(Arc::clone(midx)));
1075        }
1076        let bytes = load_multi_pack_index_lookup_data(midx_path)?;
1077        let midx = match MultiPackIndexOidLookup::parse(bytes, self.format) {
1078            Ok(midx) => Arc::new(midx),
1079            Err(GitError::InvalidFormat(message))
1080                if message.starts_with("multi-pack-index hash id ") =>
1081            {
1082                let actual = message
1083                    .strip_prefix("multi-pack-index hash id ")
1084                    .and_then(|rest| rest.split_whitespace().next())
1085                    .unwrap_or("0");
1086                let expected = match self.format {
1087                    ObjectFormat::Sha1 => 1,
1088                    ObjectFormat::Sha256 => 2,
1089                };
1090                eprintln!(
1091                    "error: multi-pack-index hash version {actual} does not match version {expected}"
1092                );
1093                return Ok(None);
1094            }
1095            Err(err) => return Err(err),
1096        };
1097        self.multi_pack_oid_lookups
1098            .write()
1099            .insert(midx_path.to_path_buf(), Arc::clone(&midx));
1100        Ok(Some(midx))
1101    }
1102
1103    pub(crate) fn cached_multi_pack_index(
1104        &self,
1105        midx_path: &Path,
1106    ) -> Result<Option<Arc<MultiPackIndex>>> {
1107        if !midx_path.exists() {
1108            return Ok(None);
1109        }
1110        if let Some(midx) = self.multi_pack_indexes.read().get(midx_path) {
1111            return Ok(Some(Arc::clone(midx)));
1112        }
1113        let bytes = load_multi_pack_index_lookup_data(midx_path)?;
1114        let midx = match MultiPackIndex::parse(bytes.as_bytes(), self.format) {
1115            Ok(midx) => Arc::new(midx),
1116            Err(GitError::InvalidFormat(message))
1117                if message.starts_with("multi-pack-index hash id ") =>
1118            {
1119                let actual = message
1120                    .strip_prefix("multi-pack-index hash id ")
1121                    .and_then(|rest| rest.split_whitespace().next())
1122                    .unwrap_or("0");
1123                let expected = match self.format {
1124                    ObjectFormat::Sha1 => 1,
1125                    ObjectFormat::Sha256 => 2,
1126                };
1127                eprintln!(
1128                    "error: multi-pack-index hash version {actual} does not match version {expected}"
1129                );
1130                return Ok(None);
1131            }
1132            Err(err) => return Err(err),
1133        };
1134        self.multi_pack_indexes
1135            .write()
1136            .insert(midx_path.to_path_buf(), Arc::clone(&midx));
1137        Ok(Some(midx))
1138    }
1139
1140    /// Registry snapshot for this database's pack directory. With `force_rescan`,
1141    /// the directory is re-read; when the fingerprint and pack set match the
1142    /// cached snapshot, the same `Arc` is returned so miss handling can tell that
1143    /// no new packs appeared.
1144    pub(crate) fn cached_pack_registry(
1145        &self,
1146        pack_dir: &Path,
1147        force_rescan: bool,
1148    ) -> Result<Arc<PackRegistrySnapshot>> {
1149        if !force_rescan && let Some(registry) = self.cached_loaded_pack_registry(pack_dir)? {
1150            return Ok(registry);
1151        }
1152        let scanned = Arc::new(scan_pack_registry(pack_dir, self.format)?);
1153        if let Ok(mut cache) = self.pack_registry.lock() {
1154            match cache.as_ref() {
1155                Some(existing)
1156                    if existing.fingerprint == scanned.fingerprint
1157                        && same_registered_pack_set(&existing.packs, &scanned.packs) =>
1158                {
1159                    return Ok(Arc::clone(existing));
1160                }
1161                _ => {
1162                    *cache = Some(Arc::clone(&scanned));
1163                }
1164            }
1165        }
1166        Ok(scanned)
1167    }
1168
1169    pub(crate) fn find_in_pack_registry(
1170        &self,
1171        registry: Arc<PackRegistrySnapshot>,
1172        oid: &ObjectId,
1173    ) -> Result<Option<PackLookup>> {
1174        let hinted_pack_index = registry.cached_hint();
1175        if let Some(pack_index) = hinted_pack_index {
1176            let pack = &registry.packs[pack_index];
1177            match pack.index(self.format) {
1178                Ok(index) => {
1179                    if let Some(entry) = index.find(oid) {
1180                        return Ok(Some(PackLookup::from_registered(
1181                            Arc::clone(pack),
1182                            entry.offset,
1183                        )));
1184                    }
1185                }
1186                Err(_) => {
1187                    eprintln!("error: packfile {} index unavailable", pack.pack.display());
1188                }
1189            }
1190        }
1191        for (pack_index, pack) in registry.packs.iter().enumerate() {
1192            if Some(pack_index) == hinted_pack_index {
1193                continue;
1194            }
1195            let index = match pack.index(self.format) {
1196                Ok(index) => index,
1197                Err(_) => {
1198                    eprintln!("error: packfile {} index unavailable", pack.pack.display());
1199                    continue;
1200                }
1201            };
1202            if let Some(entry) = index.find(oid) {
1203                registry.remember_hint(pack_index);
1204                return Ok(Some(PackLookup::from_registered(
1205                    Arc::clone(pack),
1206                    entry.offset,
1207                )));
1208            }
1209        }
1210        Ok(None)
1211    }
1212
1213    /// Read `oid` from any pack *other than* the one named by `exclude`, used as
1214    /// a corruption fallback: a redundant packed copy survives one pack's
1215    /// damage. Scans the on-disk `.idx` files directly (bypassing the registry
1216    /// cache, whose first hit is the excluded pack) and decodes from the first
1217    /// other pack that both indexes the object and parses cleanly.
1218    pub(crate) fn read_packed_object_from_other_packs(
1219        &self,
1220        oid: &ObjectId,
1221        exclude: &PackLookup,
1222    ) -> Result<Option<Arc<EncodedObject>>> {
1223        let pack_dir = self.objects_dir.join("pack");
1224        let Ok(entries) = fs::read_dir(&pack_dir) else {
1225            return Ok(None);
1226        };
1227        let excluded_pack = exclude.pack_path().to_path_buf();
1228        for entry in entries {
1229            let idx_path = entry?.path();
1230            if idx_path.extension().and_then(|ext| ext.to_str()) != Some("idx") {
1231                continue;
1232            }
1233            let pack_path = idx_path.with_extension("pack");
1234            if pack_path == excluded_pack {
1235                continue;
1236            }
1237            let Ok(idx_bytes) = fs::read(&idx_path) else {
1238                continue;
1239            };
1240            let Ok(index) = PackIndex::parse(&idx_bytes, self.format) else {
1241                continue;
1242            };
1243            let Some(entry) = index.find(oid) else {
1244                continue;
1245            };
1246            let candidate = PackLookup::from_path(pack_path, entry.offset);
1247            if let Ok(object) = self.read_packed_object_at_lookup(oid, &candidate) {
1248                return Ok(Some(object));
1249            }
1250        }
1251        Ok(None)
1252    }
1253
1254    pub(crate) fn pack_oid_at_offset(
1255        &self,
1256        pack_lookup: &PackLookup,
1257        offset: u64,
1258    ) -> Result<Option<ObjectId>> {
1259        let index_path = match &pack_lookup.registered {
1260            Some(pack) => pack.idx.clone(),
1261            None => pack_lookup.pack.with_extension("idx"),
1262        };
1263        match pack_lookup.pack_index(self) {
1264            Ok(index) => {
1265                if let Some(reverse) = self.cached_pack_reverse_index(&index_path, &index)? {
1266                    Ok(reverse.oid_at_offset(&index, offset))
1267                } else {
1268                    Ok(index.oid_at_offset_linear(offset))
1269                }
1270            }
1271            Err(_) => self.midx_oid_for_pack_offset(pack_lookup, offset),
1272        }
1273    }
1274
1275    pub(crate) fn read_ofs_delta_base_from_other_sources(
1276        &self,
1277        pack_lookup: &PackLookup,
1278        base_offset: u64,
1279    ) -> Result<Option<Arc<EncodedObject>>> {
1280        let Some(base_oid) = self.pack_oid_at_offset(pack_lookup, base_offset)? else {
1281            return Ok(None);
1282        };
1283        if let Ok(mut cache) = self.decoded.lock()
1284            && let Some(object) = cache.get(&base_oid)
1285        {
1286            return Ok(Some(object));
1287        }
1288        if let Ok(object) = self.loose.read_object(&base_oid) {
1289            return Ok(Some(object));
1290        }
1291        if let Some(object) = self.read_packed_object_from_other_packs(&base_oid, pack_lookup)? {
1292            return Ok(Some(object));
1293        }
1294        for alternate in &self.alternates {
1295            if let Ok(object) =
1296                Self::without_alternates(alternate, self.format).read_object(&base_oid)
1297            {
1298                return Ok(Some(object));
1299            }
1300        }
1301        Ok(None)
1302    }
1303
1304    pub(crate) fn find_pack_containing(&self, oid: &ObjectId) -> Result<Option<PackLookup>> {
1305        if oid.format() != self.format {
1306            return Err(GitError::InvalidObjectId(format!(
1307                "object {oid} uses {}, store uses {}",
1308                oid.format().name(),
1309                self.format.name()
1310            )));
1311        }
1312        let pack_dir = self.objects_dir.join("pack");
1313        // Hot path: a previously cached pack registry or multi-pack-index already
1314        // names every pack, and locating `oid` in them is pure in-memory index
1315        // work. Try that first so a warm handle does not parse indexes or hash
1316        // pack paths on every lookup.
1317        if let Some(midx) = self.cached_loaded_multi_pack_index_oid_lookup()
1318            && let Some(pack_paths) = self.midx_oid_lookup_pack_paths(&pack_dir, &midx, oid)?
1319        {
1320            return Ok(Some(pack_paths));
1321        }
1322        if let Some(registry) = self.cached_loaded_pack_registry(&pack_dir)?
1323            && let Some(pack_paths) = self.find_in_pack_registry(registry, oid)?
1324        {
1325            return Ok(Some(pack_paths));
1326        }
1327
1328        if !pack_dir.exists() {
1329            return Ok(None);
1330        }
1331        if let Some(pack_paths) = self.find_midx_pack_containing(&pack_dir, oid)? {
1332            return Ok(Some(pack_paths));
1333        }
1334        // Search the cached registry first. On a complete miss, re-scan the
1335        // directory once (picking up any pack added since the registry was
1336        // cached) and search again, so newly written packs are still found.
1337        let registry = self.cached_pack_registry(&pack_dir, false)?;
1338        if let Some(pack_paths) = self.find_in_pack_registry(Arc::clone(&registry), oid)? {
1339            return Ok(Some(pack_paths));
1340        }
1341        let refreshed = self.cached_pack_registry(&pack_dir, true)?;
1342        if Arc::ptr_eq(&registry, &refreshed) {
1343            // The re-scan produced the same registry, so nothing new appeared.
1344            return Ok(None);
1345        }
1346        self.find_in_pack_registry(refreshed, oid)
1347    }
1348
1349    pub(crate) fn packed_object_storage_info(
1350        &self,
1351        oid: &ObjectId,
1352    ) -> Result<Option<ObjectStorageInfo>> {
1353        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
1354            return Ok(None);
1355        };
1356        let index = pack_lookup.pack_index(self).ok();
1357        let pack = match pack_lookup.pack_bytes(self) {
1358            Ok(pack) => Some(pack),
1359            Err(_err) if index.is_some() => None,
1360            Err(err) => return Err(err),
1361        };
1362        let trailer_offset = pack
1363            .as_ref()
1364            .map(|pack| {
1365                (pack.len() as u64)
1366                    .checked_sub(self.format.raw_len() as u64)
1367                    .ok_or_else(|| {
1368                        GitError::InvalidFormat("pack file shorter than checksum".into())
1369                    })
1370            })
1371            .transpose()?;
1372        let delta_base = match &pack {
1373            Some(pack) => pack_entry_delta_base(self.format, pack, pack_lookup.offset)?,
1374            None => None,
1375        };
1376        let delta_base_offset = match &delta_base {
1377            Some(PackDeltaBase::Offset(offset)) => Some(*offset),
1378            Some(PackDeltaBase::Ref(_)) | None => None,
1379        };
1380        let offset_info = if let Some(index) = &index {
1381            scan_pack_index_offsets(index, pack_lookup.offset, trailer_offset, delta_base_offset)?
1382        } else if let Some(pack) = &pack {
1383            let end_offset =
1384                scan_pack_offsets_without_index(self.format, pack, pack_lookup.offset)?
1385                    .ok_or_else(|| {
1386                        GitError::InvalidFormat(format!(
1387                            "pack offset {} not found",
1388                            pack_lookup.offset
1389                        ))
1390                    })?;
1391            let delta_base_oid = match delta_base_offset {
1392                Some(offset) => self
1393                    .midx_oid_for_pack_offset(&pack_lookup, offset)?
1394                    .ok_or_else(|| {
1395                        GitError::InvalidFormat(format!("ofs-delta base offset {offset} not found"))
1396                    })?,
1397                None => zero_oid(self.format)?,
1398            };
1399            PackIndexOffsetInfo {
1400                end_offset,
1401                delta_base_oid: delta_base_offset.map(|_| delta_base_oid),
1402            }
1403        } else {
1404            return Err(GitError::InvalidFormat(
1405                "packed object metadata source unavailable".into(),
1406            ));
1407        };
1408        let disk_size = offset_info
1409            .end_offset
1410            .checked_sub(pack_lookup.offset)
1411            .ok_or_else(|| GitError::InvalidFormat("pack index offsets are not sorted".into()))?;
1412        let deltabase = match delta_base {
1413            Some(PackDeltaBase::Offset(_)) => offset_info.delta_base_oid.ok_or_else(|| {
1414                // scan_pack_index_offsets returns Err when delta_base_offset is
1415                // Some but no matching entry is found, so this is unreachable for
1416                // valid packs; propagate as an error rather than panic to keep a
1417                // malformed pack from taking down the process if that invariant
1418                // ever drifts.
1419                GitError::InvalidFormat("ofs-delta base oid missing from pack index".into())
1420            })?,
1421            Some(PackDeltaBase::Ref(oid)) => oid,
1422            None => zero_oid(self.format)?,
1423        };
1424        Ok(Some(ObjectStorageInfo {
1425            disk_size,
1426            deltabase,
1427        }))
1428    }
1429
1430    pub(crate) fn midx_oid_for_pack_offset(
1431        &self,
1432        pack_lookup: &PackLookup,
1433        offset: u64,
1434    ) -> Result<Option<ObjectId>> {
1435        let pack_dir = self.objects_dir.join("pack");
1436        let midx_path = pack_dir.join("multi-pack-index");
1437        let Some(midx) = self.cached_multi_pack_index(&midx_path)? else {
1438            return Ok(None);
1439        };
1440        let Some(pack_name) = pack_lookup
1441            .pack_path()
1442            .file_name()
1443            .and_then(|name| name.to_str())
1444        else {
1445            return Ok(None);
1446        };
1447        let idx_name = pack_name
1448            .strip_suffix(".pack")
1449            .map(|stem| format!("{stem}.idx"))
1450            .unwrap_or_else(|| pack_name.to_string());
1451        let Some(pack_int_id) = midx
1452            .pack_names
1453            .iter()
1454            .position(|candidate| candidate == &idx_name)
1455        else {
1456            return Ok(None);
1457        };
1458        Ok(midx
1459            .objects
1460            .iter()
1461            .find(|entry| entry.pack_int_id == pack_int_id as u32 && entry.offset == offset)
1462            .map(|entry| entry.oid))
1463    }
1464
1465    pub(crate) fn find_midx_pack_containing(
1466        &self,
1467        pack_dir: &Path,
1468        oid: &ObjectId,
1469    ) -> Result<Option<PackLookup>> {
1470        let midx_path = pack_dir.join("multi-pack-index");
1471        if let Some(midx) = self.cached_multi_pack_index_oid_lookup(&midx_path)?
1472            && let Some(pack_lookup) = self.midx_oid_lookup_pack_paths(pack_dir, &midx, oid)?
1473        {
1474            return Ok(Some(pack_lookup));
1475        }
1476        self.find_incremental_midx_pack_containing(pack_dir, oid)
1477    }
1478
1479    pub(crate) fn midx_oid_lookup_pack_paths(
1480        &self,
1481        pack_dir: &Path,
1482        midx: &MultiPackIndexOidLookup,
1483        oid: &ObjectId,
1484    ) -> Result<Option<PackLookup>> {
1485        let Some(entry) = midx.find(oid)? else {
1486            return Ok(None);
1487        };
1488        let Some(pack_name) = midx.pack_name(entry.pack_int_id) else {
1489            return Err(GitError::InvalidFormat(
1490                "multi-pack-index object points past pack table".into(),
1491            ));
1492        };
1493        let pack_file_name = pack_name
1494            .strip_suffix(".idx")
1495            .map(|stem| format!("{stem}.pack"))
1496            .unwrap_or_else(|| pack_name.to_string());
1497        let pack = pack_dir.join(pack_file_name);
1498        Ok(Some(PackLookup::from_path(pack, entry.offset)))
1499    }
1500
1501    pub(crate) fn find_incremental_midx_pack_containing(
1502        &self,
1503        pack_dir: &Path,
1504        oid: &ObjectId,
1505    ) -> Result<Option<PackLookup>> {
1506        let chain = read_incremental_midx_chain(pack_dir)?;
1507        if chain.is_empty() {
1508            return Ok(None);
1509        }
1510        let midx_dir = pack_dir.join("multi-pack-index.d");
1511        for checksum in chain.iter().rev() {
1512            let path = midx_dir.join(format!("multi-pack-index-{checksum}.midx"));
1513            if !path.exists() {
1514                continue;
1515            }
1516            let bytes = load_multi_pack_index_lookup_data(&path)?;
1517            let midx = match MultiPackIndexOidLookup::parse(bytes, self.format) {
1518                Ok(midx) => midx,
1519                Err(_) => continue,
1520            };
1521            if let Some(pack_lookup) = self.midx_oid_lookup_pack_paths(pack_dir, &midx, oid)? {
1522                return Ok(Some(pack_lookup));
1523            }
1524        }
1525        Ok(None)
1526    }
1527
1528    pub(crate) fn cached_loaded_multi_pack_index_oid_lookup(
1529        &self,
1530    ) -> Option<Arc<MultiPackIndexOidLookup>> {
1531        let midx_path = self.objects_dir.join("pack").join("multi-pack-index");
1532        self.multi_pack_oid_lookups
1533            .read()
1534            .get(&midx_path)
1535            .map(Arc::clone)
1536    }
1537
1538    /// The pack registry for `pack_dir` *only if already scanned and cached* —
1539    /// never touches the filesystem. Used by the lookup hot path to skip
1540    /// per-object pack-dir metadata checks once a handle is warm. A cold cache
1541    /// returns `None`, so the caller falls back to the scanning path. A complete
1542    /// miss still forces one rescan, preserving the new-pack discovery semantics.
1543    pub(crate) fn cached_loaded_pack_registry(
1544        &self,
1545        _pack_dir: &Path,
1546    ) -> Result<Option<Arc<PackRegistrySnapshot>>> {
1547        let cache = match self.pack_registry.lock() {
1548            Ok(cache) => cache,
1549            Err(_) => return Ok(None),
1550        };
1551        Ok(cache.as_ref().map(Arc::clone))
1552    }
1553}
1554impl ObjectReader for FileObjectDatabase {
1555    fn reusable_delta_base(&self, oid: &ObjectId) -> Result<Option<ObjectId>> {
1556        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
1557            return Ok(None);
1558        };
1559        let pack = pack_lookup.pack_bytes(self)?;
1560        match pack_entry_delta_base(self.format, &pack, pack_lookup.offset)? {
1561            Some(PackDeltaBase::Ref(base_oid)) => Ok(Some(base_oid)),
1562            Some(PackDeltaBase::Offset(base_offset)) => {
1563                let base_oid = self
1564                    .pack_oid_at_offset(&pack_lookup, base_offset)?
1565                    .ok_or_else(|| {
1566                        GitError::InvalidFormat(format!(
1567                            "ofs-delta base offset {base_offset} not found"
1568                        ))
1569                    })?;
1570                Ok(Some(base_oid))
1571            }
1572            None => Ok(None),
1573        }
1574    }
1575
1576    fn is_promised_object(&self, oid: &ObjectId) -> bool {
1577        // Gate on a configured promisor remote, exactly like git's
1578        // `is_promisor_object` (which short-circuits when
1579        // `repo_has_promisor_remote()` is false). Without this, a `.promisor`
1580        // sidecar left in an ordinary repository would wrongly excuse missing
1581        // objects from fsck connectivity checks.
1582        self.promisor_remote_present && self.promisor_objects().contains(oid)
1583    }
1584
1585    fn has_shallow_grafts(&self) -> bool {
1586        !self
1587            .shallow_grafts
1588            .get_or_init(|| {
1589                let shallow_file = self
1590                    .objects_dir
1591                    .parent()
1592                    .map(|git_dir| git_dir.join("shallow"));
1593                match shallow_file {
1594                    Some(path) => read_shallow_grafts(&path, self.format),
1595                    None => HashSet::new(),
1596                }
1597            })
1598            .is_empty()
1599    }
1600
1601    fn is_shallow_graft(&self, oid: &ObjectId) -> bool {
1602        self.shallow_grafts
1603            .get_or_init(|| {
1604                let shallow_file = self
1605                    .objects_dir
1606                    .parent()
1607                    .map(|git_dir| git_dir.join("shallow"));
1608                match shallow_file {
1609                    Some(path) => read_shallow_grafts(&path, self.format),
1610                    None => HashSet::new(),
1611                }
1612            })
1613            .contains(oid)
1614    }
1615
1616    fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
1617        let read_oid = self.replacements.resolve(oid)?;
1618        self.read_object_raw(&read_oid)
1619    }
1620}
1621
1622impl FileObjectDatabase {
1623    fn read_object_raw(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
1624        if let Some(object) = implied_empty_tree_object(self.format, oid) {
1625            return Ok(object);
1626        }
1627        // A corrupt loose copy must not shadow a good packed copy: git's
1628        // `oid_object_info_extended` consults every source, so a repacked object
1629        // whose loose file was later corrupted still reads fine from the pack. If
1630        // a packed copy exists, prefer it WITHOUT touching the corrupt loose file
1631        // (which would otherwise emit a spurious `inflate:` diagnostic on each
1632        // probe). Only when no pack copy exists do we read (and, if corrupt,
1633        // surface the error from) the loose file.
1634        if let Some(pack_lookup) = self.find_pack_containing(oid)? {
1635            match self.read_packed_object_at_lookup(oid, &pack_lookup) {
1636                Ok(object) => return Ok(object),
1637                Err(GitError::NotFound(_)) => {}
1638                // A corrupt packed copy must not be fatal when another good copy
1639                // exists: git's `oid_object_info_extended` keeps consulting the
1640                // remaining sources (loose, other packs, alternates) when a pack
1641                // read fails. Fall through to the loose/other-pack probes and
1642                // only surface the packed error if every source comes up empty.
1643                Err(packed_err) => {
1644                    if let Ok(object) = self.loose.read_object(oid) {
1645                        return Ok(object);
1646                    }
1647                    // Try any *other* pack that also holds the object (a
1648                    // redundant copy survives one pack's corruption).
1649                    if let Some(object) =
1650                        self.read_packed_object_from_other_packs(oid, &pack_lookup)?
1651                    {
1652                        return Ok(object);
1653                    }
1654                    for alternate in &self.alternates {
1655                        if let Ok(object) =
1656                            Self::without_alternates(alternate, self.format).read_object_raw(oid)
1657                        {
1658                            return Ok(object);
1659                        }
1660                    }
1661                    return Err(packed_err);
1662                }
1663            }
1664        }
1665        let loose_err = match self.loose.read_object(oid) {
1666            Ok(object) => return Ok(object),
1667            Err(GitError::NotFound(_)) => None,
1668            Err(err) => Some(err),
1669        };
1670        if let Some(object) = self.read_packed_object(oid)? {
1671            return Ok(object);
1672        }
1673        for alternate in &self.alternates {
1674            match Self::without_alternates(alternate, self.format).read_object_raw(oid) {
1675                Ok(object) => return Ok(object),
1676                Err(GitError::NotFound(_)) => {}
1677                Err(err) => return Err(err),
1678            }
1679        }
1680        // Hard miss against every store. If an earlier enumeration built a loose
1681        // cache, an object written loose afterward by a sibling handle could have
1682        // been skipped above. Mirror git's `oid_object_info_extended`
1683        // reprepare-on-miss: drop stale cache state and retry an exact loose path
1684        // probe once before declaring the object missing.
1685        self.loose.invalidate_cache();
1686        match self.loose.read_object(oid) {
1687            Ok(object) => return Ok(object),
1688            Err(GitError::NotFound(_)) => {}
1689            Err(err) => return Err(err),
1690        }
1691        // No good copy in any store. If the local loose copy was corrupt (not
1692        // merely absent), surface that error — it is more specific than a plain
1693        // "not found".
1694        if let Some(err) = loose_err {
1695            return Err(err);
1696        }
1697        Err(GitError::object_not_found_in(
1698            *oid,
1699            MissingObjectContext::Read,
1700        ))
1701    }
1702}
1703impl FileObjectDatabase {
1704    fn promisor_objects(&self) -> &HashSet<ObjectId> {
1705        self.promisor_objects.get_or_init(|| {
1706            let mut promised =
1707                promisor_pack_object_ids(&self.objects_dir, self.format).unwrap_or_default();
1708            let mut pending = promised.iter().copied().collect::<Vec<_>>();
1709            while let Some(oid) = pending.pop() {
1710                let Ok(object) = self.read_object(&oid) else {
1711                    continue;
1712                };
1713                for link in promisor_object_links(self.format, &object) {
1714                    if promised.insert(link) {
1715                        pending.push(link);
1716                    }
1717                }
1718            }
1719            promised
1720        })
1721    }
1722
1723    fn freshen_existing_object(&self, oid: &ObjectId) -> Result<bool> {
1724        if self.freshen_loose_object(oid)? {
1725            return Ok(true);
1726        }
1727        if self.freshen_packed_object(oid)? {
1728            return Ok(true);
1729        }
1730        for alternate in &self.alternates {
1731            if Self::without_alternates(alternate, self.format).freshen_existing_object(oid)? {
1732                return Ok(true);
1733            }
1734        }
1735        // A previous negative loose-cache probe may predate a sibling write.
1736        self.loose.invalidate_cache();
1737        self.freshen_loose_object(oid)
1738    }
1739
1740    fn freshen_loose_object(&self, oid: &ObjectId) -> Result<bool> {
1741        let path = self.loose.object_path(oid)?;
1742        freshen_file_mtime(&path)
1743    }
1744
1745    fn freshen_packed_object(&self, oid: &ObjectId) -> Result<bool> {
1746        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
1747            return Ok(false);
1748        };
1749        if !pack_lookup.pack_path().with_extension("mtimes").exists() {
1750            return freshen_file_mtime(pack_lookup.pack_path());
1751        }
1752
1753        // Cruft packs carry per-object mtimes in their `.mtimes` sidecar. Git
1754        // deliberately never freshens the pack itself: doing so would make
1755        // every unreachable object in it look recent. If another, non-cruft
1756        // pack also contains the object, freshen that copy; otherwise return a
1757        // miss so the caller writes a new loose copy with its own mtime.
1758        let pack_dir = self.objects_dir.join("pack");
1759        let Ok(entries) = fs::read_dir(pack_dir) else {
1760            return Ok(false);
1761        };
1762        for entry in entries {
1763            let idx_path = entry?.path();
1764            if idx_path.extension().and_then(|ext| ext.to_str()) != Some("idx")
1765                || idx_path.with_extension("mtimes").exists()
1766            {
1767                continue;
1768            }
1769            let pack_path = idx_path.with_extension("pack");
1770            if !pack_path.exists() {
1771                continue;
1772            }
1773            let Ok(index_bytes) = fs::read(&idx_path) else {
1774                continue;
1775            };
1776            let Ok(index) = PackIndex::parse(&index_bytes, self.format) else {
1777                continue;
1778            };
1779            if index.find(oid).is_some() {
1780                return freshen_file_mtime(&pack_path);
1781            }
1782        }
1783        Ok(false)
1784    }
1785}
1786pub(crate) fn freshen_file_mtime(path: &Path) -> Result<bool> {
1787    let file = match fs::OpenOptions::new().read(true).open(path) {
1788        Ok(file) => file,
1789        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
1790        Err(err) => return Err(GitError::Io(err.to_string())),
1791    };
1792    file.set_modified(std::time::SystemTime::now())
1793        .map_err(|err| GitError::Io(err.to_string()))?;
1794    Ok(true)
1795}
1796
1797pub(crate) fn promisor_pack_object_ids(
1798    objects_dir: &Path,
1799    format: ObjectFormat,
1800) -> Result<HashSet<ObjectId>> {
1801    let pack_dir = objects_dir.join("pack");
1802    let mut oids = HashSet::new();
1803    if !pack_dir.exists() {
1804        return Ok(oids);
1805    }
1806    for entry in fs::read_dir(pack_dir)? {
1807        let path = entry?.path();
1808        if path.extension().and_then(|ext| ext.to_str()) != Some("idx") {
1809            continue;
1810        }
1811        if !path.with_extension("pack").exists() || !path.with_extension("promisor").exists() {
1812            continue;
1813        }
1814        let index = PackIndex::parse(&fs::read(path)?, format)?;
1815        oids.extend(index.entries.into_iter().map(|entry| entry.oid));
1816    }
1817    Ok(oids)
1818}
1819
1820pub(crate) fn promisor_object_links(format: ObjectFormat, object: &EncodedObject) -> Vec<ObjectId> {
1821    match object.object_type {
1822        ObjectType::Commit => Commit::parse_ref(format, &object.body)
1823            .map(|commit| {
1824                let mut links = Vec::with_capacity(commit.parents.len() + 1);
1825                links.push(commit.tree);
1826                links.extend(commit.parents);
1827                links
1828            })
1829            .unwrap_or_default(),
1830        ObjectType::Tree => TreeEntries::new(format, &object.body)
1831            .filter_map(|entry| entry.ok().map(|entry| entry.oid))
1832            .collect(),
1833        ObjectType::Tag => Tag::parse_ref(format, &object.body)
1834            .map(|tag| vec![tag.object])
1835            .unwrap_or_default(),
1836        ObjectType::Blob => Vec::new(),
1837    }
1838}
1839impl ObjectWriter for FileObjectDatabase {
1840    fn write_object(&self, object: EncodedObject) -> Result<ObjectId> {
1841        // Mirror git's freshen semantics (`write_object_file`:
1842        // `freshen_packed_object || freshen_loose_object`): an object already
1843        // present in a normal pack is not written again, but its backing pack is
1844        // touched so concurrent GC treats it as recent. Cruft packs are excluded
1845        // from freshening, so rewriting a cruft-only object creates a loose copy
1846        // whose per-object mtime can be honored by the next cruft repack.
1847        let oid = object.object_id(self.format)?;
1848        if self.freshen_existing_object(&oid)? {
1849            return Ok(oid);
1850        }
1851        self.loose.write_object(object)
1852    }
1853}