Skip to content

Commit 7306fa9

Browse files
noirbizarreLee-W
authored andcommitted
feat(template): allow to override the template from cli, configuration and plugins
Fixes #132 Fixes #384 Fixes #433 Closes #376 Closes #640
1 parent eca64aa commit 7306fa9

15 files changed

+606
-12
lines changed

commitizen/changelog.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@
3232
from datetime import date
3333
from typing import TYPE_CHECKING, Callable, Iterable, cast
3434

35-
from jinja2 import Environment, PackageLoader
35+
from jinja2 import (
36+
BaseLoader,
37+
ChoiceLoader,
38+
Environment,
39+
FileSystemLoader,
40+
PackageLoader,
41+
)
3642

3743
from commitizen import out
3844
from commitizen.bump import normalize_tag
@@ -49,6 +55,8 @@
4955
if TYPE_CHECKING:
5056
from commitizen.version_schemes import VersionScheme
5157

58+
DEFAULT_TEMPLATE = "keep_a_changelog_template.j2"
59+
5260

5361
def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None:
5462
return next((tag for tag in tags if tag.rev == commit.rev), None)
@@ -187,11 +195,18 @@ def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterab
187195
return sorted_tree
188196

189197

190-
def render_changelog(tree: Iterable) -> str:
191-
loader = PackageLoader("commitizen", "templates")
198+
def render_changelog(
199+
tree: Iterable,
200+
loader: BaseLoader | None = None,
201+
template: str | None = None,
202+
**kwargs,
203+
) -> str:
204+
loader = ChoiceLoader(
205+
[FileSystemLoader("."), loader or PackageLoader("commitizen", "templates")]
206+
)
192207
env = Environment(loader=loader, trim_blocks=True)
193-
jinja_template = env.get_template("keep_a_changelog_template.j2")
194-
changelog: str = jinja_template.render(tree=tree)
208+
jinja_template = env.get_template(template or DEFAULT_TEMPLATE)
209+
changelog: str = jinja_template.render(tree=tree, **kwargs)
195210
return changelog
196211

197212

commitizen/cli.py

+48
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import logging
55
import sys
6+
from copy import deepcopy
67
from functools import partial
78
from pathlib import Path
89
from types import TracebackType
@@ -20,6 +21,51 @@
2021
)
2122

2223
logger = logging.getLogger(__name__)
24+
25+
26+
class ParseKwargs(argparse.Action):
27+
"""
28+
Parse arguments in the for `key=value`.
29+
30+
Quoted strings are automatically unquoted.
31+
Can be submitted multiple times:
32+
33+
ex:
34+
-k key=value -k double-quotes="value" -k single-quotes='value'
35+
36+
will result in
37+
38+
namespace["opt"] == {
39+
"key": "value",
40+
"double-quotes": "value",
41+
"single-quotes": "value",
42+
}
43+
"""
44+
45+
def __call__(self, parser, namespace, kwarg, option_string=None):
46+
kwargs = getattr(namespace, self.dest, None) or {}
47+
key, value = kwarg.split("=", 1)
48+
kwargs[key] = value.strip("'\"")
49+
setattr(namespace, self.dest, kwargs)
50+
51+
52+
tpl_arguments = (
53+
{
54+
"name": ["--template", "-t"],
55+
"help": (
56+
"changelog template file name "
57+
"(relative to the current working directory)"
58+
),
59+
},
60+
{
61+
"name": ["--extra", "-e"],
62+
"action": ParseKwargs,
63+
"dest": "extras",
64+
"metavar": "EXTRA",
65+
"help": "a changelog extra variable (in the form 'key=value')",
66+
},
67+
)
68+
2369
data = {
2470
"prog": "cz",
2571
"description": (
@@ -210,6 +256,7 @@
210256
"default": None,
211257
"help": "keep major version at zero, even for breaking changes",
212258
},
259+
*deepcopy(tpl_arguments),
213260
{
214261
"name": ["--prerelease-offset"],
215262
"type": int,
@@ -299,6 +346,7 @@
299346
"default": None,
300347
"choices": version_schemes.KNOWN_SCHEMES,
301348
},
349+
*deepcopy(tpl_arguments),
302350
],
303351
},
304352
{

commitizen/commands/bump.py

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self, config: BaseConfig, arguments: dict):
5151
"annotated_tag",
5252
"major_version_zero",
5353
"prerelease_offset",
54+
"template",
5455
]
5556
if arguments[key] is not None
5657
},
@@ -77,6 +78,8 @@ def __init__(self, config: BaseConfig, arguments: dict):
7778
self.scheme = get_version_scheme(
7879
self.config, arguments["version_scheme"] or deprecated_version_type
7980
)
81+
self.template = arguments["template"] or self.config.settings.get("template")
82+
self.extras = arguments["extras"]
8083

8184
def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool:
8285
"""Check if reading the whole git tree up to HEAD is needed."""
@@ -273,6 +276,8 @@ def __call__(self): # noqa: C901
273276
"unreleased_version": new_tag_version,
274277
"incremental": True,
275278
"dry_run": dry_run,
279+
"template": self.template,
280+
"extras": self.extras,
276281
},
277282
)
278283
changelog_cmd()

commitizen/commands/changelog.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ def __init__(self, config: BaseConfig, args):
6565
"merge_prerelease"
6666
) or self.config.settings.get("changelog_merge_prerelease")
6767

68+
self.template = args.get("template") or self.config.settings.get("template")
69+
self.extras = args.get("extras") or {}
70+
6871
def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str:
6972
"""Try to find the 'start_rev'.
7073
@@ -183,7 +186,13 @@ def __call__(self):
183186
)
184187
if self.change_type_order:
185188
tree = changelog.order_changelog_tree(tree, self.change_type_order)
186-
changelog_out = changelog.render_changelog(tree)
189+
190+
extras = self.cz.template_extras.copy()
191+
extras.update(self.config.settings["extras"])
192+
extras.update(self.extras)
193+
changelog_out = changelog.render_changelog(
194+
tree, loader=self.cz.template_loader, template=self.template, **extras
195+
)
187196
changelog_out = changelog_out.lstrip("\n")
188197

189198
if self.dry_run:

commitizen/cz/base.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

33
from abc import ABCMeta, abstractmethod
4-
from typing import Callable
4+
from typing import Any, Callable
55

6+
from jinja2 import BaseLoader
67
from prompt_toolkit.styles import Style, merge_styles
78

89
from commitizen import git
10+
from commitizen.changelog import DEFAULT_TEMPLATE
911
from commitizen.config.base_config import BaseConfig
1012
from commitizen.defaults import Questions
1113

@@ -43,6 +45,10 @@ class BaseCommitizen(metaclass=ABCMeta):
4345
# Executed only at the end of the changelog generation
4446
changelog_hook: Callable[[str, str | None], str] | None = None
4547

48+
template: str = DEFAULT_TEMPLATE
49+
template_loader: BaseLoader | None = None
50+
template_extras: dict[str, Any] = {}
51+
4652
def __init__(self, config: BaseConfig):
4753
self.config = config
4854
if not self.config.settings.get("style"):

commitizen/defaults.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class Settings(TypedDict, total=False):
5252
post_bump_hooks: list[str] | None
5353
prerelease_offset: int
5454
encoding: str
55-
always_signoff: bool
55+
template: str | None
56+
extras: dict[str, Any]
5657

5758

5859
name: str = "cz_conventional_commits"
@@ -94,6 +95,8 @@ class Settings(TypedDict, total=False):
9495
"prerelease_offset": 0,
9596
"encoding": encoding,
9697
"always_signoff": False,
98+
"template": None,
99+
"extras": {},
97100
}
98101

99102
MAJOR = "MAJOR"
@@ -125,3 +128,4 @@ class Settings(TypedDict, total=False):
125128
bump_message = "bump: version $current_version → $new_version"
126129

127130
commit_parser = r"^((?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):\s(?P<message>.*)?" # noqa
131+
version_parser = r"(?P<version>([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?(\w+)?)"

docs/bump.md

+20
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog]
5858
[--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}]
5959
[--check-consistency] [--annotated-tag] [--gpg-sign]
6060
[--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero]
61+
[--template TEMPLATE] [--extra EXTRA]
6162
[MANUAL_VERSION]
6263

6364
positional arguments:
@@ -99,6 +100,10 @@ options:
99100
--version-scheme {pep440,semver}
100101
choose version scheme
101102

103+
--template TEMPLATE, -t TEMPLATE
104+
changelog template file name (relative to the current working directory)
105+
--extra EXTRA, -e EXTRA
106+
a changelog extra variable (in the form 'key=value')
102107
```
103108
104109
### `--files-only`
@@ -250,6 +255,21 @@ Can I transition from one to the other?
250255
251256
Yes, you shouldn't have any issues.
252257
258+
### `--template`
259+
260+
Provides your own changelog jinja template.
261+
See [the template customization section](customization.md#customizing-the-changelog-template)
262+
263+
### `--extra`
264+
265+
Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter.
266+
267+
```bash
268+
cz bump --changelog --extra key=value -e short="quoted value"
269+
```
270+
271+
See [the template customization section](customization.md#customizing-the-changelog-template).
272+
253273
## Avoid raising errors
254274
255275
Some situations from commitizen raise an exit code different than 0.

docs/changelog.md

+21
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ update_changelog_on_bump = true
1414
```bash
1515
$ cz changelog --help
1616
usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--unreleased-version UNRELEASED_VERSION] [--incremental] [--start-rev START_REV]
17+
[--template TEMPLATE] [--extra EXTRA]
1718
[rev_range]
1819

1920
positional arguments:
@@ -31,6 +32,11 @@ optional arguments:
3132
start rev of the changelog. If not set, it will generate changelog from the start
3233
--merge-prerelease
3334
collect all changes from prereleases into next non-prerelease. If not set, it will include prereleases in the changelog
35+
start rev of the changelog.If not set, it will generate changelog from the start
36+
--template TEMPLATE, -t TEMPLATE
37+
changelog template file name (relative to the current working directory)
38+
--extra EXTRA, -e EXTRA
39+
a changelog extra variable (in the form 'key=value')
3440
```
3541
3642
### Examples
@@ -186,6 +192,21 @@ cz changelog --merge-prerelease
186192
changelog_merge_prerelease = true
187193
```
188194
195+
### `template`
196+
197+
Provides your own changelog jinja template by using the `template` settings or the `--template` parameter.
198+
See [the template customization section](customization.md#customizing-the-changelog-template)
199+
200+
### `extras`
201+
202+
Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter.
203+
204+
```bash
205+
cz changelog --extra key=value -e short="quoted value"
206+
```
207+
208+
See [the template customization section](customization.md#customizing-the-changelog-template)
209+
189210
## Hooks
190211
191212
Supported hook methods:

docs/config.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Type: `int`
166166

167167
Default: `0`
168168

169-
In some circumstances, a prerelease cannot start with a 0, e.g. in an embedded project individual characters are encoded as bytes. This can be done by specifying an offset from which to start counting. [prerelease-offset] |
169+
In some circumstances, a prerelease cannot start with a 0, e.g. in an embedded project individual characters are encoded as bytes. This can be done by specifying an offset from which to start counting. [prerelease-offset]
170170

171171
### `pre_bump_hooks`
172172

@@ -192,6 +192,22 @@ Default: `utf-8`
192192

193193
Sets the character encoding to be used when parsing commit messages. [Read more][encoding]
194194

195+
### `template`
196+
197+
Type: `str`
198+
199+
Default: `None` (provided by plugin)
200+
201+
Provide custom changelog jinja template path relative to the current working directory. [Read more][template-customization]
202+
203+
### `extras`
204+
205+
Type: `dict[str, Any]`
206+
207+
Default: `{}`
208+
209+
Provide extra variables to the changelog template. [Read more][template-customization]
210+
195211
## Configuration file
196212

197213
### pyproject.toml or .cz.toml
@@ -364,5 +380,6 @@ setup(
364380
[additional-features]: https://github.com/tmbo/questionary#additional-features
365381
[customization]: customization.md
366382
[shortcuts]: customization.md#shortcut-keys
383+
[template-customization]: customization.md#customizing-the-changelog-template
367384
[annotated-tags-vs-lightweight]: https://stackoverflow.com/a/11514139/2047185
368385
[encoding]: tutorials/writing_commits.md#writing-commits

0 commit comments

Comments
 (0)