use std::sync::OnceLock;
use regex::Regex;
struct RevertPatterns {
leading_revert: Regex,
fix_revert: Regex,
}
fn patterns() -> &'static RevertPatterns {
static PATTERNS: OnceLock<RevertPatterns> = OnceLock::new();
PATTERNS.get_or_init(|| {
RevertPatterns {
leading_revert: Regex::new(r"(?i)^\s*revert\b")
.expect("leading_revert pattern compiles"),
fix_revert: Regex::new(r"(?i)^\s*fix\w*\b.*\brevert\b")
.expect("fix_revert pattern compiles"),
}
})
}
pub fn is_revert(message: &str) -> bool {
let first_line = message.lines().next().unwrap_or(message);
let p = patterns();
p.leading_revert.is_match(first_line) || p.fix_revert.is_match(first_line)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn patterns_compile() {
let _ = patterns();
}
#[test]
fn matches_git_auto_revert() {
assert!(is_revert("Revert \"feat: add login\""));
assert!(is_revert("revert \"fix: thing\""));
}
#[test]
fn matches_conventional_commit_revert() {
assert!(is_revert("revert: bad merge"));
assert!(is_revert("revert(auth): drop broken guard"));
assert!(is_revert("Revert(scope): something"));
}
#[test]
fn matches_leading_revert_word() {
assert!(is_revert("Revert this change"));
assert!(is_revert("REVERT everything"));
assert!(is_revert(" revert with leading spaces"));
}
#[test]
fn matches_fix_revert() {
assert!(is_revert("Fix botched revert of #42"));
assert!(is_revert("fix: re-apply after revert"));
assert!(is_revert("fixes the revert that broke CI"));
}
#[test]
fn ignores_body_only_mentions() {
let msg = "feat: add caching\n\nThis effectively reverts the earlier approach.";
assert!(!is_revert(msg));
}
#[test]
fn rejects_prose_and_false_positives() {
assert!(!is_revert("Refactor revert handling"));
assert!(!is_revert("Add tests for revert path"));
assert!(!is_revert("reverting the decision later"));
assert!(!is_revert("reverted earlier per discussion"));
assert!(!is_revert("feat: add login"));
assert!(!is_revert("Fix bug in feature"));
assert!(!is_revert(""));
}
}