Skip to content

Commit 2f63807

Browse files
committed
feat(template): allow to override the template from cli, configuration and plugins
Fixes #132 Fixes #384 Fixes #433 Closes #376
1 parent 61c3024 commit 2f63807

15 files changed

+508
-9
lines changed

Diff for: commitizen/changelog.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@
3131
from datetime import date
3232
from typing import Callable, Dict, Iterable, List, Optional, Tuple
3333

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

3642
from commitizen import defaults
3743
from commitizen.bump import normalize_tag
3844
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
3945
from commitizen.git import GitCommit, GitTag
4046

47+
DEFAULT_TEMPLATE = "keep_a_changelog_template.j2"
48+
4149

4250
def get_commit_tag(commit: GitCommit, tags: List[GitTag]) -> Optional[GitTag]:
4351
return next((tag for tag in tags if tag.rev == commit.rev), None)
@@ -139,11 +147,18 @@ def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterab
139147
return sorted_tree
140148

141149

142-
def render_changelog(tree: Iterable) -> str:
143-
loader = PackageLoader("commitizen", "templates")
150+
def render_changelog(
151+
tree: Iterable,
152+
loader: Optional[BaseLoader] = None,
153+
template: Optional[str] = None,
154+
**kwargs,
155+
) -> str:
156+
loader = ChoiceLoader(
157+
[FileSystemLoader("."), loader or PackageLoader("commitizen", "templates")]
158+
)
144159
env = Environment(loader=loader, trim_blocks=True)
145-
jinja_template = env.get_template("keep_a_changelog_template.j2")
146-
changelog: str = jinja_template.render(tree=tree)
160+
jinja_template = env.get_template(template or DEFAULT_TEMPLATE)
161+
changelog: str = jinja_template.render(tree=tree, **kwargs)
147162
return changelog
148163

149164

Diff for: commitizen/cli.py

+28
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,20 @@
190190
"default": None,
191191
"help": "keep major version at zero, even for breaking changes",
192192
},
193+
{
194+
"name": ["--template", "-t"],
195+
"help": (
196+
"changelog template file name "
197+
"(relative to the current working directory)"
198+
),
199+
},
200+
{
201+
"name": ["--extra", "-e"],
202+
"action": "append",
203+
"dest": "extras",
204+
"metavar": "EXTRA",
205+
"help": "a changelog extra variable (in the form 'key=value')",
206+
},
193207
{
194208
"name": "manual_version",
195209
"type": str,
@@ -246,6 +260,20 @@
246260
"If not set, it will generate changelog from the start"
247261
),
248262
},
263+
{
264+
"name": ["--template", "-t"],
265+
"help": (
266+
"changelog template file name "
267+
"(relative to the current working directory)"
268+
),
269+
},
270+
{
271+
"name": ["--extra", "-e"],
272+
"action": "append",
273+
"dest": "extras",
274+
"metavar": "EXTRA",
275+
"help": "a changelog extra variable (in the form 'key=value')",
276+
},
249277
],
250278
},
251279
{

Diff for: commitizen/commands/bump.py

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(self, config: BaseConfig, arguments: dict):
4646
"gpg_sign",
4747
"annotated_tag",
4848
"major_version_zero",
49+
"template",
4950
]
5051
if arguments[key] is not None
5152
},
@@ -58,6 +59,8 @@ def __init__(self, config: BaseConfig, arguments: dict):
5859
self.no_verify = arguments["no_verify"]
5960
self.check_consistency = arguments["check_consistency"]
6061
self.retry = arguments["retry"]
62+
self.template = arguments["template"] or self.config.settings.get("template")
63+
self.extras = arguments["extras"]
6164

6265
def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool:
6366
"""Check if reading the whole git tree up to HEAD is needed."""
@@ -244,6 +247,8 @@ def __call__(self): # noqa: C901
244247
"unreleased_version": new_tag_version,
245248
"incremental": True,
246249
"dry_run": dry_run,
250+
"template": self.template,
251+
"extras": self.extras,
247252
},
248253
)
249254
changelog_cmd()

Diff for: commitizen/commands/changelog.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def __init__(self, config: BaseConfig, args):
4949
self.tag_format = args.get("tag_format") or self.config.settings.get(
5050
"tag_format"
5151
)
52+
self.template = args.get("template") or self.config.settings.get("template")
53+
self.extras = dict(
54+
extra.split("=") for extra in args.get("extras") or [] if "=" in extra
55+
)
5256

5357
def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
5458
"""Try to find the 'start_rev'.
@@ -161,7 +165,13 @@ def __call__(self):
161165
)
162166
if self.change_type_order:
163167
tree = changelog.order_changelog_tree(tree, self.change_type_order)
164-
changelog_out = changelog.render_changelog(tree)
168+
169+
extras = self.cz.template_extras.copy()
170+
extras.update(self.config.settings["extras"])
171+
extras.update(self.extras)
172+
changelog_out = changelog.render_changelog(
173+
tree, loader=self.cz.template_loader, template=self.template, **extras
174+
)
165175
changelog_out = changelog_out.lstrip("\n")
166176

167177
if self.dry_run:

Diff for: commitizen/cz/base.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
from __future__ import annotations
2+
13
from abc import ABCMeta, abstractmethod
2-
from typing import Callable, Dict, List, Optional, Tuple
4+
from typing import Any, Callable, Dict, List, Optional, Tuple
35

6+
from jinja2 import BaseLoader
47
from prompt_toolkit.styles import Style, merge_styles
58

69
from commitizen import git
10+
from commitizen.changelog import DEFAULT_TEMPLATE
711
from commitizen.config.base_config import BaseConfig
812
from commitizen.defaults import Questions
913

@@ -40,6 +44,10 @@ class BaseCommitizen(metaclass=ABCMeta):
4044
# Executed only at the end of the changelog generation
4145
changelog_hook: Optional[Callable[[str, Optional[str]], str]] = None
4246

47+
template: str = DEFAULT_TEMPLATE
48+
template_loader: Optional[BaseLoader] = None
49+
template_extras: dict[str, Any] = {}
50+
4351
def __init__(self, config: BaseConfig):
4452
self.config = config
4553
if not self.config.settings.get("style"):

Diff for: commitizen/defaults.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import pathlib
24
from collections import OrderedDict
35
from typing import Any, Dict, Iterable, List, MutableMapping, Optional, Tuple, Union
@@ -40,6 +42,8 @@ class Settings(TypedDict, total=False):
4042
style: Optional[List[Tuple[str, str]]]
4143
customize: CzSettings
4244
major_version_zero: bool
45+
template: Optional[str]
46+
extras: dict[str, Any]
4347

4448

4549
name: str = "cz_conventional_commits"
@@ -65,6 +69,8 @@ class Settings(TypedDict, total=False):
6569
"update_changelog_on_bump": False,
6670
"use_shortcuts": False,
6771
"major_version_zero": False,
72+
"template": None,
73+
"extras": {},
6874
}
6975

7076
MAJOR = "MAJOR"

Diff for: docs/bump.md

+16
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog]
6060
[--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}]
6161
[--check-consistency] [--annotated-tag] [--gpg-sign]
6262
[--changelog-to-stdout] [--retry] [--major-version-zero]
63+
[--template TEMPLATE] [--extra EXTRA]
6364
[MANUAL_VERSION]
6465

6566
positional arguments:
@@ -95,6 +96,10 @@ options:
9596
Output changelog to the stdout
9697
--retry retry commit if it fails the 1st time
9798
--major-version-zero keep major version at zero, even for breaking changes
99+
--template TEMPLATE, -t TEMPLATE
100+
changelog template file name (relative to the current working directory)
101+
--extra EXTRA, -e EXTRA
102+
a changelog extra variable (in the form 'key=value')
98103
```
99104
100105
### `--files-only`
@@ -213,6 +218,17 @@ We recommend setting `major_version_zero = true` in your configuration file whil
213218
is in its initial development. Remove that configuration using a breaking-change commit to bump
214219
your project’s major version to `v1.0.0` once your project has reached maturity.
215220
221+
### `--template`
222+
223+
Provides your own changelog jinja template.
224+
See [the template customization section](customization.md#customizing-the-changelog-template)
225+
226+
### `--extra`
227+
228+
Provides your own changelog extra variables by using the `extras` settings or the `--extra` parameter.
229+
See [the template customization section](customization.md#customizing-the-changelog-template)
230+
231+
216232
## Avoid raising errors
217233
218234
Some situations from commitizen rise an exit code different than 0.

Diff for: docs/changelog.md

+15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This command will generate a changelog following the committing rules establishe
77
```bash
88
$ cz changelog --help
99
usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--unreleased-version UNRELEASED_VERSION] [--incremental] [--start-rev START_REV]
10+
[--template TEMPLATE] [--extra EXTRA]
1011
[rev_range]
1112

1213
positional arguments:
@@ -22,6 +23,10 @@ optional arguments:
2223
--incremental generates changelog from last created version, useful if the changelog has been manually modified
2324
--start-rev START_REV
2425
start rev of the changelog.If not set, it will generate changelog from the start
26+
--template TEMPLATE, -t TEMPLATE
27+
changelog template file name (relative to the current working directory)
28+
--extra EXTRA, -e EXTRA
29+
a changelog extra variable (in the form 'key=value')
2530
```
2631
2732
### Examples
@@ -161,6 +166,16 @@ cz changelog --start-rev="v0.2.0"
161166
changelog_start_rev = "v0.2.0"
162167
```
163168
169+
### `template`
170+
171+
Provides your own changelog jinja template by using the `template` settings or the `--template` parameter.
172+
See [the template customization section](customization.md#customizing-the-changelog-template)
173+
174+
### `extras`
175+
176+
Provides your own changelog extra variables by using the `extras` settings or the `--extra` parameter.
177+
See [the template customization section](customization.md#customizing-the-changelog-template)
178+
164179
## Hooks
165180
166181
Supported hook methods:

Diff for: docs/config.md

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
| `customize` | `dict` | `None` | **This is only supported when config through `toml`.** Custom rules for committing and bumping. [See more][customization] |
2121
| `use_shortcuts` | `bool` | `false` | If enabled, commitizen will show keyboard shortcuts when selecting from a list. Define a `key` for each of your choices to set the key. [See more][shortcuts] |
2222
| `major_version_zero` | `bool` | `false` | When true, breaking changes on a `0.x` will remain as a `0.x` version. On `false`, a breaking change will bump a `0.x` version to `1.0`. [major-version-zero] |
23+
| <a name="cfg-template"></a>`template` | `str` | `None` | Provide custom changelog jinja template path relative to the current working directory. [See more (template customization)][template-customization] |
24+
| <a name="cfg-extras"></a>`extras` | `dict` | `{}` | Provide extra variables to the changelog template. [See more (template customization)][template-customization] |
2325

2426
## pyproject.toml or .cz.toml
2527

@@ -119,4 +121,5 @@ commitizen:
119121
[additional-features]: https://github.com/tmbo/questionary#additional-features
120122
[customization]: customization.md
121123
[shortcuts]: customization.md#shortcut-keys
124+
[template-customization]: customization.md#customizing-the-changelog-template
122125
[annotated-tags-vs-lightweight]: https://stackoverflow.com/a/11514139/2047185

Diff for: docs/customization.md

+82
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,85 @@ from commitizen.cz.exception import CzException
381381
class NoSubjectProvidedException(CzException):
382382
...
383383
```
384+
385+
## Customizing the changelog template
386+
387+
Commitizen gives you the possibility to provide your own changelog template, by:
388+
389+
- either by providing one with you customization class
390+
- either by providing one from the current working directory and setting it:
391+
- as [configuration][template-config]
392+
- as `--template` parameter to both `bump` and `changelog` commands
393+
- either by providing a template with the same name as the default template
394+
395+
By default, the template used is the `keep_a_changelog_template.j2` file from the commitizen repository.
396+
397+
### Providing a template with your customization class
398+
399+
There is 3 parameters available to hcange the template rendering from your custom `BaseCommitizen`.
400+
401+
| Parameter | Type | Default | Description |
402+
| ----------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
403+
| `template` | `str` | `None` | Provide your own template name (default to `keep_a_changelog_template.j2`) |
404+
| `template_loader` | `str` | `None` | Override the default template loader (so you can provide template from you customization class) |
405+
| `template_extras` | `dict` | `None` | Provide some extra template parameters |
406+
407+
Let's see an example.
408+
409+
```python
410+
from commitizen.cz.base import BaseCommitizen
411+
from jinja2 import PackageLoader
412+
413+
414+
class MyPlugin(BaseCommitizen):
415+
template = "CHANGELOG.md.jinja"
416+
template_loader = PackageLoader("my_plugin", "templates")
417+
template_extras = {"key": "value"}
418+
```
419+
420+
This snippet will:
421+
422+
- use `CHANGELOG.md.jinja` as template name
423+
- search for it in the `templates` directory for `my_plugin` package
424+
- add the `key=value` variable in the template
425+
426+
### Providing a template from the current working directory
427+
428+
Users can provides their own template from their current working directory (your project root) by:
429+
430+
- providing a template with the same name (`keep_a_changelog_template.j2` unless overriden by your custom class)
431+
- setting your template path as `template` configuration
432+
- giving your template path as `--template` parameter to `bump` and `changelog` commands
433+
434+
!!! Note
435+
The path is relative to the current working directory, aka. your project root most of the time,
436+
437+
### Template variables
438+
439+
The default template use a single `tree` variable which is a list of entries (a release) with the following format:
440+
441+
| Name | Type | Description |
442+
| ---- | ---- | ----------- |
443+
| version | `str` | The release version |
444+
| date | `datetime` | The release date |
445+
| changes | `list[tuple[str, list[Change]]]` | The release sorted changes list in the form `(type,changes)` |
446+
447+
Each `Change` has the following fields:
448+
449+
| Name | Type | Description |
450+
| ---- | ---- | ----------- |
451+
| scope | `Optional[str]` | An optionnal scope |
452+
| message | `str` | The commit message body |
453+
454+
!!! Note
455+
The field values depend on the customization class and/or the settings you provide
456+
457+
When using another template (either provided by a plugin or by yourself), you can also pass extra template variables
458+
by:
459+
460+
- defining them in your configuration with the [`extras` settings][extras-config]
461+
- providing them on the commandline with the `--extra/-e` parameter to `bump` and `changelog` commands
462+
463+
464+
[template-config]: config.md#cfg-template
465+
[extras-config]: config.md#cfg-extras

0 commit comments

Comments
 (0)