Skip to content

Commit 9abaeda

Browse files
committed
Merge branch 'davidkna-discover-x-fs'
2 parents ceb6dff + aac5169 commit 9abaeda

File tree

4 files changed

+135
-1
lines changed

4 files changed

+135
-1
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

git-discover/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ thiserror = "1.0.26"
2525
[dev-dependencies]
2626
git-testtools = { path = "../tests/tools" }
2727
is_ci = "1.1.1"
28+
29+
[target.'cfg(target_os = "macos")'.dev-dependencies]
30+
defer = "0.1.0"
31+
tempfile = "3.2.0"

git-discover/src/upwards.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub enum Error {
1010
NoGitRepository { path: PathBuf },
1111
#[error("Could find a git repository in '{}' or in any of its parents within ceiling height of {}", .path.display(), .ceiling_height)]
1212
NoGitRepositoryWithinCeiling { path: PathBuf, ceiling_height: usize },
13+
#[error("Could find a git repository in '{}' or in any of its parents within device limits below '{}'", .path.display(), .limit.display())]
14+
NoGitRepositoryWithinFs { path: PathBuf, limit: PathBuf },
1315
#[error("None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.")]
1416
NoMatchingCeilingDir,
1517
#[error("Could find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())]
@@ -38,6 +40,11 @@ pub struct Options<'a> {
3840
pub ceiling_dirs: &'a [PathBuf],
3941
/// If true, and `ceiling_dirs` is not empty, we expect at least one ceiling directory to match or else there will be an error.
4042
pub match_ceiling_dir_or_error: bool,
43+
/// if `true` avoid crossing filesystem boundaries.
44+
/// Only supported on Unix-like systems.
45+
// TODO: test on Linux
46+
// TODO: Handle WASI once https://github.com/rust-lang/rust/issues/71213 is resolved
47+
pub cross_fs: bool,
4148
}
4249

4350
impl Default for Options<'_> {
@@ -46,11 +53,14 @@ impl Default for Options<'_> {
4653
required_trust: git_sec::Trust::Reduced,
4754
ceiling_dirs: &[],
4855
match_ceiling_dir_or_error: true,
56+
cross_fs: false,
4957
}
5058
}
5159
}
5260

5361
pub(crate) mod function {
62+
#[cfg(unix)]
63+
use std::fs;
5464
use std::path::{Path, PathBuf};
5565

5666
use git_sec::Trust;
@@ -63,12 +73,14 @@ pub(crate) mod function {
6373
///
6474
/// Fail if no valid-looking git repository could be found.
6575
// TODO: tests for trust-based discovery
76+
#[cfg_attr(not(unix), allow(unused_variables))]
6677
pub fn discover_opts(
6778
directory: impl AsRef<Path>,
6879
Options {
6980
required_trust,
7081
ceiling_dirs,
7182
match_ceiling_dir_or_error,
83+
cross_fs,
7284
}: Options<'_>,
7385
) -> Result<(crate::repository::Path, Trust), Error> {
7486
// Absolutize the path so that `Path::parent()` _actually_ gives
@@ -77,7 +89,11 @@ pub(crate) mod function {
7789
// working with paths paths that contain '..'.)
7890
let cwd = std::env::current_dir().ok();
7991
let dir = git_path::absolutize(directory.as_ref(), cwd.as_deref());
80-
if !dir.is_dir() {
92+
let dir_metadata = dir.metadata().map_err(|_| Error::InaccessibleDirectory {
93+
path: dir.to_path_buf(),
94+
})?;
95+
96+
if !dir_metadata.is_dir() {
8197
return Err(Error::InaccessibleDirectory { path: dir.into_owned() });
8298
}
8399
let mut dir_made_absolute = cwd.as_deref().map_or(false, |cwd| {
@@ -101,6 +117,9 @@ pub(crate) mod function {
101117
None
102118
};
103119

120+
#[cfg(unix)]
121+
let initial_device = device_id(&dir_metadata);
122+
104123
let mut cursor = dir.clone().into_owned();
105124
let mut current_height = 0;
106125
'outer: loop {
@@ -112,6 +131,24 @@ pub(crate) mod function {
112131
}
113132
current_height += 1;
114133

134+
#[cfg(unix)]
135+
if current_height != 0 && !cross_fs {
136+
let metadata = if cursor.as_os_str().is_empty() {
137+
Path::new(".")
138+
} else {
139+
cursor.as_ref()
140+
}
141+
.metadata()
142+
.map_err(|_| Error::InaccessibleDirectory { path: cursor.clone() })?;
143+
144+
if device_id(&metadata) != initial_device {
145+
return Err(Error::NoGitRepositoryWithinFs {
146+
path: dir.into_owned(),
147+
limit: cursor.clone(),
148+
});
149+
}
150+
}
151+
115152
for append_dot_git in &[false, true] {
116153
if *append_dot_git {
117154
cursor.push(".git");
@@ -217,6 +254,20 @@ pub(crate) mod function {
217254
.min()
218255
}
219256

257+
#[cfg(target_os = "linux")]
258+
/// Returns the device ID of the directory.
259+
fn device_id(m: &fs::Metadata) -> u64 {
260+
use std::os::linux::fs::MetadataExt;
261+
m.st_dev()
262+
}
263+
264+
#[cfg(all(unix, not(target_os = "linux")))]
265+
/// Returns the device ID of the directory.
266+
fn device_id(m: &fs::Metadata) -> u64 {
267+
use std::os::unix::fs::MetadataExt;
268+
m.dev()
269+
}
270+
220271
/// Find the location of the git repository directly in `directory` or in any of its parent directories, and provide
221272
/// the trust level derived from Path ownership.
222273
///

git-discover/tests/upwards/mod.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,77 @@ fn from_existing_worktree() -> crate::Result {
195195
Ok(())
196196
}
197197

198+
#[cfg(target_os = "macos")]
199+
#[test]
200+
fn cross_fs() -> crate::Result {
201+
use git_discover::upwards::Options;
202+
use std::os::unix::fs::symlink;
203+
use std::process::Command;
204+
205+
let top_level_repo = git_testtools::scripted_fixture_repo_writable("make_basic_repo.sh")?;
206+
207+
let _cleanup = {
208+
// Create an empty dmg file
209+
let dmg_location = tempfile::tempdir()?;
210+
let dmg_file = dmg_location.path().join("temp.dmg");
211+
Command::new("hdiutil")
212+
.args(&["create", "-size", "1m"])
213+
.arg(&dmg_file)
214+
.status()?;
215+
216+
// Mount dmg file into temporary location
217+
let mount_point = tempfile::tempdir()?;
218+
Command::new("hdiutil")
219+
.args(&["attach", "-nobrowse", "-mountpoint"])
220+
.arg(mount_point.path())
221+
.arg(&dmg_file)
222+
.status()?;
223+
224+
// Ensure that the mount point is always cleaned up
225+
let cleanup = defer::defer({
226+
let arg = mount_point.path().to_owned();
227+
move || {
228+
Command::new("hdiutil")
229+
.arg("detach")
230+
.arg(arg)
231+
.status()
232+
.expect("detach temporary test dmg filesystem successfully");
233+
}
234+
});
235+
236+
// Symlink the mount point into the repo
237+
symlink(mount_point.path(), top_level_repo.path().join("remote"))?;
238+
cleanup
239+
};
240+
241+
let res = git_discover::upwards(top_level_repo.path().join("remote"))
242+
.expect_err("the cross-fs option should prevent us from discovering the repo");
243+
assert!(matches!(
244+
res,
245+
git_discover::upwards::Error::NoGitRepositoryWithinFs { .. }
246+
));
247+
248+
let (repo_path, _trust) = git_discover::upwards_opts(
249+
&top_level_repo.path().join("remote"),
250+
Options {
251+
cross_fs: true,
252+
..Default::default()
253+
},
254+
)
255+
.expect("the cross-fs option should allow us to discover the repo");
256+
257+
assert_eq!(
258+
repo_path
259+
.into_repository_and_work_tree_directories()
260+
.1
261+
.expect("work dir")
262+
.file_name(),
263+
top_level_repo.path().file_name()
264+
);
265+
266+
Ok(())
267+
}
268+
198269
fn repo_path() -> crate::Result<PathBuf> {
199270
git_testtools::scripted_fixture_repo_read_only("make_basic_repo.sh")
200271
}

0 commit comments

Comments
 (0)