use std::path::{Path, PathBuf};
use crate::context::{CASE_INSENSITIVE_PATHS, absolute, path_string};
pub fn root_of(start: &Path, home: &Path) -> Option<PathBuf> {
start
.ancestors()
.find(|dir| dir.join(".git").exists())
.into_iter()
.chain([start])
.find(|root| trusted(root, home))
.map(Path::to_path_buf)
}
pub fn trusted(root: &Path, home: &Path) -> bool {
let canonical = |p: &Path| std::fs::canonicalize(p).ok();
root.parent().is_some()
&& !contains(root, home)
&& !matches!((canonical(root), canonical(home)), (Some(r), Some(h)) if contains(&r, &h))
}
pub fn contains(dir: &Path, path: &Path) -> bool {
let fold = |p: &Path| {
let s = path_string(&absolute(p).unwrap_or_else(|_| p.to_path_buf()));
let s = s.trim_end_matches('/').to_owned();
if CASE_INSENSITIVE_PATHS {
s.to_lowercase()
} else {
s
}
};
let (dir, path) = (fold(dir), fold(path));
path == dir
|| path
.strip_prefix(&dir)
.is_some_and(|rest| rest.starts_with('/'))
}
#[cfg(test)]
mod tests {
use super::{root_of, trusted};
#[test]
fn finds_git_root_or_falls_back() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let repo = dir.path().join("repo");
let nested = repo.join("a/b");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(root_of(&nested, &home), Some(nested.clone()));
std::fs::create_dir(repo.join(".git")).unwrap();
assert_eq!(root_of(&nested, &home), Some(repo.clone()));
std::fs::remove_dir(repo.join(".git")).unwrap();
std::fs::write(repo.join(".git"), "gitdir: ../main/.git/worktrees/x").unwrap();
assert_eq!(root_of(&nested, &home), Some(repo));
}
#[test]
fn home_its_ancestors_and_roots_are_never_the_project() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join("code/app")).unwrap();
assert_eq!(root_of(&home, &home), None, "no repository, cwd = home");
assert_eq!(root_of(dir.path(), &home), None, "ancestor of home");
let root = dir.path().ancestors().last().unwrap();
assert_eq!(root_of(root, &home), None, "filesystem root");
let app = home.join("code/app");
assert_eq!(root_of(&app, &home), Some(app.clone()));
std::fs::create_dir(home.join(".git")).unwrap();
assert_eq!(
root_of(&app, &home),
Some(app),
"a dotfiles repository in home"
);
assert_eq!(root_of(&home, &home), None);
assert!(!trusted(&home.join("code/.."), &home));
assert!(trusted(&dir.path().join("homework"), &home));
}
#[cfg(unix)]
#[test]
fn a_link_to_home_is_not_a_project() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir(&home).unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&home, &link).unwrap();
assert_eq!(root_of(&link, &home), None);
assert!(!trusted(std::path::Path::new("/"), &home));
}
}