From 123e27b254a42f42fce51869fb7ad1b90e7004d7 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Thu, 22 Oct 2026 16:36:41 -0300 Subject: [PATCH] feat(commands): add --add-author paramater to include author in changelog Signed-off-by: Emmanuel Arias --- commitizen/changelog.py | 35 +++++++++ commitizen/cli.py | 13 ++++ commitizen/commands/bump.py | 2 + commitizen/commands/changelog.py | 4 + commitizen/git.py | 19 +++++ commitizen/templates/CHANGELOG.adoc.j2 | 4 +- commitizen/templates/CHANGELOG.md.j2 | 4 +- commitizen/templates/CHANGELOG.rst.j2 | 4 +- commitizen/templates/CHANGELOG.textile.j2 | 4 +- ...ion_when_use_help_option_py_3_10_bump_.txt | 8 +- ...hen_use_help_option_py_3_10_changelog_.txt | 8 +- ...ion_when_use_help_option_py_3_11_bump_.txt | 8 +- ...hen_use_help_option_py_3_11_changelog_.txt | 8 +- ...ion_when_use_help_option_py_3_12_bump_.txt | 8 +- ...hen_use_help_option_py_3_12_changelog_.txt | 8 +- ...ion_when_use_help_option_py_3_13_bump_.txt | 8 +- ...hen_use_help_option_py_3_13_changelog_.txt | 8 +- ...ion_when_use_help_option_py_3_14_bump_.txt | 8 +- ...hen_use_help_option_py_3_14_changelog_.txt | 8 +- tests/test_changelog.py | 75 +++++++++++++++++++ tests/test_git.py | 44 +++++++++++ 21 files changed, 270 insertions(+), 18 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index d8b8acccae..7ba77f07eb 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -31,6 +31,7 @@ from collections import OrderedDict, defaultdict from dataclasses import dataclass from datetime import date +from enum import Enum from itertools import chain from typing import TYPE_CHECKING, Any @@ -85,6 +86,13 @@ class IncrementalMergeInfo: index: int | None = None +class AuthorSource(str, Enum): + """Source of the author shown in each changelog entry (`--add-author`)""" + + AUTHOR = "author" + SIGNED_OFF_BY = "signed-off-by" + + def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None: return next((tag for tag in tags if tag.rev == commit.rev), None) @@ -100,6 +108,7 @@ def generate_tree_from_commits( changelog_release_hook: ChangelogReleaseHook | None = None, rules: TagRules | None = None, during_version_bump: bool = False, + author_source: AuthorSource | None = None, ) -> Generator[dict[str, Any], None, None]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser, re.MULTILINE) @@ -159,6 +168,7 @@ def generate_tree_from_commits( commit, changes, change_type_map, + author_source, ) release = { @@ -171,12 +181,33 @@ def generate_tree_from_commits( yield release +def resolve_changelog_author( + commit: GitCommit, author_source: AuthorSource +) -> tuple[str, str]: + """Get the author name and email to show in a changelog entry. + + Args: + commit: The commit of the changelog entry. + author_source: Where to read the author from. + + Returns: + A `(name, email)` tuple. For `SIGNED_OFF_BY`, it is the first + `Signed-off-by` trailer, or the commit author when there is no trailer. + """ + if author_source is AuthorSource.SIGNED_OFF_BY and ( + signed_off_by := commit.signed_off_by + ): + return signed_off_by + return commit.author, commit.author_email + + def process_commit_message( hook: MessageBuilderHook | None, parsed: re.Match[str], commit: GitCommit, ref_changes: MutableMapping[str | None, list], change_type_map: Mapping[str, str] | None = None, + author_source: AuthorSource | None = None, ) -> None: message: dict[str, Any] = { "sha1": commit.rev, @@ -185,6 +216,10 @@ def process_commit_message( "author_email": commit.author_email, **parsed.groupdict(), } + if author_source is not None: + name, email = resolve_changelog_author(commit, author_source) + message["changelog_author"] = name + message["changelog_author_email"] = email processed_msg = hook(message, commit) if hook else message if not processed_msg: diff --git a/commitizen/cli.py b/commitizen/cli.py index 3ee03d99ce..3f1a9448c9 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -79,6 +79,19 @@ def __call__( "metavar": "EXTRA", "help": "Changelog extra variables (in the form 'key=value').", }, + { + "name": ["--add-author"], + "nargs": "?", + "const": "author", + "default": None, + "choices": ["author", "signed-off-by"], + "help": ( + "Add the author to each changelog entry. " + "'author' (default) uses the commit author. " + "'signed-off-by' uses the first Signed-off-by trailer, " + "or the commit author if the commit has no trailer." + ), + }, ) data = { diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 3e92775e28..cb0f0329cc 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -40,6 +40,7 @@ class BumpArgs(Settings, total=False): + add_author: str | None allow_no_commit: bool | None annotated_tag_message: str | None build_metadata: str | None @@ -330,6 +331,7 @@ def __call__(self) -> None: "dry_run": dry_run, # governs logic for merge_prerelease "during_version_bump": self.arguments["prerelease"] is None, + "add_author": self.arguments.get("add_author"), } if self.changelog_to_stdout: try: diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index abe03a9241..b1ff46ac39 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -44,6 +44,7 @@ class ChangelogArgs(TypedDict, total=False): export_template: str during_version_bump: bool | None allow_no_commit: bool | None # Internal-only when invoked by bump. + add_author: str | None class Changelog: @@ -127,6 +128,8 @@ def __init__(self, config: BaseConfig, arguments: ChangelogArgs) -> None: self.during_version_bump: bool = arguments.get("during_version_bump") or False # Internal flag used when changelog is invoked from `cz bump --allow-no-commit`. self.allow_no_commit: bool = bool(arguments.get("allow_no_commit")) + add_author = arguments.get("add_author") + self.author_source = changelog.AuthorSource(add_author) if add_author else None def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> str: """Try to find the 'start_rev'. @@ -277,6 +280,7 @@ def __call__(self) -> None: changelog_release_hook=self.cz.changelog_release_hook, rules=self.tag_rules, during_version_bump=self.during_version_bump, + author_source=self.author_source, ) if self.change_type_order: tree = changelog.generate_ordered_changelog_tree( diff --git a/commitizen/git.py b/commitizen/git.py index ce9f440c95..5a1fb3d9af 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import re from enum import Enum from functools import lru_cache from pathlib import Path @@ -14,6 +15,12 @@ from collections.abc import Sequence +_SIGNED_OFF_BY_PATTERN = re.compile( + r"^Signed-off-by:\s*(?P.+?)\s*<(?P[^>]+)>\s*$", + re.MULTILINE | re.IGNORECASE, +) + + class EOLType(Enum): """The EOL type from `git config core.eol`.""" @@ -78,6 +85,18 @@ def __init__( def message(self) -> str: return f"{self.title}\n\n{self.body}".strip() + @property + def signed_off_by(self) -> tuple[str, str] | None: + """Get the first `Signed-off-by` trailer of the commit body. + + Returns: + `(name, email)` tuple, or `None` when the body has no trailer. + """ + match = _SIGNED_OFF_BY_PATTERN.search(self.body) + if match is None: + return None + return match.group("name"), match.group("email") + @classmethod def from_rev_and_commit(cls, rev_and_commit: str) -> GitCommit: """Create a GitCommit instance from a formatted commit string. diff --git a/commitizen/templates/CHANGELOG.adoc.j2 b/commitizen/templates/CHANGELOG.adoc.j2 index fe16c5de3d..1fa5707279 100644 --- a/commitizen/templates/CHANGELOG.adoc.j2 +++ b/commitizen/templates/CHANGELOG.adoc.j2 @@ -10,9 +10,9 @@ {% for change in changes %} {% if change.scope %} -* *{{ change.scope }}*: {{ change.message }} +* *{{ change.scope }}*: {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% elif change.message %} -* {{ change.message }} +* {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% endif %} {% endfor %} {% endfor %} diff --git a/commitizen/templates/CHANGELOG.md.j2 b/commitizen/templates/CHANGELOG.md.j2 index de63880d66..7e85b67d67 100644 --- a/commitizen/templates/CHANGELOG.md.j2 +++ b/commitizen/templates/CHANGELOG.md.j2 @@ -10,9 +10,9 @@ {% for change in changes %} {% if change.scope %} -- **{{ change.scope }}**: {{ change.message }} +- **{{ change.scope }}**: {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% elif change.message %} -- {{ change.message }} +- {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% endif %} {% endfor %} {% endfor %} diff --git a/commitizen/templates/CHANGELOG.rst.j2 b/commitizen/templates/CHANGELOG.rst.j2 index 4287108b55..f825f21202 100644 --- a/commitizen/templates/CHANGELOG.rst.j2 +++ b/commitizen/templates/CHANGELOG.rst.j2 @@ -14,9 +14,9 @@ {% for change in changes %} {% if change.scope %} -- **{{ change.scope }}**: {{ change.message }} +- **{{ change.scope }}**: {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% elif change.message %} -- {{ change.message }} +- {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% endif %} {% endfor %} {% endfor %} diff --git a/commitizen/templates/CHANGELOG.textile.j2 b/commitizen/templates/CHANGELOG.textile.j2 index db55f4caad..d35d08321d 100644 --- a/commitizen/templates/CHANGELOG.textile.j2 +++ b/commitizen/templates/CHANGELOG.textile.j2 @@ -10,9 +10,9 @@ h3. {{ change_key }} {% for change in changes %} {% if change.scope %} -- *{{ change.scope }}*: {{ change.message }} +- *{{ change.scope }}*: {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% elif change.message %} -- {{ change.message }} +- {{ change.message }}{{ " (" ~ change.changelog_author ~ " <" ~ change.changelog_author_email ~ ">)" if change.changelog_author }} {% endif %} {% endfor %} {% endfor %} diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt index fa696d063d..d6f06af3eb 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt @@ -8,7 +8,8 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--version-files-only] [--annotated-tag-message ANNOTATED_TAG_MESSAGE] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] - [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] + [--add-author [{author,signed-off-by}]] [--file-name FILE_NAME] + [--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA] [--get-next] @@ -73,6 +74,11 @@ options: working directory). --extra EXTRA, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --file-name FILE_NAME File name of changelog (default: 'CHANGELOG.md'). --prerelease-offset PRERELEASE_OFFSET diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt index 2d1135af74..30b33577e8 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt @@ -3,7 +3,8 @@ usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--start-rev START_REV] [--merge-prerelease] [--version-scheme {pep440,semver,semver2}] [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] - [--extra EXTRA] [--tag-format TAG_FORMAT] + [--extra EXTRA] [--add-author [{author,signed-off-by}]] + [--tag-format TAG_FORMAT] [rev_range] Generate changelog (note that it will overwrite existing files) @@ -38,5 +39,10 @@ options: working directory). --extra EXTRA, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --tag-format TAG_FORMAT The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt index fa696d063d..d6f06af3eb 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt @@ -8,7 +8,8 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--version-files-only] [--annotated-tag-message ANNOTATED_TAG_MESSAGE] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] - [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] + [--add-author [{author,signed-off-by}]] [--file-name FILE_NAME] + [--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA] [--get-next] @@ -73,6 +74,11 @@ options: working directory). --extra EXTRA, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --file-name FILE_NAME File name of changelog (default: 'CHANGELOG.md'). --prerelease-offset PRERELEASE_OFFSET diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt index 2d1135af74..30b33577e8 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt @@ -3,7 +3,8 @@ usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--start-rev START_REV] [--merge-prerelease] [--version-scheme {pep440,semver,semver2}] [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] - [--extra EXTRA] [--tag-format TAG_FORMAT] + [--extra EXTRA] [--add-author [{author,signed-off-by}]] + [--tag-format TAG_FORMAT] [rev_range] Generate changelog (note that it will overwrite existing files) @@ -38,5 +39,10 @@ options: working directory). --extra EXTRA, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --tag-format TAG_FORMAT The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt index fa696d063d..d6f06af3eb 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt @@ -8,7 +8,8 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--version-files-only] [--annotated-tag-message ANNOTATED_TAG_MESSAGE] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] - [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] + [--add-author [{author,signed-off-by}]] [--file-name FILE_NAME] + [--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA] [--get-next] @@ -73,6 +74,11 @@ options: working directory). --extra EXTRA, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --file-name FILE_NAME File name of changelog (default: 'CHANGELOG.md'). --prerelease-offset PRERELEASE_OFFSET diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt index 2d1135af74..30b33577e8 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt @@ -3,7 +3,8 @@ usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--start-rev START_REV] [--merge-prerelease] [--version-scheme {pep440,semver,semver2}] [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] - [--extra EXTRA] [--tag-format TAG_FORMAT] + [--extra EXTRA] [--add-author [{author,signed-off-by}]] + [--tag-format TAG_FORMAT] [rev_range] Generate changelog (note that it will overwrite existing files) @@ -38,5 +39,10 @@ options: working directory). --extra EXTRA, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --tag-format TAG_FORMAT The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt index 8e4c863585..b2bdcadb71 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt @@ -8,7 +8,8 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--version-files-only] [--annotated-tag-message ANNOTATED_TAG_MESSAGE] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] - [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] + [--add-author [{author,signed-off-by}]] [--file-name FILE_NAME] + [--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA] [--get-next] @@ -72,6 +73,11 @@ options: Changelog template file name (relative to the current working directory). --extra, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --file-name FILE_NAME File name of changelog (default: 'CHANGELOG.md'). --prerelease-offset PRERELEASE_OFFSET diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt index 50ab468d64..72d1383c4c 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt @@ -3,7 +3,8 @@ usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--start-rev START_REV] [--merge-prerelease] [--version-scheme {pep440,semver,semver2}] [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] - [--extra EXTRA] [--tag-format TAG_FORMAT] + [--extra EXTRA] [--add-author [{author,signed-off-by}]] + [--tag-format TAG_FORMAT] [rev_range] Generate changelog (note that it will overwrite existing files) @@ -37,5 +38,10 @@ options: Changelog template file name (relative to the current working directory). --extra, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --tag-format TAG_FORMAT The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt index 8e4c863585..b2bdcadb71 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt @@ -8,7 +8,8 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--version-files-only] [--annotated-tag-message ANNOTATED_TAG_MESSAGE] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] - [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] + [--add-author [{author,signed-off-by}]] [--file-name FILE_NAME] + [--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA] [--get-next] @@ -72,6 +73,11 @@ options: Changelog template file name (relative to the current working directory). --extra, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --file-name FILE_NAME File name of changelog (default: 'CHANGELOG.md'). --prerelease-offset PRERELEASE_OFFSET diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt index 50ab468d64..72d1383c4c 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt @@ -3,7 +3,8 @@ usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--start-rev START_REV] [--merge-prerelease] [--version-scheme {pep440,semver,semver2}] [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] - [--extra EXTRA] [--tag-format TAG_FORMAT] + [--extra EXTRA] [--add-author [{author,signed-off-by}]] + [--tag-format TAG_FORMAT] [rev_range] Generate changelog (note that it will overwrite existing files) @@ -37,5 +38,10 @@ options: Changelog template file name (relative to the current working directory). --extra, -e EXTRA Changelog extra variables (in the form 'key=value'). + --add-author [{author,signed-off-by}] + Add the author to each changelog entry. 'author' + (default) uses the commit author. 'signed-off-by' uses + the first Signed-off-by trailer, or the commit author + if the commit has no trailer. --tag-format TAG_FORMAT The format of the tag, wrap around simple quotes. diff --git a/tests/test_changelog.py b/tests/test_changelog.py index ee0d972262..1a260d7a00 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1183,6 +1183,81 @@ def test_generate_tree_from_commits(gitcommits, tags, merge_prereleases): assert "parents" in change +SIGNED_BODY = "Signed-off-by: test " + + +@pytest.mark.parametrize( + ("author_source", "body", "expected"), + [ + pytest.param( + changelog.AuthorSource.AUTHOR, + SIGNED_BODY, + ("Commitizen", "author@cz.dev"), + id="author-ignores-trailer", + ), + pytest.param( + changelog.AuthorSource.SIGNED_OFF_BY, + SIGNED_BODY, + ("test", "test@mail.com"), + id="signed-off-by", + ), + pytest.param( + changelog.AuthorSource.SIGNED_OFF_BY, + "", + ("Commitizen", "author@cz.dev"), + id="signed-off-by-fallback-to-author", + ), + ], +) +def test_generate_tree_from_commits_with_author_source( + tags, + author_source: changelog.AuthorSource, + body: str, + expected: tuple[str, str], +): + """Each change gets `changelog_author` and `changelog_author_email` (`--add-author`).""" + # Arrange + commit = git.GitCommit( + "test_rev", + "feat: add author", + body=body, + author="Commitizen", + author_email="author@cz.dev", + ) + + # Act + tree = changelog.generate_tree_from_commits( + [commit], + tags, + ConventionalCommitsCz.commit_parser, + ConventionalCommitsCz.changelog_pattern, + author_source=author_source, + ) + + # Assert + (change,) = next(tree)["changes"]["feat"] + assert (change["changelog_author"], change["changelog_author_email"]) == expected + + +def test_generate_tree_from_commits_without_author_source(tags): + """Without `--add-author`, the change has no extra keys (backward compatible).""" + # Arrange + commit = git.GitCommit("test_rev", "feat: no author", body=SIGNED_BODY) + + # Act + tree = changelog.generate_tree_from_commits( + [commit], + tags, + ConventionalCommitsCz.commit_parser, + ConventionalCommitsCz.changelog_pattern, + ) + + # Assert + (change,) = next(tree)["changes"]["feat"] + assert "changelog_author" not in change + assert "changelog_author_email" not in change + + def test_generate_tree_from_commits_with_no_commits(tags): parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.bump_pattern diff --git a/tests/test_git.py b/tests/test_git.py index db0ce4039b..5f587a1515 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -101,6 +101,50 @@ def test_git_message_with_empty_body(): assert commit.message == commit_title +@pytest.mark.parametrize( + ("body", "expected"), + [ + pytest.param( + "Some body\n\nSigned-off-by: test ", + ("test", "test@mail.com"), + id="one-trailer", + ), + pytest.param( + "signed-off-by: test ", + ("test", "test@mail.com"), + id="lowercase-key", + ), + pytest.param("Some body without trailer", None, id="no-trailer"), + pytest.param("", None, id="empty-body"), + ], +) +def test_git_commit_signed_off_by(body: str, expected: tuple[str, str] | None): + """Read the first `Signed-off-by` trailer from the commit body.""" + # Arrange + commit = git.GitCommit("test_rev", "feat: title", body=body) + + # Act + result = commit.signed_off_by + + # Assert + assert result == expected + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_commits_signed_off_by(util: UtilFixture): + """`git log` keeps the trailer in the body, so `signed_off_by` can read it.""" + # Arrange + util.create_file_and_commit( + "feat: signed commit\n\nSigned-off-by: test " + ) + + # Act + commit = git.get_commits()[0] + + # Assert + assert commit.signed_off_by == ("test", "test@mail.com") + + @pytest.mark.usefixtures("tmp_commitizen_project") def test_get_log_as_str_list_empty(): """