diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs index 56275eed58..fc1553c5bb 100644 --- a/git-cliff-core/src/changelog.rs +++ b/git-cliff-core/src/changelog.rs @@ -238,6 +238,8 @@ mod test { default_scope: None, scope: None, skip: Some(true), + field: None, + pattern: None, }, CommitParser { message: Regex::new("feat*").ok(), @@ -246,6 +248,8 @@ mod test { default_scope: Some(String::from("other")), scope: None, skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new("^fix*").ok(), @@ -254,6 +258,8 @@ mod test { default_scope: None, scope: None, skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new("doc:").ok(), @@ -262,6 +268,8 @@ mod test { default_scope: None, scope: Some(String::from("documentation")), skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new("docs:").ok(), @@ -270,6 +278,8 @@ mod test { default_scope: None, scope: Some(String::from("documentation")), skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new(r"match\((.*)\):.*").ok(), @@ -278,6 +288,8 @@ mod test { default_scope: None, scope: None, skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new(".*").ok(), @@ -286,6 +298,8 @@ mod test { default_scope: Some(String::from("other")), scope: None, skip: None, + field: None, + pattern: None, }, ]), protect_breaking_commits: None, diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index 96a8c2d4e6..43019c815d 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -258,6 +258,33 @@ impl Commit<'_> { ) { regex_checks.push((body_regex, body.to_string())) } + if let (Some(field_name), Some(pattern_regex)) = + (parser.field.as_ref(), parser.pattern.as_ref()) + { + regex_checks.push(( + pattern_regex, + match field_name.as_str() { + "id" => Some(self.id.clone()), + "message" => Some(self.message.clone()), + "body" => self + .conv + .as_ref() + .and_then(|v| v.body()) + .map(|v| v.to_string()), + "author.name" => self.author.name.clone(), + "author.email" => self.author.email.clone(), + "committer.name" => self.committer.name.clone(), + "committer.email" => self.committer.email.clone(), + _ => None, + } + .ok_or_else(|| { + AppError::FieldError(format!( + "field {} does not have a value", + field_name + )) + })?, + )); + } for (regex, text) in regex_checks { if regex.is_match(&text) { if self.skip_commit(parser, protect_breaking) { @@ -420,6 +447,8 @@ mod test { default_scope: Some(String::from("test_scope")), scope: None, skip: None, + field: None, + pattern: None, }], false, false, @@ -568,4 +597,36 @@ mod test { Commit::from(String::from("thisisinvalidsha1 style: add formatting")) ); } + + #[test] + fn parse_commit_field() -> Result<()> { + let mut commit = Commit::new( + String::from("8f55e69eba6e6ce811ace32bd84cc82215673cb6"), + String::from("feat: do something"), + ); + + commit.author = Signature { + name: Some("John Doe".to_string()), + email: None, + timestamp: 0x0, + }; + + let parsed_commit = commit.parse( + &[CommitParser { + message: None, + body: None, + group: Some(String::from("Test group")), + default_scope: None, + scope: None, + skip: None, + field: Some(String::from("author.name")), + pattern: Regex::new("John Doe").ok(), + }], + false, + false, + )?; + + assert_eq!(Some(String::from("Test group")), parsed_commit.group); + Ok(()) + } } diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs index 1502a47bcd..63f97192b2 100644 --- a/git-cliff-core/src/config.rs +++ b/git-cliff-core/src/config.rs @@ -100,6 +100,11 @@ pub struct CommitParser { pub scope: Option, /// Whether to skip this commit group. pub skip: Option, + /// Field name of the commit to match the regex against. + pub field: Option, + /// Regex for matching the field value. + #[serde(with = "serde_regex", default)] + pub pattern: Option, } /// TextProcessor, e.g. for modifying commit messages. diff --git a/git-cliff-core/src/error.rs b/git-cliff-core/src/error.rs index 1e093c705b..a3acb63a5f 100644 --- a/git-cliff-core/src/error.rs +++ b/git-cliff-core/src/error.rs @@ -57,6 +57,10 @@ pub enum Error { /// Error that may occur while parsing integers. #[error("Failed to parse integer: `{0}`")] IntParseError(#[from] std::num::TryFromIntError), + /// Error that may occur while processing parsers that define field and + /// value matchers + #[error("Field error: `{0}`")] + FieldError(String), /// Error that may occur while parsing a SemVer version or version /// requirement. #[error("Semver error: `{0}`")] diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs index 775afb0610..cc8c638c52 100644 --- a/git-cliff-core/tests/integration_test.rs +++ b/git-cliff-core/tests/integration_test.rs @@ -1,4 +1,7 @@ -use git_cliff_core::commit::Commit; +use git_cliff_core::commit::{ + Commit, + Signature, +}; use git_cliff_core::config::{ ChangelogConfig, CommitParser, @@ -55,6 +58,8 @@ fn generate_changelog() -> Result<()> { default_scope: None, scope: None, skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new("^fix").ok(), @@ -63,6 +68,8 @@ fn generate_changelog() -> Result<()> { default_scope: None, scope: None, skip: None, + field: None, + pattern: None, }, CommitParser { message: Regex::new("^test").ok(), @@ -71,6 +78,18 @@ fn generate_changelog() -> Result<()> { default_scope: None, scope: Some(String::from("tests")), skip: None, + field: None, + pattern: None, + }, + CommitParser { + message: None, + body: None, + group: Some(String::from("docs")), + default_scope: None, + scope: None, + skip: None, + field: Some(String::from("author.name")), + pattern: Regex::new("John Doe").ok(), }, ]), protect_breaking_commits: None, @@ -95,6 +114,17 @@ fn generate_changelog() -> Result<()> { limit_commits: None, }; + let mut commit_with_author = Commit::new( + String::from("hjdfas32"), + String::from("docs(cool): testing author filtering"), + ); + + commit_with_author.author = Signature { + name: Some("John Doe".to_string()), + email: None, + timestamp: 0x0, + }; + let releases = vec![ Release { version: Some(String::from("v2.0.0")), @@ -133,6 +163,7 @@ fn generate_changelog() -> Result<()> { String::from("1234"), String::from("fix: support preprocessing (fixes #99)"), ), + commit_with_author ] .iter() .filter_map(|c| c.process(&git_config).ok()) @@ -181,6 +212,9 @@ fn generate_changelog() -> Result<()> { ## Release v2.0.0 +### docs +- *(cool)* testing author filtering + ### fix bugs - fix abc - support preprocessing [closes Issue#99] diff --git a/website/docs/configuration.md b/website/docs/configuration.md index a1d180b501..d0d0d0c65c 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -220,6 +220,15 @@ Examples: - If the commit starts with "doc", group the commit as "Documentation" and set the default scope to "other". (e.g. `docs: xyz` will be processed as `docs(other): xyz`) - `{ message = "(www)", scope = "Application"}` - If the commit contains "(www)", override the scope with "Application". Scoping order is: scope specification, conventional commit's scope and default scope. +- `{ field = "author.name", pattern = "John Doe", group = "John's stuff"}` + - If the author's name attribute of the commit matches the pattern "John Doe" (as a regex), override the scope with "John' stuff". Supported commit attributes are: + - `id` + - `message` + - `body` + - `author.name` + - `author.email` + - `committer.email` + - `committer.name` ### protect_breaking_commits