use crates::itertools::Itertools;
use crates::regex::Regex;
use impl_prelude::*;
#[derive(Debug, Default, Clone)]
pub struct CommitSubject {
min_summary: usize,
max_summary: usize,
check_wip: bool,
check_rebase_commands: bool,
tolerated_prefixes: Vec<Regex>,
allowed_prefixes: Vec<String>,
disallowed_prefixes: Vec<String>,
}
impl CommitSubject {
pub fn new() -> Self {
Self {
min_summary: 8,
max_summary: 78,
check_wip: true,
check_rebase_commands: true,
tolerated_prefixes: Vec::new(),
allowed_prefixes: Vec::new(),
disallowed_prefixes: Vec::new(),
}
}
pub fn with_summary_limits(&mut self, min: usize, max: usize) -> &mut Self {
self.min_summary = min;
self.max_summary = max;
self
}
pub fn check_work_in_progress(&mut self, wip: bool) -> &mut Self {
self.check_wip = wip;
self
}
pub fn check_rebase_commands(&mut self, rebase: bool) -> &mut Self {
self.check_rebase_commands = rebase;
self
}
pub fn with_tolerated_prefixes<I, P>(&mut self, patterns: I) -> &mut Self
where
I: IntoIterator<Item = P>,
P: Into<Regex>,
{
self.tolerated_prefixes
.extend(patterns.into_iter().map(Into::into));
self
}
pub fn with_allowed_prefixes<I, P>(&mut self, prefixes: I) -> &mut Self
where
I: IntoIterator<Item = P>,
P: Into<String>,
{
self.allowed_prefixes
.extend(prefixes.into_iter().map(Into::into));
self
}
pub fn with_disallowed_prefixes<I, P>(&mut self, prefixes: I) -> &mut Self
where
I: IntoIterator<Item = P>,
P: Into<String>,
{
self.disallowed_prefixes
.extend(prefixes.into_iter().map(Into::into));
self
}
fn is_generated_subject(summary: &str) -> bool {
false ||
summary.starts_with("Merge ") ||
summary.starts_with("Revert ")
}
}
impl Check for CommitSubject {
fn name(&self) -> &str {
"commit-subject"
}
fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult> {
let mut result = CheckResult::new();
let lines = commit.message.trim().lines().collect::<Vec<_>>();
if lines.is_empty() {
result.add_error(format!(
"commit {} has an invalid commit subject; it is empty.",
commit.sha1
));
return Ok(result);
}
let summary = &lines[0];
let summary_len = summary.len();
if summary_len < self.min_summary {
result.add_error(format!(
"commit {} has an invalid commit subject; the first line \
must be at least {} characters.",
commit.sha1, self.min_summary
));
}
let is_generated = Self::is_generated_subject(summary);
if !is_generated && self.max_summary < summary_len {
result.add_error(format!(
"commit {} has an invalid commit subject; the first line \
must be no longer than {} characters.",
commit.sha1, self.max_summary
));
}
if lines.len() >= 2 {
if lines.len() >= 2 && !lines[1].is_empty() {
result.add_error(format!(
"commit {} has an invalid commit subject; the second \
line must be empty.",
commit.sha1
));
}
if lines.len() == 2 {
result.add_error(format!(
"commit {} has an invalid commit subject; it cannot be \
exactly two lines.",
commit.sha1
));
} else if lines[2].is_empty() {
result.add_error(format!(
"commit {} has an invalid commit subject; the third \
line must not be empty.",
commit.sha1
));
}
}
if self.check_wip && (summary.starts_with("WIP") || summary.starts_with("wip")) {
result.add_error(format!(
"commit {} cannot be merged; it is marked as a \
work-in-progress (WIP).",
commit.sha1
));
}
if self.check_rebase_commands {
if summary.starts_with("fixup! ") {
result.add_error(format!(
"commit {} cannot be merged; it is marked as a fixup \
commit.",
commit.sha1
));
} else if summary.starts_with("squash! ") {
result.add_error(format!(
"commit {} cannot be merged; it is marked as a commit \
to be squashed.",
commit.sha1
));
}
}
if !is_generated {
let is_tolerated = self.tolerated_prefixes.iter().any(|regex| {
regex
.find(summary)
.map(|found| found.start() == 0)
.unwrap_or(false)
});
if !is_tolerated {
if !self.allowed_prefixes.is_empty() {
let is_ok = self
.allowed_prefixes
.iter()
.any(|prefix| summary.starts_with(prefix));
if !is_ok {
result.add_error(format!(
"commit {} cannot be merged; it must start with one \
of the following prefixes: `{}`.",
commit.sha1,
self.allowed_prefixes.iter().format("`, `")
));
}
}
let is_ok = self
.disallowed_prefixes
.iter()
.all(|prefix| !summary.starts_with(prefix));
if !is_ok {
result.add_error(format!(
"commit {} cannot be merged; it cannot start with \
any of the following prefixes: `{}`.",
commit.sha1,
self.disallowed_prefixes.iter().format("`, `")
));
}
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use checks::test::*;
use checks::CommitSubject;
use crates::regex::Regex;
const BAD_TOPIC: &str = "5f7284fe1599265c90550b681a4bf0763bc1de21";
#[test]
fn test_check_subject() {
let check = CommitSubject::new();
let result = run_check("test_check_subject", BAD_TOPIC, check);
test_result_errors(result, &[
"commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
first line must be at least 8 characters.",
"commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
first line must be no longer than 78 characters.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
second line must be empty.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
cannot be exactly two lines.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
second line must be empty.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
third line must not be empty.",
"commit e478f630b586e331753477eba88059d644927be8 cannot be merged; it is marked as a \
work-in-progress (WIP).",
"commit 9039b9a4813fa019229e960033fe1ae8514a0c8e cannot be merged; it is marked as a \
work-in-progress (WIP).",
"commit 11dbbbff3f32445d74d1a8d96df0a49381c81ba0 cannot be merged; it is marked as a \
work-in-progress (WIP).",
"commit 54d673ff559a72ce6343bd9526a950d79034b24e cannot be merged; it is marked as a \
fixup commit.",
"commit 5f7284fe1599265c90550b681a4bf0763bc1de21 cannot be merged; it is marked as a \
commit to be squashed.",
]);
}
#[test]
fn test_check_subject_allowed_prefixes() {
let mut check = CommitSubject::new();
check
.check_work_in_progress(false)
.check_rebase_commands(false)
.with_allowed_prefixes(vec!["commit message "]);
let result = run_check("test_check_subject_allowed_prefixes", BAD_TOPIC, check);
test_result_errors(result, &[
"commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
first line must be at least 8 characters.",
"commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c cannot be merged; it must start with one of the following prefixes: `commit message `.",
"commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
first line must be no longer than 78 characters.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
second line must be empty.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
cannot be exactly two lines.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
second line must be empty.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
third line must not be empty.",
"commit e478f630b586e331753477eba88059d644927be8 cannot be merged; it must start with \
one of the following prefixes: `commit message `.",
"commit 9039b9a4813fa019229e960033fe1ae8514a0c8e cannot be merged; it must start with \
one of the following prefixes: `commit message `.",
"commit 11dbbbff3f32445d74d1a8d96df0a49381c81ba0 cannot be merged; it must start with \
one of the following prefixes: `commit message `.",
"commit 54d673ff559a72ce6343bd9526a950d79034b24e cannot be merged; it must start with \
one of the following prefixes: `commit message `.",
"commit 5f7284fe1599265c90550b681a4bf0763bc1de21 cannot be merged; it must start with \
one of the following prefixes: `commit message `.",
]);
}
#[test]
fn test_check_subject_disallowed_prefixes() {
let mut check = CommitSubject::new();
check
.check_work_in_progress(false)
.check_rebase_commands(false)
.with_disallowed_prefixes(vec!["commit message "]);
let result = run_check("test_check_subject_disallowed_prefixes", BAD_TOPIC, check);
test_result_errors(result, &[
"commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
first line must be at least 8 characters.",
"commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
first line must be no longer than 78 characters.",
"commit 1afc6b3584580488917fc61aa5e5298e98583805 cannot be merged; it cannot start \
with any of the following prefixes: `commit message `.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
second line must be empty.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
cannot be exactly two lines.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 cannot be merged; it cannot start \
with any of the following prefixes: `commit message `.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
second line must be empty.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
third line must not be empty.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 cannot be merged; it cannot start \
with any of the following prefixes: `commit message `.",
]);
}
#[test]
fn test_check_subject_tolerated_prefixes() {
let mut check = CommitSubject::new();
check
.check_work_in_progress(false)
.check_rebase_commands(false)
.with_tolerated_prefixes(vec![
Regex::new("^(commit message )").unwrap(),
Regex::new("^([Ww][Ii][Pp]|fixup|squash)").unwrap(),
Regex::new("hort").unwrap(),
]).with_allowed_prefixes(vec!["allowed prefix "])
.with_disallowed_prefixes(vec!["commit message "]);
let result = run_check("test_check_subject_tolerated_prefixes", BAD_TOPIC, check);
test_result_errors(
result,
&[
"commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; \
the first line must be at least 8 characters.",
"commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c cannot be merged; it must start \
with one of the following prefixes: `allowed prefix `.",
"commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; \
the first line must be no longer than 78 characters.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; \
the second line must be empty.",
"commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; \
it cannot be exactly two lines.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; \
the second line must be empty.",
"commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; \
the third line must not be empty.",
],
);
}
}