Edit on GitHub

cz_keep_a_changelog_plugin

Python Support Pypi Latest Version Python Types Documentation License

CI pre-commit Coverage Pypi Trusted Publisher: enabled Ruff Checks: flake-8, isort, mypy

Commitizen Conventional Commits Keep a Changelog GitHub last commit


Adds full compliance of Commitizen with Keep a Changelog v1.1.0.

Features

  • Complete map of Conventional Commits-types to Keep a Changelog-types of changes:
    • feat: Added
    • fix: Fixed
    • perf: Changed
    • refactor: Changed
    • revert: Removed
  • Enables to replace issue numbers in the changelog by a markdown link to the issue:
    #123 -> [#123](https://github.com/user/repo/issues/123)
  • Keep a Changelog-like header for changelog file.

Installation

Using pip:

# Install
pip install cz-keep-a-changelog-plugin

# Keep it updated
pip upgrade cz-keep-a-changelog-plugin

Using uv:

# Install
uv install cz-keep-a-changelog-plugin

# Keep it updated
uv upgrade cz-keep-a-changelog-plugin
Important


Package name uses -!

Usage

Configuration

In the project's pyproject.toml set:

...

[tool.commitizen]
name = "cz_keep_a_changelog_plugin"
...

[tool.commitizen.cz_keep_a_changelog_plugin]
issue_url_template = "https://cola5.github.io/cz-keep-a-changelog-plugin/issues/{{issue}}"
Important


Plugin name uses _!

Pre-Commit

To use in pre-commit, add this to your pre-commit-config.yml. Run pre-commit autoupdate to get the latest version:

repos:
  - repo: https://github.com/commitizen-tools/commitizen
    rev: main
    hooks:
      - id: commitizen
        additional_dependencies:
          - cz-keep-a-changelog-plugin==<VERSION>
        stages: [commit-msg]
      - id: commitizen-branch
        additional_dependencies:
          - cz-keep-a-changelog-plugin==<VERSION>
        stages: [pre-push]
Important


Package name uses -!


 1"""
 2.. include:: ../README.md
 3   :end-before: # Commitizen - Keep a Changelog-Plugin
 4
 5---
 6
 7.. include:: ../README.md
 8   :start-after: # Commitizen - Keep a Changelog-Plugin
 9
10---
11"""  # noqa: D212, D415
12
13from cz_keep_a_changelog_plugin.cz_keep_a_changelog_plugin import (
14    CzKeepAChangelogPlugin,
15)
16from cz_keep_a_changelog_plugin.cz_keep_a_changelog_plugin import (
17    CzPluginSettings,
18)
19from cz_keep_a_changelog_plugin.version import (
20    CZ_KEEP_A_CHANGELOG_PLUGIN_VERSION,
21)
22
23__docformat__ = "google"
24__license__ = "MIT"
25__version__ = CZ_KEEP_A_CHANGELOG_PLUGIN_VERSION
26
27__all__ = (
28    "CzKeepAChangelogPlugin",
29    "CzPluginSettings",
30    "__license__",
31    "__version__",
32)

API Documentation

class CzKeepAChangelogPlugin(commitizen.cz.conventional_commits.conventional_commits.ConventionalCommitsCz):
 41class CzKeepAChangelogPlugin(ConventionalCommitsCz):
 42    """'Keep a Changelog'-Conventional Commits-Plugin."""
 43
 44    bump_pattern: str | None = r"^(\w+(\(.+\))?!?):"
 45    bump_map: dict[str, str] | None = OrderedDict(
 46        (
 47            (r"^.+!$", MAJOR),
 48            (r"^feat", MINOR),
 49            (r"^revert", MINOR),
 50            (r"^fix", PATCH),
 51            (r"^perf", PATCH),
 52            (r"^refactor", PATCH),
 53        )
 54    )
 55    bump_map_major_version_zero: dict[str, str] | None = OrderedDict(
 56        (
 57            (r"^.+!$", MINOR),
 58            (r"^feat", MINOR),
 59            (r"^revert", MINOR),
 60            (r"^fix", PATCH),
 61            (r"^perf", PATCH),
 62            (r"^refactor", PATCH),
 63        )
 64    )
 65
 66    commit_parser: str | None = (
 67        r"^((?P<change_type>feat|fix|perf|refactor|revert)"
 68        r"(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):"
 69        r"\s(?P<message>.*)?"
 70    )
 71    changelog_pattern: str | None = bump_pattern
 72    change_type_map: dict[str, str] | None = {
 73        "feat": "Added",
 74        "fix": "Fixed",
 75        "perf": "Changed",
 76        "refactor": "Changed",
 77        "revert": "Removed",
 78    }
 79    change_type_order: list[str] | None = [
 80        "Added",
 81        "Changed",
 82        "Deprecated",
 83        "Removed",
 84        "Fixed",
 85        "Security",
 86    ]
 87
 88    template_header: str = "CHANGELOG_HEADER.md.j2"
 89    template_loader: j2.BaseLoader = j2.PackageLoader(
 90        "cz_keep_a_changelog_plugin", "templates"
 91    )
 92
 93    def __init__(self, config: BaseConfig) -> None:
 94        """Initializes the plugin.
 95
 96        Args:
 97            config: The base config of `commitizen`.
 98        """
 99        super().__init__(config)
100        self.plugin_settings: CzPluginSettings = self.config.settings.get(
101            "cz_keep_a_changelog_plugin", {}
102        )
103        """The plugin settings set in TOML."""
104
105    def changelog_hook(
106        self,
107        changelog_out: str,
108        partial_changelog: str | None,
109    ) -> str:
110        """Customizes the complete changelog or uses it to call further
111        functionality.
112
113        Here, the header will be added if not done before.
114
115        Args:
116            changelog_out: The full changelog.
117            partial_changelog: The partial (if used `incremental`) changelog,
118                else `None`.
119
120        Returns:
121            Must return the complete changelog.
122        """
123        loader = j2.ChoiceLoader(
124            [j2.FileSystemLoader("."), self.template_loader]
125        )
126        env = j2.Environment(loader=loader, trim_blocks=True)
127        jinja_template_header = env.get_template(self.template_header)
128        header = jinja_template_header.render(
129            **self.template_extras,
130            **self.config.settings["extras"],
131        )
132        header = header.strip()
133        if changelog_out.startswith(header):
134            return changelog_out
135        return "\n\n".join((header, changelog_out))
136
137    def changelog_message_builder_hook(
138        self,
139        message: dict[str, Any],
140        commit: git.GitCommit,
141    ) -> Iterable[dict[str, Any]] | dict[str, Any] | None:
142        """Customizes the changelog message with extra information, like adding
143        links.
144
145        This function is executed per parsed commit. Each `GitCommit` contains
146        the following attrs: `rev`, `title`, `body`, `author`, `author_email`.
147
148        Here, the issue number is replaced by a markdown link using the issue
149        url template.
150
151        Args:
152            message: The message to customize.
153            commit: The original `GitCommit` which contains the following attrs:
154                `rev`, `title`, `body`, `author`, `author_email`.
155
156        Returns:
157            A customized message or a falsy value to ignore the commit.
158        """
159        if "issue_url_template" in self.plugin_settings:
160            issue_url_template = j2.Template(
161                self.plugin_settings["issue_url_template"]
162            )
163            message["message"] = replace_github_issues(
164                message["message"], issue_url_template
165            )
166        return message
167
168    def questions(self) -> list[CzQuestion]:
169        """Returns the questions regarding the commit message.
170
171        Returns:
172            The questions.
173        """
174        return [
175            {
176                "type": "list",
177                "name": "prefix",
178                "message": "Select the type of change you are committing",
179                "choices": [
180                    {
181                        "value": "feat",
182                        "name": (
183                            "feat: A new feature. Correlates with MINOR in "
184                            "SemVer"
185                        ),
186                        "key": "f",
187                    },
188                    {
189                        "value": "fix",
190                        "name": (
191                            "fix: A bug fix. Correlates with PATCH in SemVer"
192                        ),
193                        "key": "x",
194                    },
195                    {
196                        "value": "docs",
197                        "name": "docs: Documentation only changes",
198                        "key": "d",
199                    },
200                    {
201                        "value": "style",
202                        "name": (
203                            "style: Changes that do not affect the meaning of "
204                            "the code (white-space, formatting, missing "
205                            "semi-colons, etc)"
206                        ),
207                        "key": "s",
208                    },
209                    {
210                        "value": "refactor",
211                        "name": (
212                            "refactor: A code change that neither fixes a bug "
213                            "nor adds a feature"
214                        ),
215                        "key": "r",
216                    },
217                    {
218                        "value": "perf",
219                        "name": "perf: A code change that improves performance",
220                        "key": "p",
221                    },
222                    {
223                        "value": "test",
224                        "name": (
225                            "test: Adding missing tests or correcting existing "
226                            "tests"
227                        ),
228                        "key": "t",
229                    },
230                    {
231                        "value": "build",
232                        "name": (
233                            "build: Changes that affect the build system or "
234                            "external dependencies (example scopes: pip, "
235                            "docker, npm)"
236                        ),
237                        "key": "b",
238                    },
239                    {
240                        "value": "ci",
241                        "name": (
242                            "ci: Changes to CI configuration files and scripts "
243                            "(example scopes: GitLabCI)"
244                        ),
245                        "key": "c",
246                    },
247                    {
248                        "value": "chore",
249                        "name": (
250                            "Other changes that don't modify src or test files"
251                        ),
252                        "key": "h",
253                    },
254                    {
255                        "value": "revert",
256                        "name": (
257                            "Reverts a previous commit. Put reverted commit "
258                            "SHAs into footer as'Refs: 676104e, a215868'"
259                        ),
260                        "key": "v",
261                    },
262                ],
263            },
264            {
265                "type": "input",
266                "name": "scope",
267                "message": (
268                    "What is the scope of this change? (class or file name): "
269                    "(press [enter] to skip)\n"
270                ),
271                "filter": _parse_scope,
272            },
273            {
274                "type": "input",
275                "name": "subject",
276                "filter": _parse_subject,
277                "message": (
278                    "Write a short and imperative summary of the code changes: "
279                    "(lower case and no period)\n"
280                ),
281            },
282            {
283                "type": "input",
284                "name": "body",
285                "message": (
286                    "Provide additional contextual information about the code "
287                    "changes: (press [enter] to skip)\n"
288                ),
289                "filter": multiple_line_breaker,
290            },
291            {
292                "type": "confirm",
293                "name": "is_breaking_change",
294                "message": (
295                    "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer"
296                ),
297                "default": False,
298            },
299            {
300                "type": "input",
301                "name": "footer",
302                "message": (
303                    "Footer. Information about Breaking Changes and reference "
304                    "issues that this commit closes: (press [enter] to skip)\n"
305                ),
306            },
307        ]

'Keep a Changelog'-Conventional Commits-Plugin.

CzKeepAChangelogPlugin(config: commitizen.config.base_config.BaseConfig)
 93    def __init__(self, config: BaseConfig) -> None:
 94        """Initializes the plugin.
 95
 96        Args:
 97            config: The base config of `commitizen`.
 98        """
 99        super().__init__(config)
100        self.plugin_settings: CzPluginSettings = self.config.settings.get(
101            "cz_keep_a_changelog_plugin", {}
102        )
103        """The plugin settings set in TOML."""

Initializes the plugin.

Arguments:
  • config: The base config of commitizen.
bump_pattern: str | None = '^(\\w+(\\(.+\\))?!?):'
bump_map: dict[str, str] | None = OrderedDict([('^.+!$', 'MAJOR'), ('^feat', 'MINOR'), ('^revert', 'MINOR'), ('^fix', 'PATCH'), ('^perf', 'PATCH'), ('^refactor', 'PATCH')])
bump_map_major_version_zero: dict[str, str] | None = OrderedDict([('^.+!$', 'MINOR'), ('^feat', 'MINOR'), ('^revert', 'MINOR'), ('^fix', 'PATCH'), ('^perf', 'PATCH'), ('^refactor', 'PATCH')])
commit_parser: str | None = '^((?P<change_type>feat|fix|perf|refactor|revert)(?:\\((?P<scope>[^()\\r\\n]*)\\)|\\()?(?P<breaking>!)?|\\w+!):\\s(?P<message>.*)?'
changelog_pattern: str | None = '^(\\w+(\\(.+\\))?!?):'
change_type_map: dict[str, str] | None = {'feat': 'Added', 'fix': 'Fixed', 'perf': 'Changed', 'refactor': 'Changed', 'revert': 'Removed'}
change_type_order: list[str] | None = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
template_header: str = 'CHANGELOG_HEADER.md.j2'
template_loader: jinja2.loaders.BaseLoader = <jinja2.loaders.PackageLoader object>
plugin_settings: CzPluginSettings

The plugin settings set in TOML.

def changelog_hook(self, changelog_out: str, partial_changelog: str | None) -> str:
105    def changelog_hook(
106        self,
107        changelog_out: str,
108        partial_changelog: str | None,
109    ) -> str:
110        """Customizes the complete changelog or uses it to call further
111        functionality.
112
113        Here, the header will be added if not done before.
114
115        Args:
116            changelog_out: The full changelog.
117            partial_changelog: The partial (if used `incremental`) changelog,
118                else `None`.
119
120        Returns:
121            Must return the complete changelog.
122        """
123        loader = j2.ChoiceLoader(
124            [j2.FileSystemLoader("."), self.template_loader]
125        )
126        env = j2.Environment(loader=loader, trim_blocks=True)
127        jinja_template_header = env.get_template(self.template_header)
128        header = jinja_template_header.render(
129            **self.template_extras,
130            **self.config.settings["extras"],
131        )
132        header = header.strip()
133        if changelog_out.startswith(header):
134            return changelog_out
135        return "\n\n".join((header, changelog_out))

Customizes the complete changelog or uses it to call further functionality.

Here, the header will be added if not done before.

Arguments:
  • changelog_out: The full changelog.
  • partial_changelog: The partial (if used incremental) changelog, else None.
Returns:

Must return the complete changelog.

def changelog_message_builder_hook( self, message: dict[str, typing.Any], commit: commitizen.git.GitCommit) -> Iterable[dict[str, typing.Any]] | dict[str, typing.Any] | None:
137    def changelog_message_builder_hook(
138        self,
139        message: dict[str, Any],
140        commit: git.GitCommit,
141    ) -> Iterable[dict[str, Any]] | dict[str, Any] | None:
142        """Customizes the changelog message with extra information, like adding
143        links.
144
145        This function is executed per parsed commit. Each `GitCommit` contains
146        the following attrs: `rev`, `title`, `body`, `author`, `author_email`.
147
148        Here, the issue number is replaced by a markdown link using the issue
149        url template.
150
151        Args:
152            message: The message to customize.
153            commit: The original `GitCommit` which contains the following attrs:
154                `rev`, `title`, `body`, `author`, `author_email`.
155
156        Returns:
157            A customized message or a falsy value to ignore the commit.
158        """
159        if "issue_url_template" in self.plugin_settings:
160            issue_url_template = j2.Template(
161                self.plugin_settings["issue_url_template"]
162            )
163            message["message"] = replace_github_issues(
164                message["message"], issue_url_template
165            )
166        return message

Customizes the changelog message with extra information, like adding links.

This function is executed per parsed commit. Each GitCommit contains the following attrs: rev, title, body, author, author_email.

Here, the issue number is replaced by a markdown link using the issue url template.

Arguments:
  • message: The message to customize.
  • commit: The original GitCommit which contains the following attrs: rev, title, body, author, author_email.
Returns:

A customized message or a falsy value to ignore the commit.

def questions( self) -> list[typing.Union[commitizen.question.ListQuestion, commitizen.question.InputQuestion, commitizen.question.ConfirmQuestion]]:
168    def questions(self) -> list[CzQuestion]:
169        """Returns the questions regarding the commit message.
170
171        Returns:
172            The questions.
173        """
174        return [
175            {
176                "type": "list",
177                "name": "prefix",
178                "message": "Select the type of change you are committing",
179                "choices": [
180                    {
181                        "value": "feat",
182                        "name": (
183                            "feat: A new feature. Correlates with MINOR in "
184                            "SemVer"
185                        ),
186                        "key": "f",
187                    },
188                    {
189                        "value": "fix",
190                        "name": (
191                            "fix: A bug fix. Correlates with PATCH in SemVer"
192                        ),
193                        "key": "x",
194                    },
195                    {
196                        "value": "docs",
197                        "name": "docs: Documentation only changes",
198                        "key": "d",
199                    },
200                    {
201                        "value": "style",
202                        "name": (
203                            "style: Changes that do not affect the meaning of "
204                            "the code (white-space, formatting, missing "
205                            "semi-colons, etc)"
206                        ),
207                        "key": "s",
208                    },
209                    {
210                        "value": "refactor",
211                        "name": (
212                            "refactor: A code change that neither fixes a bug "
213                            "nor adds a feature"
214                        ),
215                        "key": "r",
216                    },
217                    {
218                        "value": "perf",
219                        "name": "perf: A code change that improves performance",
220                        "key": "p",
221                    },
222                    {
223                        "value": "test",
224                        "name": (
225                            "test: Adding missing tests or correcting existing "
226                            "tests"
227                        ),
228                        "key": "t",
229                    },
230                    {
231                        "value": "build",
232                        "name": (
233                            "build: Changes that affect the build system or "
234                            "external dependencies (example scopes: pip, "
235                            "docker, npm)"
236                        ),
237                        "key": "b",
238                    },
239                    {
240                        "value": "ci",
241                        "name": (
242                            "ci: Changes to CI configuration files and scripts "
243                            "(example scopes: GitLabCI)"
244                        ),
245                        "key": "c",
246                    },
247                    {
248                        "value": "chore",
249                        "name": (
250                            "Other changes that don't modify src or test files"
251                        ),
252                        "key": "h",
253                    },
254                    {
255                        "value": "revert",
256                        "name": (
257                            "Reverts a previous commit. Put reverted commit "
258                            "SHAs into footer as'Refs: 676104e, a215868'"
259                        ),
260                        "key": "v",
261                    },
262                ],
263            },
264            {
265                "type": "input",
266                "name": "scope",
267                "message": (
268                    "What is the scope of this change? (class or file name): "
269                    "(press [enter] to skip)\n"
270                ),
271                "filter": _parse_scope,
272            },
273            {
274                "type": "input",
275                "name": "subject",
276                "filter": _parse_subject,
277                "message": (
278                    "Write a short and imperative summary of the code changes: "
279                    "(lower case and no period)\n"
280                ),
281            },
282            {
283                "type": "input",
284                "name": "body",
285                "message": (
286                    "Provide additional contextual information about the code "
287                    "changes: (press [enter] to skip)\n"
288                ),
289                "filter": multiple_line_breaker,
290            },
291            {
292                "type": "confirm",
293                "name": "is_breaking_change",
294                "message": (
295                    "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer"
296                ),
297                "default": False,
298            },
299            {
300                "type": "input",
301                "name": "footer",
302                "message": (
303                    "Footer. Information about Breaking Changes and reference "
304                    "issues that this commit closes: (press [enter] to skip)\n"
305                ),
306            },
307        ]

Returns the questions regarding the commit message.

Returns:

The questions.

class CzPluginSettings(typing.TypedDict):
35class CzPluginSettings(TypedDict, total=False):
36    """Plugin Settings."""
37
38    issue_url_template: str

Plugin Settings.

issue_url_template: str
__license__ = 'MIT'
__version__ = '0.0.5'