use std::sync::OnceLock;
use regex::Regex;
struct TicketPatterns {
jira: Regex,
gh_action: Regex,
gh_bare: Regex,
azdo: Regex,
}
fn patterns() -> &'static TicketPatterns {
static PATTERNS: OnceLock<TicketPatterns> = OnceLock::new();
PATTERNS.get_or_init(|| {
TicketPatterns {
jira: Regex::new(r"\b[A-Z][A-Z0-9]*-\d+\b").expect("jira pattern compiles"),
gh_action: Regex::new(r"(?i)\b(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)\s+#\d+\b")
.expect("gh_action pattern compiles"),
gh_bare: Regex::new(r"(?m)(?:^|\s)#\d+\b").expect("gh_bare pattern compiles"),
azdo: Regex::new(r"\bAB#\d+\b").expect("azdo pattern compiles"),
}
})
}
struct ExtractPatterns {
azdo: Regex,
jira: Regex,
gh_bare: Regex,
}
fn extract_patterns() -> &'static ExtractPatterns {
static EXTRACT: OnceLock<ExtractPatterns> = OnceLock::new();
EXTRACT.get_or_init(|| {
ExtractPatterns {
azdo: Regex::new(r"\bAB#\d+\b").expect("azdo extract pattern compiles"),
jira: Regex::new(r"\b[A-Z][A-Z0-9]*-\d+\b").expect("jira extract pattern compiles"),
gh_bare: Regex::new(r"(?:^|\s)(#\d+)\b").expect("gh_bare extract pattern compiles"),
}
})
}
pub fn is_ticketed(message: &str) -> bool {
let p = patterns();
p.jira.is_match(message)
|| p.gh_action.is_match(message)
|| p.gh_bare.is_match(message)
|| p.azdo.is_match(message)
}
pub fn extract_ticket_id(message: &str) -> Option<String> {
let p = extract_patterns();
if let Some(m) = p.azdo.find(message) {
return Some(m.as_str().to_string());
}
if let Some(m) = p.jira.find(message) {
return Some(m.as_str().to_string());
}
if let Some(caps) = p.gh_bare.captures(message) {
if let Some(m) = caps.get(1) {
return Some(m.as_str().to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn patterns_compile() {
let _ = patterns();
}
#[test]
fn extract_patterns_compile() {
let _ = extract_patterns();
}
#[test]
fn extract_ticket_id_bb_2746() {
assert_eq!(
extract_ticket_id("BB-2746: refactor auth service"),
Some("BB-2746".to_string())
);
}
#[test]
fn extract_ticket_id_sre_3104() {
assert_eq!(
extract_ticket_id("SRE-3104: increase RDS connection timeout"),
Some("SRE-3104".to_string())
);
}
#[test]
fn extract_ticket_id_dre_405() {
assert_eq!(
extract_ticket_id("DRE-405 fix demand calculation"),
Some("DRE-405".to_string())
);
}
#[test]
fn extract_ticket_id_returns_none_for_plain_message() {
assert_eq!(extract_ticket_id("misc cleanup"), None);
assert_eq!(extract_ticket_id("update README"), None);
assert_eq!(extract_ticket_id("bump version to 1.2.3"), None);
}
#[test]
fn extract_ticket_id_github_bare_ref() {
assert_eq!(extract_ticket_id("fixes #99"), Some("#99".to_string()));
}
#[test]
fn extract_ticket_id_azdo_ref() {
assert_eq!(
extract_ticket_id("AB#42 implement feature"),
Some("AB#42".to_string())
);
}
#[test]
fn extract_ticket_id_azdo_preferred_over_jira() {
assert_eq!(
extract_ticket_id("AB#10 fixes PROJ-99"),
Some("AB#10".to_string())
);
}
#[test]
fn extract_ticket_id_jira_preferred_over_gh_bare() {
assert_eq!(
extract_ticket_id("ENG-7 closes #10"),
Some("ENG-7".to_string())
);
}
#[test]
fn extract_ticket_id_multiline_body() {
let msg = "Refactor module structure\n\nRelates to SRE-999.\n";
assert_eq!(extract_ticket_id(msg), Some("SRE-999".to_string()));
}
#[test]
fn jira_style_is_ticketed() {
assert!(is_ticketed("ENG-123: add feature"));
assert!(is_ticketed("PROJ-1 initial commit"));
assert!(is_ticketed("Backport from upstream (ABC-4567)"));
}
#[test]
fn linear_style_is_ticketed() {
assert!(is_ticketed("FE-456 fix login"));
assert!(is_ticketed("API-9 add endpoint"));
}
#[test]
fn github_action_keyword_is_ticketed() {
assert!(is_ticketed("Fix race condition, fixes #123"));
assert!(is_ticketed("closes #45"));
assert!(is_ticketed("Resolves #7 by reworking auth"));
assert!(is_ticketed("CLOSED #99")); }
#[test]
fn github_bare_hash_ref_is_ticketed() {
assert!(is_ticketed("Bug from #123 still present"));
assert!(is_ticketed("#42 follow-up"));
}
#[test]
fn plain_message_is_not_ticketed() {
assert!(!is_ticketed("misc cleanup"));
assert!(!is_ticketed("update README"));
assert!(!is_ticketed("bump version to 1.2.3"));
assert!(!is_ticketed("set color to #abc123"));
assert!(!is_ticketed("eng-123 lowercase doesn't count"));
}
#[test]
fn multiline_body_with_ticket_is_ticketed() {
let msg = "Refactor module structure\n\nMoves things around.\nRelates to PROJ-789.\n";
assert!(is_ticketed(msg));
let msg2 = "First line no ticket\n\nSecond paragraph mentions #321 explicitly.";
assert!(is_ticketed(msg2));
}
#[test]
fn azdo_ab_ref_is_ticketed() {
assert!(is_ticketed("AB#1234 implement new feature"));
assert!(is_ticketed("Refactor module (AB#42)"));
assert!(is_ticketed("First line\n\nbody mentions AB#7 explicitly"));
}
#[test]
fn bare_hash_without_ab_prefix_is_not_azdo() {
let p = patterns();
assert!(!p.azdo.is_match("#1234 some work"));
assert!(!p.azdo.is_match("fixes #99"));
assert!(!p.jira.is_match("AB#1234"));
}
#[test]
fn empty_message_is_not_ticketed() {
assert!(!is_ticketed(""));
assert!(!is_ticketed("\n\n"));
}
}