codeowners_parser/
ownership.rs1use std::path::{Path, PathBuf};
8
9use crate::parser::{parse_codeowners_file_with_positions, CodeownersLine, ParsedLine};
10use crate::pattern::pattern_matches;
11
12pub fn find_codeowners(start: &Path) -> Option<PathBuf> {
29 const CANDIDATES: [&str; 3] = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"];
30 let mut current = Some(start);
31 while let Some(dir) = current {
32 for candidate in CANDIDATES {
33 let path = dir.join(candidate);
34 if path.is_file() {
35 return Some(path);
36 }
37 }
38 current = dir.parent();
39 }
40 None
41}
42
43pub fn get_repo_root(codeowners_path: &Path, fallback: &Path) -> PathBuf {
49 codeowners_path
50 .parent()
51 .and_then(|p| {
52 if p.ends_with(".github") || p.ends_with("docs") {
53 p.parent()
54 } else {
55 Some(p)
56 }
57 })
58 .map(|p| p.to_path_buf())
59 .unwrap_or_else(|| fallback.to_path_buf())
60}
61
62#[derive(Debug, Clone)]
67pub struct OwnershipResult {
68 pub line_number: u32,
70 pub pattern: String,
72 pub owners: Vec<String>,
76}
77
78pub fn check_file_ownership(content: &str, file_path: &str) -> Option<OwnershipResult> {
96 let lines = parse_codeowners_file_with_positions(content);
97 check_file_ownership_parsed(&lines, file_path)
98}
99
100pub fn check_file_ownership_parsed(
112 lines: &[ParsedLine],
113 file_path: &str,
114) -> Option<OwnershipResult> {
115 let file_path = file_path.trim_start_matches("./");
116
117 let mut matching_rule = None;
118 for parsed_line in lines {
119 if let CodeownersLine::Rule { pattern, owners } = &parsed_line.content {
120 if pattern_matches(pattern, file_path) {
121 matching_rule = Some(OwnershipResult {
122 line_number: parsed_line.line_number,
123 pattern: pattern.clone(),
124 owners: owners.clone(),
125 });
126 }
127 }
128 }
129
130 matching_rule
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_check_file_ownership() {
139 let content = "*.rs @rust-team\n/src/ @src-team\n/src/main.rs @main-owner";
140 let result = check_file_ownership(content, "src/main.rs").unwrap();
141 assert_eq!(result.pattern, "/src/main.rs");
142 assert_eq!(result.owners, vec!["@main-owner"]);
143 }
144
145 #[test]
146 fn test_check_file_no_owner() {
147 let content = "*.rs @rust-team";
148 let result = check_file_ownership(content, "README.md");
149 assert!(result.is_none());
150 }
151
152 #[test]
153 fn test_check_file_ownership_last_match_wins() {
154 let content = "* @default\n*.rs @rust";
155 let result = check_file_ownership(content, "main.rs").unwrap();
156 assert_eq!(result.pattern, "*.rs");
157 assert_eq!(result.owners, vec!["@rust"]);
158 }
159
160 #[test]
161 fn test_check_file_ownership_strips_leading_dot_slash() {
162 let content = "*.rs @rust";
163 let result = check_file_ownership(content, "./src/main.rs").unwrap();
164 assert_eq!(result.owners, vec!["@rust"]);
165 }
166
167 #[test]
168 fn test_get_repo_root_github_subdir() {
169 let path = PathBuf::from("/project/.github/CODEOWNERS");
170 let fallback = PathBuf::from("/project");
171 assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/project"));
172 }
173
174 #[test]
175 fn test_get_repo_root_top_level() {
176 let path = PathBuf::from("/project/CODEOWNERS");
177 let fallback = PathBuf::from("/project");
178 assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/project"));
179 }
180
181 #[test]
182 fn test_get_repo_root_docs_directory() {
183 let path = PathBuf::from("/project/docs/CODEOWNERS");
184 let fallback = PathBuf::from("/project");
185 assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/project"));
186 }
187
188 #[test]
189 fn test_get_repo_root_fallback() {
190 let path = PathBuf::from("/CODEOWNERS");
191 let fallback = PathBuf::from("/fallback");
192 assert_eq!(get_repo_root(&path, &fallback), PathBuf::from("/"));
193 }
194}