1pub struct CommitType {
16 pub name: &'static str,
17 pub emoji: &'static str,
18}
19
20pub const COMMIT_TYPES: &[CommitType] = &[
22 CommitType {
23 name: "build",
24 emoji: "👷",
25 },
26 CommitType {
27 name: "chore",
28 emoji: "🔧",
29 },
30 CommitType {
31 name: "docs",
32 emoji: "📝️",
33 },
34 CommitType {
35 name: "feat",
36 emoji: "✨",
37 },
38 CommitType {
39 name: "fix",
40 emoji: "🐛",
41 },
42 CommitType {
43 name: "perf",
44 emoji: "⚡️",
45 },
46 CommitType {
47 name: "refactor",
48 emoji: "♻️",
49 },
50 CommitType {
51 name: "revert",
52 emoji: "⏪️",
53 },
54 CommitType {
55 name: "style",
56 emoji: "🎨",
57 },
58 CommitType {
59 name: "test",
60 emoji: "🚨",
61 },
62 CommitType {
63 name: "add",
64 emoji: "➕",
65 },
66 CommitType {
67 name: "remove",
68 emoji: "➖",
69 },
70];
71
72pub struct BranchPrefix {
74 pub name: &'static str,
75 pub dots: bool,
79}
80
81pub const BRANCH_PREFIXES: &[BranchPrefix] = &[
82 BranchPrefix {
83 name: "add",
84 dots: false,
85 },
86 BranchPrefix {
87 name: "automation",
88 dots: false,
89 },
90 BranchPrefix {
91 name: "build",
92 dots: false,
93 },
94 BranchPrefix {
95 name: "chore",
96 dots: true,
97 },
98 BranchPrefix {
99 name: "docs",
100 dots: false,
101 },
102 BranchPrefix {
103 name: "feat",
104 dots: false,
105 },
106 BranchPrefix {
107 name: "fix",
108 dots: false,
109 },
110 BranchPrefix {
111 name: "hotfix",
112 dots: false,
113 },
114 BranchPrefix {
115 name: "perf",
116 dots: false,
117 },
118 BranchPrefix {
119 name: "refactor",
120 dots: false,
121 },
122 BranchPrefix {
123 name: "remove",
124 dots: false,
125 },
126 BranchPrefix {
127 name: "revert",
128 dots: false,
129 },
130 BranchPrefix {
131 name: "style",
132 dots: false,
133 },
134 BranchPrefix {
135 name: "test",
136 dots: false,
137 },
138];
139
140#[allow(dead_code)]
143pub const COMMIT_ONLY: &[(&str, &str)] = &[];
146
147#[allow(dead_code)]
148pub const BRANCH_ONLY: &[(&str, &str)] = &[
150 (
151 "hotfix",
152 "an urgency, not a kind of change — the commits inside are still fix:",
153 ),
154 (
155 "automation",
156 "bot-authored branches; their commits carry their own types",
157 ),
158];
159
160pub fn branch_prefix(name: &str) -> Option<&'static BranchPrefix> {
161 BRANCH_PREFIXES.iter().find(|p| p.name == name)
162}
163
164pub fn emoji_for(commit_type: &str) -> &'static str {
165 COMMIT_TYPES
166 .iter()
167 .find(|t| t.name == commit_type)
168 .map(|t| t.emoji)
169 .unwrap_or("")
170}
171
172pub fn type_for_emoji(emoji: &str) -> Option<&'static str> {
179 COMMIT_TYPES
180 .iter()
181 .find(|t| t.emoji == emoji)
182 .map(|t| t.name)
183}
184
185pub fn branch_contract() -> String {
188 let plain: Vec<&str> = BRANCH_PREFIXES
189 .iter()
190 .filter(|p| !p.dots)
191 .map(|p| p.name)
192 .collect();
193 let dotted: Vec<&str> = BRANCH_PREFIXES
194 .iter()
195 .filter(|p| p.dots)
196 .map(|p| p.name)
197 .collect();
198 format!(
199 "^(({})/[[:alnum:]_-]+|({})/[[:alnum:]_.-]+)$",
200 plain.join("|"),
201 dotted.join("|")
202 )
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use std::collections::BTreeSet;
209
210 fn names<T>(v: &[T], f: impl Fn(&T) -> &'static str) -> BTreeSet<&'static str> {
211 v.iter().map(f).collect()
212 }
213
214 #[test]
219 fn the_two_vocabularies_are_reconciled() {
220 let types = names(COMMIT_TYPES, |t| t.name);
221 let prefixes = names(BRANCH_PREFIXES, |p| p.name);
222 let commit_only: BTreeSet<&str> = COMMIT_ONLY.iter().map(|(n, _)| *n).collect();
223 let branch_only: BTreeSet<&str> = BRANCH_ONLY.iter().map(|(n, _)| *n).collect();
224
225 let unexplained: Vec<_> = types
226 .difference(&prefixes)
227 .filter(|n| !commit_only.contains(*n))
228 .collect();
229 assert!(
230 unexplained.is_empty(),
231 "commit types that are not branch prefixes and not listed in COMMIT_ONLY: {unexplained:?}"
232 );
233
234 let unexplained: Vec<_> = prefixes
235 .difference(&types)
236 .filter(|n| !branch_only.contains(*n))
237 .collect();
238 assert!(
239 unexplained.is_empty(),
240 "branch prefixes that are not commit types and not listed in BRANCH_ONLY: {unexplained:?}"
241 );
242
243 for n in &commit_only {
245 assert!(
246 !prefixes.contains(n),
247 "{n} is listed COMMIT_ONLY but IS a branch prefix"
248 );
249 }
250 for n in &branch_only {
251 assert!(
252 !types.contains(n),
253 "{n} is listed BRANCH_ONLY but IS a commit type"
254 );
255 }
256 }
257
258 #[test]
259 fn no_duplicates() {
260 assert_eq!(names(COMMIT_TYPES, |t| t.name).len(), COMMIT_TYPES.len());
261 assert_eq!(
262 names(BRANCH_PREFIXES, |p| p.name).len(),
263 BRANCH_PREFIXES.len()
264 );
265 }
266
267 #[test]
272 fn each_type_has_its_own_emoji() {
273 let emojis = names(COMMIT_TYPES, |t| t.emoji);
274 assert_eq!(
275 emojis.len(),
276 COMMIT_TYPES.len(),
277 "two commit types share an emoji: {emojis:?}"
278 );
279 }
280
281 #[test]
283 fn an_emoji_names_the_type_it_was_written_for() {
284 for t in COMMIT_TYPES {
285 assert_eq!(type_for_emoji(t.emoji), Some(t.name), "{}", t.name);
286 assert_eq!(emoji_for(t.name), t.emoji);
287 }
288 assert_eq!(type_for_emoji("🚀"), None);
289 assert_eq!(type_for_emoji(""), None);
290 }
291
292 #[test]
294 fn only_chore_allows_dots() {
295 let dotted: Vec<&str> = BRANCH_PREFIXES
296 .iter()
297 .filter(|p| p.dots)
298 .map(|p| p.name)
299 .collect();
300 assert_eq!(dotted, vec!["chore"]);
301 }
302
303 #[test]
305 fn the_contract_string_lists_every_prefix() {
306 let c = branch_contract();
307 for p in BRANCH_PREFIXES {
308 assert!(c.contains(p.name), "{} missing from the contract", p.name);
309 }
310 }
311}