Skip to main content

gix_ref/store/file/
overlay_iter.rs

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