Skip to content

Commit 277c41f

Browse files
committed
git-discover: add cross_fs option to upwards discovery
1 parent a046c70 commit 277c41f

File tree

4 files changed

+112
-1
lines changed

4 files changed

+112
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ 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+
tempfile = "3.2.0"

git-discover/src/upwards.rs

Lines changed: 48 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,20 @@ pub(crate) mod function {
112131
}
113132
current_height += 1;
114133

134+
#[cfg(unix)]
135+
if current_height != 0 && !cross_fs {
136+
let metadata = cursor.metadata().map_err(|_| Error::InaccessibleDirectory {
137+
path: cursor.to_path_buf(),
138+
})?;
139+
140+
if device_id(&metadata) != initial_device {
141+
return Err(Error::NoGitRepositoryWithinFs {
142+
path: dir.into_owned(),
143+
limit: cursor.to_path_buf(),
144+
});
145+
}
146+
}
147+
115148
for append_dot_git in &[false, true] {
116149
if *append_dot_git {
117150
cursor.push(".git");
@@ -217,6 +250,20 @@ pub(crate) mod function {
217250
.min()
218251
}
219252

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

git-discover/tests/upwards/mod.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,66 @@ 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+
// Create an empty dmg file
208+
let dmg_location = tempfile::tempdir()?;
209+
let dmg_file = dmg_location.path().join("temp.dmg");
210+
Command::new("hdiutil")
211+
.args(&["create", "-size", "1m"])
212+
.arg(&dmg_file)
213+
.status()?;
214+
215+
// Mount dmg file into temporary location
216+
let mount_point = tempfile::tempdir()?;
217+
Command::new("hdiutil")
218+
.args(&["attach", "-nobrowse", "-mountpoint"])
219+
.arg(mount_point.path())
220+
.arg(&dmg_file)
221+
.status()?;
222+
223+
// Symlink the mount point into the repo
224+
symlink(mount_point.path(), top_level_repo.path().join("remote"))?;
225+
226+
// Disovery tests
227+
let res = git_discover::upwards(top_level_repo.path().join("remote"))
228+
.expect_err("the cross-fs option should prevent us from discovering the repo");
229+
assert!(matches!(
230+
res,
231+
git_discover::upwards::Error::NoGitRepositoryWithinFs { .. }
232+
));
233+
234+
let (repo_path, _trust) = git_discover::upwards_opts(
235+
&top_level_repo.path().join("remote"),
236+
Options {
237+
cross_fs: true,
238+
..Default::default()
239+
},
240+
)
241+
.expect("the cross-fs option should allow us to discover the repo");
242+
243+
assert_eq!(
244+
repo_path
245+
.into_repository_and_work_tree_directories()
246+
.1
247+
.expect("work dir")
248+
.file_name(),
249+
top_level_repo.path().file_name()
250+
);
251+
252+
// Cleanup
253+
Command::new("hdiutil").arg("detach").arg(mount_point.path()).status()?;
254+
255+
Ok(())
256+
}
257+
198258
fn repo_path() -> crate::Result<PathBuf> {
199259
git_testtools::scripted_fixture_repo_read_only("make_basic_repo.sh")
200260
}

0 commit comments

Comments
 (0)