Skip to content

Commit abfc6fd

Browse files
committed
feat: add relativize_with_prefix().
With it, a path 'a' with prefix 'b' will be '../a'.
1 parent d767d22 commit abfc6fd

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

gix-path/src/convert.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::path::Component;
12
use std::{
23
borrow::Cow,
34
ffi::{OsStr, OsString},
@@ -288,3 +289,48 @@ pub fn normalize<'a>(path: Cow<'a, Path>, current_dir: &Path) -> Option<Cow<'a,
288289
}
289290
.into()
290291
}
292+
293+
/// Rebuild the worktree-relative `relative_path` to be relative to `prefix`, which is the worktree-relative
294+
/// path equivalent to the position of the user, or current working directory.
295+
/// This is a no-op if `prefix` is empty.
296+
///
297+
/// Note that both `relative_path` and `prefix` are assumed to be [normalized](normalize()), and failure to do so
298+
/// will lead to incorrect results.
299+
///
300+
/// Note that both input paths are expected to be equal in terms of case too, as comparisons will be case-sensitive.
301+
pub fn relativize_with_prefix<'a>(relative_path: &'a Path, prefix: &Path) -> Cow<'a, Path> {
302+
if prefix.as_os_str().is_empty() {
303+
return Cow::Borrowed(relative_path);
304+
}
305+
debug_assert!(
306+
relative_path.components().all(|c| matches!(c, Component::Normal(_))),
307+
"BUG: all input is expected to be normalized, but relative_path was not"
308+
);
309+
debug_assert!(
310+
prefix.components().all(|c| matches!(c, Component::Normal(_))),
311+
"BUG: all input is expected to be normalized, but prefix was not"
312+
);
313+
314+
let mut buf = PathBuf::new();
315+
let mut rpc = relative_path.components().peekable();
316+
let mut equal_thus_far = true;
317+
for pcomp in prefix.components() {
318+
if equal_thus_far {
319+
if let (Component::Normal(pname), Some(Component::Normal(rpname))) = (pcomp, rpc.peek()) {
320+
if &pname == rpname {
321+
rpc.next();
322+
continue;
323+
} else {
324+
equal_thus_far = false;
325+
}
326+
}
327+
}
328+
buf.push(Component::ParentDir);
329+
}
330+
buf.extend(rpc);
331+
if buf.as_os_str().is_empty() {
332+
Cow::Borrowed(Path::new("."))
333+
} else {
334+
Cow::Owned(buf)
335+
}
336+
}

gix-path/tests/convert/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,39 @@ mod join_bstr_unix_pathsep {
6262
assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("/hi"));
6363
}
6464
}
65+
66+
mod relativize_with_prefix {
67+
fn r(path: &str, prefix: &str) -> String {
68+
gix_path::to_unix_separators_on_windows(
69+
gix_path::os_str_into_bstr(gix_path::relativize_with_prefix(path.as_ref(), prefix.as_ref()).as_os_str())
70+
.expect("no illformed UTF-8"),
71+
)
72+
.to_string()
73+
}
74+
75+
#[test]
76+
fn basics() {
77+
assert_eq!(
78+
r("a", "a"),
79+
".",
80+
"reaching the prefix is signalled by a '.', the current dir"
81+
);
82+
assert_eq!(r("a/b/c", "a/b"), "c", "'c' is clearly within the current directory");
83+
assert_eq!(
84+
r("c/b/c", "a/b"),
85+
"../../c/b/c",
86+
"when there is a complete disjoint prefix, we have to get out of it with ../"
87+
);
88+
assert_eq!(
89+
r("a/a", "a/b"),
90+
"../a",
91+
"when there is mismatch, we have to get out of the CWD"
92+
);
93+
assert_eq!(
94+
r("a/a", ""),
95+
"a/a",
96+
"empty prefix means nothing happens (and no work is done)"
97+
);
98+
assert_eq!(r("", ""), "", "empty stays empty");
99+
}
100+
}

0 commit comments

Comments
 (0)