miyabi_worktree/
paths.rs

1use std::path::{Path, PathBuf};
2
3/// Normalize a filesystem path to eliminate redundant components.
4///
5/// This manually resolves `.` and `..` components without touching the filesystem,
6/// making it Windows-friendly via `dunce::simplified`.
7pub 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                // Skip current directory references
17            },
18            Component::ParentDir => {
19                // Pop the last component if possible (resolve ..)
20                if !components.is_empty() {
21                    components.pop();
22                }
23            },
24            comp => {
25                // Keep normal components, prefix, and root dir
26                components.push(comp);
27            },
28        }
29    }
30
31    components.iter().collect()
32}
33
34/// Utility for constructing and normalizing worktree-related paths.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct WorktreePaths {
37    base: PathBuf,
38}
39
40impl WorktreePaths {
41    /// Create a new helper anchored at the provided base path.
42    pub fn new<P: AsRef<Path>>(base: P) -> Self {
43        Self {
44            base: normalize_path(base),
45        }
46    }
47
48    /// Returns the normalized base directory.
49    pub fn base(&self) -> &Path {
50        &self.base
51    }
52
53    /// Join the base directory with a relative component and normalize the result.
54    pub fn join<P: AsRef<Path>>(&self, relative: P) -> PathBuf {
55        normalize_path(self.base.join(relative))
56    }
57
58    /// Replaces the current base directory.
59    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        // After normalization: "./.worktrees/../.worktrees/issue-42" -> ".worktrees/issue-42"
75        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}