Skip to content

Commit 2585dfb

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

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-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("..");
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,36 @@ 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+
use gix_path::relativize_with_prefix;
68+
69+
fn r(path: &str, prefix: &str) -> String {
70+
relativize_with_prefix(path.as_ref(), prefix.as_ref())
71+
.to_str()
72+
.expect("no illformed UTF-8")
73+
.to_owned()
74+
}
75+
76+
#[test]
77+
fn basics() {
78+
assert_eq!(
79+
r("a", "a"),
80+
".",
81+
"reaching the prefix is signalled by a '.', the current dir"
82+
);
83+
assert_eq!(r("a/b/c", "a/b"), "c", "'c' is clearly within the current directory");
84+
assert_eq!(
85+
r("c/b/c", "a/b"),
86+
"../../c/b/c",
87+
"when there is mismatch, we have to get out of the CWD"
88+
);
89+
assert_eq!(
90+
r("a/a", "a/b"),
91+
"../a",
92+
"when there is mismatch, we have to get out of the CWD"
93+
);
94+
assert_eq!(r("a/a", ""), "a/a", "empty prefix means nothing happens");
95+
assert_eq!(r("", ""), "", "empty stays empty");
96+
}
97+
}

0 commit comments

Comments
 (0)