gix_ref/store/file/
overlay_iter.rs

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