From ef4650fc15402c8867f6a3eee71b567ce8aa3ca9 Mon Sep 17 00:00:00 2001 From: Tim Hsiung Date: Sat, 9 May 2026 19:48:54 +0800 Subject: [PATCH] fix(cz_customize): derive bump_map_major_version_zero from bump_map Previously, when a `cz_customize` user supplied a custom `bump_map` but no explicit `bump_map_major_version_zero`, `cz bump` with `major_version_zero = true` silently fell back to `defaults.BUMP_MAP_MAJOR_VERSION_ZERO` -- a totally unrelated mapping that ignored the user's bump rules. The user's intent (e.g. `feat` = PATCH while major version is 0.x) was overridden. When the user provides `bump_map` but not `bump_map_major_version_zero`, derive the latter from the former by demoting any `MAJOR` rule to `MINOR` -- the only difference in the default mapping pair. Users who want a custom split between the two maps can still set `bump_map_major_version_zero` explicitly. Closes #1728 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- commitizen/cz/customize/customize.py | 29 ++++++++++ tests/test_cz_customize.py | 80 ++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 8fcc63fac..8f8857d21 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import OrderedDict from pathlib import Path from typing import TYPE_CHECKING, Any @@ -19,11 +20,29 @@ from commitizen import defaults from commitizen.cz.base import BaseCommitizen +from commitizen.defaults import MAJOR, MINOR from commitizen.exceptions import MissingCzCustomizeConfigError __all__ = ["CustomizeCommitsCz"] +def _derive_major_version_zero( + bump_map: Mapping[str, str], +) -> OrderedDict[str, str]: + """Derive a ``bump_map_major_version_zero`` from a user-supplied + ``bump_map`` by demoting any ``MAJOR`` rule to ``MINOR``. + + See #1728: when a ``cz_customize`` user supplies ``bump_map`` but not + ``bump_map_major_version_zero``, the latter previously fell through to + ``defaults.BUMP_MAP_MAJOR_VERSION_ZERO``, silently overriding the + user's intent during ``major_version_zero = true`` bumps. + """ + return OrderedDict( + (pattern, MINOR if increment == MAJOR else increment) + for pattern, increment in bump_map.items() + ) + + class CustomizeCommitsCz(BaseCommitizen): bump_pattern = defaults.BUMP_PATTERN bump_map = defaults.BUMP_MAP @@ -49,6 +68,16 @@ def __init__(self, config: BaseConfig) -> None: if value := self.custom_settings.get(attr_name): setattr(self, attr_name, value) + # When the user supplies a custom ``bump_map`` but no matching + # ``bump_map_major_version_zero``, derive the latter so that bumps + # under ``major_version_zero = true`` use the user's mapping rather + # than the (totally unrelated) ``defaults.BUMP_MAP_MAJOR_VERSION_ZERO`` + # fallback. See #1728. + if self.custom_settings.get("bump_map") and not self.custom_settings.get( + "bump_map_major_version_zero" + ): + self.bump_map_major_version_zero = _derive_major_version_zero(self.bump_map) + def questions(self) -> list[CzQuestion]: return self.custom_settings.get("questions", [{}]) # type: ignore[return-value] diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py index 311eea19a..e07863612 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -412,6 +412,86 @@ def test_bump_map_unicode(config_with_unicode): } +def test_bump_map_major_version_zero_is_derived_from_bump_map(): + """Regression test for #1728: when the user provides ``bump_map`` but no + explicit ``bump_map_major_version_zero``, the latter is derived from the + former (``MAJOR`` → ``MINOR``) instead of falling through to the default + ``defaults.BUMP_MAP_MAJOR_VERSION_ZERO``.""" + config = BaseConfig() + config.settings.update( + { + "name": "cz_customize", + "customize": { + "bump_pattern": r"^(feat|fix|docs)", + "bump_map": { + "break": "MAJOR", + "feat": "PATCH", + "docs": "PATCH", + }, + }, + } + ) + + cz = CustomizeCommitsCz(config) + + # Same patterns, MAJOR demoted to MINOR. + assert dict(cz.bump_map_major_version_zero) == { + "break": "MINOR", + "feat": "PATCH", + "docs": "PATCH", + } + + +def test_bump_map_major_version_zero_explicit_user_value_wins(): + """If the user explicitly sets ``bump_map_major_version_zero``, that + value is used verbatim (no derivation).""" + config = BaseConfig() + config.settings.update( + { + "name": "cz_customize", + "customize": { + "bump_pattern": r"^(feat|fix|docs)", + "bump_map": { + "break": "MAJOR", + "feat": "PATCH", + }, + "bump_map_major_version_zero": { + "break": "MAJOR", # NB: kept as MAJOR + "feat": "PATCH", + }, + }, + } + ) + + cz = CustomizeCommitsCz(config) + + assert dict(cz.bump_map_major_version_zero) == { + "break": "MAJOR", + "feat": "PATCH", + } + + +def test_bump_map_major_version_zero_falls_back_to_defaults_without_bump_map(): + """When the user provides neither ``bump_map`` nor + ``bump_map_major_version_zero``, the class default applies.""" + from commitizen import defaults + + config = BaseConfig() + config.settings.update( + { + "name": "cz_customize", + "customize": { + # No bump_map, no bump_map_major_version_zero. + "schema_pattern": r"^(feat|fix): (.*)$", + }, + } + ) + + cz = CustomizeCommitsCz(config) + + assert cz.bump_map_major_version_zero is defaults.BUMP_MAP_MAJOR_VERSION_ZERO + + def test_change_type_order(config): cz = CustomizeCommitsCz(config) assert cz.change_type_order == [