Skip to content

Commit

Permalink
feat: Support for additional template variables
Browse files Browse the repository at this point in the history
Additional template variables for the Jinja template can be passed via CLI or can be defined in a configuration file.

To pass variables via CLI, the option `-j KEY=VALUE` or `--jinja-context KEY=VALUE` can be given multiple times.

To define variables in a configuration file, the variables can be defined in a "[jinja_context]"  table.

```toml
[jinja_context]
KEY = "VALUE"
```

All values are accessible in the template through the `jinja_context` (dict) variable.

Issue-17: #17
PR-73: #73
Co-authored-by: Timothée Mazzucotelli <[email protected]>
  • Loading branch information
chme and pawamoy authored Mar 23, 2024
1 parent 156afe1 commit 58a4d88
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 21 deletions.
18 changes: 2 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ Automatic Changelog generator using Jinja2 templates. From git logs to change lo
- Parses [Git trailers][git-trailers], allowing to reference
issues, PRs, etc., in your commit messages
in a clean, provider-agnostic way.
- Template context injection,
to furthermore customize how your changelog will be rendered.

- Todo:
- [Plugin architecture][issue-19],
to support more commit conventions and git services.
- [Template context injection][issue-17],
to furthermore customize how your changelog will be rendered.
- [Easy access to "Breaking Changes"][issue-14] in the templates.
- [Commits/dates/versions range limitation ability][issue-16].

[jinja2]: http://jinja.pocoo.org/
[keep-a-changelog]: http://keepachangelog.com/en/1.0.0/
Expand All @@ -47,9 +46,6 @@ Automatic Changelog generator using Jinja2 templates. From git logs to change lo
[git-trailers]: https://git-scm.com/docs/git-interpret-trailers

[issue-14]: https://github.com/pawamoy/git-changelog/issues/14
[issue-15]: https://github.com/pawamoy/git-changelog/issues/15
[issue-16]: https://github.com/pawamoy/git-changelog/issues/16
[issue-17]: https://github.com/pawamoy/git-changelog/issues/17
[issue-19]: https://github.com/pawamoy/git-changelog/issues/19

## Installation
Expand All @@ -69,16 +65,6 @@ pipx install git-changelog

## Usage

```
git-changelog [--config-file [PATH ...]] [-b] [-B VERSION] [-h] [-i]
[-g VERSION_REGEX] [-m MARKER_LINE] [-o OUTPUT]
[-p {github,gitlab,bitbucket}] [-r] [-R] [-I INPUT]
[-c {angular,conventional,basic}] [-s SECTIONS]
[-t {angular,keepachangelog}] [-T] [-E] [-Z] [-F FILTER_COMMITS]
[-V] [--debug-info]
[REPOSITORY]
```

Simply run `git-changelog` in your repository to output a changelog on standard output.
To show the different options and their descriptions, use `git-changelog -h`.

Expand Down
102 changes: 100 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,109 @@ git-changelog --template keepachangelog
You can also write and use your own changelog templates.
Templates are single files written using the [Jinja][jinja] templating engine.
You can get inspiration from
[the source of our built-in templates](https://github.com/pawamoy/git-changelog/tree/master/src/git_changelog/templates).
[the source of our built-in templates][builtin-templates].

Prefix value passed to the `--template` option with `path:` to use a custom template:
Prefix the value passed to the `--template` option with `path:` to use a custom template:

```bash
git-changelog --template path:mytemplate.md
```

### Writing a changelog template

To write your own changelog template,
we recommend using our [keepachangelog built-in template][keepachangelog-template]
as a starting point.

From there, simply modify the different Jinja macros:

- `render_commit()`, which accepts a [Commit][git_changelog.build.Commit] object
- `render_section()`, which accepts a [Section][git_changelog.build.Section] object
- `render_version()`, which accepts a [Version][git_changelog.build.Version] object

Then, also update the template at the end, to change the changelog's header
or add a changelog footer for example.

The variables available in the template are `changelog`,
which is a [Changelog][git_changelog.build.Changelog] instance,
and `in_place`, which is a boolean, and tells whether the changelog
is being updated in-place.

> QUESTION: **How to get spacing right?**
> Although spacing (line jumps) is not super important in Markdown contents
> (it won't change HTML output), it is best if you get spacing right,
> as it makes prettier changelog files, and will reduce the noise
> in diffs when you commit an update to your changelog.
>
> To manage spacing (in Jinja terms, [control whitespace][control-whitespace])
> Jinja allows to "eat" spaces on the left or right of an expression,
> by adding a dash after/before the percent sign: `{%-` and `-%}`.
> However, spacing is not always easy to get right with Jinja,
> so here are two tips that we find helpful:
>
> - **To collapse content up**, eat spaces on the **left**, and add new lines
> at the **top** of the Jinja block:
>
> ```django
> Some text.
> {%- if some_condition %}
>
> Some content.
> {%- endif %}
> ```
>
> If the condition is true, there will be exactly one blank line
> between "Some text" and "Some content". If not, there won't be
> extreanous trailing blank lines :+1:
>
> - **To collapse content down**, eat spaces on the **right**, and add new lines
> at the **bottom** of the Jinja block:
>
> ```django
> {% if some_condition -%}
> Some content.
>
> {% endif -%}
> Some text.
> ```
>
> If the condition is true, there will be exactly one blank line
> between "Some content" and "Some text". If not, there won't be
> extreanous leading blank lines :+1:
### Extra Jinja context

[(--jinja-context)](cli.md#jinja_context)

Your custom changelog templates can support user-provided extra Jinja context.
This extra context is available in the `jinja_context` variable, which is a dictionary,
and is passed by users with the `-j`, `--jinja-context` CLI option
or with the `jinja_context` configuration option.

For example, you could let users specify their own changelog footer
by adding this at the end of your template:

```django
{% if jinja_context.footer %}
{{ jinja_context.footer }}
{% endif %}
```

Then users would be able to provide their own footer with the CLI option:

```bash
git-changelog -t path:changelog.md -j footer="Copyright 2024 My Company"
```

...or with the configuration option:

```toml
template = "path:changelog.md"

[jinja_context]
footer = "Copyright 2024 My Company"
```

## Filter commits

[(--filter-commits)](cli.md#filter_commits)
Expand Down Expand Up @@ -589,3 +684,6 @@ and `--marker-line`.
[semver]: https://semver.org/
[git-trailers]: https://git-scm.com/docs/git-interpret-trailers
[softprops/action-gh-release]: https://github.com/softprops/action-gh-release
[keepachangelog-template]: https://github.com/pawamoy/git-changelog/tree/main/src/git_changelog/templates/keepachangelog.md
[builtin-templates]: https://github.com/pawamoy/git-changelog/tree/main/src/git_changelog/templates
[control-whitespace]: https://jinja.palletsprojects.com/en/3.1.x/templates/#whitespace-control
46 changes: 43 additions & 3 deletions src/git_changelog/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
]
"""Default configuration files read by git-changelog."""

DEFAULT_SETTINGS = {
DEFAULT_SETTINGS: dict[str, Any] = {
"bump": None,
"bump_latest": None,
"convention": "basic",
Expand All @@ -70,6 +70,7 @@
"repository": ".",
"sections": None,
"template": "keepachangelog",
"jinja_context": {},
"version_regex": DEFAULT_VERSION_REGEX,
"zerover": True,
}
Expand Down Expand Up @@ -100,6 +101,22 @@ def _comma_separated_list(value: str) -> list[str]:
return value.split(",")


class _ParseDictAction(argparse.Action):
def __call__(
self,
parser: argparse.ArgumentParser, # noqa: ARG002
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | Sequence[Any] | None = None, # noqa: ARG002
):
attribute = getattr(namespace, self.dest)
if not isinstance(attribute, dict):
setattr(namespace, self.dest, {})
if isinstance(values, str):
key, value = values.split("=", 1)
getattr(namespace, self.dest)[key] = value


providers: dict[str, type[ProviderRefParser]] = {
"github": GitHub,
"gitlab": GitLab,
Expand Down Expand Up @@ -333,6 +350,15 @@ def get_parser() -> argparse.ArgumentParser:
dest="filter_commits",
help="The Git revision-range filter to use (e.g. `v1.2.0..`). Default: no filter.",
)
parser.add_argument(
"-j",
"--jinja-context",
action=_ParseDictAction,
metavar="KEY=VALUE",
dest="jinja_context",
help="Pass additional key/value pairs to the template. Option can be used multiple times. "
"The key/value pairs are accessible as 'jinja_context' in the template.",
)
parser.add_argument(
"-V",
"--version",
Expand Down Expand Up @@ -458,11 +484,16 @@ def parse_settings(args: list[str] | None = None) -> dict:
elif str(config_file).strip().lower() in ("yes", "default", "on", "true", "1"):
config_file = DEFAULT_CONFIG_FILES

jinja_context = explicit_opts_dict.pop("jinja_context", {})

settings = read_config(config_file)

# CLI arguments override the config file settings
settings.update(explicit_opts_dict)

# Merge jinja context, CLI values override config file values.
settings["jinja_context"].update(jinja_context)

# TODO: remove at some point
if "bump_latest" in explicit_opts_dict:
warnings.warn("`--bump-latest` is deprecated in favor of `--bump=auto`", FutureWarning, stacklevel=1)
Expand All @@ -487,6 +518,7 @@ def build_and_render(
bump: str | None = None,
zerover: bool = True, # noqa: FBT001,FBT002
filter_commits: str | None = None,
jinja_context: dict[str, Any] | None = None,
) -> tuple[Changelog, str]:
"""Build a changelog and render it.
Expand All @@ -511,6 +543,7 @@ def build_and_render(
bump: Whether to try and bump to a given version.
zerover: Keep major version at zero, even for breaking changes.
filter_commits: The Git revision-range used to filter commits in git-log.
jinja_context: Key/value pairs passed to the Jinja template.
Raises:
ValueError: When some arguments are incompatible or missing.
Expand Down Expand Up @@ -593,7 +626,14 @@ def build_and_render(
)

# render new entries
rendered = jinja_template.render(changelog=changelog, in_place=True).rstrip("\n") + "\n"
rendered = (
jinja_template.render(
changelog=changelog,
jinja_context=jinja_context,
in_place=True,
).rstrip("\n")
+ "\n"
)

# find marker line(s) in current changelog
marker = lines.index(marker_line)
Expand All @@ -612,7 +652,7 @@ def build_and_render(

# overwrite output file
else:
rendered = jinja_template.render(changelog=changelog)
rendered = jinja_template.render(changelog=changelog, jinja_context=jinja_context)

# write result in specified output
if output is sys.stdout:
Expand Down
45 changes: 45 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import os
import sys
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Iterator

import pytest
Expand All @@ -19,6 +20,8 @@
if TYPE_CHECKING:
from pathlib import Path

from tests.helpers import GitRepo


if sys.version_info >= (3, 11):
from contextlib import chdir
Expand Down Expand Up @@ -212,3 +215,45 @@ def test_show_debug_info(capsys: pytest.CaptureFixture) -> None:
assert "system" in captured
assert "environment" in captured
assert "packages" in captured


# IMPORTANT: See top module comment.
def test_jinja_context(repo: GitRepo) -> None:
"""Render template with custom template variables.
Parameters:
repo: Temporary Git repository (fixture).
"""
repo.path.joinpath("conf.toml").write_text(
dedent(
"""[jinja_context]
k1 = "ignored"
k2 = "v2"
k3 = "v3"
""",
),
)

template = repo.path.joinpath(".custom_template.md.jinja")
template.write_text("{% for key, val in jinja_context.items() %}{{ key }} = {{ val }}\n{% endfor %}")

exit_code = cli.main(
[
"--config-file",
str(repo.path / "conf.toml"),
"-o",
str(repo.path / "CHANGELOG.md"),
"-t",
f"path:{template}",
"--jinja-context",
"k1=v1",
"-j",
"k3=v3",
str(repo.path),
],
)

assert exit_code == 0

contents = repo.path.joinpath("CHANGELOG.md").read_text()
assert contents == "k1 = v1\nk2 = v2\nk3 = v3\n"

0 comments on commit 58a4d88

Please sign in to comment.