rumdl_lib/utils/
project_root.rs1use std::path::{Path, PathBuf};
11
12use super::upward_walk::{UpwardWalk, absolutize};
13
14const PROJECT_MARKERS: &[&str] = &[".git", ".rumdl.toml", "pyproject.toml", ".markdownlint.json"];
17
18pub fn discover_project_root_from(start_dir: &Path) -> PathBuf {
29 let found = UpwardWalk::new(start_dir).find(|dir| PROJECT_MARKERS.iter().any(|marker| dir.join(marker).exists()));
30 canonicalize_or_keep(found.unwrap_or_else(|| absolutize(start_dir)))
31}
32
33fn canonicalize_or_keep(path: PathBuf) -> PathBuf {
34 path.canonicalize().unwrap_or(path)
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use std::fs;
41 use tempfile::tempdir;
42
43 #[test]
44 fn test_discovers_root_via_git_marker() {
45 let temp = tempdir().unwrap();
46 let root = temp.path().canonicalize().unwrap();
47 fs::create_dir_all(root.join(".git")).unwrap();
48 let nested = root.join("a").join("b").join("c");
49 fs::create_dir_all(&nested).unwrap();
50
51 assert_eq!(discover_project_root_from(&nested), root);
52 }
53
54 #[test]
55 fn test_discovers_root_via_rumdl_toml_marker() {
56 let temp = tempdir().unwrap();
57 let root = temp.path().canonicalize().unwrap();
58 fs::write(root.join(".rumdl.toml"), "").unwrap();
59 let nested = root.join("docs");
60 fs::create_dir_all(&nested).unwrap();
61
62 assert_eq!(discover_project_root_from(&nested), root);
63 }
64
65 #[test]
66 fn test_discovers_root_via_pyproject_toml_marker() {
67 let temp = tempdir().unwrap();
68 let root = temp.path().canonicalize().unwrap();
69 fs::write(root.join("pyproject.toml"), "").unwrap();
70 let nested = root.join("src");
71 fs::create_dir_all(&nested).unwrap();
72
73 assert_eq!(discover_project_root_from(&nested), root);
74 }
75
76 #[test]
77 fn test_marker_at_ancestor_wins_over_deeper_start() {
78 let temp = tempdir().unwrap();
82 let root = temp.path().canonicalize().unwrap();
83 fs::write(root.join(".git"), "stub").unwrap();
84 let deeply_nested = root.join("a").join("b").join("c").join("d");
85 fs::create_dir_all(&deeply_nested).unwrap();
86
87 assert_eq!(discover_project_root_from(&deeply_nested), root);
88 }
89
90 #[test]
91 fn test_first_marker_wins_when_nested_projects() {
92 let temp = tempdir().unwrap();
95 let outer = temp.path().canonicalize().unwrap();
96 fs::write(outer.join(".git"), "stub").unwrap();
97 let inner = outer.join("subproject");
98 fs::create_dir_all(&inner).unwrap();
99 fs::write(inner.join(".rumdl.toml"), "").unwrap();
100 let start = inner.join("docs");
101 fs::create_dir_all(&start).unwrap();
102
103 assert_eq!(discover_project_root_from(&start), inner, "closest marker should win");
104 }
105
106 #[cfg(unix)]
108 #[test]
109 fn test_canonicalizes_symlinked_root() {
110 let temp = tempdir().unwrap();
111 let real_root = temp.path().canonicalize().unwrap().join("real");
112 fs::create_dir_all(&real_root).unwrap();
113 fs::create_dir_all(real_root.join(".git")).unwrap();
114
115 let link = temp.path().canonicalize().unwrap().join("link");
116 if std::os::unix::fs::symlink(&real_root, &link).is_err() {
117 return;
118 }
119
120 let from_link = discover_project_root_from(&link);
121 assert_eq!(from_link, real_root, "symlink should canonicalize to real path");
122 }
123}