gix_ref/store/file/
find.rs

1use std::{
2    borrow::Cow,
3    io::{self, Read},
4    path::{Path, PathBuf},
5};
6
7pub use error::Error;
8
9use crate::name::is_pseudo_ref;
10use crate::{
11    file,
12    store_impl::{file::loose, packed},
13    BStr, BString, FullNameRef, PartialName, PartialNameRef, Reference,
14};
15
16/// ### Finding References - notes about precomposed unicode.
17///
18/// Generally, ref names and the target of symbolic refs are stored as-is if [`Self::precompose_unicode`] is `false`.
19/// If `true`, refs are stored as precomposed unicode in `packed-refs`, but stored as is on disk as it is then assumed
20/// to be indifferent, i.e. `"a\u{308}"` is the same as `"รค"`.
21///
22/// This also means that when refs are packed for transmission to another machine, both their names and the target of
23/// symbolic references need to be precomposed.
24///
25/// Namespaces are left as is as they never get past the particular repository that uses them.
26impl file::Store {
27    /// Find a single reference by the given `path` which is required to be a valid reference name.
28    ///
29    /// Returns `Ok(None)` if no such ref exists.
30    ///
31    /// ### Note
32    ///
33    /// * The lookup algorithm follows the one in [the git documentation][git-lookup-docs].
34    /// * The packed buffer is checked for modifications each time the method is called. See [`file::Store::try_find_packed()`]
35    ///   for a version with more control.
36    ///
37    /// [git-lookup-docs]: https://github.com/git/git/blob/5d5b1473453400224ebb126bf3947e0a3276bdf5/Documentation/revisions.txt#L34-L46
38    pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result<Option<Reference>, Error>
39    where
40        Name: TryInto<&'a PartialNameRef, Error = E>,
41        Error: From<E>,
42    {
43        let packed = self.assure_packed_refs_uptodate()?;
44        self.find_one_with_verified_input(partial.try_into()?, packed.as_ref().map(|b| &***b))
45    }
46
47    /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error.
48    ///
49    /// Find only loose references, that is references that aren't in the packed-refs buffer.
50    /// All symbolic references are loose references.
51    /// `HEAD` is always a loose reference.
52    pub fn try_find_loose<'a, Name, E>(&self, partial: Name) -> Result<Option<loose::Reference>, Error>
53    where
54        Name: TryInto<&'a PartialNameRef, Error = E>,
55        Error: From<E>,
56    {
57        self.find_one_with_verified_input(partial.try_into()?, None)
58            .map(|r| r.map(Into::into))
59    }
60
61    /// Similar to [`file::Store::find()`], but allows to pass a snapshotted packed buffer instead.
62    pub fn try_find_packed<'a, Name, E>(
63        &self,
64        partial: Name,
65        packed: Option<&packed::Buffer>,
66    ) -> Result<Option<Reference>, Error>
67    where
68        Name: TryInto<&'a PartialNameRef, Error = E>,
69        Error: From<E>,
70    {
71        self.find_one_with_verified_input(partial.try_into()?, packed)
72    }
73
74    pub(crate) fn find_one_with_verified_input(
75        &self,
76        partial_name: &PartialNameRef,
77        packed: Option<&packed::Buffer>,
78    ) -> Result<Option<Reference>, Error> {
79        fn decompose_if(mut r: Reference, input_changed_to_precomposed: bool) -> Reference {
80            if input_changed_to_precomposed {
81                use gix_object::bstr::ByteSlice;
82                let decomposed = r
83                    .name
84                    .0
85                    .to_str()
86                    .ok()
87                    .map(|name| gix_utils::str::decompose(name.into()));
88                if let Some(Cow::Owned(decomposed)) = decomposed {
89                    r.name.0 = decomposed.into();
90                }
91            }
92            r
93        }
94        let mut buf = BString::default();
95        let mut precomposed_partial_name_storage = packed.filter(|_| self.precompose_unicode).and_then(|_| {
96            use gix_object::bstr::ByteSlice;
97            let precomposed = partial_name.0.to_str().ok()?;
98            let precomposed = gix_utils::str::precompose(precomposed.into());
99            match precomposed {
100                Cow::Owned(precomposed) => Some(PartialName(precomposed.into())),
101                Cow::Borrowed(_) => None,
102            }
103        });
104        let precomposed_partial_name = precomposed_partial_name_storage
105            .as_ref()
106            .map(std::convert::AsRef::as_ref);
107        for consider_pseudo_ref in [true, false] {
108            if !consider_pseudo_ref && !is_pseudo_ref(partial_name.as_bstr()) {
109                break;
110            }
111            'try_directories: for inbetween in &["", "tags", "heads", "remotes"] {
112                match self.find_inner(
113                    inbetween,
114                    partial_name,
115                    precomposed_partial_name,
116                    packed,
117                    &mut buf,
118                    consider_pseudo_ref,
119                ) {
120                    Ok(Some(r)) => return Ok(Some(decompose_if(r, precomposed_partial_name.is_some()))),
121                    Ok(None) => {
122                        if consider_pseudo_ref && is_pseudo_ref(partial_name.as_bstr()) {
123                            break 'try_directories;
124                        }
125                        continue;
126                    }
127                    Err(err) => return Err(err),
128                }
129            }
130        }
131        if partial_name.as_bstr() != "HEAD" {
132            if let Some(mut precomposed) = precomposed_partial_name_storage {
133                precomposed = precomposed.join("HEAD".into()).expect("HEAD is valid name");
134                precomposed_partial_name_storage = Some(precomposed);
135            }
136            self.find_inner(
137                "remotes",
138                partial_name
139                    .to_owned()
140                    .join("HEAD".into())
141                    .expect("HEAD is valid name")
142                    .as_ref(),
143                precomposed_partial_name_storage
144                    .as_ref()
145                    .map(std::convert::AsRef::as_ref),
146                None,
147                &mut buf,
148                true, /* consider-pseudo-ref */
149            )
150            .map(|res| res.map(|r| decompose_if(r, precomposed_partial_name_storage.is_some())))
151        } else {
152            Ok(None)
153        }
154    }
155
156    fn find_inner(
157        &self,
158        inbetween: &str,
159        partial_name: &PartialNameRef,
160        precomposed_partial_name: Option<&PartialNameRef>,
161        packed: Option<&packed::Buffer>,
162        path_buf: &mut BString,
163        consider_pseudo_ref: bool,
164    ) -> Result<Option<Reference>, Error> {
165        let full_name = precomposed_partial_name
166            .unwrap_or(partial_name)
167            .construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref);
168        let content_buf = self.ref_contents(full_name).map_err(|err| Error::ReadFileContents {
169            source: err,
170            path: self.reference_path(full_name),
171        })?;
172
173        match content_buf {
174            None => {
175                if let Some(packed) = packed {
176                    if let Some(full_name) = packed::find::transform_full_name_for_lookup(full_name) {
177                        let full_name_backing;
178                        let full_name = match &self.namespace {
179                            Some(namespace) => {
180                                full_name_backing = namespace.to_owned().into_namespaced_name(full_name);
181                                full_name_backing.as_ref()
182                            }
183                            None => full_name,
184                        };
185                        if let Some(packed_ref) = packed.try_find_full_name(full_name)? {
186                            let mut res: Reference = packed_ref.into();
187                            if let Some(namespace) = &self.namespace {
188                                res.strip_namespace(namespace);
189                            }
190                            return Ok(Some(res));
191                        }
192                    }
193                }
194                Ok(None)
195            }
196            Some(content) => Ok(Some(
197                loose::Reference::try_from_path(full_name.to_owned(), &content)
198                    .map(Into::into)
199                    .map(|mut r: Reference| {
200                        if let Some(namespace) = &self.namespace {
201                            r.strip_namespace(namespace);
202                        }
203                        r
204                    })
205                    .map_err(|err| Error::ReferenceCreation {
206                        source: err,
207                        relative_path: full_name.to_path().to_owned(),
208                    })?,
209            )),
210        }
211    }
212}
213
214impl file::Store {
215    pub(crate) fn to_base_dir_and_relative_name<'a>(
216        &self,
217        name: &'a FullNameRef,
218        is_reflog: bool,
219    ) -> (Cow<'_, Path>, &'a FullNameRef) {
220        let commondir = self.common_dir_resolved();
221        let linked_git_dir =
222            |worktree_name: &BStr| commondir.join("worktrees").join(gix_path::from_bstr(worktree_name));
223        name.category_and_short_name()
224            .map(|(c, sn)| {
225                use crate::Category::*;
226                let sn = FullNameRef::new_unchecked(sn);
227                match c {
228                    LinkedPseudoRef { name: worktree_name } => {
229                        if is_reflog {
230                            (linked_git_dir(worktree_name).into(), sn)
231                        } else {
232                            (commondir.into(), name)
233                        }
234                    }
235                    Tag | LocalBranch | RemoteBranch | Note => (commondir.into(), name),
236                    MainRef | MainPseudoRef => (commondir.into(), sn),
237                    LinkedRef { name: worktree_name } => {
238                        if sn.category().is_some_and(|cat| cat.is_worktree_private()) {
239                            if is_reflog {
240                                (linked_git_dir(worktree_name).into(), sn)
241                            } else {
242                                (commondir.into(), name)
243                            }
244                        } else {
245                            (commondir.into(), sn)
246                        }
247                    }
248                    PseudoRef | Bisect | Rewritten | WorktreePrivate => (self.git_dir.as_path().into(), name),
249                }
250            })
251            .unwrap_or((commondir.into(), name))
252    }
253
254    /// Implements the logic required to transform a fully qualified refname into a filesystem path
255    pub(crate) fn reference_path_with_base<'b>(&self, name: &'b FullNameRef) -> (Cow<'_, Path>, Cow<'b, Path>) {
256        let (base, name) = self.to_base_dir_and_relative_name(name, false);
257        (
258            base,
259            match &self.namespace {
260                None => gix_path::to_native_path_on_windows(name.as_bstr()),
261                Some(namespace) => {
262                    gix_path::to_native_path_on_windows(namespace.to_owned().into_namespaced_name(name).into_inner())
263                }
264            },
265        )
266    }
267
268    /// Implements the logic required to transform a fully qualified refname into a filesystem path
269    pub(crate) fn reference_path(&self, name: &FullNameRef) -> PathBuf {
270        let (base, relative_path) = self.reference_path_with_base(name);
271        base.join(relative_path)
272    }
273
274    /// Read the file contents with a verified full reference path and return it in the given vector if possible.
275    pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result<Option<Vec<u8>>> {
276        let (base, relative_path) = self.reference_path_with_base(name);
277        if self.prohibit_windows_device_names
278            && relative_path
279                .components()
280                .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok())
281                .any(|c| gix_validate::path::component_is_windows_device(c.as_ref()))
282        {
283            return Err(std::io::Error::new(
284                std::io::ErrorKind::Other,
285                format!("Illegal use of reserved Windows device name in \"{}\"", name.as_bstr()),
286            ));
287        }
288
289        let ref_path = base.join(relative_path);
290        match std::fs::File::open(&ref_path) {
291            Ok(mut file) => {
292                let mut buf = Vec::with_capacity(128);
293                if let Err(err) = file.read_to_end(&mut buf) {
294                    return if ref_path.is_dir() { Ok(None) } else { Err(err) };
295                }
296                Ok(buf.into())
297            }
298            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
299            #[cfg(windows)]
300            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => Ok(None),
301            Err(err) => Err(err),
302        }
303    }
304}
305
306///
307pub mod existing {
308    pub use error::Error;
309
310    use crate::{
311        file::{self},
312        store_impl::{
313            file::{find, loose},
314            packed,
315        },
316        PartialNameRef, Reference,
317    };
318
319    impl file::Store {
320        /// Similar to [`file::Store::try_find()`] but a non-existing ref is treated as error.
321        pub fn find<'a, Name, E>(&self, partial: Name) -> Result<Reference, Error>
322        where
323            Name: TryInto<&'a PartialNameRef, Error = E>,
324            crate::name::Error: From<E>,
325        {
326            let packed = self.assure_packed_refs_uptodate().map_err(find::Error::PackedOpen)?;
327            self.find_existing_inner(partial, packed.as_ref().map(|b| &***b))
328        }
329
330        /// Similar to [`file::Store::find()`], but supports a stable packed buffer.
331        pub fn find_packed<'a, Name, E>(
332            &self,
333            partial: Name,
334            packed: Option<&packed::Buffer>,
335        ) -> Result<Reference, Error>
336        where
337            Name: TryInto<&'a PartialNameRef, Error = E>,
338            crate::name::Error: From<E>,
339        {
340            self.find_existing_inner(partial, packed)
341        }
342
343        /// Similar to [`file::Store::find()`] won't handle packed-refs.
344        pub fn find_loose<'a, Name, E>(&self, partial: Name) -> Result<loose::Reference, Error>
345        where
346            Name: TryInto<&'a PartialNameRef, Error = E>,
347            crate::name::Error: From<E>,
348        {
349            self.find_existing_inner(partial, None).map(Into::into)
350        }
351
352        /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error.
353        pub(crate) fn find_existing_inner<'a, Name, E>(
354            &self,
355            partial: Name,
356            packed: Option<&packed::Buffer>,
357        ) -> Result<Reference, Error>
358        where
359            Name: TryInto<&'a PartialNameRef, Error = E>,
360            crate::name::Error: From<E>,
361        {
362            let path = partial
363                .try_into()
364                .map_err(|err| Error::Find(find::Error::RefnameValidation(err.into())))?;
365            match self.find_one_with_verified_input(path, packed) {
366                Ok(Some(r)) => Ok(r),
367                Ok(None) => Err(Error::NotFound {
368                    name: path.to_partial_path().to_owned(),
369                }),
370                Err(err) => Err(err.into()),
371            }
372        }
373    }
374
375    mod error {
376        use std::path::PathBuf;
377
378        use crate::store_impl::file::find;
379
380        /// The error returned by [file::Store::find_existing()][crate::file::Store::find()].
381        #[derive(Debug, thiserror::Error)]
382        #[allow(missing_docs)]
383        pub enum Error {
384            #[error("An error occurred while trying to find a reference")]
385            Find(#[from] find::Error),
386            #[error("The ref partially named {name:?} could not be found")]
387            NotFound { name: PathBuf },
388        }
389    }
390}
391
392mod error {
393    use std::{convert::Infallible, io, path::PathBuf};
394
395    use crate::{file, store_impl::packed};
396
397    /// The error returned by [file::Store::find()].
398    #[derive(Debug, thiserror::Error)]
399    #[allow(missing_docs)]
400    pub enum Error {
401        #[error("The ref name or path is not a valid ref name")]
402        RefnameValidation(#[from] crate::name::Error),
403        #[error("The ref file {path:?} could not be read in full")]
404        ReadFileContents { source: io::Error, path: PathBuf },
405        #[error("The reference at \"{relative_path}\" could not be instantiated")]
406        ReferenceCreation {
407            source: file::loose::reference::decode::Error,
408            relative_path: PathBuf,
409        },
410        #[error("A packed ref lookup failed")]
411        PackedRef(#[from] packed::find::Error),
412        #[error("Could not open the packed refs buffer when trying to find references.")]
413        PackedOpen(#[from] packed::buffer::open::Error),
414    }
415
416    impl From<Infallible> for Error {
417        fn from(_: Infallible) -> Self {
418            unreachable!("this impl is needed to allow passing a known valid partial path as parameter")
419        }
420    }
421}