fix(cz_customize): set commit_parser default that captures change_type - #1970
Conversation
CustomizeCommitsCz previously inherited the `commit_parser =
r"(?P<message>.*)"` default from `BaseCommitizen`. That default
matches everything but does not capture a `change_type` named group,
so even when a `cz_customize` user configured `changelog_pattern`,
`change_type_map` and `change_type_order`, the changelog generator
could not group commits and emitted a single ungrouped bullet list.
Set a conventional-commits-style default that captures
`change_type`, `scope`, `breaking` and `message` named groups.
Users with a different commit format can still override via
`customize.commit_parser`.
End-to-end check on the exact reproducer from the issue now produces a
properly grouped changelog:
## Unreleased
### feat
- **DL-4567**: new feature test
### fix
- **DL-1234**: qweqwe
### chore
- update deps
Closes commitizen-tools#466
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1970 +/- ##
=======================================
Coverage 98.23% 98.23%
=======================================
Files 61 61
Lines 2779 2780 +1
=======================================
+ Hits 2730 2731 +1
Misses 49 49 ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Pull request overview
This PR addresses cz_customize changelog grouping by providing a default commit_parser that extracts change_type, enabling sectioned changelog output when users configure changelog_pattern but don’t explicitly set customize.commit_parser.
Changes:
- Add a conventional-commits-shaped
commit_parserdefault toCustomizeCommitsCzsochange_typecan be derived for grouping. - Add a regression test asserting the default parser extracts
change_type,scope,breaking, andmessagewithout explicit config.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| commitizen/cz/customize/customize.py | Adds a new class-level default commit_parser intended to extract change_type for changelog grouping. |
| tests/test_cz_customize.py | Adds a regression test verifying the new default parser extracts the expected named groups. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Wrap the conventional-commits prefix in an optional group so subjects that don't follow the format (e.g. 'bug fix: x', 'ticket-123 do stuff', '✨ feature: x') are still captured under <message> with change_type=None instead of being silently dropped by generate_tree_from_commits. Also broaden <change_type> from \w+ to [\w-]+ to accommodate hyphenated types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Manny7717
left a comment
There was a problem hiding this comment.
Verified locally on head 4b78268 (worktrees against origin/master 4184174).
Fix is correct. cz_customize inherited BaseCommitizen.commit_parser = r"(?P<message>.*)" — no change_type named group ever matched, so changelogs generated with only changelog_pattern (+ optional change_type_map/change_type_order) came out completely ungrouped (#466). The new default is a conventional-commits-style parser with an OPTIONAL prefix, which is the key design property: conforming subjects get change_type/scope/breaking extracted, and non-conforming subjects still match with change_type=None and the full message captured (.+) — nothing is dropped. Verified directly: feat(scope): a feature → {change_type: feat, scope: scope, message: a feature}; plain message without prefix → {change_type: None, message: plain message without prefix}.
Scope is tight: commit_parser feeds commands/changelog.py only — bump increment detection uses cz.bump_map/bump_pattern (unchanged), and the __init__ setattr loop still lets an explicit customize.commit_parser override win, so the new default applies solely when the user configures none. The multiline ^...$ with (?P<message>.+)$ can't match a fully-empty subject — but git rejects empty messages anyway, so no real-world regression surface.
Regression-proven: both new tests FAIL on base (inherited parser yields no change_type / the old .match() dropped nothing but extracted nothing), PASS on head.
No regressions: full test_changelog.py + test_cz_customize.py + test_changelog_command.py on head = 225 passed; base = 223 passed + the 2 intended failures — zero new failures. ruff clean on the changed file.
One non-blocking note: the prefix class [\w-]+ is deliberately permissive (customize users define arbitrary types), so a message like note: fix typo will group under "note" when change_type_map includes it — that's the intended contract, but worth a line in docs/config/customization.md if this lands, since it changes grouping for existing users who relied on the ungrouped default.
Description
Closes #466.
Why
cz_customizelets users define their own commit schema viachangelog_pattern,change_type_order, andchange_type_map— all the ingredients for a grouped changelog. However, until now, runningcz changelogwith those settings produced a single flat bullet list with no### Feat/### Fixheadings, making the feature effectively unusable without an extra, non-obvious step.The root cause is the
commit_parserdefault inherited fromBaseCommitizen.BaseCommitizen.commit_parserisr"(?P<message>.*)"(commitizen/cz/base.py:58) — a catch-all with nochange_typenamed group. The changelog generator incommitizen/changelog.py:195callsmsg.pop("change_type", None)to key commits into sections; without that group, every commit yieldschange_type = Noneand lands in an ungrouped bucket (changelog.py:198).A maintainer confirmed the gap in December 2021 (issue comment by @woile): "I think the
commit_parserregex is not exposed in the customize… and it looks like the customize is using the default from the base." Users who stumbled across the fix documented it in later comments — e.g. a community workaround posted in 2025 showing the exact four-group regex that had to be copy-pasted into everycz_customizeconfig. Triage against master (v4.15.1) in #1964 verified the bug is still present:cz changelog --dry-runwithchangelog_patternset still produces an ungrouped list.The fix gives
CustomizeCommitsCzits owncommit_parserclass attribute — a conventional-commits-shaped default that works for any schema following the<type>[(scope)][!]: <message>convention — so users who only setchangelog_patternget grouping for free.What changed
commitizen/cz/customize/customize.pycommit_parserclass attribute (conventional-commits-shaped default withchange_type,scope,breaking, andmessagenamed groups)tests/test_cz_customize.pytest_commit_parser_default_extracts_change_type— regression test verifying all four named groups are extracted without anycustomize.commit_parserconfig keyHow it works
commitizen/cz/customize/customize.py:32–45after the PR is applied) sits between the existingchange_type_orderdefault and__init__. It is a four-group regex:__init__override loop (commitizen/cz/customize/customize.py:40–50) already iterates over"commit_parser"and callssetattrif the user provided their own value. The class-level default is therefore transparently superseded by anycustomize.commit_parserkey — zero behaviour change for existing configs that already set one.\w+instead of a fixed alternation?cz_customizeis format-agnostic: it cannot know which types a project has defined. Hard-codingfeat|fix|chore|…would silently drop commits whose types aren't in the list.\w+captures any single-token type, which matches every conventional-commits derivative. An alternative — deriving the alternation fromchangelog_patternat runtime — was considered but is fragile:changelog_patternis a broad filter regex, not a structured grammar, so parsing it reliably would require a regex-of-regexes approach.commit_parseryourself"? The three other customisable attributes (changelog_pattern,change_type_order,change_type_map) all have sensible inherited defaults; a user who sets only those reasonably expects changelog grouping to work.generate_tree_from_commits(commitizen/changelog.py:92–171) compilescommit_parserwithre.MULTILINEand callsmap_pat.match(commit.message)per commit.process_commit_message(changelog.py:174–198) then callsparsed.groupdict()— so the named groups flow directly into thechangesdict that drives section headings.Backward compatibility
customize.commit_parserexplicitly are unaffected — the__init__loop (customize.py:40–50) overwrites the class default with their value viasetattr.commit_parserknowing it would match everything toNone) will now see headings. This is the desired fix; the old behaviour was a bug, not a feature.test_cz_customize.pytests still pass — the new default only changes what happens when nocommit_parseris configured.BaseCommitizen.commit_parser(commitizen/cz/base.py:58) is unchanged; other built-incz_*plugins are unaffected.Checklist
Was generative AI tooling used to co-author this PR?
Generated-by: Claude following the guidelines
Code Changes
uv run poe alllocally to ensure this change passes linter check and testsExpected Behavior
cz_customizewithchangelog_pattern+change_type_order, nocommit_parserkeycz changelog --dry-runrenders### feat,### fix,### choreheadingscz_customizewith an explicitcustomize.commit_parsercz_*(e.g.cz_conventional_commits)BaseCommitizen.commit_parseris unchangedcz_customizewith commits that don't match the new default (e.g.ticket-123 do stuff)change_type = None— same as before;changelog_patterncontrols what's includedSteps to Test This Pull Request