Skip to content

Commit e4f4c4b

Browse files
committed
path::discover() now returns the shortest path. (#301)
If and only if it canonicalized the source path. That way, users will still get a familiar path. This is due to `parent()` not operating in the file system, which otherwise would be equivalent to `..`, but that's not how we work. Maybe we should overhaul the way this works to use `../` instead and just 'absoluteize' the path later (std::path::absolute()) is on the way for that.
1 parent 9cb8385 commit e4f4c4b

File tree

5 files changed

+62
-31
lines changed

5 files changed

+62
-31
lines changed

git-path/src/convert.rs

+2
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,15 @@ pub fn to_unix_separators_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<
183183
/// Replaces windows path separators with slashes, unconditionally.
184184
///
185185
/// **Note** Do not use these and prefer the conditional versions of this method.
186+
// TODO: use https://lib.rs/crates/path-slash to handle escapes
186187
pub fn to_unix_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> {
187188
replace(path, b'\\', b'/')
188189
}
189190

190191
/// Find backslashes and replace them with slashes, which typically resembles a unix path, unconditionally.
191192
///
192193
/// **Note** Do not use these and prefer the conditional versions of this method.
194+
// TODO: use https://lib.rs/crates/path-slash to handle escapes
193195
pub fn to_windows_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> {
194196
replace(path, b'/', b'\\')
195197
}

git-repository/src/path/discover.rs

+50-23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ pub enum Error {
88
InaccessibleDirectory { path: PathBuf },
99
#[error("Could find a git repository in '{}' or in any of its parents", .path.display())]
1010
NoGitRepository { path: PathBuf },
11+
#[error("Could find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())]
12+
NoTrustedGitRepository {
13+
path: PathBuf,
14+
candidate: PathBuf,
15+
required: git_sec::Trust,
16+
},
1117
#[error("Could not determine trust level for path '{}'.", .path.display())]
1218
CheckTrust {
1319
path: PathBuf,
@@ -37,6 +43,7 @@ impl Default for Options {
3743
pub(crate) mod function {
3844
use super::{Error, Options};
3945
use git_sec::Trust;
46+
use std::path::PathBuf;
4047
use std::{
4148
borrow::Cow,
4249
path::{Component, Path},
@@ -57,23 +64,20 @@ pub(crate) mod function {
5764
// us the parent directory. (`Path::parent` just strips off the last
5865
// path component, which means it will not do what you expect when
5966
// working with paths paths that contain '..'.)
60-
let directory = maybe_canonicalize(directory.as_ref()).map_err(|_| Error::InaccessibleDirectory {
61-
path: directory.as_ref().into(),
62-
})?;
63-
if !directory.is_dir() {
64-
return Err(Error::InaccessibleDirectory {
65-
path: directory.into_owned(),
66-
});
67+
let directory = directory.as_ref();
68+
let dir = maybe_canonicalize(directory).map_err(|_| Error::InaccessibleDirectory { path: directory.into() })?;
69+
let is_canonicalized = dir.as_ref() != directory;
70+
if !dir.is_dir() {
71+
return Err(Error::InaccessibleDirectory { path: dir.into_owned() });
6772
}
6873

69-
let filter_by_trust =
70-
|x: &std::path::Path, kind: crate::path::Kind| -> Result<Option<(crate::Path, git_sec::Trust)>, Error> {
71-
let trust =
72-
git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;
73-
Ok((trust >= required_trust).then(|| (crate::Path::from_dot_git_dir(x, kind), trust)))
74-
};
74+
let filter_by_trust = |x: &std::path::Path| -> Result<Option<git_sec::Trust>, Error> {
75+
let trust =
76+
git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?;
77+
Ok((trust >= required_trust).then(|| (trust)))
78+
};
7579

76-
let mut cursor = directory.clone();
80+
let mut cursor = dir.clone();
7781
'outer: loop {
7882
for append_dot_git in &[false, true] {
7983
if *append_dot_git {
@@ -83,11 +87,38 @@ pub(crate) mod function {
8387
}
8488
}
8589
if let Ok(kind) = path::is::git(&cursor) {
86-
match filter_by_trust(&cursor, kind)? {
87-
Some(res) => break 'outer Ok(res),
90+
match filter_by_trust(&cursor)? {
91+
Some(trust) => {
92+
// TODO: test this more
93+
let path = if is_canonicalized {
94+
match std::env::current_dir() {
95+
Ok(cwd) => {
96+
let short_path_components = cwd
97+
.strip_prefix(&cursor.parent().expect(".git appended"))
98+
.expect("cwd is always within canonicalized candidate")
99+
.components()
100+
.count();
101+
if short_path_components < cursor.components().count() {
102+
let mut p = PathBuf::new();
103+
p.extend(std::iter::repeat("..").take(short_path_components));
104+
p.push(".git");
105+
p
106+
} else {
107+
cursor.into_owned()
108+
}
109+
}
110+
Err(_) => cursor.into_owned(),
111+
}
112+
} else {
113+
cursor.into_owned()
114+
};
115+
break 'outer Ok((crate::Path::from_dot_git_dir(path, kind), trust));
116+
}
88117
None => {
89-
break 'outer Err(Error::NoGitRepository {
90-
path: directory.into_owned(),
118+
break 'outer Err(Error::NoTrustedGitRepository {
119+
path: dir.into_owned(),
120+
candidate: cursor.into_owned(),
121+
required: required_trust,
91122
})
92123
}
93124
}
@@ -100,11 +131,7 @@ pub(crate) mod function {
100131
}
101132
match cursor.parent() {
102133
Some(parent) => cursor = parent.to_owned().into(),
103-
None => {
104-
break Err(Error::NoGitRepository {
105-
path: directory.into_owned(),
106-
})
107-
}
134+
None => break Err(Error::NoGitRepository { path: dir.into_owned() }),
108135
}
109136
}
110137
}

git-repository/src/path/is.rs

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub fn bare(git_dir_candidate: impl AsRef<Path>) -> bool {
2828
/// What constitutes a valid git repository, and what's yet to be implemented, returning the guessed repository kind
2929
/// purely based on the presence of files. Note that the git-config ultimately decides what's bare.
3030
///
31+
/// * [ ] git files
3132
/// * [x] a valid head
3233
/// * [ ] git common directory
3334
/// * [ ] respect GIT_COMMON_DIR

git-repository/src/path/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ impl Path {
2727
pub fn from_dot_git_dir(dir: impl Into<PathBuf>, kind: Kind) -> Self {
2828
let dir = dir.into();
2929
match kind {
30-
Kind::WorkTree => Path::WorkTree(dir.parent().expect("this is a sub-directory").to_owned()),
30+
Kind::WorkTree => Path::WorkTree(if dir == std::path::Path::new(".git") {
31+
PathBuf::from(".")
32+
} else {
33+
dir.parent().expect("this is a sub-directory").to_owned()
34+
}),
3135
Kind::Bare => Path::Repository(dir),
3236
}
3337
}

git-repository/tests/discover/mod.rs

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod existing {
2-
use std::path::{Component, PathBuf};
2+
use std::path::PathBuf;
33

44
use git_repository::Kind;
55

@@ -75,12 +75,9 @@ mod existing {
7575
let (path, trust) = git_repository::path::discover(&dir)?;
7676
assert_eq!(path.kind(), Kind::WorkTree);
7777
assert_eq!(
78-
path.as_ref()
79-
.components()
80-
.filter(|c| matches!(c, Component::ParentDir | Component::CurDir))
81-
.count(),
82-
0,
83-
"there are no relative path components anymore"
78+
path.as_ref(),
79+
std::path::Path::new(".."),
80+
"there is only the minimal amount of relative path components to see this worktree"
8481
);
8582
assert_ne!(
8683
path.as_ref().canonicalize()?,

0 commit comments

Comments
 (0)