From a0ef51d6036346ee146806c99f5638951d701fa0 Mon Sep 17 00:00:00 2001 From: Tim Hsiung Date: Sat, 9 May 2026 19:20:23 +0800 Subject: [PATCH 1/2] fix(cz_customize): default commit_parser extracts change_type CustomizeCommitsCz previously inherited the `commit_parser = r"(?P.*)"` 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 #466 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- commitizen/cz/customize/customize.py | 12 ++++++++ tests/test_cz_customize.py | 43 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 8fcc63fac..8ca08c939 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -29,6 +29,18 @@ class CustomizeCommitsCz(BaseCommitizen): bump_map = defaults.BUMP_MAP bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO change_type_order = defaults.CHANGE_TYPE_ORDER + # A conventional-commits-style default so that ``cz_customize`` users who + # configure ``changelog_pattern`` (and optionally ``change_type_map`` / + # ``change_type_order``) but no explicit ``commit_parser`` still get a + # changelog grouped by change type. It captures any word as + # ``change_type``, an optional scope, an optional ``!`` breaking marker, + # and the message subject. Users with a different commit format can + # still override this via ``customize.commit_parser`` (#466). + commit_parser = ( + r"^(?P\w+)" + r"(?:\((?P[^()\r\n]*)\))?" + r"(?P!)?:\s*(?P.*)$" + ) def __init__(self, config: BaseConfig) -> None: super().__init__(config) diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py index 311eea19a..daeb73a39 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -591,6 +591,49 @@ def test_commit_parser_unicode(config_with_unicode): ) +def test_commit_parser_default_extracts_change_type(): + """Regression test for #466: when ``customize.commit_parser`` is not set, + the default must still extract a ``change_type`` named group so that the + changelog can be grouped by type (e.g. ``### Feat``, ``### Fix``). + Previously the default was ``r"(?P.*)"`` -- inherited from + ``BaseCommitizen`` -- which left every commit ungrouped. + """ + import re + + config = BaseConfig() + config.settings.update( + { + "name": "cz_customize", + "customize": { + # No ``commit_parser`` provided -- exercises the new default. + "changelog_pattern": r"^(feat|fix|chore)(\(.+\))?(!)?", + }, + } + ) + cz = CustomizeCommitsCz(config) + + pattern = re.compile(cz.commit_parser, re.MULTILINE) + + feat = pattern.match("feat(scope): a feature") + assert feat is not None + assert feat.group("change_type") == "feat" + assert feat.group("scope") == "scope" + assert feat.group("breaking") is None + assert feat.group("message") == "a feature" + + breaking = pattern.match("fix!: breaking fix") + assert breaking is not None + assert breaking.group("change_type") == "fix" + assert breaking.group("breaking") == "!" + assert breaking.group("message") == "breaking fix" + + no_scope = pattern.match("chore: tidy up") + assert no_scope is not None + assert no_scope.group("change_type") == "chore" + assert no_scope.group("scope") is None + assert no_scope.group("message") == "tidy up" + + def test_changelog_pattern(config): cz = CustomizeCommitsCz(config) assert cz.changelog_pattern == "^(feature|bug fix)?(!)?" From 4b782681c9c34ae21c335c5d0372776c3ad08657 Mon Sep 17 00:00:00 2001 From: Tim Hsiung <26526132+bearomorphism@users.noreply.github.com> Date: Sat, 9 May 2026 22:26:52 +0800 Subject: [PATCH 2/2] fix(cz_customize): make default commit_parser non-dropping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 with change_type=None instead of being silently dropped by generate_tree_from_commits. Also broaden from \w+ to [\w-]+ to accommodate hyphenated types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- commitizen/cz/customize/customize.py | 13 +++-- tests/test_cz_customize.py | 71 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 8ca08c939..b3d2ab81a 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -32,14 +32,13 @@ class CustomizeCommitsCz(BaseCommitizen): # A conventional-commits-style default so that ``cz_customize`` users who # configure ``changelog_pattern`` (and optionally ``change_type_map`` / # ``change_type_order``) but no explicit ``commit_parser`` still get a - # changelog grouped by change type. It captures any word as - # ``change_type``, an optional scope, an optional ``!`` breaking marker, - # and the message subject. Users with a different commit format can - # still override this via ``customize.commit_parser`` (#466). + # changelog grouped by change type. The conventional prefix is optional, + # so non-conforming subjects remain included as ungrouped messages. + # Users with a different commit format can still override this via + # ``customize.commit_parser`` (#466). commit_parser = ( - r"^(?P\w+)" - r"(?:\((?P[^()\r\n]*)\))?" - r"(?P!)?:\s*(?P.*)$" + r"^((?P[\w-]+)(?:\((?P[^()\r\n]*)\))?" + r"(?P!)?:\s+)?(?P.+)$" ) def __init__(self, config: BaseConfig) -> None: diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py index daeb73a39..aff957c22 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -634,6 +634,77 @@ def test_commit_parser_default_extracts_change_type(): assert no_scope.group("message") == "tidy up" +def test_commit_parser_default_keeps_non_conforming_subjects(): + import re + + config = BaseConfig() + config.settings.update( + { + "name": "cz_customize", + "customize": { + # No ``commit_parser`` provided -- exercises the new default. + "changelog_pattern": r".*", + }, + } + ) + cz = CustomizeCommitsCz(config) + + pattern = re.compile(cz.commit_parser, re.MULTILINE) + + cases = [ + ( + "bug fix: do stuff", + { + "change_type": None, + "scope": None, + "breaking": None, + "message": "bug fix: do stuff", + }, + ), + ( + "ticket-123 do stuff", + { + "change_type": None, + "scope": None, + "breaking": None, + "message": "ticket-123 do stuff", + }, + ), + ( + "✨ feature: shiny", + { + "change_type": None, + "scope": None, + "breaking": None, + "message": "✨ feature: shiny", + }, + ), + ( + "feat: ok", + { + "change_type": "feat", + "scope": None, + "breaking": None, + "message": "ok", + }, + ), + ( + "feat(api)!: bang", + { + "change_type": "feat", + "scope": "api", + "breaking": "!", + "message": "bang", + }, + ), + ] + + for subject, expected_groups in cases: + match = pattern.match(subject) + assert match is not None + assert match.groupdict() == expected_groups + + def test_changelog_pattern(config): cz = CustomizeCommitsCz(config) assert cz.changelog_pattern == "^(feature|bug fix)?(!)?"