Skip to content

Commit

Permalink
feat(changelog): changelog_message_build_hook can now generate mult…
Browse files Browse the repository at this point in the history
…iple changelog entries from a single commit (#1003)
  • Loading branch information
noirbizarre authored Mar 7, 2024
1 parent 41f9b82 commit ec2da06
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 30 deletions.
62 changes: 36 additions & 26 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from collections import OrderedDict, defaultdict
from dataclasses import dataclass
from datetime import date
from typing import TYPE_CHECKING, Callable, Iterable
from typing import TYPE_CHECKING, Iterable

from jinja2 import (
BaseLoader,
Expand All @@ -52,6 +52,7 @@
)

if TYPE_CHECKING:
from commitizen.cz.base import MessageBuilderHook
from commitizen.version_schemes import VersionScheme


Expand Down Expand Up @@ -111,7 +112,7 @@ def generate_tree_from_commits(
changelog_pattern: str,
unreleased_version: str | None = None,
change_type_map: dict[str, str] | None = None,
changelog_message_builder_hook: Callable | None = None,
changelog_message_builder_hook: MessageBuilderHook | None = None,
merge_prerelease: bool = False,
scheme: VersionScheme = DEFAULT_SCHEME,
) -> Iterable[dict]:
Expand Down Expand Up @@ -156,39 +157,48 @@ def generate_tree_from_commits(
continue

# Process subject from commit message
message = map_pat.match(commit.message)
if message:
parsed_message: dict = message.groupdict()

if changelog_message_builder_hook:
parsed_message = changelog_message_builder_hook(parsed_message, commit)
if parsed_message:
change_type = parsed_message.pop("change_type", None)
if change_type_map:
change_type = change_type_map.get(change_type, change_type)
changes[change_type].append(parsed_message)
if message := map_pat.match(commit.message):
process_commit_message(
changelog_message_builder_hook,
message,
commit,
changes,
change_type_map,
)

# Process body from commit message
body_parts = commit.body.split("\n\n")
for body_part in body_parts:
message_body = body_map_pat.match(body_part)
if not message_body:
continue
parsed_message_body: dict = message_body.groupdict()

if changelog_message_builder_hook:
parsed_message_body = changelog_message_builder_hook(
parsed_message_body, commit
if message := body_map_pat.match(body_part):
process_commit_message(
changelog_message_builder_hook,
message,
commit,
changes,
change_type_map,
)
if parsed_message_body:
change_type = parsed_message_body.pop("change_type", None)
if change_type_map:
change_type = change_type_map.get(change_type, change_type)
changes[change_type].append(parsed_message_body)

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


def process_commit_message(
hook: MessageBuilderHook | None,
parsed: re.Match[str],
commit: GitCommit,
changes: dict[str | None, list],
change_type_map: dict[str, str] | None = None,
):
message: dict = parsed.groupdict()

if processed := hook(message, commit) if hook else message:
messages = [processed] if isinstance(processed, dict) else processed
for msg in messages:
change_type = msg.pop("change_type", None)
if change_type_map:
change_type = change_type_map.get(change_type, change_type)
changes[change_type].append(msg)


def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterable:
if len(set(change_type_order)) != len(change_type_order):
raise InvalidConfigurationError(
Expand Down
4 changes: 2 additions & 2 deletions commitizen/cz/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import Any, Callable, Protocol
from typing import Any, Callable, Iterable, Protocol

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


class BaseCommitizen(metaclass=ABCMeta):
Expand Down
4 changes: 2 additions & 2 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ You can customize it of course, and this are the variables you need to add to yo
| `commit_parser` | `str` | NO | Regex which should provide the variables explained in the [changelog description][changelog-des] |
| `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 |
| `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 | 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_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 |

```python
Expand All @@ -339,7 +339,7 @@ class StrangeCommitizen(BaseCommitizen):
def changelog_message_builder_hook(
self, parsed_message: dict, commit: git.GitCommit
) -> dict | None:
) -> dict | list | None:
rev = commit.rev
m = parsed_message["message"]
parsed_message[
Expand Down
26 changes: 26 additions & 0 deletions tests/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,32 @@ def changelog_message_builder_hook(message: dict, commit: git.GitCommit):
assert RE_HEADER.match(line), f"Line {no} should not be there: {line}"


def test_render_changelog_with_changelog_message_builder_hook_multiple_entries(
gitcommits, tags, any_changelog_format: ChangelogFormat
):
def changelog_message_builder_hook(message: dict, commit: git.GitCommit):
messages = [message.copy(), message.copy(), message.copy()]
for idx, msg in enumerate(messages):
msg["message"] = "Message #{idx}"
return messages

parser = ConventionalCommitsCz.commit_parser
changelog_pattern = ConventionalCommitsCz.changelog_pattern
loader = ConventionalCommitsCz.template_loader
template = any_changelog_format.template
tree = changelog.generate_tree_from_commits(
gitcommits,
tags,
parser,
changelog_pattern,
changelog_message_builder_hook=changelog_message_builder_hook,
)
result = changelog.render_changelog(tree, loader, template)

for idx in range(3):
assert "Message #{idx}" in result


def test_changelog_message_builder_hook_can_access_and_modify_change_type(
gitcommits, tags, any_changelog_format: ChangelogFormat
):
Expand Down

0 comments on commit ec2da06

Please sign in to comment.