use std::path::{Path, PathBuf};
#[must_use]
pub fn find_root_from_markers(
start: &Path,
markers: &[String],
stop_pattern: Option<®ex::Regex>,
) -> PathBuf {
let mut dir = Some(start);
while let Some(d) = dir {
if markers.iter().any(|m| d.join(m).exists())
|| stop_pattern.is_some_and(|re| re.is_match(&d.to_string_lossy()))
{
return d.to_path_buf();
}
dir = d.parent();
}
start.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn stop_pattern_matches_a_path_with_no_trailing_slash() {
let dir = tempdir().unwrap();
let root = std::fs::canonicalize(dir.path()).unwrap();
let workspace = root.join("xx");
let leaf = workspace.join("a").join("b");
std::fs::create_dir_all(&leaf).unwrap();
let re = regex::Regex::new("/xx/$").unwrap();
assert_eq!(
find_root_from_markers(&leaf, &[], Some(&re)),
leaf,
"`/xx/$` matches nothing, so the walk falls back to cwd-only"
);
let re = regex::Regex::new("/xx$").unwrap();
assert_eq!(find_root_from_markers(&leaf, &[], Some(&re)), workspace);
}
#[test]
fn stop_pattern_is_an_unanchored_search() {
let dir = tempdir().unwrap();
let root = std::fs::canonicalize(dir.path()).unwrap();
let workspace = root.join("xx");
std::fs::create_dir_all(&workspace).unwrap();
for pattern in ["/xx$", ".*/xx$"] {
let re = regex::Regex::new(pattern).unwrap();
assert_eq!(
find_root_from_markers(&workspace, &[], Some(&re)),
workspace,
"{pattern} should match"
);
}
let re = regex::Regex::new(".*/xx/$").unwrap();
assert_eq!(
find_root_from_markers(&workspace, &[], Some(&re)),
workspace,
"no match ⇒ cwd-only fallback returns the start dir (which happens to be xx here)"
);
}
#[test]
fn the_start_dir_is_a_candidate() {
let dir = tempdir().unwrap();
let root = std::fs::canonicalize(dir.path()).unwrap();
let re =
regex::Regex::new(&format!("^{}$", regex::escape(&root.to_string_lossy()))).unwrap();
assert_eq!(find_root_from_markers(&root, &[], Some(&re)), root);
}
}