pub struct CommitType {
pub name: &'static str,
pub emoji: &'static str,
}
pub const COMMIT_TYPES: &[CommitType] = &[
CommitType {
name: "build",
emoji: "👷",
},
CommitType {
name: "chore",
emoji: "🔧",
},
CommitType {
name: "docs",
emoji: "📝️",
},
CommitType {
name: "feat",
emoji: "✨",
},
CommitType {
name: "fix",
emoji: "🐛",
},
CommitType {
name: "perf",
emoji: "⚡️",
},
CommitType {
name: "refactor",
emoji: "♻️",
},
CommitType {
name: "revert",
emoji: "⏪️",
},
CommitType {
name: "style",
emoji: "🎨",
},
CommitType {
name: "test",
emoji: "🚨",
},
CommitType {
name: "add",
emoji: "➕",
},
CommitType {
name: "remove",
emoji: "➖",
},
];
pub struct BranchPrefix {
pub name: &'static str,
pub dots: bool,
}
pub const BRANCH_PREFIXES: &[BranchPrefix] = &[
BranchPrefix {
name: "add",
dots: false,
},
BranchPrefix {
name: "automation",
dots: false,
},
BranchPrefix {
name: "build",
dots: false,
},
BranchPrefix {
name: "chore",
dots: true,
},
BranchPrefix {
name: "docs",
dots: false,
},
BranchPrefix {
name: "feat",
dots: false,
},
BranchPrefix {
name: "fix",
dots: false,
},
BranchPrefix {
name: "hotfix",
dots: false,
},
BranchPrefix {
name: "perf",
dots: false,
},
BranchPrefix {
name: "refactor",
dots: false,
},
BranchPrefix {
name: "remove",
dots: false,
},
BranchPrefix {
name: "revert",
dots: false,
},
BranchPrefix {
name: "style",
dots: false,
},
BranchPrefix {
name: "test",
dots: false,
},
];
#[allow(dead_code)]
pub const COMMIT_ONLY: &[(&str, &str)] = &[];
#[allow(dead_code)]
pub const BRANCH_ONLY: &[(&str, &str)] = &[
(
"hotfix",
"an urgency, not a kind of change — the commits inside are still fix:",
),
(
"automation",
"bot-authored branches; their commits carry their own types",
),
];
pub fn branch_prefix(name: &str) -> Option<&'static BranchPrefix> {
BRANCH_PREFIXES.iter().find(|p| p.name == name)
}
pub fn emoji_for(commit_type: &str) -> &'static str {
COMMIT_TYPES
.iter()
.find(|t| t.name == commit_type)
.map(|t| t.emoji)
.unwrap_or("")
}
pub fn type_for_emoji(emoji: &str) -> Option<&'static str> {
COMMIT_TYPES
.iter()
.find(|t| t.emoji == emoji)
.map(|t| t.name)
}
pub fn branch_contract() -> String {
let plain: Vec<&str> = BRANCH_PREFIXES
.iter()
.filter(|p| !p.dots)
.map(|p| p.name)
.collect();
let dotted: Vec<&str> = BRANCH_PREFIXES
.iter()
.filter(|p| p.dots)
.map(|p| p.name)
.collect();
format!(
"^(({})/[[:alnum:]_-]+|({})/[[:alnum:]_.-]+)$",
plain.join("|"),
dotted.join("|")
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn names<T>(v: &[T], f: impl Fn(&T) -> &'static str) -> BTreeSet<&'static str> {
v.iter().map(f).collect()
}
#[test]
fn the_two_vocabularies_are_reconciled() {
let types = names(COMMIT_TYPES, |t| t.name);
let prefixes = names(BRANCH_PREFIXES, |p| p.name);
let commit_only: BTreeSet<&str> = COMMIT_ONLY.iter().map(|(n, _)| *n).collect();
let branch_only: BTreeSet<&str> = BRANCH_ONLY.iter().map(|(n, _)| *n).collect();
let unexplained: Vec<_> = types
.difference(&prefixes)
.filter(|n| !commit_only.contains(*n))
.collect();
assert!(
unexplained.is_empty(),
"commit types that are not branch prefixes and not listed in COMMIT_ONLY: {unexplained:?}"
);
let unexplained: Vec<_> = prefixes
.difference(&types)
.filter(|n| !branch_only.contains(*n))
.collect();
assert!(
unexplained.is_empty(),
"branch prefixes that are not commit types and not listed in BRANCH_ONLY: {unexplained:?}"
);
for n in &commit_only {
assert!(
!prefixes.contains(n),
"{n} is listed COMMIT_ONLY but IS a branch prefix"
);
}
for n in &branch_only {
assert!(
!types.contains(n),
"{n} is listed BRANCH_ONLY but IS a commit type"
);
}
}
#[test]
fn no_duplicates() {
assert_eq!(names(COMMIT_TYPES, |t| t.name).len(), COMMIT_TYPES.len());
assert_eq!(
names(BRANCH_PREFIXES, |p| p.name).len(),
BRANCH_PREFIXES.len()
);
}
#[test]
fn each_type_has_its_own_emoji() {
let emojis = names(COMMIT_TYPES, |t| t.emoji);
assert_eq!(
emojis.len(),
COMMIT_TYPES.len(),
"two commit types share an emoji: {emojis:?}"
);
}
#[test]
fn an_emoji_names_the_type_it_was_written_for() {
for t in COMMIT_TYPES {
assert_eq!(type_for_emoji(t.emoji), Some(t.name), "{}", t.name);
assert_eq!(emoji_for(t.name), t.emoji);
}
assert_eq!(type_for_emoji("🚀"), None);
assert_eq!(type_for_emoji(""), None);
}
#[test]
fn only_chore_allows_dots() {
let dotted: Vec<&str> = BRANCH_PREFIXES
.iter()
.filter(|p| p.dots)
.map(|p| p.name)
.collect();
assert_eq!(dotted, vec!["chore"]);
}
#[test]
fn the_contract_string_lists_every_prefix() {
let c = branch_contract();
for p in BRANCH_PREFIXES {
assert!(c.contains(p.name), "{} missing from the contract", p.name);
}
}
}