use crate::integrations::search_client::IndexInfo;
use std::path::{Path, PathBuf};
use tracing::warn;
pub fn find_git_root(start: &Path) -> PathBuf {
let mut current = start.to_path_buf();
loop {
if current.join(".git").exists() {
return current;
}
match current.parent() {
Some(parent) => current = parent.to_path_buf(),
None => return start.to_path_buf(),
}
}
}
pub fn repo_root_from_cwd() -> PathBuf {
let cwd = match std::env::current_dir() {
Ok(d) => d,
Err(e) => {
warn!("trusty-review index auto-derive: cannot read cwd: {e}");
return PathBuf::from(".");
}
};
let git_root = find_git_root(&cwd);
git_root.canonicalize().unwrap_or(git_root)
}
pub fn best_matching_index(indexes: &[IndexInfo], repo_root: &Path) -> Option<String> {
let mut best: Option<(&IndexInfo, usize)> = None;
for info in indexes {
let Some(rp_str) = &info.root_path else {
continue;
};
let rp = Path::new(rp_str.as_str());
let canonical_rp = rp.canonicalize().unwrap_or_else(|_| rp.to_path_buf());
if repo_root.starts_with(&canonical_rp) {
let len = rp_str.len();
if best.is_none() || len > best.as_ref().unwrap().1 {
best = Some((info, len));
}
}
}
best.map(|(info, _)| info.id.clone())
}
pub fn resolve_index_from_list(indexes: &[IndexInfo], repo_root: &Path) -> Option<String> {
if indexes.is_empty() {
warn!("trusty-review index auto-derive: no indexes registered in trusty-search daemon");
return None;
}
let result = best_matching_index(indexes, repo_root);
if result.is_none() {
warn!(
repo_root = %repo_root.display(),
"trusty-review index auto-derive: no index root_path matches the repo root; \
falling back to \"main\". Register an index with `trusty-search index .` \
or set TRUSTY_SEARCH_INDEX explicitly."
);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::integrations::search_client::IndexInfo;
fn make_index(id: &str, root_path: Option<&str>) -> IndexInfo {
IndexInfo {
id: id.to_string(),
name: None,
root_path: root_path.map(|s| s.to_string()),
}
}
#[test]
fn find_git_root_returns_cwd_when_no_git() {
let dir = tempfile::tempdir().unwrap();
let result = find_git_root(dir.path());
assert_eq!(result, dir.path());
}
#[test]
fn find_git_root_finds_git_in_parent() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join(".git")).unwrap();
let sub = root.path().join("src").join("deep");
std::fs::create_dir_all(&sub).unwrap();
let result = find_git_root(&sub);
assert_eq!(result, root.path());
}
#[test]
fn resolve_returns_none_on_empty_list() {
let result = best_matching_index(&[], Path::new("/home/user/project"));
assert!(result.is_none());
}
#[test]
fn resolve_returns_none_on_no_match() {
let indexes = vec![
make_index("other-project", Some("/home/user/other")),
make_index("third", Some("/srv/third")),
];
let result = best_matching_index(&indexes, Path::new("/home/user/my-project"));
assert!(
result.is_none(),
"no index whose root_path is a prefix — should return None"
);
}
#[test]
fn resolve_picks_exact_match() {
let indexes = vec![
make_index("my-project", Some("/home/user/my-project")),
make_index("other", Some("/home/user/other")),
];
let result = best_matching_index(&indexes, Path::new("/home/user/my-project"));
assert_eq!(result.as_deref(), Some("my-project"));
}
#[test]
fn resolve_picks_longest_root_match() {
let indexes = vec![
make_index("monorepo", Some("/home/user/monorepo")),
make_index("api", Some("/home/user/monorepo/api")),
];
let result = best_matching_index(&indexes, Path::new("/home/user/monorepo/api/src"));
assert_eq!(
result.as_deref(),
Some("api"),
"the more specific (longer) root_path must win"
);
}
#[test]
fn resolve_skips_indexes_without_root_path() {
let indexes = vec![
make_index("no-root", None),
make_index("with-root", Some("/home/user/project")),
];
let result = best_matching_index(&indexes, Path::new("/home/user/project"));
assert_eq!(
result.as_deref(),
Some("with-root"),
"indexes without root_path must be ignored"
);
}
#[test]
fn resolve_all_without_root_path_returns_none() {
let indexes = vec![make_index("a", None), make_index("b", None)];
let result = best_matching_index(&indexes, Path::new("/home/user/project"));
assert!(
result.is_none(),
"all indexes lack root_path — must return None"
);
}
#[test]
fn resolve_index_from_list_returns_none_for_empty() {
let result = resolve_index_from_list(&[], Path::new("/home/user/project"));
assert!(result.is_none());
}
#[test]
fn resolve_index_from_list_returns_match() {
let indexes = vec![make_index("my-index", Some("/home/user/my-project"))];
let result = resolve_index_from_list(&indexes, Path::new("/home/user/my-project/src"));
assert_eq!(result.as_deref(), Some("my-index"));
}
}