diff --git a/src/git.rs b/src/git.rs index 0c0a9ae..eca382b 100644 --- a/src/git.rs +++ b/src/git.rs @@ -175,6 +175,25 @@ Link: Hello"; assert_eq!(f.get("Link"), Some(&"Hello".to_string())); } + #[test] + fn test_footer_with_multiline_body_parse_commit_message() { + let input = "feat(cli): add dummy option + +Hello, there! +I'm from Japan! + +Link: Hello"; + let (subject, body, footer) = parse_commit_message(input); + + let mut f = HashMap::new(); + f.insert("Link".to_string(), "Hello".to_string()); + assert_eq!(subject, "feat(cli): add dummy option"); + assert_eq!(body, Some("Hello, there! +I'm from Japan!".to_string())); + assert!(footer.is_some()); + assert_eq!(f.get("Link"), Some(&"Hello".to_string())); + } + #[test] fn test_multiple_footers_parse_commit_message() { let input = "feat(cli): add dummy option diff --git a/src/rule.rs b/src/rule.rs index fd73dea..18a975d 100644 --- a/src/rule.rs +++ b/src/rule.rs @@ -6,9 +6,9 @@ use serde::{Deserialize, Serialize}; use self::{ body_empty::BodyEmpty, body_max_length::BodyMaxLength, description_empty::DescriptionEmpty, description_format::DescriptionFormat, description_max_length::DescriptionMaxLength, - r#type::Type, scope::Scope, scope_empty::ScopeEmpty, scope_format::ScopeFormat, - scope_max_length::ScopeMaxLength, subject_empty::SubjectEmpty, type_empty::TypeEmpty, - type_format::TypeFormat, type_max_length::TypeMaxLength, + footers_empty::FootersEmpty, r#type::Type, scope::Scope, scope_empty::ScopeEmpty, + scope_format::ScopeFormat, scope_max_length::ScopeMaxLength, subject_empty::SubjectEmpty, + type_empty::TypeEmpty, type_format::TypeFormat, type_max_length::TypeMaxLength, }; pub mod body_empty; @@ -16,6 +16,7 @@ pub mod body_max_length; pub mod description_empty; pub mod description_format; pub mod description_max_length; +pub mod footers_empty; pub mod scope; pub mod scope_empty; pub mod scope_format; @@ -50,6 +51,10 @@ pub struct Rules { #[serde(skip_serializing_if = "Option::is_none")] pub description_max_length: Option, + #[serde(rename = "footers-empty")] + #[serde(skip_serializing_if = "Option::is_none")] + pub footers_empty: Option, + #[serde(rename = "scope")] #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, @@ -190,6 +195,7 @@ impl Default for Rules { description_empty: DescriptionEmpty::default().into(), description_format: None, description_max_length: None, + footers_empty: None, scope: None, scope_empty: None, scope_format: None, diff --git a/src/rule/footers_empty.rs b/src/rule/footers_empty.rs new file mode 100644 index 0000000..9f5abe9 --- /dev/null +++ b/src/rule/footers_empty.rs @@ -0,0 +1,95 @@ +use crate::{message::Message, result::Violation, rule::Rule}; +use serde::{Deserialize, Serialize}; + +use super::Level; + +/// FootersEmpty represents the footer-empty rule. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct FootersEmpty { + /// Level represents the level of the rule. + /// + // Note that currently the default literal is not supported. + // See: https://github.com/serde-rs/serde/issues/368 + level: Option, +} + +/// FooterEmpty represents the footer-empty rule. +impl Rule for FootersEmpty { + const NAME: &'static str = "footers-empty"; + const LEVEL: Level = Level::Error; + + fn message(&self, _message: &Message) -> String { + "footers are empty".to_string() + } + + fn validate(&self, message: &Message) -> Option { + if message.footers.is_none() { + return Some(Violation { + level: self.level.unwrap_or(Self::LEVEL), + message: self.message(message), + }); + } + + None + } +} + +/// Default implementation of FooterEmpty. +impl Default for FootersEmpty { + fn default() -> Self { + Self { + level: Some(Self::LEVEL), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[test] + fn test_non_empty_footer() { + let rule = FootersEmpty::default(); + + let mut f = HashMap::new(); + f.insert("Link".to_string(), "hello".to_string()); + + let message = Message { + body: Some("Hello world".to_string()), + description: Some("broadcast $destroy event on scope destruction".to_string()), + footers: Some(f), + r#type: Some("feat".to_string()), + raw: "feat(scope): broadcast $destroy event on scope destruction + +Hello world + +Link: hello" + .to_string(), + scope: Some("scope".to_string()), + subject: Some("feat(scope): broadcast $destroy event on scope destruction".to_string()), + }; + + assert!(rule.validate(&message).is_none()); + } + + #[test] + fn test_empty_footer() { + let rule = FootersEmpty::default(); + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("feat".to_string()), + raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(), + scope: Some("scope".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!(violation.unwrap().message, "footers are empty".to_string()); + } +} diff --git a/web/src/content/docs/rules/footers-empty.md b/web/src/content/docs/rules/footers-empty.md new file mode 100644 index 0000000..103a7c9 --- /dev/null +++ b/web/src/content/docs/rules/footers-empty.md @@ -0,0 +1,30 @@ +--- +title: Footers Empty +description: Check if the footers exists +--- + +* Default: `error` + +## ❌ Bad + +```console +feat(cli): user logout handler +``` + +## ✅ Good + +```console +feat(cli): add new flag + +Link: https://keisukeyamashita.github.io/commitlint-rs/ +``` + +## Example + +### Footers must exist + +```yaml +rules: + footers-empty: + level: error +```