Skip to content

feat(changelog): adds a changelog_release_hook called for each release in the changelog #1018

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

from commitizen import out
from commitizen.bump import normalize_tag
from commitizen.cz.base import ChangelogReleaseHook
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
from commitizen.git import GitCommit, GitTag
from commitizen.version_schemes import (
Expand Down Expand Up @@ -113,6 +114,7 @@ def generate_tree_from_commits(
unreleased_version: str | None = None,
change_type_map: dict[str, str] | None = None,
changelog_message_builder_hook: MessageBuilderHook | None = None,
changelog_release_hook: ChangelogReleaseHook | None = None,
merge_prerelease: bool = False,
scheme: VersionScheme = DEFAULT_SCHEME,
) -> Iterable[dict]:
Expand Down Expand Up @@ -143,11 +145,14 @@ def generate_tree_from_commits(
commit_tag, used_tags, merge_prerelease, scheme=scheme
):
used_tags.append(commit_tag)
yield {
release = {
"version": current_tag_name,
"date": current_tag_date,
"changes": changes,
}
if changelog_release_hook:
release = changelog_release_hook(release, commit_tag)
yield release
current_tag_name = commit_tag.name
current_tag_date = commit_tag.date
changes = defaultdict(list)
Expand Down Expand Up @@ -178,7 +183,14 @@ def generate_tree_from_commits(
change_type_map,
)

yield {"version": current_tag_name, "date": current_tag_date, "changes": changes}
release = {
"version": current_tag_name,
"date": current_tag_date,
"changes": changes,
}
if changelog_release_hook:
release = changelog_release_hook(release, commit_tag)
yield release


def process_commit_message(
Expand Down
6 changes: 5 additions & 1 deletion commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from commitizen import bump, changelog, defaults, factory, git, out

from commitizen.config import BaseConfig
from commitizen.cz.base import MessageBuilderHook
from commitizen.cz.base import MessageBuilderHook, ChangelogReleaseHook
from commitizen.exceptions import (
DryRunExit,
NoCommitsFoundError,
Expand Down Expand Up @@ -154,6 +154,9 @@ def __call__(self):
changelog_message_builder_hook: MessageBuilderHook | None = (
self.cz.changelog_message_builder_hook
)
changelog_release_hook: ChangelogReleaseHook | None = (
self.cz.changelog_release_hook
)
merge_prerelease = self.merge_prerelease

if self.export_template_to:
Expand Down Expand Up @@ -207,6 +210,7 @@ def __call__(self):
unreleased_version,
change_type_map=change_type_map,
changelog_message_builder_hook=changelog_message_builder_hook,
changelog_release_hook=changelog_release_hook,
merge_prerelease=merge_prerelease,
scheme=self.scheme,
)
Expand Down
9 changes: 9 additions & 0 deletions commitizen/cz/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ def __call__(
) -> dict[str, Any] | Iterable[dict[str, Any]] | None: ...


class ChangelogReleaseHook(Protocol):
def __call__(
self, release: dict[str, Any], tag: git.GitTag | None
) -> dict[str, Any]: ...


class BaseCommitizen(metaclass=ABCMeta):
bump_pattern: str | None = None
bump_map: dict[str, str] | None = None
Expand Down Expand Up @@ -48,6 +54,9 @@ class BaseCommitizen(metaclass=ABCMeta):
# Executed only at the end of the changelog generation
changelog_hook: Callable[[str, str | None], str] | None = None

# Executed for each release in the changelog
changelog_release_hook: ChangelogReleaseHook | None = None

# Plugins can override templates and provide extra template data
template_loader: BaseLoader = PackageLoader("commitizen", "templates")
template_extras: dict[str, Any] = {}
Expand Down
5 changes: 5 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ You can customize it of course, and this are the variables you need to add to yo
| `change_type_map` | `dict` | NO | Convert the title of the change type that will appear in the changelog, if a value is not found, the original will be provided |
| `changelog_message_builder_hook` | `method: (dict, git.GitCommit) -> dict | list | None` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. Each GitCommit contains the following attrs: `rev`, `title`, `body`, `author`, `author_email`. Returning a falsy value ignore the commit. |
| `changelog_hook` | `method: (full_changelog: str, partial_changelog: Optional[str]) -> str` | NO | Receives the whole and partial (if used incremental) changelog. Useful to send slack messages or notify a compliance department. Must return the full_changelog |
| `changelog_release_hook` | `method: (release: dict, tag: git.GitTag) -> dict` | NO | Receives each generated changelog release and its associated tag. Useful to enrich a releases before they are rendered. Must return the update release

```python
from commitizen.cz.base import BaseCommitizen
Expand Down Expand Up @@ -347,6 +348,10 @@ class StrangeCommitizen(BaseCommitizen):
] = f"{m} {rev} [{commit.author}]({commit.author_email})"
return parsed_message

def changelog_release_hook(self, release: dict, tag: git.GitTag) -> dict:
release["author"] = tag.author
return release

def changelog_hook(
self, full_changelog: str, partial_changelog: Optional[str]
) -> str:
Expand Down
22 changes: 22 additions & 0 deletions tests/commands/test_changelog_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,28 @@ def test_changelog_hook_customize(mocker: MockFixture, config_customize):
changelog_hook_mock.assert_called_with(full_changelog, full_changelog)


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_changelog_release_hook(mocker: MockFixture, config):
def changelog_release_hook(release: dict, tag: git.GitTag) -> dict:
return release

for i in range(3):
create_file_and_commit("feat: new file")
create_file_and_commit("refactor: is in changelog")
create_file_and_commit("Merge into master")
git.tag(f"0.{i + 1}.0")

# changelog = Changelog(config, {})
changelog = Changelog(
config, {"unreleased_version": None, "incremental": True, "dry_run": False}
)
mocker.patch.object(changelog.cz, "changelog_release_hook", changelog_release_hook)
spy = mocker.spy(changelog.cz, "changelog_release_hook")
changelog()

assert spy.call_count == 3


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_changelog_with_non_linear_merges_commit_order(
mocker: MockFixture, config_customize
Expand Down
21 changes: 21 additions & 0 deletions tests/test_changelog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

from pathlib import Path
from typing import Optional

import pytest
from jinja2 import FileSystemLoader
Expand Down Expand Up @@ -1404,6 +1405,26 @@ def changelog_message_builder_hook(message: dict, commit: git.GitCommit):
), f"Line {no}: type {change_type} should have been overridden"


def test_render_changelog_with_changelog_release_hook(
gitcommits, tags, any_changelog_format: ChangelogFormat
):
def changelog_release_hook(release: dict, tag: Optional[git.GitTag]) -> dict:
release["extra"] = "whatever"
return release

parser = ConventionalCommitsCz.commit_parser
changelog_pattern = ConventionalCommitsCz.changelog_pattern
tree = changelog.generate_tree_from_commits(
gitcommits,
tags,
parser,
changelog_pattern,
changelog_release_hook=changelog_release_hook,
)
for release in tree:
assert release["extra"] == "whatever"


def test_get_smart_tag_range_returns_an_extra_for_a_range(tags):
start, end = (
tags[0],
Expand Down