Skip to content

Commit 1c207df

Browse files
committed
feat(changelog): changelog_message_build_hook can now generate multiple changelog entries from a single commit
1 parent a9c2652 commit 1c207df

File tree

4 files changed

+66
-30
lines changed

4 files changed

+66
-30
lines changed

Diff for: commitizen/changelog.py

+36-26
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from collections import OrderedDict, defaultdict
3232
from dataclasses import dataclass
3333
from datetime import date
34-
from typing import TYPE_CHECKING, Callable, Iterable
34+
from typing import TYPE_CHECKING, Iterable
3535

3636
from jinja2 import (
3737
BaseLoader,
@@ -52,6 +52,7 @@
5252
)
5353

5454
if TYPE_CHECKING:
55+
from commitizen.cz.base import MessageBuilderHook
5556
from commitizen.version_schemes import VersionScheme
5657

5758

@@ -111,7 +112,7 @@ def generate_tree_from_commits(
111112
changelog_pattern: str,
112113
unreleased_version: str | None = None,
113114
change_type_map: dict[str, str] | None = None,
114-
changelog_message_builder_hook: Callable | None = None,
115+
changelog_message_builder_hook: MessageBuilderHook | None = None,
115116
merge_prerelease: bool = False,
116117
scheme: VersionScheme = DEFAULT_SCHEME,
117118
) -> Iterable[dict]:
@@ -156,39 +157,48 @@ def generate_tree_from_commits(
156157
continue
157158

158159
# Process subject from commit message
159-
message = map_pat.match(commit.message)
160-
if message:
161-
parsed_message: dict = message.groupdict()
162-
163-
if changelog_message_builder_hook:
164-
parsed_message = changelog_message_builder_hook(parsed_message, commit)
165-
if parsed_message:
166-
change_type = parsed_message.pop("change_type", None)
167-
if change_type_map:
168-
change_type = change_type_map.get(change_type, change_type)
169-
changes[change_type].append(parsed_message)
160+
if message := map_pat.match(commit.message):
161+
process_commit_message(
162+
changelog_message_builder_hook,
163+
message,
164+
commit,
165+
changes,
166+
change_type_map,
167+
)
170168

171169
# Process body from commit message
172170
body_parts = commit.body.split("\n\n")
173171
for body_part in body_parts:
174-
message_body = body_map_pat.match(body_part)
175-
if not message_body:
176-
continue
177-
parsed_message_body: dict = message_body.groupdict()
178-
179-
if changelog_message_builder_hook:
180-
parsed_message_body = changelog_message_builder_hook(
181-
parsed_message_body, commit
172+
if message := body_map_pat.match(body_part):
173+
process_commit_message(
174+
changelog_message_builder_hook,
175+
message,
176+
commit,
177+
changes,
178+
change_type_map,
182179
)
183-
if parsed_message_body:
184-
change_type = parsed_message_body.pop("change_type", None)
185-
if change_type_map:
186-
change_type = change_type_map.get(change_type, change_type)
187-
changes[change_type].append(parsed_message_body)
188180

189181
yield {"version": current_tag_name, "date": current_tag_date, "changes": changes}
190182

191183

184+
def process_commit_message(
185+
hook: MessageBuilderHook | None,
186+
parsed: re.Match[str],
187+
commit: GitCommit,
188+
changes: dict[str | None, list],
189+
change_type_map: dict[str, str] | None = None,
190+
):
191+
message: dict = parsed.groupdict()
192+
193+
if processed := hook(message, commit) if hook else message:
194+
messages = [processed] if isinstance(processed, dict) else processed
195+
for msg in messages:
196+
change_type = msg.pop("change_type", None)
197+
if change_type_map:
198+
change_type = change_type_map.get(change_type, change_type)
199+
changes[change_type].append(msg)
200+
201+
192202
def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterable:
193203
if len(set(change_type_order)) != len(change_type_order):
194204
raise InvalidConfigurationError(

Diff for: commitizen/cz/base.py

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

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

66
from jinja2 import BaseLoader, PackageLoader
77
from prompt_toolkit.styles import Style, merge_styles
@@ -14,7 +14,7 @@
1414
class MessageBuilderHook(Protocol):
1515
def __call__(
1616
self, message: dict[str, Any], commit: git.GitCommit
17-
) -> dict[str, Any] | None: ...
17+
) -> dict[str, Any] | Iterable[dict[str, Any]] | None: ...
1818

1919

2020
class BaseCommitizen(metaclass=ABCMeta):

Diff for: docs/customization.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ You can customize it of course, and this are the variables you need to add to yo
318318
| `commit_parser` | `str` | NO | Regex which should provide the variables explained in the [changelog description][changelog-des] |
319319
| `changelog_pattern` | `str` | NO | Regex to validate the commits, this is useful to skip commits that don't meet your ruling standards like a Merge. Usually the same as bump_pattern |
320320
| `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 |
321-
| `changelog_message_builder_hook` | `method: (dict, git.GitCommit) -> dict | 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. |
321+
| `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. |
322322
| `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 |
323323

324324
```python
@@ -339,7 +339,7 @@ class StrangeCommitizen(BaseCommitizen):
339339
340340
def changelog_message_builder_hook(
341341
self, parsed_message: dict, commit: git.GitCommit
342-
) -> dict | None:
342+
) -> dict | list | None:
343343
rev = commit.rev
344344
m = parsed_message["message"]
345345
parsed_message[

Diff for: tests/test_changelog.py

+26
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,32 @@ def changelog_message_builder_hook(message: dict, commit: git.GitCommit):
13471347
assert RE_HEADER.match(line), f"Line {no} should not be there: {line}"
13481348

13491349

1350+
def test_render_changelog_with_changelog_message_builder_hook_multiple_entries(
1351+
gitcommits, tags, any_changelog_format: ChangelogFormat
1352+
):
1353+
def changelog_message_builder_hook(message: dict, commit: git.GitCommit):
1354+
messages = [message.copy(), message.copy(), message.copy()]
1355+
for idx, msg in enumerate(messages):
1356+
msg["message"] = "Message #{idx}"
1357+
return messages
1358+
1359+
parser = ConventionalCommitsCz.commit_parser
1360+
changelog_pattern = ConventionalCommitsCz.changelog_pattern
1361+
loader = ConventionalCommitsCz.template_loader
1362+
template = any_changelog_format.template
1363+
tree = changelog.generate_tree_from_commits(
1364+
gitcommits,
1365+
tags,
1366+
parser,
1367+
changelog_pattern,
1368+
changelog_message_builder_hook=changelog_message_builder_hook,
1369+
)
1370+
result = changelog.render_changelog(tree, loader, template)
1371+
1372+
for idx in range(3):
1373+
assert "Message #{idx}" in result
1374+
1375+
13501376
def test_changelog_message_builder_hook_can_access_and_modify_change_type(
13511377
gitcommits, tags, any_changelog_format: ChangelogFormat
13521378
):

0 commit comments

Comments
 (0)