use super::*;
pub(crate) struct OwnershipEntry {
pub pattern: String,
pub owners: Vec<String>,
}
pub(crate) fn load_codeowners(repo_root: &Path) -> Vec<OwnershipEntry> {
let candidates = [
repo_root.join(".github/CODEOWNERS"),
repo_root.join("CODEOWNERS"),
repo_root.join("docs/CODEOWNERS"),
];
let content = candidates.iter().find_map(|p| fs::read_to_string(p).ok());
let Some(content) = content else {
return Vec::new();
};
parse_codeowners(&content)
}
pub(crate) fn parse_codeowners(content: &str) -> Vec<OwnershipEntry> {
let mut entries = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.split_whitespace();
let Some(pattern) = parts.next() else {
continue;
};
let owners: Vec<String> = parts.map(|s| s.to_string()).collect();
if owners.is_empty() {
continue;
}
entries.push(OwnershipEntry {
pattern: pattern.to_string(),
owners,
});
}
entries
}
pub(crate) fn find_owner(path: &str, codeowners: &[OwnershipEntry]) -> String {
let mut best_match: Option<&[String]> = None;
for entry in codeowners {
if codeowners_pattern_matches(&entry.pattern, path) {
best_match = Some(&entry.owners);
}
}
if let Some(owners) = best_match {
return owners.join(", ");
}
path_based_module(path)
}
pub(crate) fn path_based_module(path: &str) -> String {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 3 {
parts[1].to_string()
} else if parts.len() == 2 {
parts[0].to_string()
} else {
"root".to_string()
}
}
pub(crate) fn codeowners_pattern_matches(pattern: &str, path: &str) -> bool {
let pattern = pattern.trim_start_matches('/');
if let Some(rest) = pattern.strip_prefix("**/") {
if rest.ends_with('/') {
let dir_segment = rest.trim_end_matches('/');
return path.starts_with(&format!("{dir_segment}/"))
|| path.contains(&format!("/{dir_segment}/"));
}
if let Some(suffix) = rest.strip_prefix('*') {
return path.ends_with(suffix);
}
return path == rest || path.ends_with(&format!("/{rest}"));
}
if pattern.starts_with('*') && !pattern[1..].contains('*') {
let suffix = &pattern[1..]; return path.ends_with(suffix);
}
if pattern.ends_with('/') {
let prefix = pattern.trim_end_matches('/');
return path.starts_with(prefix) && path.as_bytes().get(prefix.len()) == Some(&b'/');
}
if let Some(prefix) = pattern.strip_suffix("/*") {
return path.starts_with(prefix) && path.as_bytes().get(prefix.len()) == Some(&b'/');
}
if path == pattern {
return true;
}
if path.starts_with(pattern) && path.as_bytes().get(pattern.len()) == Some(&b'/') {
return true;
}
false
}
pub(crate) fn build_ownership_map(
repo_root: &Path,
file_paths: &[String],
) -> Vec<(String, String)> {
let codeowners = load_codeowners(repo_root);
file_paths
.iter()
.map(|path| {
let owner = find_owner(path, &codeowners);
(path.clone(), owner)
})
.collect()
}