From 93779b21667fd5f7ec28635e36e428035d69afb5 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 24 Jun 2022 09:06:49 -0700 Subject: [PATCH 1/7] fix(commit): pass footer token and separator to template Currently, when a conventional commit has footers, only the footers' values (the part after the separator token, such as `:`) are passed to the template. This means that when multiple footers, such as `Signed-off-by:` and `Co-authored-by:` are present, it isn't currently possible for the template to determine the name of the footer. This makes actually using data from footers in templates impractical in most cases. This commit fixes this by changing the `Serialize` impl for `Commit` to pass the commit's footers as a structured object rather than a string. The structured `Footer` type includes the footer's token (which is what `git_conventional` calls the name preceding the separator token), the separator, and the value. I didn't make the new `Footer` type and `Commit::footers` method public, because it isn't strictly necessary to add them to the `git-cliff-core` public API to fix this issue. However, we can make them public in a follow-up PR if this is considered useful. Fixes #96 BREAKING CHANGE: This changes type of the `commit.footers` array exposed to templates. Currently, when a template uses `commit.footers`, it can treat the values as strings. After this change, the footer object will need to have its fields unpacked in order to use them. However, the impact of this breakage is probably not that severe, since it's not really practical to use footers in templates with the current system. --- git-cliff-core/src/commit.rs | 116 ++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 9 deletions(-) diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index adb192f1fa..973850b401 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -49,6 +49,24 @@ pub struct Link { pub href: String, } +/// A conventional commit footer. +#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +struct Footer<'a> { + /// Token of the footer. + /// + /// This is the part of the footer preceding the separator. For example, for + /// the `Signed-off-by: ` footer, this would be `Signed-off-by`. + token: &'a str, + /// The separator between the footer token and its value. + /// + /// This is typically either `:` or `#`. + separator: &'a str, + /// The value of the footer. + value: &'a str, + /// A flag to signal that the footer describes a breaking change. + breaking: bool, +} + impl<'a> From<&GitCommit<'a>> for Commit<'a> { fn from(commit: &GitCommit<'a>) -> Self { Self::new( @@ -208,6 +226,16 @@ impl Commit<'_> { } Ok(self) } + + /// Returns an iterator over this commit's [`Footer`]s, if this is a + /// conventional commit. + /// + /// If this commit is not conventional, the returned iterator will be empty. + fn footers(&self) -> impl Iterator> { + self.conv + .iter() + .flat_map(|conv| conv.footers().iter().map(Footer::from)) + } } impl Serialize for Commit<'_> { @@ -215,21 +243,28 @@ impl Serialize for Commit<'_> { where S: Serializer, { + /// A wrapper to serialize commit footers from an iterator using + /// `Serializer::collect_seq` without having to + struct SerializeFooters<'a>(&'a Commit<'a>); + impl Serialize for SerializeFooters<'_> { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: Serializer, + { + serializer.collect_seq(self.0.footers()) + } + } + let mut commit = serializer.serialize_struct("Commit", 9)?; commit.serialize_field("id", &self.id)?; match &self.conv { Some(conv) => { commit.serialize_field("message", conv.description())?; commit.serialize_field("body", &conv.body())?; - commit.serialize_field( - "footers", - &conv - .footers() - .to_vec() - .iter() - .map(|f| f.value()) - .collect::>(), - )?; + commit.serialize_field("footers", &SerializeFooters(self))?; commit.serialize_field( "group", self.group.as_ref().unwrap_or(&conv.type_().to_string()), @@ -263,6 +298,17 @@ impl Serialize for Commit<'_> { } } +impl<'a> From<&'a git_conventional::Footer<'a>> for Footer<'a> { + fn from(footer: &'a git_conventional::Footer<'a>) -> Self { + Self { + token: footer.token().as_str(), + separator: footer.separator().as_str(), + value: footer.value(), + breaking: footer.breaking(), + } + } +} + #[cfg(test)] mod test { use super::*; @@ -304,6 +350,58 @@ mod test { assert_eq!(Some(String::from("test_scope")), commit.default_scope); } + #[test] + fn conventional_footers() { + let cfg = crate::config::GitConfig { + conventional_commits: Some(true), + ..Default::default() + }; + let test_cases = vec![ + ( + Commit::new( + String::from("123123"), + String::from( + "test(commit): add test\n\nSigned-off-by: Test User \ + ", + ), + ), + vec![Footer { + token: "Signed-off-by", + separator: ":", + value: "Test User ", + breaking: false, + }], + ), + ( + Commit::new( + String::from("123124"), + String::from( + "fix(commit): break stuff\n\nBREAKING CHANGE: This commit \ + breaks stuff\nSigned-off-by: Test User ", + ), + ), + vec![ + Footer { + token: "BREAKING CHANGE", + separator: ":", + value: "This commit breaks stuff", + breaking: true, + }, + Footer { + token: "Signed-off-by", + separator: ":", + value: "Test User ", + breaking: false, + }, + ], + ), + ]; + for (commit, footers) in &test_cases { + let commit = commit.process(&cfg).expect("commit should process"); + assert_eq!(&commit.footers().collect::>(), footers); + } + } + #[test] fn parse_link() { let test_cases = vec![ From a54bca57acc91a886d52bb9d3d87866eb5c18f32 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 24 Jun 2022 09:34:08 -0700 Subject: [PATCH 2/7] docs(README): discuss footers in README Signed-off-by: Eliza Weisman --- README.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cba03a7f06..6be1832d3a 100644 --- a/README.md +++ b/README.md @@ -669,7 +669,14 @@ following context is generated to use for templating: "scope": "[scope]", "message": "", "body": "[body]", - "footers": ["[footer]", "[footer]"], + "footers": [ + { + "token": "", + "separator": "", + "value": "", "breaking": false, "conventional": true, @@ -684,6 +691,31 @@ following context is generated to use for templating: } ``` +##### Footers + +A conventional commit's body may end with any number of structured key-value +pairs known as [_footers_]. These consist of a string token naming the footer, a +separator (which is either `: ` or ` #`), and a value, similar to [the git +trailers convention][trailers]. For example: + +- `Signed-off-by: User Name ` +- `Reviewed-by: User Name ` +- `Fixes #1234` +- `BREAKING CHANGE: breaking change description` + +When a conventional commit contains footers, the footers are passed to the +template in a `footers` array in the commit object. Each footer is represented +by an object with the following fields: + +- `"token"`, the name of the footer (preceeding the separator character) +- `separator`, the footer's separator string (either `: ` or ` #`) +- `value`, the value following the separator character +- `breaking`, which is `true` if this is a `BREAKING CHANGE:` footer, and + `false` otherwise + +[_footers_]: https://www.conventionalcommits.org/en/v1.0.0/#specification +[trailers]: https://git-scm.com/docs/git-interpret-trailers) + ##### Breaking Changes `breaking` flag is set to `true` when the commit has an exclamation mark after the commit type and scope, e.g.: @@ -702,6 +734,9 @@ BREAKING CHANGE: this is a breaking change `breaking_description` is set to the explanation of the breaking change. This description is expected to be present in the `BREAKING CHANGE` footer. However, if it's not provided, the `message` is expected to describe the breaking change. +If the `BREAKING CHANGE:` footer is present, the footer will also be included in +`commit.footers`. + #### Non-Conventional Commits > conventional_commits = **false** From ee9e563dc75cd1018988acff09230b6f6ccfabf1 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 1 Jul 2022 11:09:45 -0700 Subject: [PATCH 3/7] docs(examples): Add footers to `detailed.toml` Signed-off-by: Eliza Weisman --- examples/detailed.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/detailed.toml b/examples/detailed.toml index b6b57fdd35..631f900664 100644 --- a/examples/detailed.toml +++ b/examples/detailed.toml @@ -23,7 +23,10 @@ body = """ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {% for commit in commits %} - - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))\ + - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }})) + {% for footer in commit.footers -%} + , {{ footer.token }}{{ footer.separator }}{{ footer.value }}\ + {% endfor %} {% endfor %} {% endfor %}\n """ From c5dbc11378ebff421ab5f1c4963225e3b0a5a5a4 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 1 Jul 2022 11:10:09 -0700 Subject: [PATCH 4/7] refac(commit): address review feedback Signed-off-by: Eliza Weisman --- git-cliff-core/src/commit.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index 973850b401..be87679dd6 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -67,6 +67,17 @@ struct Footer<'a> { breaking: bool, } +impl<'a> From<&'a git_conventional::Footer<'a>> for Footer<'a> { + fn from(footer: &'a git_conventional::Footer<'a>) -> Self { + Self { + token: footer.token().as_str(), + separator: footer.separator().as_str(), + value: footer.value(), + breaking: footer.breaking(), + } + } +} + impl<'a> From<&GitCommit<'a>> for Commit<'a> { fn from(commit: &GitCommit<'a>) -> Self { Self::new( @@ -244,7 +255,8 @@ impl Serialize for Commit<'_> { S: Serializer, { /// A wrapper to serialize commit footers from an iterator using - /// `Serializer::collect_seq` without having to + /// `Serializer::collect_seq` without having to allocate in order to + /// `collect` the footers into a new to `Vec`. struct SerializeFooters<'a>(&'a Commit<'a>); impl Serialize for SerializeFooters<'_> { fn serialize( @@ -298,17 +310,6 @@ impl Serialize for Commit<'_> { } } -impl<'a> From<&'a git_conventional::Footer<'a>> for Footer<'a> { - fn from(footer: &'a git_conventional::Footer<'a>) -> Self { - Self { - token: footer.token().as_str(), - separator: footer.separator().as_str(), - value: footer.value(), - breaking: footer.breaking(), - } - } -} - #[cfg(test)] mod test { use super::*; From 9f78b0cb5498a1db6129167eee162b97978011cb Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 1 Jul 2022 11:13:04 -0700 Subject: [PATCH 5/7] docs(README): address README review feedback Signed-off-by: Eliza Weisman --- README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6be1832d3a..93b5f00592 100644 --- a/README.md +++ b/README.md @@ -693,28 +693,21 @@ following context is generated to use for templating: ##### Footers -A conventional commit's body may end with any number of structured key-value -pairs known as [_footers_]. These consist of a string token naming the footer, a -separator (which is either `: ` or ` #`), and a value, similar to [the git -trailers convention][trailers]. For example: +A conventional commit's body may end with any number of structured key-value pairs known as [_footers_](https://www.conventionalcommits.org/en/v1.0.0/#specification). These consist of a string token naming the footer, a separator (which is either `: ` or ` #`), and a value, similar to [the git trailers convention](https://git-scm.com/docs/git-interpret-trailers). + +For example: - `Signed-off-by: User Name ` - `Reviewed-by: User Name ` - `Fixes #1234` - `BREAKING CHANGE: breaking change description` -When a conventional commit contains footers, the footers are passed to the -template in a `footers` array in the commit object. Each footer is represented -by an object with the following fields: +When a conventional commit contains footers, the footers are passed to the template in a `footers` array in the commit object. Each footer is represented by an object with the following fields: - `"token"`, the name of the footer (preceeding the separator character) - `separator`, the footer's separator string (either `: ` or ` #`) - `value`, the value following the separator character -- `breaking`, which is `true` if this is a `BREAKING CHANGE:` footer, and - `false` otherwise - -[_footers_]: https://www.conventionalcommits.org/en/v1.0.0/#specification -[trailers]: https://git-scm.com/docs/git-interpret-trailers) +- `breaking`, which is `true` if this is a `BREAKING CHANGE:` footer, and `false` otherwise ##### Breaking Changes From 9f33f3999420e310f33f4d2b00bc38b2c2ecabc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Tue, 12 Jul 2022 14:07:51 +0200 Subject: [PATCH 6/7] refactor(example): update detailed example about newline issues --- examples/detailed.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/detailed.toml b/examples/detailed.toml index 631f900664..bd355c12b9 100644 --- a/examples/detailed.toml +++ b/examples/detailed.toml @@ -23,10 +23,10 @@ body = """ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {% for commit in commits %} - - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }})) + - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))\ {% for footer in commit.footers -%} , {{ footer.token }}{{ footer.separator }}{{ footer.value }}\ - {% endfor %} + {% endfor %}\ {% endfor %} {% endfor %}\n """ From 26a81e0fb733fa93c9c01ac30ba222900dc0f0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Tue, 12 Jul 2022 14:26:08 +0200 Subject: [PATCH 7/7] test(fixture): add test fixture for commit footers --- .../fixtures/test-commit-footers/cliff.toml | 30 +++++++++++++++++++ .../fixtures/test-commit-footers/commit.sh | 9 ++++++ .../fixtures/test-commit-footers/expected.md | 18 +++++++++++ 3 files changed, 57 insertions(+) create mode 100644 .github/fixtures/test-commit-footers/cliff.toml create mode 100755 .github/fixtures/test-commit-footers/commit.sh create mode 100644 .github/fixtures/test-commit-footers/expected.md diff --git a/.github/fixtures/test-commit-footers/cliff.toml b/.github/fixtures/test-commit-footers/cliff.toml new file mode 100644 index 0000000000..1cd862a283 --- /dev/null +++ b/.github/fixtures/test-commit-footers/cliff.toml @@ -0,0 +1,30 @@ +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://tera.netlify.app/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))\ + {% for footer in commit.footers -%} + , {{ footer.token }}{{ footer.separator }}{{ footer.value }}\ + {% endfor %}\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ + +""" diff --git a/.github/fixtures/test-commit-footers/commit.sh b/.github/fixtures/test-commit-footers/commit.sh new file mode 100755 index 0000000000..d19473d0d9 --- /dev/null +++ b/.github/fixtures/test-commit-footers/commit.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add feature 1" -m "footer: test" + +GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "feat: add feature 2" -m "Signed-off-by: bot" +git tag v0.1.0 + +GIT_COMMITTER_DATE="2021-01-23 01:23:47" git commit --allow-empty -m "fix: fix feature 1" -m "footer1: xyz" -m "footer2: abc" diff --git a/.github/fixtures/test-commit-footers/expected.md b/.github/fixtures/test-commit-footers/expected.md new file mode 100644 index 0000000000..6a08cda770 --- /dev/null +++ b/.github/fixtures/test-commit-footers/expected.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Fix + +- Fix feature 1 ([540f28b](540f28b88861802ca6c196482c5c70933593561b)), footer1:xyz, footer2:abc + +## [0.1.0] - 2021-01-23 + +### Feat + +- Add feature 1 ([376fd60](376fd6043cb27af83973f31dd6aab87486d8e554)), footer:test +- Add feature 2 ([fc086fa](fc086faec7a5bd4429f62f01c4a871631f63be68)), Signed-off-by:bot + +