gix_discover/upwards/
mod.rs

1mod types;
2pub use types::{Error, Options};
3
4mod util;
5
6pub(crate) mod function {
7    use std::{borrow::Cow, ffi::OsStr, path::Path};
8
9    use gix_sec::Trust;
10
11    use super::{Error, Options};
12    #[cfg(unix)]
13    use crate::upwards::util::device_id;
14    use crate::{
15        is::git_with_metadata as is_git_with_metadata,
16        is_git,
17        upwards::util::{find_ceiling_height, shorten_path_with_cwd},
18        DOT_GIT_DIR,
19    };
20
21    /// Find the location of the git repository directly in `directory` or in any of its parent directories and provide
22    /// an associated Trust level by looking at the git directory's ownership, and control discovery using `options`.
23    ///
24    /// Fail if no valid-looking git repository could be found.
25    // TODO: tests for trust-based discovery
26    #[cfg_attr(not(unix), allow(unused_variables))]
27    pub fn discover_opts(
28        directory: &Path,
29        Options {
30            required_trust,
31            ceiling_dirs,
32            match_ceiling_dir_or_error,
33            cross_fs,
34            current_dir,
35            dot_git_only,
36        }: Options<'_>,
37    ) -> Result<(crate::repository::Path, Trust), Error> {
38        // Normalize the path so that `Path::parent()` _actually_ gives
39        // us the parent directory. (`Path::parent` just strips off the last
40        // path component, which means it will not do what you expect when
41        // working with paths that contain '..'.)
42        let cwd = current_dir.map_or_else(
43            || {
44                // The paths we return are relevant to the repository, but at this time it's impossible to know
45                // what `core.precomposeUnicode` is going to be. Hence, the one using these paths will have to
46                // transform the paths as needed, because we can't. `false` means to leave the obtained path as is.
47                gix_fs::current_dir(false).map(Cow::Owned)
48            },
49            |cwd| Ok(Cow::Borrowed(cwd)),
50        )?;
51        #[cfg(windows)]
52        let directory = dunce::simplified(directory);
53        let dir = gix_path::normalize(directory.into(), cwd.as_ref()).ok_or_else(|| Error::InvalidInput {
54            directory: directory.into(),
55        })?;
56        let dir_metadata = dir.metadata().map_err(|_| Error::InaccessibleDirectory {
57            path: dir.to_path_buf(),
58        })?;
59
60        if !dir_metadata.is_dir() {
61            return Err(Error::InaccessibleDirectory { path: dir.into_owned() });
62        }
63        let mut dir_made_absolute = !directory.is_absolute()
64            && cwd
65                .as_ref()
66                .strip_prefix(dir.as_ref())
67                .or_else(|_| dir.as_ref().strip_prefix(cwd.as_ref()))
68                .is_ok();
69
70        let filter_by_trust = |x: &Path| -> Result<Option<Trust>, Error> {
71            let trust = Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;
72            Ok((trust >= required_trust).then_some(trust))
73        };
74
75        let max_height = if !ceiling_dirs.is_empty() {
76            let max_height = find_ceiling_height(&dir, &ceiling_dirs, cwd.as_ref());
77            if max_height.is_none() && match_ceiling_dir_or_error {
78                return Err(Error::NoMatchingCeilingDir);
79            }
80            max_height
81        } else {
82            None
83        };
84
85        #[cfg(unix)]
86        let initial_device = device_id(&dir_metadata);
87
88        let mut cursor = dir.clone().into_owned();
89        let mut current_height = 0;
90        let mut cursor_metadata = Some(dir_metadata);
91        'outer: loop {
92            if max_height.is_some_and(|x| current_height > x) {
93                return Err(Error::NoGitRepositoryWithinCeiling {
94                    path: dir.into_owned(),
95                    ceiling_height: current_height,
96                });
97            }
98            current_height += 1;
99
100            #[cfg(unix)]
101            if current_height != 0 && !cross_fs {
102                let metadata = cursor_metadata.take().map_or_else(
103                    || {
104                        if cursor.as_os_str().is_empty() {
105                            Path::new(".")
106                        } else {
107                            cursor.as_ref()
108                        }
109                        .metadata()
110                        .map_err(|_| Error::InaccessibleDirectory { path: cursor.clone() })
111                    },
112                    Ok,
113                )?;
114
115                if device_id(&metadata) != initial_device {
116                    return Err(Error::NoGitRepositoryWithinFs {
117                        path: dir.into_owned(),
118                        limit: cursor.clone(),
119                    });
120                }
121                cursor_metadata = Some(metadata);
122            }
123
124            let mut cursor_metadata_backup = None;
125            let started_as_dot_git = cursor.file_name() == Some(OsStr::new(DOT_GIT_DIR));
126            let dir_manipulation = if dot_git_only { &[true] as &[_] } else { &[true, false] };
127            for append_dot_git in dir_manipulation {
128                if *append_dot_git && !started_as_dot_git {
129                    cursor.push(DOT_GIT_DIR);
130                    cursor_metadata_backup = cursor_metadata.take();
131                }
132                if let Ok(kind) = match cursor_metadata.take() {
133                    Some(metadata) => is_git_with_metadata(&cursor, metadata, &cwd),
134                    None => is_git(&cursor),
135                } {
136                    match filter_by_trust(&cursor)? {
137                        Some(trust) => {
138                            // TODO: test this more, it definitely doesn't always find the shortest path to a directory
139                            let path = if dir_made_absolute {
140                                shorten_path_with_cwd(cursor, cwd.as_ref())
141                            } else {
142                                cursor
143                            };
144                            break 'outer Ok((
145                                crate::repository::Path::from_dot_git_dir(path, kind, cwd.as_ref()).ok_or_else(
146                                    || Error::InvalidInput {
147                                        directory: directory.into(),
148                                    },
149                                )?,
150                                trust,
151                            ));
152                        }
153                        None => {
154                            break 'outer Err(Error::NoTrustedGitRepository {
155                                path: dir.into_owned(),
156                                candidate: cursor,
157                                required: required_trust,
158                            })
159                        }
160                    }
161                }
162
163                // Usually `.git` (started_as_dot_git == true) will be a git dir, but if not we can quickly skip over it.
164                if *append_dot_git || started_as_dot_git {
165                    cursor.pop();
166                    if let Some(metadata) = cursor_metadata_backup.take() {
167                        cursor_metadata = Some(metadata);
168                    }
169                }
170            }
171            if cursor.parent().is_some_and(|p| p.as_os_str().is_empty()) {
172                cursor = cwd.to_path_buf();
173                dir_made_absolute = true;
174            }
175            if !cursor.pop() {
176                if dir_made_absolute
177                    || matches!(
178                        cursor.components().next(),
179                        Some(std::path::Component::RootDir | std::path::Component::Prefix(_))
180                    )
181                {
182                    break Err(Error::NoGitRepository { path: dir.into_owned() });
183                } else {
184                    dir_made_absolute = true;
185                    debug_assert!(!cursor.as_os_str().is_empty());
186                    // TODO: realpath or normalize? No test runs into this.
187                    cursor = gix_path::normalize(cursor.clone().into(), cwd.as_ref())
188                        .ok_or_else(|| Error::InvalidInput {
189                            directory: cursor.clone(),
190                        })?
191                        .into_owned();
192                }
193            }
194        }
195    }
196
197    /// Find the location of the git repository directly in `directory` or in any of its parent directories, and provide
198    /// the trust level derived from Path ownership.
199    ///
200    /// Fail if no valid-looking git repository could be found.
201    pub fn discover(directory: &Path) -> Result<(crate::repository::Path, Trust), Error> {
202        discover_opts(directory, Default::default())
203    }
204}