diff --git a/.github/fixtures/test-conventional-commit/cliff.toml b/.github/fixtures/test-conventional-commit/cliff.toml new file mode 100644 index 0000000000..847a4cc86a --- /dev/null +++ b/.github/fixtures/test-conventional-commit/cliff.toml @@ -0,0 +1,35 @@ +[changelog] +# template for the changelog footer +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**]: {{ commit.breaking_description }}{% endif %} + {{ commit.message }}: {{ commit.body }}\ + {% for footer in commit.footers %} + - {{ footer.token }}{{ footer.separator }} {{ footer.value }}\ + {% endfor %} + {% endfor %}\ +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# process each line of a commit as an individual commit +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features", default_scope = "app" }, + { message = "^fix" }, +] diff --git a/.github/fixtures/test-conventional-commit/commit.sh b/.github/fixtures/test-conventional-commit/commit.sh new file mode 100755 index 0000000000..b55c2524a3 --- /dev/null +++ b/.github/fixtures/test-conventional-commit/commit.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e + +GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit" +GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit --allow-empty -m \ + "feat(web): feature 1, breaking change in footer + +Body feature 1 + +BREAKING CHANGE: breaking change description feature 1 +Signed-off-by: user1 +Reviewed-by: user2 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty -m \ + "feat(web)!: feature 2, breaking chain in description + +Body feature 2 + +Signed-off-by: user3 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty -m \ + "feat!: feature 3, use default scope = app + +Body feature 2 + +Signed-off-by: user3 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty -m \ + "fix(scope): fix 1, use scope as group + +Body fix 1 + +Fix: #1 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m \ + "fix(front-end): fix 2, no footer + +Body fix 2 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:14" git commit --allow-empty -m \ + "fix(front-end): fix 3 and 4, no body but footer + +Fix: #3 +Fix: #4 +" + +git tag v0.1.0 diff --git a/.github/fixtures/test-conventional-commit/expected.md b/.github/fixtures/test-conventional-commit/expected.md new file mode 100644 index 0000000000..aefd3fa0c5 --- /dev/null +++ b/.github/fixtures/test-conventional-commit/expected.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Features + +*(web)* [**breaking**]: breaking change description feature 1 +feature 1, breaking change in footer: Body feature 1 +- BREAKING CHANGE: breaking change description feature 1 +- Signed-off-by: user1 +- Reviewed-by: user2 + +*(web)* [**breaking**]: feature 2, breaking chain in description +feature 2, breaking chain in description: Body feature 2 +- Signed-off-by: user3 + +*(app)* [**breaking**]: feature 3, use default scope = app +feature 3, use default scope = app: Body feature 2 +- Signed-off-by: user3 + +## Fix + +*(scope)* +fix 1, use scope as group: Body fix 1 +- Fix: #1 + +*(front-end)* +fix 2, no footer: Body fix 2 + +*(front-end)* +fix 3 and 4, no body but footer: +- Fix: #3 +- Fix: #4 + + diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/cliff.toml b/.github/fixtures/test-from-context-does-not-discard-fields/cliff.toml new file mode 100644 index 0000000000..847a4cc86a --- /dev/null +++ b/.github/fixtures/test-from-context-does-not-discard-fields/cliff.toml @@ -0,0 +1,35 @@ +[changelog] +# template for the changelog footer +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**]: {{ commit.breaking_description }}{% endif %} + {{ commit.message }}: {{ commit.body }}\ + {% for footer in commit.footers %} + - {{ footer.token }}{{ footer.separator }} {{ footer.value }}\ + {% endfor %} + {% endfor %}\ +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# process each line of a commit as an individual commit +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features", default_scope = "app" }, + { message = "^fix" }, +] diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/commit.sh b/.github/fixtures/test-from-context-does-not-discard-fields/commit.sh new file mode 100755 index 0000000000..b55c2524a3 --- /dev/null +++ b/.github/fixtures/test-from-context-does-not-discard-fields/commit.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e + +GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit" +GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit --allow-empty -m \ + "feat(web): feature 1, breaking change in footer + +Body feature 1 + +BREAKING CHANGE: breaking change description feature 1 +Signed-off-by: user1 +Reviewed-by: user2 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty -m \ + "feat(web)!: feature 2, breaking chain in description + +Body feature 2 + +Signed-off-by: user3 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty -m \ + "feat!: feature 3, use default scope = app + +Body feature 2 + +Signed-off-by: user3 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty -m \ + "fix(scope): fix 1, use scope as group + +Body fix 1 + +Fix: #1 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m \ + "fix(front-end): fix 2, no footer + +Body fix 2 +" + +GIT_COMMITTER_DATE="2022-04-06 01:25:14" git commit --allow-empty -m \ + "fix(front-end): fix 3 and 4, no body but footer + +Fix: #3 +Fix: #4 +" + +git tag v0.1.0 diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/expected.md b/.github/fixtures/test-from-context-does-not-discard-fields/expected.md new file mode 100644 index 0000000000..aefd3fa0c5 --- /dev/null +++ b/.github/fixtures/test-from-context-does-not-discard-fields/expected.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Features + +*(web)* [**breaking**]: breaking change description feature 1 +feature 1, breaking change in footer: Body feature 1 +- BREAKING CHANGE: breaking change description feature 1 +- Signed-off-by: user1 +- Reviewed-by: user2 + +*(web)* [**breaking**]: feature 2, breaking chain in description +feature 2, breaking chain in description: Body feature 2 +- Signed-off-by: user3 + +*(app)* [**breaking**]: feature 3, use default scope = app +feature 3, use default scope = app: Body feature 2 +- Signed-off-by: user3 + +## Fix + +*(scope)* +fix 1, use scope as group: Body fix 1 +- Fix: #1 + +*(front-end)* +fix 2, no footer: Body fix 2 + +*(front-end)* +fix 3 and 4, no body but footer: +- Fix: #3 +- Fix: #4 + + diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml index 6512af6280..4e8bf01961 100644 --- a/.github/workflows/test-fixtures.yml +++ b/.github/workflows/test-fixtures.yml @@ -33,6 +33,7 @@ jobs: command: --latest - fixtures-name: test-commit-footers - fixtures-name: test-commit-preprocessors + - fixtures-name: test-conventional-commit - fixtures-name: test-custom-scope - fixtures-name: test-limit-commits - fixtures-name: test-skip-breaking-changes @@ -93,6 +94,8 @@ jobs: command: --bump --unreleased --with-tag-message "Some text" - fixtures-name: test-from-context command: --from-context context.json + - fixtures-name: test-from-context-does-not-discard-fields + command: --context | git cliff --from-context - - fixtures-name: test-always-render-unreleased command: --unreleased - fixtures-name: test-always-render diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index 0f349efa36..3db8d2dbb8 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -28,6 +28,7 @@ use serde::ser::{ }; use serde::{ Deserialize, + Deserializer, Serialize, }; use serde_json::value::Value; @@ -143,6 +144,10 @@ pub struct Commit<'a> { #[cfg(feature = "bitbucket")] #[deprecated(note = "Use `remote` field instead")] pub bitbucket: crate::contributor::RemoteContributor, + + /// Message of the normal commit, to avoid lossly message conversion between + /// conventional commit + pub raw_message: Option, } impl<'a> From for Commit<'a> { @@ -191,6 +196,11 @@ impl Commit<'_> { } } + /// Get raw commit message uses for converting to conventional commit + pub fn raw_message(&self) -> &str { + self.raw_message.as_deref().unwrap_or(&self.message) + } + /// Processes the commit. /// /// * converts commit to a conventional commit @@ -226,7 +236,7 @@ impl Commit<'_> { /// Returns the commit with its conventional type set. pub fn into_conventional(mut self) -> Result { match ConventionalCommit::parse(Box::leak( - self.message.to_string().into_boxed_str(), + self.raw_message().to_string().into_boxed_str(), )) { Ok(conv) => { self.conv = Some(conv); @@ -423,8 +433,9 @@ impl Serialize for Commit<'_> { } } - let mut commit = serializer.serialize_struct("Commit", 9)?; + let mut commit = serializer.serialize_struct("Commit", 20)?; commit.serialize_field("id", &self.id)?; + commit.serialize_field("raw_message", &self.raw_message())?; if let Some(conv) = &self.conv { commit.serialize_field("message", conv.description())?; commit.serialize_field("body", &conv.body())?; @@ -476,6 +487,24 @@ impl Serialize for Commit<'_> { } } +/// Deserialize commits into conventional commits if they are convertible +/// +/// Serialized commits cannot be deserialized into commits that have +/// [`Commit::conv`]. Thus, we need to manually convert them using +/// [`Commit::into_conventional`]. +/// +/// This function is only used in [`crate::release::Release::commits`] +pub(crate) fn commits_to_conventional_commits<'de, 'a, D: Deserializer<'de>>( + deserializer: D, +) -> std::result::Result>, D::Error> { + let commits = Vec::>::deserialize(deserializer)?; + let commits = commits + .into_iter() + .map(|commit| commit.clone().into_conventional().unwrap_or(commit)) + .collect(); + Ok(commits) +} + #[cfg(test)] mod test { use super::*; diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs index fec293f51f..dbf1d64568 100644 --- a/git-cliff-core/src/release.rs +++ b/git-cliff-core/src/release.rs @@ -1,3 +1,4 @@ +use crate::commit::commits_to_conventional_commits; use crate::error::Result; use crate::{ commit::Commit, @@ -34,6 +35,7 @@ pub struct Release<'a> { /// git tag's message. pub message: Option, /// Commits made for the release. + #[serde(deserialize_with = "commits_to_conventional_commits")] pub commits: Vec>, /// Commit ID of the tag. #[serde(rename = "commit_id")]