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

Commit footers have values but no names #96

Closed
hawkw opened this issue Jun 18, 2022 · 3 comments · Fixed by #97
Closed

Commit footers have values but no names #96

hawkw opened this issue Jun 18, 2022 · 3 comments · Fixed by #97
Assignees
Labels
bug Something isn't working

Comments

@hawkw
Copy link
Contributor

hawkw commented Jun 18, 2022

Describe the bug

When a conventional commit has footers (such as Signed-off-by: Someone <email>), the footers array provided to the changelog template contains the value (the part after the :) but not the name of the footer. This makes it difficult to implement any kind of special templating based on the name of the footer. For example, if I want to add Signed-off-by emails to a changelog entry, but not Reviewed-by, it's currently impossible to determine what commit footer that value came from.

To Reproduce

In a repository where commits contain multiple footers with different names, create a template with {{ commit.footers }} in it somewhere. Note that only the values of the footer are output.

Expected behavior

The entire footer (including the name) should be exposed to the template. This could either be by making each entry in the commit.footers array be the entire footer value, including both the name and the value after the colon, or by changing commit.footers from an array to a map of footer names to footer values.

System

  • OS Information:

    :; uname -a
    Linux noctis 5.17.3 #1-NixOS SMP PREEMPT Wed Apr 13 17:27:43 UTC 2022 x86_64 GNU/Linux
    
  • Project Version:

    :; git-cliff --version
    git-cliff 0.7.0
    

Additional context

While looking at the code for serializing commits as JSON, I noticed that the Serialize impl maps over the list of footers and calls f.value():

&conv
.footers()
.to_vec()
.iter()
.map(|f| f.value())
.collect::<Vec<&str>>(),

Although I'm not familiar with the git_conventional crate's API, my assumption is that this is discarding the footer's name. :)

I'm happy to open a PR changing this behavior, which I imagine would be fairly straightforward. The one design decision is what we should output instead: is it preferable to emit the footers as a map of footer name to footer value (which could break existing templates, but seems nicer), or as an array of the footer's name and value as a single string? Or, is there some other approach that I haven't thought of?

@hawkw hawkw added the bug Something isn't working label Jun 18, 2022
@orhun
Copy link
Owner

orhun commented Jun 23, 2022

Hello, thanks for submitting this issue and the detailed explanation! 🐻

I would like to go over the possible options that you suggested and talk about the pros and cons.

1. Single string solution

According to git_conventional API, footers consist of 3 fields:

pub struct Footer<'a> {
    token: FooterToken<'a>,
    sep: FooterSeparator,
    value: &'a str,
}

(Note that all the fields are private.)

So doing this would be enough:

commit.serialize_field(
    "footers",
    &conv
        .footers()
        .to_vec()
        .iter()
        .map(|f| {
            format!("{}{} {}", f.token(), f.separator(), f.value())
        })
        .collect::<Vec<String>>(),
)?;

Although this looks like a feasible solution and the context is compatible with the previous versions of git-cliff, I think that we lose a bit of flexibility here. You made a good point about using different part of footer values. It would be nice if we could use footers as-is and access the fields by e.g. footer.token, footer.value, etc. However, as I pointed out, git_conventional::Footer fields are private. So we need to define our own Footer type and serialize it.

2. Footer object method

Simply define the following struct:

/// Commit footer.
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Footer {
	/// Token of the footer.
	pub token:     String,
	/// The separator between the footer token and its value.
	pub separator: String,
	/// The value of the footer.
	pub value:     String,
	/// A flag to signal that the footer describes a breaking change.
	pub breaking:  bool,
}

impl From<git_conventional::Footer<'_>> for Footer {
	fn from(f: git_conventional::Footer) -> Self {
		Self {
			token:     f.token().to_string(),
			separator: f.separator().to_string(),
			value:     f.value().to_string(),
			breaking:  f.breaking(),
		}
	}
}

Then serialize this type instead:

commit.serialize_field(
    "footers",
    &conv
        .footers()
        .to_vec()
        .into_iter()
        .map(Footer::from)
        .collect::<Vec<Footer>>(),
)?;

It can easily be used in templates like this:

{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | upper_first }}
    {% for commit in commits %}
        - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}
         {% for footer in commit.footers %}\
            \t- {{ footer.token }}{{ footer.separator }} {{ footer.value }}
        {% endfor %}\
    {% endfor %}
{% endfor %}\n

The drawback is it is not backwards compatible and if you try to use commit.footers directly you will get [object] instead of a string. But it is a good compromise for increasing the templating flexibility I would say.


I'm happy to open a PR changing this behavior, which I imagine would be fairly straightforward. The one design decision is what we should output instead: is it preferable to emit the footers as a map of footer name to footer value (which could break existing templates, but seems nicer), or as an array of the footer's name and value as a single string? Or, is there some other approach that I haven't thought of?

I think the Footer object approach is the way to go. Feel free to share your thoughts and move on with the PR! 🐻

@hawkw
Copy link
Contributor Author

hawkw commented Jun 23, 2022

I think the Footer object approach is the way to go. Feel free to share your thoughts and move on with the PR! 🐻

That's what I thought, too, thanks for the confirmation. The breaking change is kind of unfortunate, though, so I wanted to ask.

I suppose a third option is to keep the existing commit.footers, and add a new commit.named_footers (or something) that includes the keys and values. IMO, this is a bit more awkward, but it wouldn't break existing templates.

@orhun
Copy link
Owner

orhun commented Jun 23, 2022

I suppose a third option is to keep the existing commit.footers, and add a new commit.named_footers (or something) that includes the keys and values. IMO, this is a bit more awkward, but it wouldn't break existing templates.

I would say that commit.footers would be more appropriate and also it feels like it is how it should have been implemented in the beginning. Surprisingly, no one caught this until now!

hawkw added a commit to hawkw/git-cliff that referenced this issue Jun 24, 2022
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.
@orhun orhun closed this as completed in #97 Jul 12, 2022
orhun added a commit that referenced this issue Jul 12, 2022
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants