use crate::parser::CodeOwnerRule;
use globset::{Glob, GlobSetBuilder};
use ignore::{DirEntry, WalkBuilder, WalkState};
use rustc_hash::FxHashMap;
use std::error::Error;
use std::path::Path;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
fn build_globset_with_mapping(
rules: &[&CodeOwnerRule],
) -> Result<(globset::GlobSet, FxHashMap<usize, usize>), globset::Error> {
let mut builder = GlobSetBuilder::new();
let mut globset_idx_to_rule_idx = FxHashMap::default();
let mut globset_idx = 0;
for (rule_idx, rule) in rules.iter().enumerate() {
let pattern = &rule.pattern;
let is_directory = rule.original_path.ends_with('/');
let is_anchored = rule.original_path.starts_with('/');
match (is_anchored, is_directory) {
(true, true) => {
builder.add(Glob::new(pattern)?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
builder.add(Glob::new(&format!("{}/**", pattern))?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
}
(true, false) => {
builder.add(Glob::new(pattern)?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
}
(false, true) => {
builder.add(Glob::new(&format!("**/{}", pattern))?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
builder.add(Glob::new(&format!("**/{}/**", pattern))?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
}
(false, false) => {
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
builder.add(Glob::new(pattern)?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
} else {
builder.add(Glob::new(&format!("**/{}", pattern))?);
globset_idx_to_rule_idx.insert(globset_idx, rule_idx);
globset_idx += 1;
}
}
}
}
Ok((builder.build()?, globset_idx_to_rule_idx))
}
pub fn validate_directory(
repo_path: &Path,
rules: &[CodeOwnerRule],
) -> Result<Vec<CodeOwnerRule>, Box<dyn Error>> {
let estimated_direct = rules.len() / 3;
let estimated_wildcard = rules.len() - estimated_direct;
let mut direct_rules = Vec::with_capacity(estimated_direct);
let mut wildcard_rules = Vec::with_capacity(estimated_wildcard);
for rule in rules {
if rule.pattern.contains('*')
|| rule.pattern.contains('?')
|| rule.pattern.contains('[')
|| rule.pattern.contains(']')
{
wildcard_rules.push(rule);
} else {
if rule.original_path.starts_with('/') {
direct_rules.push(rule);
} else {
wildcard_rules.push(rule);
}
}
}
let mut missing = Vec::new();
for rule in direct_rules {
let path = repo_path.join(&rule.pattern);
if !path.exists() {
missing.push(rule.clone());
}
}
if wildcard_rules.is_empty() {
return Ok(missing);
}
let (globset, idx_mapping) = build_globset_with_mapping(&wildcard_rules)?;
let num_wildcards = wildcard_rules.len();
let matched: Arc<Vec<AtomicUsize>> =
Arc::new((0..num_wildcards).map(|_| AtomicUsize::new(0)).collect());
let remaining = Arc::new(AtomicUsize::new(num_wildcards));
let thread_count = if num_wildcards > 5000 {
num_cpus::get().min(8) } else if num_wildcards > 1000 {
num_cpus::get().min(4) } else {
2 };
WalkBuilder::new(repo_path)
.standard_filters(false)
.hidden(false) .git_ignore(false) .git_global(false)
.git_exclude(false)
.threads(thread_count)
.build_parallel()
.run(|| {
let globset = globset.clone();
let matched = Arc::clone(&matched);
let remaining = Arc::clone(&remaining);
let idx_mapping = idx_mapping.clone();
Box::new(move |entry: Result<DirEntry, ignore::Error>| {
let dir_entry = match entry {
Ok(de) => de,
Err(_) => return WalkState::Continue,
};
let path = dir_entry.path();
if dir_entry.file_type().is_some_and(|ft| ft.is_dir())
&& path.file_name().is_some_and(|name| name == ".git")
{
return WalkState::Skip;
}
if remaining.load(Ordering::Relaxed) == 0 {
return WalkState::Quit;
}
if let Ok(rel_path) = path.strip_prefix(repo_path) {
let matches = globset.matches(rel_path);
if !matches.is_empty() {
for glob_idx in matches {
if let Some(&rule_idx) = idx_mapping.get(&glob_idx) {
if matched[rule_idx].fetch_add(1, Ordering::Relaxed) == 0 {
remaining.fetch_sub(1, Ordering::Relaxed);
}
}
}
}
}
WalkState::Continue
})
});
for (idx, rule) in wildcard_rules.iter().enumerate() {
if matched[idx].load(Ordering::Relaxed) == 0 {
missing.push((*rule).clone());
}
}
Ok(missing)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::CodeOwnerRule;
use std::fs;
use tempfile::tempdir;
fn rule(pattern: &str, original: &str) -> CodeOwnerRule {
CodeOwnerRule {
pattern: pattern.trim_matches('/').to_string(),
original_path: original.to_string(),
owners: vec!["@team".to_string()],
}
}
#[test]
fn detects_missing_file() {
let tmp = tempdir().unwrap();
let rules = vec![rule("missing.txt", "missing.txt")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].pattern, "missing.txt");
}
#[test]
fn passes_existing_file() {
let tmp = tempdir().unwrap();
let file_path = tmp.path().join("exists.txt");
fs::write(&file_path, "content").unwrap();
let rules = vec![rule("exists.txt", "exists.txt")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
}
#[test]
fn matches_wildcard_files() {
let tmp = tempdir().unwrap();
fs::write(tmp.path().join("foo.md"), "docs").unwrap();
let rules = vec![rule("*.md", "*.md")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
}
#[test]
fn detects_unmatched_wildcards() {
let tmp = tempdir().unwrap();
let rules = vec![rule("*.xyz", "*.xyz")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn handles_anchored_patterns() {
let tmp = tempdir().unwrap();
let src_dir = tmp.path().join("src");
fs::create_dir(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
let rules = vec![rule("src/main.rs", "/src/main.rs")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
let rules = vec![rule("main.rs", "main.rs")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
}
#[test]
fn handles_directory_patterns() {
let tmp = tempdir().unwrap();
let docs_dir = tmp.path().join("docs");
fs::create_dir(&docs_dir).unwrap();
fs::write(docs_dir.join("README.md"), "# Docs").unwrap();
let rules = vec![rule("docs", "docs/")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
let rules = vec![rule("docs", "/docs/")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
}
#[test]
fn handles_nested_patterns() {
let tmp = tempdir().unwrap();
let nested = tmp.path().join("a").join("b").join("c");
fs::create_dir_all(&nested).unwrap();
fs::write(nested.join("file.txt"), "content").unwrap();
let rules = vec![rule("file.txt", "file.txt")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
let rules = vec![rule("file.txt", "/file.txt")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn handles_complex_wildcards() {
let tmp = tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir(&src).unwrap();
fs::write(src.join("main.test.js"), "test").unwrap();
let rules = vec![rule("**/*.test.js", "**/*.test.js")];
let result = validate_directory(tmp.path(), &rules).unwrap();
assert!(result.is_empty());
}
}