#[derive(Debug, PartialEq)]
pub enum ValidationResult {
Valid,
Skip,
EmptyMessage,
Invalid,
}
pub const VALID_TYPES: &[&str] = &[
"feat", "fix", "docs", "style", "refactor", "test", "chore", "build", "ci", "perf", "revert",
];
pub fn validate_commit_message(message: &str) -> ValidationResult {
let first_line = message.lines().next().unwrap_or("").trim();
if first_line.is_empty() {
return ValidationResult::EmptyMessage;
}
if first_line.starts_with("Merge branch")
|| first_line.starts_with("Revert")
|| first_line.starts_with("Initial commit")
{
return ValidationResult::Skip;
}
use regex::Regex;
use std::sync::LazyLock;
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([a-zA-Z0-9_.\-]+\))?(!)?:\s+.+$",
)
.expect("invalid regex")
});
if RE.is_match(first_line) {
ValidationResult::Valid
} else {
ValidationResult::Invalid
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_message_is_rejected() {
assert_eq!(validate_commit_message(""), ValidationResult::EmptyMessage);
}
#[test]
fn whitespace_only_message_is_rejected() {
assert_eq!(
validate_commit_message(" "),
ValidationResult::EmptyMessage
);
}
#[test]
fn newline_only_message_is_rejected() {
assert_eq!(
validate_commit_message("\n\n"),
ValidationResult::EmptyMessage
);
}
#[test]
fn merge_branch_is_skipped() {
assert_eq!(
validate_commit_message("Merge branch 'feature/x' into main"),
ValidationResult::Skip
);
}
#[test]
fn merge_branch_simple_is_skipped() {
assert_eq!(
validate_commit_message("Merge branch 'develop'"),
ValidationResult::Skip
);
}
#[test]
fn revert_commit_is_skipped() {
assert_eq!(
validate_commit_message("Revert \"feat: add authentication\""),
ValidationResult::Skip
);
}
#[test]
fn revert_plain_is_skipped() {
assert_eq!(
validate_commit_message("Revert some change"),
ValidationResult::Skip
);
}
#[test]
fn initial_commit_is_skipped() {
assert_eq!(
validate_commit_message("Initial commit"),
ValidationResult::Skip
);
}
#[test]
fn valid_feat() {
assert_eq!(
validate_commit_message("feat: add user authentication"),
ValidationResult::Valid
);
}
#[test]
fn valid_fix() {
assert_eq!(
validate_commit_message("fix: resolve null pointer in parser"),
ValidationResult::Valid
);
}
#[test]
fn valid_docs() {
assert_eq!(
validate_commit_message("docs: update README"),
ValidationResult::Valid
);
}
#[test]
fn valid_style() {
assert_eq!(
validate_commit_message("style: fix indentation"),
ValidationResult::Valid
);
}
#[test]
fn valid_refactor() {
assert_eq!(
validate_commit_message("refactor: extract method"),
ValidationResult::Valid
);
}
#[test]
fn valid_test() {
assert_eq!(
validate_commit_message("test: add unit tests for parser"),
ValidationResult::Valid
);
}
#[test]
fn valid_chore() {
assert_eq!(
validate_commit_message("chore: update dependencies"),
ValidationResult::Valid
);
}
#[test]
fn valid_build() {
assert_eq!(
validate_commit_message("build: configure webpack"),
ValidationResult::Valid
);
}
#[test]
fn valid_ci() {
assert_eq!(
validate_commit_message("ci: add GitHub Actions workflow"),
ValidationResult::Valid
);
}
#[test]
fn valid_perf() {
assert_eq!(
validate_commit_message("perf: optimize database query"),
ValidationResult::Valid
);
}
#[test]
fn valid_revert_type() {
assert_eq!(
validate_commit_message("revert: undo feature flag"),
ValidationResult::Valid
);
}
#[test]
fn valid_with_scope() {
assert_eq!(
validate_commit_message("feat(auth): add JWT support"),
ValidationResult::Valid
);
}
#[test]
fn valid_scope_with_dot() {
assert_eq!(
validate_commit_message("fix(api.v2): handle timeout"),
ValidationResult::Valid
);
}
#[test]
fn valid_scope_with_hyphen() {
assert_eq!(
validate_commit_message("docs(user-guide): add examples"),
ValidationResult::Valid
);
}
#[test]
fn valid_scope_with_underscore() {
assert_eq!(
validate_commit_message("refactor(data_layer): simplify queries"),
ValidationResult::Valid
);
}
#[test]
fn valid_scope_with_numbers() {
assert_eq!(
validate_commit_message("fix(issue123): patch security flaw"),
ValidationResult::Valid
);
}
#[test]
fn valid_breaking_change_no_scope() {
assert_eq!(
validate_commit_message("feat!: remove deprecated endpoint"),
ValidationResult::Valid
);
}
#[test]
fn valid_breaking_change_with_scope() {
assert_eq!(
validate_commit_message("feat(api)!: change response format"),
ValidationResult::Valid
);
}
#[test]
fn invalid_no_type() {
assert_eq!(
validate_commit_message("add new feature"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_unknown_type() {
assert_eq!(
validate_commit_message("feature: add login"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_missing_colon() {
assert_eq!(
validate_commit_message("feat add login"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_missing_space_after_colon() {
assert_eq!(
validate_commit_message("feat:add login"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_empty_description() {
assert_eq!(
validate_commit_message("feat: "),
ValidationResult::Invalid
);
}
#[test]
fn invalid_only_type_and_colon() {
assert_eq!(
validate_commit_message("feat:"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_uppercase_type() {
assert_eq!(
validate_commit_message("FEAT: add login"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_scope_with_spaces() {
assert_eq!(
validate_commit_message("feat(my scope): add login"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_empty_scope() {
assert_eq!(
validate_commit_message("feat(): add login"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_random_text() {
assert_eq!(
validate_commit_message("WIP"),
ValidationResult::Invalid
);
}
#[test]
fn invalid_type_with_trailing_space_before_colon() {
assert_eq!(
validate_commit_message("feat : add login"),
ValidationResult::Invalid
);
}
#[test]
fn valid_single_char_description() {
assert_eq!(
validate_commit_message("fix: x"),
ValidationResult::Valid
);
}
#[test]
fn valid_message_with_body_after_newline() {
assert_eq!(
validate_commit_message("feat: add login\n\nThis adds a new login page."),
ValidationResult::Valid
);
}
#[test]
fn leading_whitespace_is_trimmed() {
assert_eq!(
validate_commit_message(" feat: add login"),
ValidationResult::Valid
);
}
#[test]
fn trailing_whitespace_is_trimmed() {
assert_eq!(
validate_commit_message("feat: add login "),
ValidationResult::Valid
);
}
}