Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commit)!: pass footer token and separator to template #97

Merged
merged 7 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/fixtures/test-commit-footers/cliff.toml
Original file line number Diff line number Diff line change
@@ -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 = """
<!-- generated by git-cliff -->
"""
9 changes: 9 additions & 0 deletions .github/fixtures/test-commit-footers/commit.sh
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 18 additions & 0 deletions .github/fixtures/test-commit-footers/expected.md
Original file line number Diff line number Diff line change
@@ -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

<!-- generated by git-cliff -->
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,14 @@ following context is generated to use for templating:
"scope": "[scope]",
"message": "<description>",
"body": "[body]",
"footers": ["[footer]", "[footer]"],
"footers": [
{
"token": "<name of the footer, such as 'Signed-off-by'>",
"separator": "<the separator between the token and value, such as ':'>",
"value": "<the value following the separator",
"breaking": false
}
],
"breaking_description": "<description>",
"breaking": false,
"conventional": true,
Expand All @@ -684,6 +691,24 @@ 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_](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 <user.email@example.com>`
- `Reviewed-by: User Name <user.email@example.com>`
- `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

##### Breaking Changes

`breaking` flag is set to `true` when the commit has an exclamation mark after the commit type and scope, e.g.:
Expand All @@ -702,6 +727,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**
Expand Down
3 changes: 3 additions & 0 deletions examples/detailed.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ body = """
### {{ 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
"""
Expand Down
117 changes: 108 additions & 9 deletions git-cliff-core/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ 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: <user.name>` 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<&'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(
Expand Down Expand Up @@ -208,28 +237,46 @@ 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<Item = Footer<'_>> {
self.conv
.iter()
.flat_map(|conv| conv.footers().iter().map(Footer::from))
}
}

impl Serialize for Commit<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
/// A wrapper to serialize commit footers from an iterator using
/// `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<S>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
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::<Vec<&str>>(),
)?;
commit.serialize_field("footers", &SerializeFooters(self))?;
commit.serialize_field(
"group",
self.group.as_ref().unwrap_or(&conv.type_().to_string()),
Expand Down Expand Up @@ -304,6 +351,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 \
<test@example.com>",
),
),
vec![Footer {
token: "Signed-off-by",
separator: ":",
value: "Test User <test@example.com>",
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 <test@example.com>",
),
),
vec![
Footer {
token: "BREAKING CHANGE",
separator: ":",
value: "This commit breaks stuff",
breaking: true,
},
Footer {
token: "Signed-off-by",
separator: ":",
value: "Test User <test@example.com>",
breaking: false,
},
],
),
];
for (commit, footers) in &test_cases {
let commit = commit.process(&cfg).expect("commit should process");
assert_eq!(&commit.footers().collect::<Vec<_>>(), footers);
}
}

#[test]
fn parse_link() {
let test_cases = vec![
Expand Down