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(?:\S*?\brevert\b|.*\s(?:[^\s-]\S*?)?\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 fix_revert_ignores_cli_flags() {
assert!(!is_revert(
"fix(deploy): pin the task definition on --revert scale-up"
));
assert!(!is_revert("fix: pass --revert to the deploy script"));
assert!(!is_revert("fix: add a --no-revert option"));
assert!(!is_revert("fix: honour the -revert switch"));
assert!(is_revert("fix: revert X"));
assert!(is_revert("fix(scope): revert the cache change"));
assert!(is_revert("fix:revert X"));
assert!(is_revert("fix: (revert) the bad merge"));
assert!(is_revert("fix: --revert flag, and revert the bad merge"));
assert!(is_revert("Revert \"fix: pass --revert to deploy\""));
}
#[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(""));
}
}