use std::path::{Path, PathBuf};
use globset::Glob;
use crate::git::Repo;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScopeChain {
pub remote: Option<String>,
pub cwd: PathBuf,
}
pub fn scope_chain(cwd: &Path, repo: Option<&Repo>) -> ScopeChain {
let cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
let remote = repo.and_then(|r| {
let key = crate::git::scope_key(r);
if key.starts_with("path:") { None } else { Some(key) }
});
ScopeChain { remote, cwd }
}
pub fn path_matches(pattern: &str, cwd: &Path) -> bool {
if glob_covers(pattern, cwd) {
return true;
}
if let Some(base) = pattern.strip_suffix("/**")
&& !base.is_empty()
&& glob_covers(base, cwd)
{
return true;
}
let literal = Path::new(pattern);
let literal = literal.canonicalize().unwrap_or_else(|_| literal.to_path_buf());
cwd.starts_with(&literal)
}
fn glob_covers(pattern: &str, cwd: &Path) -> bool {
let Ok(glob) = Glob::new(pattern) else {
return false;
};
let matcher = glob.compile_matcher();
if matcher.is_match(cwd.as_os_str()) || matcher.is_match(cwd.to_string_lossy().as_ref()) {
return true;
}
cwd.ancestors().any(|ancestor| matcher.is_match(ancestor.as_os_str()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob_matches_cwd() {
let cwd = PathBuf::from("/home/u/repo/src/api");
assert!(path_matches("**/src/**", &cwd));
assert!(!path_matches("**/tests/**", &cwd));
}
#[test]
fn literal_prefix_matches_nested_cwd() {
let cwd = PathBuf::from("/home/u/workspace/monorepo/services/api");
assert!(
path_matches("/home/u/workspace/monorepo", &cwd),
"a literal ancestor path should cover a nested agent"
);
assert!(!path_matches("/home/u/other", &cwd));
}
#[test]
fn glob_ancestor_component_matches() {
let cwd = PathBuf::from("/home/u/monorepo/services/api");
assert!(
path_matches("**/monorepo", &cwd),
"a glob naming an ancestor dir matches"
);
}
#[test]
fn recursive_glob_covers_an_agent_at_its_own_base_dir() {
assert!(
path_matches("/home/u/repo/**", &PathBuf::from("/home/u/repo")),
"a `<dir>/**` thread must be discoverable by an agent sitting AT `<dir>`"
);
}
#[test]
fn recursive_glob_still_covers_nested_and_still_excludes_siblings() {
assert!(path_matches("/home/u/repo/**", &PathBuf::from("/home/u/repo/src/api")));
assert!(
!path_matches("/home/u/repo/**", &PathBuf::from("/home/u/repo2")),
"matching the base dir must not widen the glob into sibling paths"
);
}
#[test]
fn recursive_glob_over_a_globbed_base_covers_that_base() {
assert!(path_matches("**/monorepo/**", &PathBuf::from("/home/u/monorepo")));
}
#[test]
fn malformed_glob_never_panics() {
let cwd = PathBuf::from("/tmp/x");
assert!(!path_matches("[unterminated", &cwd));
}
}