Skip to main content

gix_discover/upwards/
types.rs

1use std::{env, ffi::OsStr, path::PathBuf};
2
3/// The error returned by [`gix_discover::upwards()`][crate::upwards()].
4#[derive(Debug, thiserror::Error)]
5#[expect(missing_docs)]
6pub enum Error {
7    #[error("Could not obtain the current working directory")]
8    CurrentDir(#[from] std::io::Error),
9    #[error("Relative path \"{}\"tries to reach beyond root filesystem", directory.display())]
10    InvalidInput { directory: PathBuf },
11    #[error("Failed to access a directory, or path is not a directory: '{}'", .path.display())]
12    InaccessibleDirectory { path: PathBuf },
13    #[error("Could not find a git repository in '{}' or in any of its parents", .path.display())]
14    NoGitRepository { path: PathBuf },
15    #[error("Could not find a git repository in '{}' or in any of its parents within ceiling height of {}", .path.display(), .ceiling_height)]
16    NoGitRepositoryWithinCeiling { path: PathBuf, ceiling_height: usize },
17    #[error("Could not find a git repository in '{}' or in any of its parents within device limits below '{}'", .path.display(), .limit.display())]
18    NoGitRepositoryWithinFs { path: PathBuf, limit: PathBuf },
19    #[error("None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.")]
20    NoMatchingCeilingDir,
21    #[error("Could not find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())]
22    NoTrustedGitRepository {
23        path: PathBuf,
24        candidate: PathBuf,
25        required: gix_sec::Trust,
26    },
27    #[error("Could not determine trust level for path '{}'.", .path.display())]
28    CheckTrust {
29        path: PathBuf,
30        #[source]
31        err: std::io::Error,
32    },
33}
34
35/// How to obtain the trust level for a discovered repository.
36#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
37pub enum TrustPolicy {
38    /// Determine trust from repository ownership and require it to be at least the given level.
39    Required(gix_sec::Trust),
40    /// Trust computation is skipped and the given trust level is assumed.
41    Assume(gix_sec::Trust),
42}
43
44impl Default for TrustPolicy {
45    fn default() -> Self {
46        TrustPolicy::Required(gix_sec::Trust::Reduced)
47    }
48}
49
50/// Options to help guide the [discovery][crate::upwards()] of repositories, along with their options
51/// when instantiated.
52pub struct Options<'a> {
53    /// When discovering a repository, determine how trust should be obtained.
54    ///
55    /// This defaults to [`Required(Reduced)`][TrustPolicy::Required] as our default settings are geared towards avoiding abuse.
56    /// Set it to `Required(Full)` to only see repositories that [are owned by the current user][gix_sec::Trust::from_path_ownership()],
57    /// or [`TrustPolicy::Assume`] to skip trust computation and return the given trust level.
58    pub trust: TrustPolicy,
59    /// When discovering a repository, ignore any repositories that are located in these directories or any of their parents.
60    ///
61    /// Entries are made absolute and lexically normalized, but symlinks are not resolved. They must therefore use the
62    /// physical, symlink-resolved spelling of the directory to match the path traversed during discovery.
63    ///
64    /// Note that we ignore ceiling directories if the search directory is directly on top of one, which by default is an error
65    /// if `match_ceiling_dir_or_error` is true, the default.
66    pub ceiling_dirs: Vec<PathBuf>,
67    /// If true, default true, and `ceiling_dirs` is not empty, we expect at least one ceiling directory to
68    /// contain our search dir or else there will be an error.
69    pub match_ceiling_dir_or_error: bool,
70    /// if `true` avoid crossing filesystem boundaries.
71    /// Only supported on Unix-like systems.
72    // TODO: test on Linux
73    // TODO: Handle WASI once https://github.com/rust-lang/rust/issues/71213 is resolved
74    pub cross_fs: bool,
75    /// If true, limit discovery to `.git` directories.
76    ///
77    /// This  will fail to find typical bare repositories, but would find them if they happen to be named `.git`.
78    /// Use this option if repos with worktrees are the only kind of repositories you are interested in for
79    /// optimal discovery performance.
80    pub dot_git_only: bool,
81    /// If set, the _current working directory_ (absolute path) to use when resolving relative paths. Note that
82    /// that this is merely an optimization for those who discover a lot of repositories in the same process.
83    ///
84    /// If unset, the current working directory will be obtained automatically.
85    /// Note that the path here might or might not contained decomposed unicode, which may end up in a path
86    /// relevant us, like the git-dir or the worktree-dir. However, when opening the repository, it will
87    /// change decomposed unicode to precomposed unicode based on the value of `core.precomposeUnicode`, and we
88    /// don't have to deal with that value here just yet.
89    pub current_dir: Option<&'a std::path::Path>,
90}
91
92impl Default for Options<'_> {
93    fn default() -> Self {
94        Options {
95            trust: TrustPolicy::default(),
96            ceiling_dirs: vec![],
97            match_ceiling_dir_or_error: true,
98            cross_fs: false,
99            dot_git_only: false,
100            current_dir: None,
101        }
102    }
103}
104
105impl Options<'_> {
106    /// Loads discovery options overrides from the environment.
107    ///
108    /// The environment variables are:
109    /// - `GIT_CEILING_DIRECTORIES` for `ceiling_dirs`
110    ///
111    /// Note that `GIT_DISCOVERY_ACROSS_FILESYSTEM` for `cross_fs` is **not** read,
112    /// as it requires parsing of `git-config` style boolean values.
113    // TODO: test
114    pub fn apply_environment(mut self) -> Self {
115        let name = "GIT_CEILING_DIRECTORIES";
116        if let Some(ceiling_dirs) = env::var_os(name) {
117            self.ceiling_dirs = parse_ceiling_dirs(&ceiling_dirs);
118        }
119        self
120    }
121}
122
123/// Parse a byte-string of `:`-separated paths into `Vec<PathBuf>`.
124/// On Windows, paths are separated by `;`.
125/// Non-absolute paths are discarded.
126/// To match git, all paths are normalized, until an empty path is encountered.
127pub(crate) fn parse_ceiling_dirs(ceiling_dirs: &OsStr) -> Vec<PathBuf> {
128    let mut should_normalize = true;
129    let mut out = Vec::new();
130    for ceiling_dir in std::env::split_paths(ceiling_dirs) {
131        if ceiling_dir.as_os_str().is_empty() {
132            should_normalize = false;
133            continue;
134        }
135
136        // Only absolute paths are allowed
137        if ceiling_dir.is_relative() {
138            continue;
139        }
140
141        let mut dir = ceiling_dir;
142        if should_normalize {
143            if let Ok(normalized) = gix_path::realpath(&dir) {
144                dir = normalized;
145            }
146        }
147        out.push(dir);
148    }
149    out
150}
151
152#[cfg(test)]
153mod tests {
154
155    #[test]
156    #[cfg(unix)]
157    fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> {
158        use std::{fs, os::unix::fs::symlink};
159
160        use super::*;
161
162        // Setup filesystem
163        let dir = tempfile::tempdir().expect("success creating temp dir");
164        let direct_path = dir.path().join("direct");
165        let symlink_path = dir.path().join("symlink");
166        fs::create_dir(&direct_path)?;
167        symlink(&direct_path, &symlink_path)?;
168
169        // Parse & build ceiling dirs string
170        let symlink_str = symlink_path.to_str().expect("symlink path is valid utf8");
171        let ceiling_dir_string = format!("{symlink_str}:relative::{symlink_str}");
172        let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str()));
173
174        assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded");
175        assert_eq!(
176            ceiling_dirs[0],
177            symlink_path.canonicalize().expect("symlink path exists"),
178            "Symlinks are resolved"
179        );
180        assert_eq!(
181            ceiling_dirs[1], symlink_path,
182            "Symlink are not resolved after empty item"
183        );
184
185        dir.close()
186    }
187
188    #[test]
189    #[cfg(windows)]
190    fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> {
191        use std::{fs, os::windows::fs::symlink_dir};
192
193        use super::*;
194
195        // Setup filesystem
196        let dir = tempfile::tempdir().expect("success creating temp dir");
197        let direct_path = dir.path().join("direct");
198        let symlink_path = dir.path().join("symlink");
199        fs::create_dir(&direct_path)?;
200        symlink_dir(&direct_path, &symlink_path)?;
201
202        // Parse & build ceiling dirs string
203        let symlink_str = symlink_path.to_str().expect("symlink path is valid utf8");
204        let ceiling_dir_string = format!("{};relative;;{}", symlink_str, symlink_str);
205        let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str()));
206
207        assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded");
208        assert_eq!(ceiling_dirs[0], direct_path, "Symlinks are resolved");
209        assert_eq!(
210            ceiling_dirs[1], symlink_path,
211            "Symlink are not resolved after empty item"
212        );
213
214        dir.close()
215    }
216}