#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConventionalLevel {
Patch,
Minor,
Major,
}
pub fn classify_commit(msg: &str) -> Option<ConventionalLevel> {
if msg.contains("BREAKING CHANGE") || msg.contains("BREAKING-CHANGE") {
return Some(ConventionalLevel::Major);
}
let subject = msg.lines().next().unwrap_or("").trim_start();
let (ty, _rest) = subject.split_once(':')?;
let (head, marker) = ty.split_once('(').map_or((ty, ""), |(h, scope_rest)| {
let after_scope = scope_rest.split_once(')').map_or("", |x| x.1);
(h, after_scope)
});
if marker.starts_with('!') || ty.ends_with('!') {
return Some(ConventionalLevel::Major);
}
match head.trim() {
"feat" => Some(ConventionalLevel::Minor),
"fix" | "perf" | "revert" => Some(ConventionalLevel::Patch),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn breaking_change_footer_is_major() {
assert_eq!(
classify_commit("feat(api): new endpoint\n\nBREAKING CHANGE: old endpoint removed"),
Some(ConventionalLevel::Major)
);
}
#[test]
fn breaking_change_without_colon_is_major() {
assert_eq!(
classify_commit("feat: x\n\nBREAKING CHANGE removed the old endpoint"),
Some(ConventionalLevel::Major)
);
assert_eq!(
classify_commit("fix: y\n\nBREAKING-CHANGE dropped the flag"),
Some(ConventionalLevel::Major)
);
}
#[test]
fn bang_shorthand_is_major_for_any_type() {
assert_eq!(
classify_commit("feat!: drop legacy auth"),
Some(ConventionalLevel::Major)
);
assert_eq!(
classify_commit("feat(core)!: rewrite pipeline"),
Some(ConventionalLevel::Major)
);
assert_eq!(
classify_commit("refactor!: drop the shim"),
Some(ConventionalLevel::Major)
);
}
#[test]
fn feat_is_minor() {
assert_eq!(
classify_commit("feat: new stage"),
Some(ConventionalLevel::Minor)
);
assert_eq!(
classify_commit("feat(build): add cache key"),
Some(ConventionalLevel::Minor)
);
}
#[test]
fn fix_perf_and_revert_are_patch() {
assert_eq!(classify_commit("fix: race"), Some(ConventionalLevel::Patch));
assert_eq!(
classify_commit("perf: faster loop"),
Some(ConventionalLevel::Patch)
);
assert_eq!(
classify_commit("revert: undo broken feature"),
Some(ConventionalLevel::Patch)
);
}
#[test]
fn non_release_worthy_types_are_none() {
assert_eq!(classify_commit("chore: update deps"), None);
assert_eq!(classify_commit("docs: fix link"), None);
assert_eq!(classify_commit("refactor: rename"), None);
assert_eq!(classify_commit("ci: pin runner"), None);
}
#[test]
fn unstructured_message_is_none() {
assert_eq!(classify_commit("random subject"), None);
}
#[test]
fn levels_order_by_strength_for_max_folds() {
assert!(ConventionalLevel::Patch < ConventionalLevel::Minor);
assert!(ConventionalLevel::Minor < ConventionalLevel::Major);
}
}