diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 8fcc63fac..b3d2ab81a 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -29,6 +29,17 @@ 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. 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-]+)(?:\((?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..aff957c22 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -591,6 +591,120 @@ 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_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)?(!)?"