metrale_bench/gate/
codeowners.rs1use std::path::Path;
15
16#[derive(Debug, Clone)]
18pub struct Rule {
19 pub pattern: String,
20 pub owners: Vec<String>,
21}
22
23pub fn load(root: &Path) -> Vec<Rule> {
25 let Ok(text) = std::fs::read_to_string(root.join(".github/CODEOWNERS")) else {
26 return Vec::new();
27 };
28 parse(&text)
29}
30
31pub fn parse(text: &str) -> Vec<Rule> {
32 text.lines()
33 .map(|l| l.split('#').next().unwrap_or("").trim())
34 .filter(|l| !l.is_empty())
35 .filter_map(|l| {
36 let mut parts = l.split_whitespace();
37 let pattern = parts.next()?.to_string();
38 let owners: Vec<String> = parts
39 .filter(|o| o.starts_with('@') || o.contains('@'))
40 .map(str::to_string)
41 .collect();
42 Some(Rule { pattern, owners })
45 })
46 .collect()
47}
48
49pub fn owners_of<'a>(rules: &'a [Rule], path: &str) -> &'a [String] {
52 rules
53 .iter()
54 .rfind(|r| matches(&r.pattern, path))
55 .map(|r| r.owners.as_slice())
56 .unwrap_or(&[])
57}
58
59pub fn owners_for_paths(rules: &[Rule], paths: &[String]) -> Vec<String> {
61 let mut out: Vec<String> = paths
62 .iter()
63 .flat_map(|p| owners_of(rules, p).iter().cloned())
64 .collect();
65 out.sort();
66 out.dedup();
67 out
68}
69
70pub fn is_supported(pattern: &str) -> bool {
74 !pattern.contains("**")
75 && !pattern.contains('?')
76 && !pattern.contains('[')
77 && !pattern.contains(']')
78 && !pattern.contains('\\')
79}
80
81fn matches(pattern: &str, path: &str) -> bool {
89 if !is_supported(pattern) {
90 return false;
91 }
92 let (body, dir_only) = match pattern.strip_suffix('/') {
93 Some(p) => (p, true),
94 None => (pattern, false),
95 };
96 let anchored = pattern.starts_with('/') || body.contains('/');
97 let body = body.strip_prefix('/').unwrap_or(body);
98 if body.is_empty() {
99 return false;
100 }
101
102 let under = |base: &str| path.starts_with(&format!("{base}/"));
103
104 if anchored {
105 return glob_segment(body, path) || under(body);
106 }
107 if path.rsplit('/').next().is_some_and(|n| star(body, n)) {
108 return true;
109 }
110 if dir_only || !body.contains('*') {
111 return path
112 .split('/')
113 .any(|segment| star(body, segment) && path != segment);
114 }
115 false
116}
117
118fn glob_segment(pattern: &str, path: &str) -> bool {
121 let pat: Vec<&str> = pattern.split('/').collect();
122 let seg: Vec<&str> = path.split('/').collect();
123 if pat.len() != seg.len() {
124 return false;
125 }
126 pat.iter().zip(&seg).all(|(p, s)| star(p, s))
127}
128
129fn star(pattern: &str, s: &str) -> bool {
131 let text: Vec<char> = s.chars().collect();
132 let mut matched = vec![false; text.len() + 1];
133 matched[0] = true;
134 for token in pattern.chars() {
135 let mut next = vec![false; text.len() + 1];
136 if token == '*' {
137 next[0] = matched[0];
138 for index in 1..=text.len() {
139 next[index] = matched[index] || next[index - 1];
140 }
141 } else {
142 for index in 1..=text.len() {
143 next[index] = matched[index - 1] && token == text[index - 1];
144 }
145 }
146 matched = next;
147 }
148 matched[text.len()]
149}
150
151#[cfg(test)]
152#[path = "codeowners_tests.rs"]
153mod codeowners_tests;