use std::path::{Path, PathBuf};
use crate::parser::{parse_codeowners_file_with_positions, CodeownersLine, ParsedLine};
use crate::pattern::pattern_matches;
pub fn find_codeowners(start: &Path) -> Option<PathBuf> {
const CANDIDATES: [&str; 3] = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"];
let mut current = Some(start);
while let Some(dir) = current {
for candidate in CANDIDATES {
let path = dir.join(candidate);
if path.is_file() {
return Some(path);
}
}
current = dir.parent();
}
None
}
pub fn get_repo_root(codeowners_path: &Path, fallback: &Path) -> PathBuf {
codeowners_path
.parent()
.and_then(|p| {
if p.ends_with(".github") || p.ends_with("docs") {
p.parent()
} else {
Some(p)
}
})
.map(|p| p.to_path_buf())
.unwrap_or_else(|| fallback.to_path_buf())
}
#[derive(Debug, Clone)]
pub struct OwnershipResult {
pub line_number: u32,
pub pattern: String,
pub owners: Vec<String>,
}
pub fn check_file_ownership(content: &str, file_path: &str) -> Option<OwnershipResult> {
let lines = parse_codeowners_file_with_positions(content);
check_file_ownership_parsed(&lines, file_path)
}
pub fn check_file_ownership_parsed(
lines: &[ParsedLine],
file_path: &str,
) -> Option<OwnershipResult> {
let file_path = file_path.trim_start_matches("./");
let mut matching_rule = None;
for parsed_line in lines {
if let CodeownersLine::Rule { pattern, owners } = &parsed_line.content {
if pattern_matches(pattern, file_path) {
matching_rule = Some(OwnershipResult {
line_number: parsed_line.line_number,
pattern: pattern.clone(),
owners: owners.clone(),
});
}
}
}
matching_rule
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_file_ownership() {
let content = "*.rs @rust-team\n/src/ @src-team\n/src/main.rs @main-owner";
let result = check_file_ownership(content, "src/main.rs").unwrap();
assert_eq!(result.pattern, "/src/main.rs");
assert_eq!(result.owners, vec!["@main-owner"]);
}
#[test]
fn test_check_file_no_owner() {
let content = "*.rs @rust-team";
let result = check_file_ownership(content, "README.md");
assert!(result.is_none());
}
#[test]
fn test_check_file_ownership_last_match_wins() {
let content = "* @default\n*.rs @rust";
let result = check_file_ownership(content, "main.rs").unwrap();
assert_eq!(result.pattern, "*.rs");
assert_eq!(result.owners, vec!["@rust"]);
}
#[test]
fn test_check_file_ownership_strips_leading_dot_slash() {
let content = "*.rs @rust";
let result = check_file_ownership(content, "./src/main.rs").unwrap();
assert_eq!(result.owners, vec!["@rust"]);
}
#[test]
fn test_get_repo_root_github_subdir() {
let path = PathBuf::from("/project/.github/CODEOWNERS");
let fallback = PathBuf::from("/project");
assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/project"));
}
#[test]
fn test_get_repo_root_top_level() {
let path = PathBuf::from("/project/CODEOWNERS");
let fallback = PathBuf::from("/project");
assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/project"));
}
#[test]
fn test_get_repo_root_docs_directory() {
let path = PathBuf::from("/project/docs/CODEOWNERS");
let fallback = PathBuf::from("/project");
assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/project"));
}
#[test]
fn test_get_repo_root_fallback() {
let path = PathBuf::from("/CODEOWNERS");
let fallback = PathBuf::from("/fallback");
assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/"));
}
}