use std::sync::OnceLock;
use regex::Regex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitEvent {
Commit,
Push,
}
pub struct Trigger;
fn pattern() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"(?:^|[\s;|&()])git\s+(commit|push)(?:\s|$)")
.expect("trigger regex must compile")
})
}
impl Trigger {
pub fn classify(cmd: &str) -> Option<GitEvent> {
let captures = pattern().captures(cmd)?;
match captures.get(1)?.as_str() {
"commit" => Some(GitEvent::Commit),
"push" => Some(GitEvent::Push),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_bare_commit() {
assert_eq!(Trigger::classify("git commit"), Some(GitEvent::Commit));
}
#[test]
fn matches_commit_with_flags() {
assert_eq!(
Trigger::classify("git commit -m 'wip'"),
Some(GitEvent::Commit),
);
}
#[test]
fn matches_bare_push() {
assert_eq!(Trigger::classify("git push"), Some(GitEvent::Push));
}
#[test]
fn matches_push_with_flags() {
assert_eq!(
Trigger::classify("git push origin main"),
Some(GitEvent::Push),
);
}
#[test]
fn matches_chained_with_double_amp() {
assert_eq!(
Trigger::classify("cargo test && git push"),
Some(GitEvent::Push),
);
}
#[test]
fn matches_chained_with_semicolon() {
assert_eq!(
Trigger::classify("cargo fmt; git commit"),
Some(GitEvent::Commit),
);
}
#[test]
fn matches_chained_with_pipe() {
assert_eq!(
Trigger::classify("echo hi | git commit -F -"),
Some(GitEvent::Commit),
);
}
#[test]
fn matches_subshell_parens() {
assert_eq!(
Trigger::classify("(git commit -m 'wip')"),
Some(GitEvent::Commit),
);
}
#[test]
fn matches_subshell_push() {
assert_eq!(
Trigger::classify("(cd subdir && git push origin main)"),
Some(GitEvent::Push),
);
}
#[test]
fn rejects_forgit() {
assert_eq!(Trigger::classify("forgit commit"), None);
}
#[test]
fn rejects_mygit() {
assert_eq!(Trigger::classify("mygit push"), None);
}
#[test]
fn rejects_committed_substring() {
assert_eq!(Trigger::classify("git committed-files-tool"), None);
assert_eq!(Trigger::classify("git committed"), None);
}
#[test]
fn rejects_unrelated_git_subcommand() {
assert_eq!(Trigger::classify("git status"), None);
assert_eq!(Trigger::classify("git log"), None);
}
#[test]
fn rejects_plain_text() {
assert_eq!(Trigger::classify("ls -la"), None);
assert_eq!(Trigger::classify(""), None);
}
#[test]
#[ignore = "design §6 known limitation; tracked for v0.2"]
fn matches_git_dash_c_commit() {
assert_eq!(
Trigger::classify("git -c user.email=x@y.z commit"),
Some(GitEvent::Commit),
);
}
#[test]
#[ignore = "design §6 deliberate non-goal; klasp gates honest agents"]
fn deliberately_misses_bash_c_quoted() {
assert_eq!(
Trigger::classify(r#"bash -c "git push""#),
Some(GitEvent::Push),
);
}
#[test]
#[ignore = "design §6 deliberate non-goal; klasp gates honest agents"]
fn deliberately_misses_eval_quoted() {
assert_eq!(
Trigger::classify(r#"eval "git commit""#),
Some(GitEvent::Commit),
);
}
#[test]
#[ignore = "design §6 deliberate non-goal; v0.2 candidate"]
fn deliberately_misses_env_prefixed() {
assert_eq!(
Trigger::classify("GIT_DIR=/elsewhere git push"),
Some(GitEvent::Push),
);
}
#[test]
#[ignore = "design §6 deliberate non-goal; shell aliases are out of scope"]
fn deliberately_misses_alias() {
assert_eq!(Trigger::classify("gp"), Some(GitEvent::Push));
}
}