1use std::path::{Path, PathBuf};
2
3pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
8 use std::path::Component;
9
10 let path = dunce::simplified(path.as_ref());
11 let mut components = Vec::new();
12
13 for component in path.components() {
14 match component {
15 Component::CurDir => {
16 },
18 Component::ParentDir => {
19 if !components.is_empty() {
21 components.pop();
22 }
23 },
24 comp => {
25 components.push(comp);
27 },
28 }
29 }
30
31 components.iter().collect()
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct WorktreePaths {
37 base: PathBuf,
38}
39
40impl WorktreePaths {
41 pub fn new<P: AsRef<Path>>(base: P) -> Self {
43 Self {
44 base: normalize_path(base),
45 }
46 }
47
48 pub fn base(&self) -> &Path {
50 &self.base
51 }
52
53 pub fn join<P: AsRef<Path>>(&self, relative: P) -> PathBuf {
55 normalize_path(self.base.join(relative))
56 }
57
58 pub fn with_base<P: AsRef<Path>>(&self, base: P) -> Self {
60 Self {
61 base: normalize_path(base),
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn normalize_drops_redundant_components() {
72 let path = normalize_path("./.worktrees/../.worktrees/issue-42");
73 assert!(path.ends_with("issue-42"));
74 assert_eq!(path.components().count(), 2);
76 }
77
78 #[test]
79 fn helper_normalizes_base_and_join() {
80 let helper = WorktreePaths::new("./.worktrees");
81 assert!(helper.base().ends_with(".worktrees"));
82
83 let issue = helper.join("issue-270");
84 assert!(issue.ends_with("issue-270"));
85 assert!(issue.starts_with(helper.base()));
86 }
87
88 #[test]
89 fn helper_can_switch_base() {
90 let helper = WorktreePaths::new(".worktrees");
91 let other = helper.with_base(".worktrees-short");
92 assert!(other.base().ends_with(".worktrees-short"));
93 assert_ne!(helper.base(), other.base());
94 }
95}