rumdl_lib/utils/
project_root.rs1use std::path::{Path, PathBuf};
11use std::sync::LazyLock;
12
13use super::upward_walk::{UpwardWalk, absolutize};
14
15const PROJECT_MARKERS: &[&str] = &[".git", ".rumdl.toml", "pyproject.toml", ".markdownlint.json"];
18
19static PROJECT_ROOT: LazyLock<PathBuf> = LazyLock::new(|| {
21 let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
22 discover_project_root_from(¤t_dir)
23});
24
25pub fn project_root() -> &'static Path {
36 &PROJECT_ROOT
37}
38
39static WORKING_DIRECTORY: LazyLock<Option<PathBuf>> =
41 LazyLock::new(|| std::env::current_dir().ok()?.canonicalize().ok());
42
43pub fn working_directory() -> Option<&'static Path> {
51 WORKING_DIRECTORY.as_deref()
52}
53
54pub fn discover_project_root_from(start_dir: &Path) -> PathBuf {
65 let found = UpwardWalk::new(start_dir).find(|dir| PROJECT_MARKERS.iter().any(|marker| dir.join(marker).exists()));
66 canonicalize_or_keep(found.unwrap_or_else(|| absolutize(start_dir)))
67}
68
69fn canonicalize_or_keep(path: PathBuf) -> PathBuf {
70 path.canonicalize().unwrap_or(path)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::fs;
77 use tempfile::tempdir;
78
79 #[test]
80 fn test_discovers_root_via_git_marker() {
81 let temp = tempdir().unwrap();
82 let root = temp.path().canonicalize().unwrap();
83 fs::create_dir_all(root.join(".git")).unwrap();
84 let nested = root.join("a").join("b").join("c");
85 fs::create_dir_all(&nested).unwrap();
86
87 assert_eq!(discover_project_root_from(&nested), root);
88 }
89
90 #[test]
91 fn test_discovers_root_via_rumdl_toml_marker() {
92 let temp = tempdir().unwrap();
93 let root = temp.path().canonicalize().unwrap();
94 fs::write(root.join(".rumdl.toml"), "").unwrap();
95 let nested = root.join("docs");
96 fs::create_dir_all(&nested).unwrap();
97
98 assert_eq!(discover_project_root_from(&nested), root);
99 }
100
101 #[test]
102 fn test_discovers_root_via_pyproject_toml_marker() {
103 let temp = tempdir().unwrap();
104 let root = temp.path().canonicalize().unwrap();
105 fs::write(root.join("pyproject.toml"), "").unwrap();
106 let nested = root.join("src");
107 fs::create_dir_all(&nested).unwrap();
108
109 assert_eq!(discover_project_root_from(&nested), root);
110 }
111
112 #[test]
113 fn test_marker_at_ancestor_wins_over_deeper_start() {
114 let temp = tempdir().unwrap();
118 let root = temp.path().canonicalize().unwrap();
119 fs::write(root.join(".git"), "stub").unwrap();
120 let deeply_nested = root.join("a").join("b").join("c").join("d");
121 fs::create_dir_all(&deeply_nested).unwrap();
122
123 assert_eq!(discover_project_root_from(&deeply_nested), root);
124 }
125
126 #[test]
127 fn test_first_marker_wins_when_nested_projects() {
128 let temp = tempdir().unwrap();
131 let outer = temp.path().canonicalize().unwrap();
132 fs::write(outer.join(".git"), "stub").unwrap();
133 let inner = outer.join("subproject");
134 fs::create_dir_all(&inner).unwrap();
135 fs::write(inner.join(".rumdl.toml"), "").unwrap();
136 let start = inner.join("docs");
137 fs::create_dir_all(&start).unwrap();
138
139 assert_eq!(discover_project_root_from(&start), inner, "closest marker should win");
140 }
141
142 #[cfg(unix)]
144 #[test]
145 fn test_canonicalizes_symlinked_root() {
146 let temp = tempdir().unwrap();
147 let real_root = temp.path().canonicalize().unwrap().join("real");
148 fs::create_dir_all(&real_root).unwrap();
149 fs::create_dir_all(real_root.join(".git")).unwrap();
150
151 let link = temp.path().canonicalize().unwrap().join("link");
152 if std::os::unix::fs::symlink(&real_root, &link).is_err() {
153 return;
154 }
155
156 let from_link = discover_project_root_from(&link);
157 assert_eq!(from_link, real_root, "symlink should canonicalize to real path");
158 }
159}