Skip to content

Commit

Permalink
fix(commit): pass footer token and separator to template
Browse files Browse the repository at this point in the history
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 orhun#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.
  • Loading branch information
hawkw committed Jun 24, 2022
1 parent 175f7d7 commit 93779b2
Showing 1 changed file with 107 additions and 9 deletions.
116 changes: 107 additions & 9 deletions git-cliff-core/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <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<&GitCommit<'a>> for Commit<'a> {
fn from(commit: &GitCommit<'a>) -> Self {
Self::new(
Expand Down Expand Up @@ -208,28 +226,45 @@ 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
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 @@ -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::*;
Expand Down Expand Up @@ -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 \
<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

0 comments on commit 93779b2

Please sign in to comment.