Skip to main content

gix_ref/store/file/
overlay_iter.rs

1use gix_object::bstr::ByteSlice;
2use gix_path::RelativePath;
3use std::{
4    borrow::Cow,
5    cmp::Ordering,
6    io::Read,
7    iter::Peekable,
8    path::{Path, PathBuf},
9};
10
11use crate::{
12    file::loose::{self, iter::SortedLoosePaths},
13    store_impl::{file, packed},
14    BStr, FullName, Namespace, Reference,
15};
16
17/// An iterator stepping through sorted input of loose references and packed references, preferring loose refs over otherwise
18/// equivalent packed references.
19///
20/// All errors will be returned verbatim, while packed errors are depleted first if loose refs also error.
21pub struct LooseThenPacked<'p, 's> {
22    git_dir: &'s Path,
23    common_dir: Option<&'s Path>,
24    hash_kind: gix_hash::Kind,
25    namespace: Option<&'s Namespace>,
26    iter_packed: Option<Peekable<packed::Iter<'p>>>,
27    iter_git_dir: Peekable<SortedLoosePaths>,
28    #[allow(dead_code)]
29    iter_common_dir: Option<Peekable<SortedLoosePaths>>,
30    buf: Vec<u8>,
31}
32
33enum IterKind {
34    Git,
35    GitAndConsumeCommon,
36    Common,
37}
38
39/// An intermediate structure to hold shared state alive long enough for iteration to happen.
40#[must_use = "Iterators should be obtained from this platform"]
41pub struct Platform<'s> {
42    store: &'s file::Store,
43    packed: Option<file::packed::SharedBufferSnapshot>,
44}
45
46impl<'p> LooseThenPacked<'p, '_> {
47    fn strip_namespace(&self, mut r: Reference) -> Reference {
48        if let Some(namespace) = &self.namespace {
49            r.strip_namespace(namespace);
50        }
51        r
52    }
53
54    fn loose_iter(&mut self, kind: IterKind) -> &mut Peekable<SortedLoosePaths> {
55        match kind {
56            IterKind::GitAndConsumeCommon => {
57                drop(self.iter_common_dir.as_mut().map(Iterator::next));
58                &mut self.iter_git_dir
59            }
60            IterKind::Git => &mut self.iter_git_dir,
61            IterKind::Common => self
62                .iter_common_dir
63                .as_mut()
64                .expect("caller knows there is a common iter"),
65        }
66    }
67
68    fn convert_packed(
69        &mut self,
70        packed: Result<packed::Reference<'p>, packed::iter::Error>,
71    ) -> Result<Reference, Error> {
72        packed
73            .map(Into::into)
74            .map(|r| self.strip_namespace(r))
75            .map_err(|err| match err {
76                packed::iter::Error::Reference {
77                    invalid_line,
78                    line_number,
79                } => Error::PackedReference {
80                    invalid_line,
81                    line_number,
82                },
83                packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"),
84            })
85    }
86
87    fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result<Reference, Error> {
88        let buf = &mut self.buf;
89        let git_dir = self.git_dir;
90        let common_dir = self.common_dir;
91        let (refpath, name) = res.map_err(Error::Traversal)?;
92        std::fs::File::open(&refpath)
93            .and_then(|mut f| {
94                buf.clear();
95                f.read_to_end(buf)
96            })
97            .map_err(|err| Error::ReadFileContents {
98                source: err,
99                path: refpath.to_owned(),
100            })?;
101        loose::Reference::try_from_path(name, buf, self.hash_kind)
102            .map_err(|err| {
103                let relative_path = refpath
104                    .strip_prefix(git_dir)
105                    .ok()
106                    .or_else(|| common_dir.and_then(|common_dir| refpath.strip_prefix(common_dir).ok()))
107                    .expect("one of our bases contains the path");
108                Error::ReferenceCreation {
109                    source: err,
110                    relative_path: relative_path.into(),
111                }
112            })
113            .map(Into::into)
114            .map(|r| self.strip_namespace(r))
115    }
116}
117
118impl Iterator for LooseThenPacked<'_, '_> {
119    type Item = Result<Reference, Error>;
120
121    fn next(&mut self) -> Option<Self::Item> {
122        fn advance_to_non_private(iter: &mut Peekable<SortedLoosePaths>) {
123            while let Some(Ok((_path, name))) = iter.peek() {
124                if name.category().is_some_and(|cat| cat.is_worktree_private()) {
125                    iter.next();
126                } else {
127                    break;
128                }
129            }
130        }
131
132        fn peek_loose<'a>(
133            git_dir: &'a mut Peekable<SortedLoosePaths>,
134            common_dir: Option<&'a mut Peekable<SortedLoosePaths>>,
135        ) -> Option<(&'a std::io::Result<(PathBuf, FullName)>, IterKind)> {
136            match common_dir {
137                Some(common_dir) => match (git_dir.peek(), {
138                    advance_to_non_private(common_dir);
139                    common_dir.peek()
140                }) {
141                    (None, None) => None,
142                    (None, Some(res)) | (Some(_), Some(res @ Err(_))) => Some((res, IterKind::Common)),
143                    (Some(res), None) | (Some(res @ Err(_)), Some(_)) => Some((res, IterKind::Git)),
144                    (Some(r_gitdir @ Ok((_, git_dir_name))), Some(r_cd @ Ok((_, common_dir_name)))) => {
145                        match git_dir_name.cmp(common_dir_name) {
146                            Ordering::Less => Some((r_gitdir, IterKind::Git)),
147                            Ordering::Equal => Some((r_gitdir, IterKind::GitAndConsumeCommon)),
148                            Ordering::Greater => Some((r_cd, IterKind::Common)),
149                        }
150                    }
151                },
152                None => git_dir.peek().map(|r| (r, IterKind::Git)),
153            }
154        }
155        match self.iter_packed.as_mut() {
156            Some(packed_iter) => match (
157                peek_loose(&mut self.iter_git_dir, self.iter_common_dir.as_mut()),
158                packed_iter.peek(),
159            ) {
160                (None, None) => None,
161                (None, Some(_)) | (Some(_), Some(Err(_))) => {
162                    let res = packed_iter.next().expect("peeked value exists");
163                    Some(self.convert_packed(res))
164                }
165                (Some((_, kind)), None) | (Some((Err(_), kind)), Some(_)) => {
166                    let res = self.loose_iter(kind).next().expect("prior peek");
167                    Some(self.convert_loose(res))
168                }
169                (Some((Ok((_, loose_name)), kind)), Some(Ok(packed))) => match loose_name.as_ref().cmp(packed.name) {
170                    Ordering::Less => {
171                        let res = self.loose_iter(kind).next().expect("prior peek");
172                        Some(self.convert_loose(res))
173                    }
174                    Ordering::Equal => {
175                        drop(packed_iter.next());
176                        let res = self.loose_iter(kind).next().expect("prior peek");
177                        Some(self.convert_loose(res))
178                    }
179                    Ordering::Greater => {
180                        let res = packed_iter.next().expect("name retrieval configured");
181                        Some(self.convert_packed(res))
182                    }
183                },
184            },
185            None => match peek_loose(&mut self.iter_git_dir, self.iter_common_dir.as_mut()) {
186                None => None,
187                Some((_, kind)) => self.loose_iter(kind).next().map(|res| self.convert_loose(res)),
188            },
189        }
190    }
191}
192
193impl<'repo> Platform<'repo> {
194    /// Return an iterator over all references, loose or packed, sorted by their name.
195    ///
196    /// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves.
197    pub fn all<'p>(&'p self) -> std::io::Result<LooseThenPacked<'p, 'repo>> {
198        self.store.iter_packed(self.packed.as_ref().map(|b| &***b))
199    }
200
201    /// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. "refs/heads/" or
202    /// "refs/heads/feature-".
203    ///
204    /// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
205    /// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that
206    /// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`.
207    ///
208    /// Prefixes are relative paths with slash-separated components.
209    pub fn prefixed<'p>(&'p self, prefix: &RelativePath) -> std::io::Result<LooseThenPacked<'p, 'repo>> {
210        self.store
211            .iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
212    }
213
214    /// Return an iterator over the pseudo references, like `HEAD` or `FETCH_HEAD`, or anything else suffixed with `HEAD`
215    /// in the root of the `.git` directory, sorted by name.
216    pub fn pseudo<'p>(&'p self) -> std::io::Result<LooseThenPacked<'p, 'repo>> {
217        self.store.iter_pseudo()
218    }
219}
220
221impl file::Store {
222    /// Return a platform to obtain iterator over all references, or prefixed ones, loose or packed, sorted by their name.
223    ///
224    /// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves.
225    ///
226    /// Note that since packed-refs are storing refs as precomposed unicode if [`Self::precompose_unicode`] is true, for consistency
227    /// we also return loose references as precomposed unicode.
228    pub fn iter(&self) -> Result<Platform<'_>, packed::buffer::open::Error> {
229        Ok(Platform {
230            store: self,
231            packed: self.assure_packed_refs_uptodate()?,
232        })
233    }
234}
235
236#[derive(Debug)]
237pub(crate) enum IterInfo<'a> {
238    Base {
239        base: &'a Path,
240        precompose_unicode: bool,
241    },
242    BaseAndIterRoot {
243        base: &'a Path,
244        iter_root: PathBuf,
245        prefix: PathBuf,
246        precompose_unicode: bool,
247    },
248    PrefixAndBase {
249        base: &'a Path,
250        prefix: &'a Path,
251        precompose_unicode: bool,
252    },
253    ComputedIterationRoot {
254        /// The root to iterate over
255        iter_root: PathBuf,
256        /// The top-level directory as boundary of all references, used to create their short-names after iteration.
257        base: &'a Path,
258        /// The original prefix.
259        prefix: Cow<'a, BStr>,
260        /// If `true`, we will convert decomposed into precomposed unicode.
261        precompose_unicode: bool,
262    },
263    Pseudo {
264        base: &'a Path,
265        precompose_unicode: bool,
266    },
267}
268
269impl<'a> IterInfo<'a> {
270    fn prefix(&self) -> Option<Cow<'_, BStr>> {
271        match self {
272            IterInfo::Base { .. } => None,
273            IterInfo::PrefixAndBase { prefix, .. } => Some(gix_path::into_bstr(*prefix)),
274            IterInfo::BaseAndIterRoot { prefix, .. } => Some(gix_path::into_bstr(prefix.clone())),
275            IterInfo::ComputedIterationRoot { prefix, .. } => Some(prefix.clone()),
276            IterInfo::Pseudo { .. } => None,
277        }
278    }
279
280    fn into_iter(self) -> Peekable<SortedLoosePaths> {
281        match self {
282            IterInfo::Base {
283                base,
284                precompose_unicode,
285            } => SortedLoosePaths::at(&base.join("refs"), base.into(), None, None, precompose_unicode),
286            IterInfo::BaseAndIterRoot {
287                base,
288                iter_root,
289                prefix: _,
290                precompose_unicode,
291            } => SortedLoosePaths::at(&iter_root, base.into(), None, None, precompose_unicode),
292            IterInfo::PrefixAndBase {
293                base,
294                prefix,
295                precompose_unicode,
296            } => SortedLoosePaths::at(&base.join(prefix), base.into(), None, None, precompose_unicode),
297            IterInfo::ComputedIterationRoot {
298                iter_root,
299                base,
300                prefix,
301                precompose_unicode,
302            } => SortedLoosePaths::at(
303                &iter_root,
304                base.into(),
305                Some(prefix.into_owned()),
306                None,
307                precompose_unicode,
308            ),
309            IterInfo::Pseudo {
310                base,
311                precompose_unicode,
312            } => SortedLoosePaths::at(base, base.into(), None, Some("HEAD".into()), precompose_unicode),
313        }
314        .peekable()
315    }
316
317    fn from_prefix(base: &'a Path, prefix: &'a RelativePath, precompose_unicode: bool) -> std::io::Result<Self> {
318        let prefix_path = gix_path::from_bstr(prefix.as_ref().as_bstr());
319        let iter_root = base.join(&prefix_path);
320        if prefix.as_ref().ends_with(b"/") {
321            Ok(IterInfo::BaseAndIterRoot {
322                base,
323                iter_root,
324                prefix: prefix_path.into_owned(),
325                precompose_unicode,
326            })
327        } else {
328            let iter_root = iter_root
329                .parent()
330                .expect("a parent is always there unless empty")
331                .to_owned();
332            Ok(IterInfo::ComputedIterationRoot {
333                base,
334                prefix: prefix.as_ref().as_bstr().into(),
335                iter_root,
336                precompose_unicode,
337            })
338        }
339    }
340}
341
342impl file::Store {
343    /// Return an iterator over all references, loose or `packed`, sorted by their name.
344    ///
345    /// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves.
346    pub fn iter_packed<'s, 'p>(
347        &'s self,
348        packed: Option<&'p packed::Buffer>,
349    ) -> std::io::Result<LooseThenPacked<'p, 's>> {
350        match self.namespace.as_ref() {
351            Some(namespace) => self.iter_from_info(
352                IterInfo::PrefixAndBase {
353                    base: self.git_dir(),
354                    prefix: namespace.to_path(),
355                    precompose_unicode: self.precompose_unicode,
356                },
357                self.common_dir().map(|base| IterInfo::PrefixAndBase {
358                    base,
359                    prefix: namespace.to_path(),
360                    precompose_unicode: self.precompose_unicode,
361                }),
362                packed,
363            ),
364            None => self.iter_from_info(
365                IterInfo::Base {
366                    base: self.git_dir(),
367                    precompose_unicode: self.precompose_unicode,
368                },
369                self.common_dir().map(|base| IterInfo::Base {
370                    base,
371                    precompose_unicode: self.precompose_unicode,
372                }),
373                packed,
374            ),
375        }
376    }
377
378    /// Return an iterator over the pseudo references, like `HEAD` or `FETCH_HEAD`, or anything else suffixed with `HEAD`
379    /// in the root of the `.git` directory, sorted by name.
380    ///
381    /// Errors are returned similarly to what would happen when loose refs were iterated by themselves.
382    pub fn iter_pseudo<'p>(&'_ self) -> std::io::Result<LooseThenPacked<'p, '_>> {
383        self.iter_from_info(
384            IterInfo::Pseudo {
385                base: self.git_dir(),
386                precompose_unicode: self.precompose_unicode,
387            },
388            None,
389            None,
390        )
391    }
392
393    /// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. `refs/heads/` or
394    /// `refs/heads/feature-`.
395    /// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively
396    /// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that
397    /// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`.
398    ///
399    /// Prefixes are relative paths with slash-separated components.
400    pub fn iter_prefixed_packed<'s, 'p>(
401        &'s self,
402        prefix: &RelativePath,
403        packed: Option<&'p packed::Buffer>,
404    ) -> std::io::Result<LooseThenPacked<'p, 's>> {
405        match self.namespace.as_ref() {
406            None => {
407                let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?;
408                let common_dir_info = self
409                    .common_dir()
410                    .map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
411                    .transpose()?;
412                self.iter_from_info(git_dir_info, common_dir_info, packed)
413            }
414            Some(namespace) => {
415                let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
416                let prefix = prefix.as_bstr().try_into().map_err(std::io::Error::other)?;
417                let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?;
418                let common_dir_info = self
419                    .common_dir()
420                    .map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode))
421                    .transpose()?;
422                self.iter_from_info(git_dir_info, common_dir_info, packed)
423            }
424        }
425    }
426
427    fn iter_from_info<'s, 'p>(
428        &'s self,
429        git_dir_info: IterInfo<'_>,
430        common_dir_info: Option<IterInfo<'_>>,
431        packed: Option<&'p packed::Buffer>,
432    ) -> std::io::Result<LooseThenPacked<'p, 's>> {
433        Ok(LooseThenPacked {
434            git_dir: self.git_dir(),
435            common_dir: self.common_dir(),
436            hash_kind: self.object_hash,
437            iter_packed: match packed {
438                Some(packed) => Some(
439                    match git_dir_info.prefix() {
440                        Some(prefix) => packed.iter_prefixed(prefix.into_owned()),
441                        None => packed.iter(),
442                    }
443                    .map_err(std::io::Error::other)?
444                    .peekable(),
445                ),
446                None => None,
447            },
448            iter_git_dir: git_dir_info.into_iter(),
449            iter_common_dir: common_dir_info.map(IterInfo::into_iter),
450            buf: Vec::new(),
451            namespace: self.namespace.as_ref(),
452        })
453    }
454}
455
456mod error {
457    use std::{io, path::PathBuf};
458
459    use gix_object::bstr::BString;
460
461    use crate::store_impl::file;
462
463    /// The error returned by the [`LooseThenPacked`][super::LooseThenPacked] iterator.
464    #[derive(Debug, thiserror::Error)]
465    #[allow(missing_docs)]
466    pub enum Error {
467        #[error("The file system could not be traversed")]
468        Traversal(#[source] io::Error),
469        #[error("The ref file {path:?} could not be read in full")]
470        ReadFileContents { source: io::Error, path: PathBuf },
471        #[error("The reference at \"{relative_path}\" could not be instantiated")]
472        ReferenceCreation {
473            source: file::loose::reference::decode::Error,
474            relative_path: PathBuf,
475        },
476        #[error("Invalid reference in line {line_number}: {invalid_line:?}")]
477        PackedReference { invalid_line: BString, line_number: usize },
478    }
479}
480pub use error::Error;