use std::path::{Path, PathBuf};
use super::model::RoomScope;
use crate::git::Repo;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScopeChain {
pub remote: Option<String>,
pub cwd: PathBuf,
pub ancestors: Vec<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)
}
});
let boundary = home_boundary();
let ancestors = ancestors_up_to(&cwd, boundary.as_deref());
ScopeChain {
remote,
cwd,
ancestors,
}
}
fn home_boundary() -> Option<PathBuf> {
std::env::var_os("HOME")
.map(PathBuf::from)
.and_then(|p| p.canonicalize().ok())
}
fn ancestors_up_to(cwd: &Path, boundary: Option<&Path>) -> Vec<PathBuf> {
let under_boundary = match boundary {
Some(b) => cwd.starts_with(b),
None => false,
};
let mut out = Vec::new();
for ancestor in cwd.ancestors() {
out.push(ancestor.to_path_buf());
if under_boundary && Some(ancestor) == boundary {
break;
}
}
out
}
pub fn room_matches(room_scope: &RoomScope, chain: &ScopeChain) -> bool {
match room_scope {
RoomScope::Remote(remote) => chain.remote.as_deref() == Some(remote.as_str()),
RoomScope::PathPrefix(prefix) => {
let prefix = prefix.canonicalize().unwrap_or_else(|_| prefix.clone());
chain.cwd.starts_with(&prefix)
}
RoomScope::Global => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn chain(remote: Option<&str>, cwd: &str) -> ScopeChain {
ScopeChain {
remote: remote.map(|s| s.to_string()),
cwd: PathBuf::from(cwd),
ancestors: PathBuf::from(cwd)
.ancestors()
.map(|p| p.to_path_buf())
.collect(),
}
}
#[test]
fn global_matches_everything() {
assert!(room_matches(
&RoomScope::Global,
&chain(None, "/anywhere/at/all")
));
}
#[test]
fn remote_matches_only_exact_remote() {
let c = chain(Some("github.com/foo/bar"), "/work/bar");
assert!(room_matches(
&RoomScope::Remote("github.com/foo/bar".to_string()),
&c
));
assert!(!room_matches(
&RoomScope::Remote("github.com/foo/other".to_string()),
&c
));
}
#[test]
fn remote_does_not_match_when_agent_has_no_remote() {
let c = chain(None, "/work/bar");
assert!(!room_matches(
&RoomScope::Remote("github.com/foo/bar".to_string()),
&c
));
}
#[test]
fn path_prefix_matches_ancestor_of_cwd() {
let c = chain(None, "/home/u/workspace/monorepo/services/api");
assert!(
room_matches(
&RoomScope::PathPrefix(PathBuf::from("/home/u/workspace/monorepo")),
&c
),
"a room at an ancestor dir should cover a nested agent"
);
}
#[test]
fn path_prefix_does_not_match_sibling_or_descendant_only() {
let c = chain(None, "/home/u/workspace/monorepo");
assert!(!room_matches(
&RoomScope::PathPrefix(PathBuf::from("/home/u/workspace/monorepo/services")),
&c
));
assert!(!room_matches(
&RoomScope::PathPrefix(PathBuf::from("/home/u/other")),
&c
));
}
#[test]
fn ancestors_stop_at_home_boundary() {
let home = PathBuf::from("/home/u");
let cwd = PathBuf::from("/home/u/a/b");
let ancestors = ancestors_up_to(&cwd, Some(&home));
assert!(ancestors.contains(&PathBuf::from("/home/u/a/b")));
assert!(ancestors.contains(&home));
assert!(
!ancestors.contains(&PathBuf::from("/home")),
"must not walk above the HOME boundary"
);
}
}