Skip to content

fix(changelog): add changelog_subject_only to skip body parsing#1974

Closed
bearomorphism wants to merge 3 commits intocommitizen-tools:masterfrom
bearomorphism:fix/1267-changelog-subject-only
Closed

fix(changelog): add changelog_subject_only to skip body parsing#1974
bearomorphism wants to merge 3 commits intocommitizen-tools:masterfrom
bearomorphism:fix/1267-changelog-subject-only

Conversation

@bearomorphism
Copy link
Copy Markdown
Collaborator

@bearomorphism bearomorphism commented May 9, 2026

Description

Closes #1267.

Why

commitizen.changelog.generate_tree_from_commits() parses both the commit subject and each \n\n-separated block of commit.body against commit_parser. So a commit such as

feat: new feature

refactor: incidental cleanup

produces two changelog entries:

### Feat
- new feature
### Refactor
- incidental cleanup

This is rarely what the author intended — they wrote one feat, the body was just a note. The maintainer ack on #1267 confirms the body-parsing is undesirable, but flipping the default would be a behaviour break for existing users (some intentionally rely on it for multi-message commits).

What changed

File Change
commitizen/defaults.py New TypedDict field changelog_subject_only: bool and DEFAULT_SETTINGS["changelog_subject_only"] = False.
commitizen/changelog.py generate_tree_from_commits() gains a subject_only: bool = False kwarg. When True, the body iteration is replaced with an empty tuple, so only the subject is matched.
commitizen/commands/changelog.py Changelog.__call__ forwards self.config.settings["changelog_subject_only"] to generate_tree_from_commits(). This single call site is shared by cz changelog and cz bump --changelog.
tests/test_changelog.py New test_generate_tree_from_commits_subject_only_skips_body_blocks exercises both modes against a commit whose body contains a parser-matching block.
tests/commands/test_changelog_command.py New test_changelog_subject_only_setting_skips_body_parsing is an end-to-end test (real config file, real cz changelog invocation) that catches typos in the config-key wiring.
tests/test_conf.py Two expected-Settings literals updated to include the new key (otherwise the strict-equality assertions in TestReadCfg::test_load_conf fail).
docs/commands/changelog.md New ### changelog_subject_only section next to the existing --merge-prerelease documentation.

How it works

  1. Default-off semantics. changelog_subject_only = false is the default in both the Settings TypedDict and DEFAULT_SETTINGS, so BaseConfig.__init__ always seeds the dict-key with False and the dict-index access in Changelog.__call__ cannot KeyError.
  2. Single chokepoint. Body iteration happens at one place — the chain([subject_match], (body_map_pat.match(block) for block in commit.body.split("\n\n"))) in generate_tree_from_commits(). Replacing the second argument of chain with () when subject_only=True is the entire behavioural change.
  3. Why from __future__ import annotations matters. The new body_matches: Iterable[re.Match[str] | None] annotation only resolves in TYPE_CHECKING blocks at runtime; without from __future__ import annotations the import would have to leave TYPE_CHECKING. The file already had it.
  4. The end-to-end test was specifically requested. Both reviewers (sonnet-4.6 and GitHub Copilot) flagged that the lib-level test alone wouldn't catch a typo in the self.config.settings["changelog_subject_only"] lookup at commands/changelog.py. The fixup commit 292feb4f adds an integration test that constructs a real config and runs cz changelog --dry-run, then verified by hand that the test fails (with KeyError) when the config key is mistyped.

Backward compatibility

  • The default false preserves the historical body-parsing behaviour for every user who hasn't opted in.
  • All 158 pre-existing tests/test_changelog.py and tests/commands/test_changelog_command.py tests pass unchanged.
  • The new subject_only kwarg on generate_tree_from_commits() defaults to False, so any external caller that imports the function continues to work.

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce (1 unit test + 1 integration test)
  • Run uv run poe all locally to ensure this change passes linter check and tests (poe lint clean; 159 changelog tests pass)
  • Manually test the changes (see "Steps to Test" below)
  • Update the documentation for the changes

Documentation Changes

  • Added a ### changelog_subject_only section to docs/commands/changelog.md
  • Run uv run poe doc locally to ensure the documentation pages render correctly
  • Check and fix any broken links (internal or external)

Expected Behavior

Setting Subject feat: X + body refactor: Y Result
changelog_subject_only unset (default false) Both subject and body block parsed Two entries: one feat, one refactor (legacy)
changelog_subject_only = true Only subject parsed One entry: feat: X

Applies to both cz changelog and cz bump --changelog.

Steps to Test This Pull Request

git fetch fork fix/1267-changelog-subject-only
git checkout fork/fix/1267-changelog-subject-only

# 1. Lib-level regression test (forces the body-skip behaviour at the call site).
uv run pytest tests/test_changelog.py -k subject_only -v

# 2. End-to-end test (real config file, real cz changelog invocation).
uv run pytest tests/commands/test_changelog_command.py::test_changelog_subject_only_setting_skips_body_parsing -v

# 3. Verify the e2e test would catch a typo in the config-key wiring.
sed -i.bak 's/changelog_subject_only/changelog_subject_only_TYPO/' commitizen/commands/changelog.py
uv run pytest tests/commands/test_changelog_command.py::test_changelog_subject_only_setting_skips_body_parsing -v
# expected: KeyError: 'changelog_subject_only_TYPO'
mv commitizen/commands/changelog.py.bak commitizen/commands/changelog.py
uv run pytest tests/commands/test_changelog_command.py::test_changelog_subject_only_setting_skips_body_parsing -v
# expected: 1 passed

# 4. Full changelog suite (no regressions).
uv run pytest tests/test_changelog.py tests/commands/test_changelog_command.py -q
# expected: 159 passed

Additional Context

Surfaced while triaging open issues in #1965. Maintainer ack on #1267 confirmed body-parsing is at-best opt-in. The end-to-end integration test was added in fixup commit 292feb4f after both review passes (sonnet-4.6 and Copilot) flagged the wiring as the only path not exercised by tests.

Closes commitizen-tools#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 commitizen-tools#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>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.23%. Comparing base (4b93a50) to head (292feb4).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1974   +/-   ##
=======================================
  Coverage   98.23%   98.23%           
=======================================
  Files          61       61           
  Lines        2779     2782    +3     
=======================================
+ Hits         2730     2733    +3     
  Misses         49       49           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Followup to fix(changelog): the new default key broke
`tests/test_conf.py::TestReadCfg` because the expected `Settings`
literals pinned the full dict and didn''t know about the new field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new configuration setting to control whether changelog generation should parse commit body blocks as additional changelog entries, addressing issue #1267 while preserving historical behavior by default.

Changes:

  • Introduces changelog_subject_only (default false) to skip parsing \n\n-separated commit body blocks when generating changelogs.
  • Plumbs the setting through the Changelog command so cz changelog (and cz bump --changelog via the same command) can honor it.
  • Adds a regression test for generate_tree_from_commits(..., subject_only=True) and documents the new setting.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
commitizen/changelog.py Adds subject_only parameter to skip body-block parsing when building the changelog tree.
commitizen/commands/changelog.py Passes changelog_subject_only from config into changelog tree generation.
commitizen/defaults.py Adds changelog_subject_only to typed settings and default settings.
tests/test_changelog.py Adds regression coverage for subject-only parsing behavior.
tests/test_conf.py Updates expected/default settings fixtures to include changelog_subject_only.
docs/commands/changelog.md Documents the new changelog_subject_only setting and its effect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 268 to 280
tree = changelog.generate_tree_from_commits(
commits,
tags,
commit_parser,
changelog_pattern,
self.unreleased_version,
change_type_map=self.change_type_map,
changelog_message_builder_hook=self.cz.changelog_message_builder_hook,
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"],
)
Add an end-to-end test that sets `changelog_subject_only = true` in
the project configuration, creates a commit with a parser-matching
block in its body, and asserts that `cz changelog --dry-run` only
emits the subject entry. Catches typos in the setting key at
`commands/changelog.py:279` (which would otherwise silently fall
back to `False`); manually verified by injecting a typo, observing
the test fail with KeyError, then restoring.

Addresses the only review finding from PR commitizen-tools#1974.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bearomorphism
Copy link
Copy Markdown
Collaborator Author

Closing this PR per maintainer-triage policy: feature-request issues should sit with the maintainers for design / scope review before any implementation lands. The issue's type: feature (or implicit equivalent) label means it's not on a "ready-to-implement" track yet.

The implementation itself is preserved on the branch (fix/1267-changelog-subject-only) — if a maintainer decides this is the direction they want, the PR can be re-opened in one click, or the work can serve as a starting point for a maintainer-led design.

This PR is being closed so that #1267 reverts to "awaiting maintainer triage / decision" rather than "PR pending review", which is the correct state for a feature request.

Closed via the round-2 triage cleanup in #1965.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The cz ch command generates a CHANGELOG.md that includes the content from the body.

2 participants