use std::sync::OnceLock;
use regex::Regex;
struct AiPatterns {
trailer_line: Regex,
claude: Regex,
copilot: Regex,
cursor: Regex,
}
fn ai_patterns() -> &'static AiPatterns {
static PATTERNS: OnceLock<AiPatterns> = OnceLock::new();
PATTERNS.get_or_init(|| AiPatterns {
trailer_line: Regex::new(r"(?im)^[Cc]o-[Aa]uthored-[Bb]y:\s*(.+)$")
.expect("trailer_line pattern compiles"),
claude: Regex::new(r"(?i)\bclaude\b").expect("claude pattern compiles"),
copilot: Regex::new(r"(?i)\bcopilot\b|GitHub\s+Copilot").expect("copilot pattern compiles"),
cursor: Regex::new(r"(?i)\bcursor\b").expect("cursor pattern compiles"),
})
}
pub fn detect_ai_tool(message: &str) -> Option<&'static str> {
let p = ai_patterns();
for caps in p.trailer_line.captures_iter(message) {
let trailer_value = caps.get(1).map(|m| m.as_str()).unwrap_or("");
if p.claude.is_match(trailer_value) {
return Some("claude");
}
if p.copilot.is_match(trailer_value) {
return Some("copilot");
}
if p.cursor.is_match(trailer_value) {
return Some("cursor");
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ai_patterns_compile() {
let _ = ai_patterns();
}
#[test]
fn detect_ai_tool_detects_claude() {
let msg =
"feat: add auth\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>";
assert_eq!(detect_ai_tool(msg), Some("claude"));
}
#[test]
fn detect_ai_tool_case_insensitive_key() {
let msg = "fix: bug\n\nco-authored-by: Claude Sonnet 4 <noreply@anthropic.com>";
assert_eq!(detect_ai_tool(msg), Some("claude"));
}
#[test]
fn detect_ai_tool_detects_copilot() {
let msg = "feat: autocomplete\n\nCo-Authored-By: GitHub Copilot <copilot@github.com>";
assert_eq!(detect_ai_tool(msg), Some("copilot"));
}
#[test]
fn detect_ai_tool_detects_copilot_bare() {
let msg = "fix: npe\n\nCo-Authored-By: copilot <noreply@github.com>";
assert_eq!(detect_ai_tool(msg), Some("copilot"));
}
#[test]
fn detect_ai_tool_detects_cursor() {
let msg = "chore: refactor\n\nCo-Authored-By: Cursor <noreply@cursor.sh>";
assert_eq!(detect_ai_tool(msg), Some("cursor"));
}
#[test]
fn detect_ai_tool_returns_none_for_human() {
let msg = "feat: auth\n\nCo-Authored-By: Alice Smith <alice@example.com>";
assert_eq!(detect_ai_tool(msg), None);
}
#[test]
fn detect_ai_tool_returns_none_for_no_trailer() {
assert_eq!(detect_ai_tool("feat: add feature"), None);
assert_eq!(detect_ai_tool(""), None);
}
#[test]
fn detect_ai_tool_priority_claude_before_copilot() {
let msg = "pair session\n\n\
Co-Authored-By: Claude Opus <noreply@anthropic.com>\n\
Co-Authored-By: GitHub Copilot <copilot@github.com>";
assert_eq!(detect_ai_tool(msg), Some("claude"));
}
#[test]
fn detect_ai_tool_priority_copilot_before_cursor() {
let msg = "pair session\n\n\
Co-Authored-By: GitHub Copilot <copilot@github.com>\n\
Co-Authored-By: Cursor <noreply@cursor.sh>";
assert_eq!(detect_ai_tool(msg), Some("copilot"));
}
}