gix_discover/upwards/
mod.rs1mod 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 #[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 let cwd = current_dir.map_or_else(
43 || {
44 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 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 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 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 pub fn discover(directory: &Path) -> Result<(crate::repository::Path, Trust), Error> {
202 discover_opts(directory, Default::default())
203 }
204}