mirror of
https://github.com/orhun/git-cliff.git
synced 2025-01-05 15:58:24 +03:00
feat(commit)!: pass footer token and separator to template (#97)
* 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. * docs(README): discuss footers in README Signed-off-by: Eliza Weisman <eliza@buoyant.io> * docs(examples): Add footers to `detailed.toml` Signed-off-by: Eliza Weisman <eliza@buoyant.io> * refac(commit): address review feedback Signed-off-by: Eliza Weisman <eliza@buoyant.io> * docs(README): address README review feedback Signed-off-by: Eliza Weisman <eliza@buoyant.io> * refactor(example): update detailed example about newline issues * test(fixture): add test fixture for commit footers Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
parent
22891992a2
commit
0bf499ec94
30
.github/fixtures/test-commit-footers/cliff.toml
vendored
Normal file
30
.github/fixtures/test-commit-footers/cliff.toml
vendored
Normal file
@ -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
.github/fixtures/test-commit-footers/commit.sh
vendored
Executable file
9
.github/fixtures/test-commit-footers/commit.sh
vendored
Executable file
@ -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
.github/fixtures/test-commit-footers/expected.md
vendored
Normal file
18
.github/fixtures/test-commit-footers/expected.md
vendored
Normal file
@ -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
README.md
30
README.md
@ -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,
|
||||
@ -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.:
|
||||
@ -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**
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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(
|
||||
@ -208,6 +237,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<Item = Footer<'_>> {
|
||||
self.conv
|
||||
.iter()
|
||||
.flat_map(|conv| conv.footers().iter().map(Footer::from))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Commit<'_> {
|
||||
@ -215,21 +254,29 @@ impl Serialize for Commit<'_> {
|
||||
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()),
|
||||
@ -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![
|
||||
|
Loading…
Reference in New Issue
Block a user