Skip to content

Commit 7898cd0

Browse files
fix(changelog): add changelog_subject_only to skip body parsing
Closes #1267. `generate_tree_from_commits()` historically parses the commit subject and each `\n\n`-separated body block against `commit_parser`, so a commit whose subject is `feat: ...` and whose body contains another `refactor: ...` line produces two changelog entries instead of one. Maintainer ack on #1267 confirms this is undesirable, but changing the default is a behavioural break. This change introduces `changelog_subject_only` (default `false`) on `Settings`. When set to `true`, the body iteration in `generate_tree_from_commits()` is skipped, leaving only the subject line to be matched. The setting is plumbed through `commands/changelog.py` so both `cz changelog` and `cz bump --changelog` honour it. A regression test exercises both modes against a commit whose body contains a parser-matching block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4d99415 commit 7898cd0

5 files changed

Lines changed: 68 additions & 5 deletions

File tree

commitizen/changelog.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def generate_tree_from_commits(
100100
changelog_release_hook: ChangelogReleaseHook | None = None,
101101
rules: TagRules | None = None,
102102
during_version_bump: bool = False,
103+
subject_only: bool = False,
103104
) -> Generator[dict[str, Any], None, None]:
104105
pat = re.compile(changelog_pattern)
105106
map_pat = re.compile(commit_parser, re.MULTILINE)
@@ -147,11 +148,15 @@ def generate_tree_from_commits(
147148
if not pat.match(commit.message):
148149
continue
149150

150-
# Process subject and body from commit message
151-
for message in chain(
152-
[map_pat.match(commit.message)],
153-
(body_map_pat.match(block) for block in commit.body.split("\n\n")),
154-
):
151+
# Process subject; optionally also parse body blocks for nested entries
152+
# (legacy default behaviour, controlled by `changelog_subject_only`).
153+
subject_match = map_pat.match(commit.message)
154+
body_matches: Iterable[re.Match[str] | None] = (
155+
()
156+
if subject_only
157+
else (body_map_pat.match(block) for block in commit.body.split("\n\n"))
158+
)
159+
for message in chain([subject_match], body_matches):
155160
if message:
156161
process_commit_message(
157162
changelog_message_builder_hook,

commitizen/commands/changelog.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def __call__(self) -> None:
276276
changelog_release_hook=self.cz.changelog_release_hook,
277277
rules=self.tag_rules,
278278
during_version_bump=self.during_version_bump,
279+
subject_only=self.config.settings["changelog_subject_only"],
279280
)
280281
if self.change_type_order:
281282
tree = changelog.generate_ordered_changelog_tree(

commitizen/defaults.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Settings(TypedDict, total=False):
4141
changelog_incremental: bool
4242
changelog_merge_prerelease: bool
4343
changelog_start_rev: str | None
44+
changelog_subject_only: bool
4445
customize: CzSettings
4546
encoding: str
4647
extras: dict[str, Any]
@@ -103,6 +104,7 @@ class Settings(TypedDict, total=False):
103104
"changelog_incremental": False,
104105
"changelog_start_rev": None,
105106
"changelog_merge_prerelease": False,
107+
"changelog_subject_only": False,
106108
"update_changelog_on_bump": False,
107109
"use_shortcuts": False,
108110
"major_version_zero": False,

docs/commands/changelog.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ This flag can be set in the configuration file with the key `changelog_merge_pre
147147
changelog_merge_prerelease = true
148148
```
149149

150+
### `changelog_subject_only`
151+
152+
By default, Commitizen parses both the subject line and any `\n\n`-separated body blocks of each commit against `commit_parser`, so a commit such as
153+
154+
```
155+
feat: new feature
156+
157+
refactor: incidental cleanup
158+
```
159+
160+
produces *two* changelog entries (one under `feat`, one under `refactor`). Set this configuration to `true` to limit changelog parsing to the subject line only.
161+
162+
```toml
163+
[tool.commitizen]
164+
# ...
165+
changelog_subject_only = true
166+
```
167+
168+
The default (`false`) preserves the historical behaviour.
169+
150170
### `--template`
151171

152172
Provide your own changelog Jinja template by using the `template` settings or the `--template` parameter.

tests/test_changelog.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,41 @@ def test_generate_tree_from_commits_with_no_commits(tags):
11911191
assert tuple(tree) == ({"changes": {}, "date": "", "version": "Unreleased"},)
11921192

11931193

1194+
def test_generate_tree_from_commits_subject_only_skips_body_blocks(tags):
1195+
"""`subject_only=True` ignores parser-matching blocks inside `commit.body`.
1196+
1197+
Regression for #1267 where, e.g., a commit subject `feat: new feature`
1198+
with a body containing `refactor: cleanup` produced two changelog
1199+
entries instead of one.
1200+
"""
1201+
parser = ConventionalCommitsCz.commit_parser
1202+
changelog_pattern = ConventionalCommitsCz.bump_pattern
1203+
1204+
commit = git.GitCommit(
1205+
rev="abc123",
1206+
title="feat: new feature",
1207+
body="some prose\n\nrefactor: incidental cleanup",
1208+
author="Commitizen",
1209+
author_email="author@cz.dev",
1210+
)
1211+
1212+
default_tree = list(
1213+
changelog.generate_tree_from_commits([commit], tags, parser, changelog_pattern)
1214+
)
1215+
assert default_tree[0]["changes"].keys() == {"feat", "refactor"}
1216+
1217+
subject_only_tree = list(
1218+
changelog.generate_tree_from_commits(
1219+
[commit],
1220+
tags,
1221+
parser,
1222+
changelog_pattern,
1223+
subject_only=True,
1224+
)
1225+
)
1226+
assert subject_only_tree[0]["changes"].keys() == {"feat"}
1227+
1228+
11941229
@pytest.mark.parametrize(
11951230
("change_type_order", "expected_reordering"),
11961231
[

0 commit comments

Comments
 (0)