diff --git a/commitizen/changelog.py b/commitizen/changelog.py index d8b8accca..514659437 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -100,6 +100,7 @@ def generate_tree_from_commits( changelog_release_hook: ChangelogReleaseHook | None = None, rules: TagRules | None = None, during_version_bump: bool = False, + subject_only: bool = False, ) -> Generator[dict[str, Any], None, None]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser, re.MULTILINE) @@ -147,11 +148,15 @@ def generate_tree_from_commits( if not pat.match(commit.message): continue - # Process subject and body from commit message - for message in chain( - [map_pat.match(commit.message)], - (body_map_pat.match(block) for block in commit.body.split("\n\n")), - ): + # Process subject; optionally also parse body blocks for nested entries + # (legacy default behaviour, controlled by `changelog_subject_only`). + subject_match = map_pat.match(commit.message) + body_matches: Iterable[re.Match[str] | None] = ( + () + if subject_only + else (body_map_pat.match(block) for block in commit.body.split("\n\n")) + ) + for message in chain([subject_match], body_matches): if message: process_commit_message( changelog_message_builder_hook, diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 5521da373..5919e4f34 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -276,6 +276,7 @@ def __call__(self) -> None: changelog_release_hook=self.cz.changelog_release_hook, rules=self.tag_rules, during_version_bump=self.during_version_bump, + subject_only=self.config.settings["changelog_subject_only"], ) if self.change_type_order: tree = changelog.generate_ordered_changelog_tree( diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 4865ccc18..1ae67beda 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -41,6 +41,7 @@ class Settings(TypedDict, total=False): changelog_incremental: bool changelog_merge_prerelease: bool changelog_start_rev: str | None + changelog_subject_only: bool customize: CzSettings encoding: str extras: dict[str, Any] @@ -103,6 +104,7 @@ class Settings(TypedDict, total=False): "changelog_incremental": False, "changelog_start_rev": None, "changelog_merge_prerelease": False, + "changelog_subject_only": False, "update_changelog_on_bump": False, "use_shortcuts": False, "major_version_zero": False, diff --git a/docs/commands/changelog.md b/docs/commands/changelog.md index 8b7a7a4d4..24fc4e878 100644 --- a/docs/commands/changelog.md +++ b/docs/commands/changelog.md @@ -147,6 +147,26 @@ This flag can be set in the configuration file with the key `changelog_merge_pre changelog_merge_prerelease = true ``` +### `changelog_subject_only` + +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 + +``` +feat: new feature + +refactor: incidental cleanup +``` + +produces *two* changelog entries (one under `feat`, one under `refactor`). Set this configuration to `true` to limit changelog parsing to the subject line only. + +```toml +[tool.commitizen] +# ... +changelog_subject_only = true +``` + +The default (`false`) preserves the historical behaviour. + ### `--template` Provide your own changelog Jinja template by using the `template` settings or the `--template` parameter. diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index b2d024ac7..9dc4adf05 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -87,6 +87,32 @@ def test_changelog_with_different_cz( file_regression.check(out, extension=".md") +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_subject_only_setting_skips_body_parsing( + util: UtilFixture, + config_path: Path, + capsys: pytest.CaptureFixture, +): + """End-to-end coverage for `changelog_subject_only` (#1267 / #1974). + + Verifies the config-key wiring in `commands/changelog.py`: when the + setting is `true`, parser-matching blocks in commit bodies are + ignored. Catches regressions where the setting key is mistyped or + not propagated to `generate_tree_from_commits`. + """ + with config_path.open("a") as f: + f.write("changelog_subject_only = true\n") + + util.create_file_and_commit("feat: add new output\n\nrefactor: incidental cleanup") + + with pytest.raises(DryRunExit): + util.run_cli("changelog", "--dry-run") + out, _ = capsys.readouterr() + + assert "add new output" in out + assert "incidental cleanup" not in out + + @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_from_start( changelog_format: ChangelogFormat, diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 11c3a6044..7fc24d5ff 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1191,6 +1191,41 @@ def test_generate_tree_from_commits_with_no_commits(tags): assert tuple(tree) == ({"changes": {}, "date": "", "version": "Unreleased"},) +def test_generate_tree_from_commits_subject_only_skips_body_blocks(tags): + """`subject_only=True` ignores parser-matching blocks inside `commit.body`. + + Regression for #1267 where, e.g., a commit subject `feat: new feature` + with a body containing `refactor: cleanup` produced two changelog + entries instead of one. + """ + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.bump_pattern + + commit = git.GitCommit( + rev="abc123", + title="feat: new feature", + body="some prose\n\nrefactor: incidental cleanup", + author="Commitizen", + author_email="author@cz.dev", + ) + + default_tree = list( + changelog.generate_tree_from_commits([commit], tags, parser, changelog_pattern) + ) + assert default_tree[0]["changes"].keys() == {"feat", "refactor"} + + subject_only_tree = list( + changelog.generate_tree_from_commits( + [commit], + tags, + parser, + changelog_pattern, + subject_only=True, + ) + ) + assert subject_only_tree[0]["changes"].keys() == {"feat"} + + @pytest.mark.parametrize( ("change_type_order", "expected_reordering"), [ diff --git a/tests/test_conf.py b/tests/test_conf.py index c004e96e1..c4983c374 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -100,6 +100,7 @@ "changelog_incremental": False, "changelog_start_rev": None, "changelog_merge_prerelease": False, + "changelog_subject_only": False, "update_changelog_on_bump": False, "use_shortcuts": False, "major_version_zero": False, @@ -140,6 +141,7 @@ "changelog_incremental": False, "changelog_start_rev": None, "changelog_merge_prerelease": False, + "changelog_subject_only": False, "update_changelog_on_bump": False, "use_shortcuts": False, "major_version_zero": False,