anodizer_core/git/
conventional.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum ConventionalLevel {
16 Patch,
18 Minor,
20 Major,
23}
24
25pub fn classify_commit(msg: &str) -> Option<ConventionalLevel> {
41 if msg.contains("BREAKING CHANGE") || msg.contains("BREAKING-CHANGE") {
42 return Some(ConventionalLevel::Major);
43 }
44 let subject = msg.lines().next().unwrap_or("").trim_start();
46 let (ty, _rest) = subject.split_once(':')?;
47 let (head, marker) = ty.split_once('(').map_or((ty, ""), |(h, scope_rest)| {
49 let after_scope = scope_rest.split_once(')').map_or("", |x| x.1);
51 (h, after_scope)
52 });
53 if marker.starts_with('!') || ty.ends_with('!') {
54 return Some(ConventionalLevel::Major);
55 }
56 match head.trim() {
57 "feat" => Some(ConventionalLevel::Minor),
58 "fix" | "perf" | "revert" => Some(ConventionalLevel::Patch),
59 _ => None,
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn breaking_change_footer_is_major() {
69 assert_eq!(
70 classify_commit("feat(api): new endpoint\n\nBREAKING CHANGE: old endpoint removed"),
71 Some(ConventionalLevel::Major)
72 );
73 }
74
75 #[test]
76 fn breaking_change_without_colon_is_major() {
77 assert_eq!(
78 classify_commit("feat: x\n\nBREAKING CHANGE removed the old endpoint"),
79 Some(ConventionalLevel::Major)
80 );
81 assert_eq!(
82 classify_commit("fix: y\n\nBREAKING-CHANGE dropped the flag"),
83 Some(ConventionalLevel::Major)
84 );
85 }
86
87 #[test]
88 fn bang_shorthand_is_major_for_any_type() {
89 assert_eq!(
90 classify_commit("feat!: drop legacy auth"),
91 Some(ConventionalLevel::Major)
92 );
93 assert_eq!(
94 classify_commit("feat(core)!: rewrite pipeline"),
95 Some(ConventionalLevel::Major)
96 );
97 assert_eq!(
98 classify_commit("refactor!: drop the shim"),
99 Some(ConventionalLevel::Major)
100 );
101 }
102
103 #[test]
104 fn feat_is_minor() {
105 assert_eq!(
106 classify_commit("feat: new stage"),
107 Some(ConventionalLevel::Minor)
108 );
109 assert_eq!(
110 classify_commit("feat(build): add cache key"),
111 Some(ConventionalLevel::Minor)
112 );
113 }
114
115 #[test]
116 fn fix_perf_and_revert_are_patch() {
117 assert_eq!(classify_commit("fix: race"), Some(ConventionalLevel::Patch));
118 assert_eq!(
119 classify_commit("perf: faster loop"),
120 Some(ConventionalLevel::Patch)
121 );
122 assert_eq!(
123 classify_commit("revert: undo broken feature"),
124 Some(ConventionalLevel::Patch)
125 );
126 }
127
128 #[test]
129 fn non_release_worthy_types_are_none() {
130 assert_eq!(classify_commit("chore: update deps"), None);
131 assert_eq!(classify_commit("docs: fix link"), None);
132 assert_eq!(classify_commit("refactor: rename"), None);
133 assert_eq!(classify_commit("ci: pin runner"), None);
134 }
135
136 #[test]
137 fn unstructured_message_is_none() {
138 assert_eq!(classify_commit("random subject"), None);
139 }
140
141 #[test]
142 fn levels_order_by_strength_for_max_folds() {
143 assert!(ConventionalLevel::Patch < ConventionalLevel::Minor);
144 assert!(ConventionalLevel::Minor < ConventionalLevel::Major);
145 }
146}