Skip to main content

grit_lib/odb/store/
files.rs

1//! On-disk object source for one `objects/` directory (MIDX, packs, loose).
2
3use std::collections::HashSet;
4use std::ops::ControlFlow;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use crate::diagnostics::DiagnosticsHandle;
9use crate::error::{Error, Result};
10use crate::hash;
11use crate::midx::midx_oid_listed_in_tip;
12use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
13use crate::pack::{self, PackLookupOptions};
14use crate::pack_store::PackStore;
15
16use super::{
17    LocalSizedRead, LooseStore, MidxObjects, MidxObjectsStatus, ObjectStore, ObjectStream,
18    PackedObjects, WritableObjectStore,
19};
20use crate::odb::{empty_tree_oid_matches, WriteOptions};
21
22/// Read/write object storage backed by one `objects/` directory tree.
23pub struct FilesSource {
24    objects_dir: PathBuf,
25    loose: LooseStore,
26    packs: PackedObjects,
27    midx: MidxObjects,
28    use_midx: bool,
29    midx_tip_listing_for_exists: bool,
30    _diagnostics: DiagnosticsHandle,
31}
32
33impl std::fmt::Debug for FilesSource {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("FilesSource")
36            .field("objects_dir", &self.objects_dir)
37            .field("use_midx", &self.use_midx)
38            .finish()
39    }
40}
41
42impl FilesSource {
43    /// Open a files source for `pack_store`'s directory.
44    ///
45    /// `use_midx` enables multi-pack-index reads when a chain is present.
46    /// `midx_tip_listing_for_exists` matches Git's MIDX membership probe for [`ObjectStore::contains`].
47    ///
48    /// # Errors
49    ///
50    /// Propagates [`MidxObjects::new`] failures other than a missing index.
51    pub fn open(
52        pack_store: Arc<PackStore>,
53        loose: LooseStore,
54        use_midx: bool,
55        midx_tip_listing_for_exists: bool,
56        diagnostics: DiagnosticsHandle,
57    ) -> Result<Self> {
58        let midx = MidxObjects::new(Arc::clone(&pack_store), use_midx)?;
59        Ok(Self {
60            objects_dir: pack_store.objects_dir().to_path_buf(),
61            loose,
62            packs: PackedObjects::new(pack_store),
63            midx,
64            use_midx,
65            midx_tip_listing_for_exists,
66            _diagnostics: diagnostics,
67        })
68    }
69
70    /// `objects/` root for this source.
71    #[must_use]
72    pub fn objects_dir(&self) -> &Path {
73        &self.objects_dir
74    }
75
76    /// Loose-object backend (writes target this store).
77    #[must_use]
78    pub fn loose_store(&self) -> &LooseStore {
79        &self.loose
80    }
81
82    /// Shared pack read backend.
83    #[must_use]
84    pub fn packed_objects(&self) -> &PackedObjects {
85        &self.packs
86    }
87
88    /// MIDX layer for this directory.
89    #[must_use]
90    pub fn midx_objects(&self) -> &MidxObjects {
91        &self.midx
92    }
93
94    /// Whether `oid` is materialized under this `objects/` tree (local loose or non-promisor pack).
95    #[must_use]
96    pub fn exists_local_materialized(&self, oid: &ObjectId) -> bool {
97        super::local_object_materialized(&self.objects_dir, oid)
98    }
99
100    /// Resolve duplicate-write short-circuit for this directory before creating a loose file.
101    ///
102    /// Returns `Some(oid)` when the object is already present locally and was freshened when
103    /// appropriate. When `options.trust_new_loose` is set, only an existing loose file stops the
104    /// write (callers may still consult alternates).
105    pub fn try_finish_duplicate_write(
106        &self,
107        oid: &ObjectId,
108        options: WriteOptions,
109    ) -> Result<Option<ObjectId>> {
110        if let Some(existing) = self.loose.try_finish_if_loose_present(oid, options)? {
111            return Ok(Some(existing));
112        }
113        if options.trust_new_loose {
114            return Ok(None);
115        }
116        if options.assume_loose_only_existence && self.exists_local_materialized(oid) {
117            if self.freshen(oid)? {
118                return Ok(Some(*oid));
119            }
120            // Cruft packs (and other unfreshenable local copies) must not block loose
121            // materialization — matches `try_freshen_existing_local` / unpack-objects.
122            return Ok(None);
123        }
124        Ok(None)
125    }
126
127    /// Write zlib-compressed store bytes for a precomputed `oid` into this source's loose store.
128    ///
129    /// # Errors
130    ///
131    /// Same as [`WritableObjectStore::write`].
132    pub fn write_zlib_prehashed(
133        &self,
134        oid: &ObjectId,
135        zlib_store: &[u8],
136        options: WriteOptions,
137    ) -> Result<ObjectId> {
138        if let Some(existing) = self.try_finish_duplicate_write(oid, options)? {
139            return Ok(existing);
140        }
141        self.loose.write_zlib_prehashed(oid, zlib_store, options)
142    }
143
144    /// Whether a loose object file exists but could not be parsed (caller may consult alternates).
145    pub fn local_loose_exists_but_unreadable(&self, oid: &ObjectId, err: &Error) -> bool {
146        if !self.loose.object_path(oid).is_file() {
147            return false;
148        }
149        matches!(
150            err,
151            Error::CorruptObject(_) | Error::LooseHashMismatch { .. } | Error::Zlib(_)
152        )
153    }
154
155    fn midx_pack_filter_active(&self) -> bool {
156        self.use_midx && self.midx.status() == MidxObjectsStatus::Active
157    }
158
159    fn midx_read_should_fallback_to_packs(err: &Error) -> bool {
160        matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
161    }
162
163    fn try_read_object_into(
164        &self,
165        oid: &ObjectId,
166        populate_delta_lru: bool,
167        buf: &mut Vec<u8>,
168    ) -> Result<Option<ObjectKind>> {
169        if empty_tree_oid_matches(self.hash_algo(), oid) {
170            buf.clear();
171            return Ok(Some(ObjectKind::Tree));
172        }
173
174        let midx_pack_filter = self.midx_pack_filter_active();
175        let pack_opts = PackLookupOptions {
176            skip_midx_covered_packs: midx_pack_filter,
177            only_midx_covered_packs: false,
178        };
179
180        if self.use_midx {
181            match self.midx.read_into(oid, buf) {
182                Ok(Some(kind)) => return Ok(Some(kind)),
183                Ok(None) => {}
184                Err(Error::Midx(_)) => {}
185                Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
186                Err(err) => return Err(err),
187            }
188        }
189
190        match pack::try_read_object_from_packs_with_options_into(
191            &self.objects_dir,
192            oid,
193            pack_opts,
194            populate_delta_lru,
195            buf,
196        ) {
197            Ok(kind) => return Ok(Some(kind)),
198            Err(Error::ObjectNotFound(_)) => {}
199            Err(err) => return Err(err),
200        }
201
202        if midx_pack_filter {
203            match pack::try_read_object_from_packs_with_options_into(
204                &self.objects_dir,
205                oid,
206                PackLookupOptions::MIDX_COVERED_PACKS,
207                populate_delta_lru,
208                buf,
209            ) {
210                Ok(kind) => return Ok(Some(kind)),
211                Err(Error::ObjectNotFound(_)) => {}
212                Err(err) => return Err(err),
213            }
214        }
215
216        #[cfg(test)]
217        crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
218        self.loose.read_into(oid, buf)
219    }
220
221    fn try_read_object(&self, oid: &ObjectId) -> Result<Option<Object>> {
222        if empty_tree_oid_matches(self.hash_algo(), oid) {
223            return Ok(Some(Object {
224                kind: ObjectKind::Tree,
225                data: Vec::new(),
226            }));
227        }
228
229        let midx_pack_filter = self.midx_pack_filter_active();
230        let pack_opts = PackLookupOptions {
231            skip_midx_covered_packs: midx_pack_filter,
232            only_midx_covered_packs: false,
233        };
234
235        if self.use_midx {
236            match self.midx.read(oid) {
237                Ok(Some(obj)) => return Ok(Some(obj)),
238                Ok(None) => {}
239                Err(Error::Midx(_)) => {}
240                Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
241                Err(err) => return Err(err),
242            }
243        }
244
245        match pack::try_read_object_from_packs_with_options(&self.objects_dir, oid, pack_opts) {
246            Ok(obj) => return Ok(Some(obj)),
247            Err(Error::ObjectNotFound(_)) => {}
248            Err(err) => return Err(err),
249        }
250
251        if midx_pack_filter {
252            match pack::try_read_object_from_packs_with_options(
253                &self.objects_dir,
254                oid,
255                PackLookupOptions::MIDX_COVERED_PACKS,
256            ) {
257                Ok(obj) => return Ok(Some(obj)),
258                Err(Error::ObjectNotFound(_)) => {}
259                Err(err) => return Err(err),
260            }
261        }
262
263        #[cfg(test)]
264        crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
265        self.loose.read(oid)
266    }
267
268    fn try_read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
269        if empty_tree_oid_matches(self.hash_algo(), oid) {
270            return Ok(Some(ObjectInfo {
271                kind: ObjectKind::Tree,
272                size: 0,
273            }));
274        }
275
276        let midx_pack_filter = self.midx_pack_filter_active();
277        let pack_opts = PackLookupOptions {
278            skip_midx_covered_packs: midx_pack_filter,
279            only_midx_covered_packs: false,
280        };
281
282        if self.use_midx {
283            match self.midx.read_info(oid) {
284                Ok(Some(info)) => return Ok(Some(info)),
285                Ok(None) => {}
286                Err(Error::Midx(_)) => {}
287                Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
288                Err(err) => return Err(err),
289            }
290        }
291
292        match pack::try_read_object_info_from_packs_with_options(&self.objects_dir, oid, pack_opts)
293        {
294            Ok(info) => return Ok(Some(info)),
295            Err(Error::ObjectNotFound(_)) => {}
296            Err(err) => return Err(err),
297        }
298
299        if midx_pack_filter {
300            match pack::try_read_object_info_from_packs_with_options(
301                &self.objects_dir,
302                oid,
303                PackLookupOptions::MIDX_COVERED_PACKS,
304            ) {
305                Ok(info) => return Ok(Some(info)),
306                Err(Error::ObjectNotFound(_)) => {}
307                Err(err) => return Err(err),
308            }
309        }
310
311        #[cfg(test)]
312        crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
313        self.loose.read_info(oid)
314    }
315
316    /// Find `oid` locally, reading it only when it is a loose object of at most `limit` bytes.
317    ///
318    /// Packed objects are not read here ([`LocalSizedRead::Packed`]); one stored whole is sized
319    /// from its entry header without inflating it. Loose objects are opened once, and inflation
320    /// stops at the header when the object is too large.
321    ///
322    /// # Errors
323    ///
324    /// Propagates pack lookup and loose read failures.
325    pub(crate) fn read_unless_larger(&self, oid: &ObjectId, limit: u64) -> Result<LocalSizedRead> {
326        let probe = || {
327            match pack::locate_packed_whole_object(&self.objects_dir, oid, false)? {
328                pack::PackedEntry::Whole(whole) => {
329                    return Ok(LocalSizedRead::Packed(Some(whole.size)))
330                }
331                pack::PackedEntry::NotWhole => return Ok(LocalSizedRead::Packed(None)),
332                pack::PackedEntry::Absent => {}
333            }
334            Ok(match self.loose.read_unless_larger(oid, limit)? {
335                Some(read) => LocalSizedRead::Loose(read),
336                None => LocalSizedRead::Missing,
337            })
338        };
339        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
340            probe()
341        } else {
342            PackStore::with_context(self.packs.pack_store().clone(), probe)
343        }
344    }
345
346    fn read_with_pack_retry(&self, oid: &ObjectId) -> Result<Option<Object>> {
347        match self.try_read_object(oid) {
348            Ok(Some(obj)) => Ok(Some(obj)),
349            Ok(None) => {
350                if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
351                    self.try_read_object(oid)
352                } else {
353                    Ok(None)
354                }
355            }
356            Err(err) => Err(err),
357        }
358    }
359
360    fn read_with_pack_retry_into(
361        &self,
362        oid: &ObjectId,
363        populate_delta_lru: bool,
364        buf: &mut Vec<u8>,
365    ) -> Result<Option<ObjectKind>> {
366        match self.try_read_object_into(oid, populate_delta_lru, buf) {
367            Ok(Some(kind)) => Ok(Some(kind)),
368            Ok(None) => {
369                if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
370                    self.try_read_object_into(oid, populate_delta_lru, buf)
371                } else {
372                    Ok(None)
373                }
374            }
375            Err(err) => Err(err),
376        }
377    }
378
379    fn read_info_with_pack_retry(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
380        match self.try_read_info(oid) {
381            Ok(Some(info)) => Ok(Some(info)),
382            Ok(None) => {
383                if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
384                    self.try_read_info(oid)
385                } else {
386                    Ok(None)
387                }
388            }
389            Err(err) => Err(err),
390        }
391    }
392
393    fn contains_in_packs(&self, oid: &ObjectId) -> Result<bool> {
394        let indexes = pack::read_local_pack_indexes_cached(&self.objects_dir)?;
395        if indexes.is_empty() && !pack_directory_has_index_files(&self.objects_dir) {
396            if self.midx_tip_listing_for_exists && self.use_midx {
397                match midx_oid_listed_in_tip(&self.objects_dir, oid) {
398                    Ok(Some(true)) => return Ok(true),
399                    Ok(Some(false)) | Ok(None) => {}
400                    Err(_) => return Ok(false),
401                }
402            }
403            return Ok(false);
404        }
405        let filter = super::PackFilter {
406            include_promisor: true,
407            include_cruft: true,
408        };
409        if self.packs.contains_filtered(oid, filter)? {
410            return Ok(true);
411        }
412        if pack::reprepare_pack_directory_on_miss(&self.objects_dir)?
413            && self.packs.contains_filtered(oid, filter)?
414        {
415            return Ok(true);
416        }
417        if self.midx_tip_listing_for_exists && self.use_midx {
418            match midx_oid_listed_in_tip(&self.objects_dir, oid) {
419                Ok(Some(true)) => return Ok(true),
420                Ok(Some(false)) | Ok(None) => {}
421                Err(_) => return Ok(false),
422            }
423        }
424        Ok(false)
425    }
426}
427
428impl FilesSource {
429    /// [`ObjectStore::read_into`], keeping delta bases resolved from packs in the delta-base
430    /// cache when `populate_delta_lru` is set.
431    pub(crate) fn read_into_with_delta_policy(
432        &self,
433        oid: &ObjectId,
434        populate_delta_lru: bool,
435        buf: &mut Vec<u8>,
436    ) -> Result<Option<ObjectKind>> {
437        let mut read = || self.read_with_pack_retry_into(oid, populate_delta_lru, buf);
438        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
439            read()
440        } else {
441            PackStore::with_context(self.packs.pack_store().clone(), read)
442        }
443    }
444}
445
446impl ObjectStore for FilesSource {
447    fn hash_algo(&self) -> HashAlgo {
448        ObjectStore::hash_algo(&self.loose)
449    }
450
451    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
452        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
453            return self.read_with_pack_retry(oid);
454        }
455        PackStore::with_context(self.packs.pack_store().clone(), || {
456            self.read_with_pack_retry(oid)
457        })
458    }
459
460    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
461        self.read_into_with_delta_policy(oid, false, buf)
462    }
463
464    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
465        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
466            return self.read_info_with_pack_retry(oid);
467        }
468        PackStore::with_context(self.packs.pack_store().clone(), || {
469            self.read_info_with_pack_retry(oid)
470        })
471    }
472
473    fn contains(&self, oid: &ObjectId) -> Result<bool> {
474        if empty_tree_oid_matches(self.hash_algo(), oid) {
475            return Ok(true);
476        }
477        if self.loose.object_path(oid).is_file() {
478            return Ok(true);
479        }
480        let check = || self.contains_in_packs(oid);
481        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
482            check()
483        } else {
484            PackStore::with_context(self.packs.pack_store().clone(), check)
485        }
486    }
487
488    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
489        if empty_tree_oid_matches(self.hash_algo(), oid) {
490            return Ok(Some(ObjectStream {
491                kind: ObjectKind::Tree,
492                size: 0,
493                reader: Box::new(std::io::Cursor::new(Vec::<u8>::new())),
494            }));
495        }
496        PackStore::with_context(self.packs.pack_store().clone(), || {
497            if self.use_midx {
498                match self.midx.open_stream(oid) {
499                    Ok(Some(stream)) => return Ok(Some(stream)),
500                    Ok(None) => {}
501                    Err(Error::Midx(_)) => {}
502                    Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
503                    Err(err) => return Err(err),
504                }
505            }
506            match self.packs.open_stream(oid) {
507                Ok(Some(stream)) => return Ok(Some(stream)),
508                Ok(None) | Err(Error::ObjectNotFound(_)) => {}
509                Err(err) => return Err(err),
510            }
511            self.loose.open_stream(oid)
512        })
513    }
514
515    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
516        let mut seen = HashSet::new();
517        PackStore::with_context(self.packs.pack_store().clone(), || {
518            let mut layer = |store: &dyn ObjectStore| -> Result<bool> {
519                super::for_each_propagate_break(store, &mut |oid| {
520                    if !seen.insert(*oid) {
521                        return ControlFlow::Continue(());
522                    }
523                    f(oid)
524                })
525            };
526            if self.use_midx && layer(&self.midx)? {
527                return Ok(());
528            }
529            if layer(&self.packs)? {
530                return Ok(());
531            }
532            let _ = layer(&self.loose)?;
533            Ok(())
534        })
535    }
536
537    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
538        ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
539        let mut seen: HashSet<ObjectId> = out.iter().copied().collect();
540        if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
541            files_lookup_prefix_layers(self, prefix, limit, out, &mut seen)
542        } else {
543            PackStore::with_context(self.packs.pack_store().clone(), || {
544                files_lookup_prefix_layers(self, prefix, limit, out, &mut seen)
545            })
546        }
547    }
548
549    fn refresh(&self) -> Result<bool> {
550        PackStore::with_context(self.packs.pack_store().clone(), || {
551            let mut changed = self.packs.refresh()?;
552            if self.midx.refresh()? {
553                changed = true;
554            }
555            Ok(changed)
556        })
557    }
558}
559
560impl WritableObjectStore for FilesSource {
561    fn write(&self, kind: ObjectKind, data: &[u8], options: WriteOptions) -> Result<ObjectId> {
562        let oid = hash::hash_object(self.hash_algo(), kind, data)?;
563        if let Some(existing) = self.try_finish_duplicate_write(&oid, options)? {
564            return Ok(existing);
565        }
566        self.loose.write(kind, data, options)
567    }
568
569    fn freshen(&self, oid: &ObjectId) -> Result<bool> {
570        if self.loose.freshen(oid)? {
571            return Ok(true);
572        }
573        PackStore::with_context(self.packs.pack_store().clone(), || {
574            self.packs.freshen_pack_containing(oid)
575        })
576    }
577}
578
579fn pack_directory_has_index_files(objects_dir: &Path) -> bool {
580    let pack_dir = objects_dir.join("pack");
581    let Ok(entries) = std::fs::read_dir(pack_dir) else {
582        return false;
583    };
584    entries
585        .filter_map(|e| e.ok())
586        .any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
587}
588
589fn files_lookup_prefix_layers(
590    src: &FilesSource,
591    prefix: &str,
592    limit: usize,
593    out: &mut Vec<ObjectId>,
594    seen: &mut HashSet<ObjectId>,
595) -> Result<()> {
596    if src.use_midx {
597        merge_prefix_matches(&src.midx, prefix, limit, out, seen)?;
598        if limit != 0 && out.len() >= limit {
599            return Ok(());
600        }
601    }
602    merge_prefix_matches(&src.packs, prefix, limit, out, seen)?;
603    if limit != 0 && out.len() >= limit {
604        return Ok(());
605    }
606    merge_prefix_matches(&src.loose, prefix, limit, out, seen)
607}
608
609fn merge_prefix_matches(
610    store: &dyn ObjectStore,
611    prefix: &str,
612    limit: usize,
613    out: &mut Vec<ObjectId>,
614    seen: &mut HashSet<ObjectId>,
615) -> Result<()> {
616    if limit != 0 && out.len() >= limit {
617        return Ok(());
618    }
619    if limit == 0 {
620        let mut scratch = Vec::new();
621        store.lookup_prefix(prefix, 0, &mut scratch)?;
622        for oid in scratch {
623            if seen.insert(oid) {
624                out.push(oid);
625            }
626        }
627        return Ok(());
628    }
629
630    let goal = limit;
631    let mut fetch = goal.saturating_sub(out.len());
632    loop {
633        let mut scratch = Vec::new();
634        store.lookup_prefix(prefix, fetch, &mut scratch)?;
635        let scratch_len = scratch.len();
636        let before = out.len();
637        for oid in scratch {
638            if seen.insert(oid) {
639                out.push(oid);
640                if out.len() >= goal {
641                    return Ok(());
642                }
643            }
644        }
645        if out.len() >= goal {
646            return Ok(());
647        }
648        if scratch_len < fetch {
649            return Ok(());
650        }
651        let need = goal.saturating_sub(out.len());
652        if need == 0 {
653            return Ok(());
654        }
655        fetch = if before == out.len() {
656            fetch.saturating_add(need)
657        } else {
658            scratch_len.saturating_add(need)
659        };
660    }
661}