metrale_bench/gate/
codeowners.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: Who owns a changed path, per `.github/CODEOWNERS`, for the telemetry table.
4//!
5//! Owner: bench gate.
6//! Invariants:
7//! - A pattern [`is_supported`] refuses (`**`, `?`, `[`, `]`, `\`) matches no path, so an
8//!   unsupported rule names nobody rather than the wrong owner.
9//! - No gate verdict reads these owners; only `telemetry.rs` calls this module.
10//!
11//! `every_pattern_in_the_real_file_is_supported` fails if the committed file uses a pattern
12//! this does not implement.
13
14use std::path::Path;
15
16/// 2026-09-26: One CODEOWNERS rule, in file order.
17#[derive(Debug, Clone)]
18pub struct Rule {
19    pub pattern: String,
20    pub owners: Vec<String>,
21}
22
23/// 2026-09-26: Parse `.github/CODEOWNERS`; a missing or unreadable file yields no rules.
24pub 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            // 2026-09-26: A pattern with no owners clears ownership for its paths, so it is
43            // kept as a rule with an empty owner list for `owners_of` to match last.
44            Some(Rule { pattern, owners })
45        })
46        .collect()
47}
48
49/// 2026-09-26: Owners of `path` from the last matching rule, or empty when nothing matches, so
50/// a specific rule placed after a broad one overrides it.
51pub 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
59/// 2026-09-26: Every owner of any of `paths`, deduplicated and sorted.
60pub 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
70/// 2026-09-26: Whether this module implements `pattern`: false for `**`, `?`, character
71/// classes and `\` escapes, none of which the committed CODEOWNERS uses
72/// (`every_pattern_in_the_real_file_is_supported`).
73pub fn is_supported(pattern: &str) -> bool {
74    !pattern.contains("**")
75        && !pattern.contains('?')
76        && !pattern.contains('[')
77        && !pattern.contains(']')
78        && !pattern.contains('\\')
79}
80
81/// 2026-09-26: Whether a CODEOWNERS pattern matches a repo-relative path, for the subset in
82/// [`is_supported`]:
83/// - a pattern with a leading `/`, or a `/` anywhere but the end, is anchored to the root and
84///   matches that path or anything under it;
85/// - an unanchored pattern matches that basename at any depth (`Cargo.toml` covers
86///   `crates/server/Cargo.toml`), and a trailing-`/` or star-free one also matches a
87///   directory of that name at any depth.
88fn 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
118/// 2026-09-26: Segment-by-segment match with the same number of segments, so `*` never
119/// crosses a `/`.
120fn 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
129/// 2026-09-26: Whether `s` matches `pattern`, where `*` matches any run of characters.
130fn 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;