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