fix(cz_customize): derive bump_map_major_version_zero from custom bump_map#1977
Conversation
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 commitizen-tools#1728 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 #1977 +/- ##
=======================================
Coverage 98.23% 98.24%
=======================================
Files 61 61
Lines 2779 2785 +6
=======================================
+ Hits 2730 2736 +6
Misses 49 49 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Pull request overview
This PR fixes an inconsistency in the cz_customize plugin where major_version_zero = true could cause Commitizen to ignore a user’s custom bump_map and instead fall back to defaults.BUMP_MAP_MAJOR_VERSION_ZERO. It derives bump_map_major_version_zero from the user-provided bump_map when the user doesn’t explicitly configure a separate major-zero map, preserving rule order and demoting MAJOR to MINOR to maintain the intent of major_version_zero.
Changes:
- Add
_derive_major_version_zero()helper to buildbump_map_major_version_zerofrombump_map(withMAJOR → MINORdemotion). - Update
CustomizeCommitsCz.__init__to derivebump_map_major_version_zerowhenbump_mapis set butbump_map_major_version_zerois not. - Add regression/behavior tests covering derivation, explicit override precedence, and default fallback behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
commitizen/cz/customize/customize.py |
Derives bump_map_major_version_zero from custom bump_map to ensure major_version_zero bumps honor user mappings. |
tests/test_cz_customize.py |
Adds tests verifying the new derivation behavior, explicit override precedence, and fallback to defaults. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Description
Closes #1728.
Why
When
cz_customizeis active and the user setsmajor_version_zero = true, commitizen's bump command selectsself.cz.bump_map_major_version_zeroinstead ofself.cz.bump_mapto determine the version increment (seecommitizen/commands/bump.py:150-154).CustomizeCommitsCzinheritsbump_map_major_version_zeroas a class attribute pointing atdefaults.BUMP_MAP_MAJOR_VERSION_ZERO(line 30 ofcommitizen/cz/customize/customize.py). The__init__attribute-copy loop (lines 40-50) only overwrites it if the user explicitly setsbump_map_major_version_zeroin their[tool.commitizen.customize]section.The result is that a user who carefully crafts a
bump_map— mappingfeatanddocstoPATCHinstead of the conventionalMINOR— finds that their mapping is completely ignored whenevermajor_version_zero = trueis set. The bump falls through todefaults.BUMP_MAP_MAJOR_VERSION_ZERO, which mapsfeattoMINOR. The user's custom rule produces aMINORincrement when they expectedPATCH, with no warning and no indication that a fallback was used. The reporter @JeannotJeannot confirmed this on commitizen 4.10.1 (Windows); a triage comment on the #1964 audit reproduced it against master (v4.15.1).The correct fix is for
CustomizeCommitsCz.__init__to derivebump_map_major_version_zerofrom the user's ownbump_mapwhenever the former is absent: copy every pattern frombump_mapand demote anyMAJORrule toMINOR(since the semantic purpose ofmajor_version_zerois to prevent1.0.0from being crossed). Users who want a completely different mapping for the0.xseries can still setbump_map_major_version_zeroexplicitly; that value takes unconditional precedence.What changed
commitizen/cz/customize/customize.py_derive_major_version_zero()module-level helper; extendCustomizeCommitsCz.__init__to call it whenbump_mapis set in customize settings butbump_map_major_version_zerois nottests/test_cz_customize.pybump_map(regression for #1728), explicit user value wins, and neither-set fallback to class defaultHow it works
MAJOR → MINORdemotion is the right semantic. The entire point ofmajor_version_zerois that a project still in0.xtreats breaking changes asMINORrather thanMAJOR(to avoid leaving0.xprematurely). Keeping a user'sMAJORrule as-is in the derived map would defeat that intent, so everyMAJORentry is demoted toMINORwhile allMINORandPATCHentries pass through unchanged.OrderedDictis used deliberately.commitizen/bump.py:find_increment(line 43) iterates the bump map in insertion order to find the first matching pattern. A plaindictwould be fine on Python 3.7+ (insertion-ordered), but the rest of the codebase typesbump_map_major_version_zeroasOrderedDict[str, str](seecommitizen/defaults.py:137andcommitizen/defaults.py:17). UsingOrderedDictin the derived value keeps the type consistent and preserves the user's pattern-priority order exactly.Strict trigger: both conditions must hold. The derivation only fires when
self.custom_settings.get("bump_map")is truthy ANDself.custom_settings.get("bump_map_major_version_zero")is falsy. This is a backwards-compatible opt-in: existing configs that already set both keys see no change; configs that set neither key continue to inheritdefaults.BUMP_MAP_MAJOR_VERSION_ZEROunchanged.self.bump_mapis read after the attribute-copy loop. Because the loop at lines 40-50 runs first,self.bump_mapalready holds the user's custom map (not the class-level default) by the time the derivation check runs. The derivedbump_map_major_version_zerois therefore always consistent with whateverbump_mapthe bump command will actually use.Backward compatibility
bump_mapandbump_map_major_version_zeroexplicitly are unaffected — thenot self.custom_settings.get("bump_map_major_version_zero")guard short-circuits derivation.defaults.BUMP_MAP_MAJOR_VERSION_ZEROas the class attribute — the derivation branch is never entered.cz_customizetests (as of master v4.15.1) continue to pass.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_customizewith custombump_map = {feat = "PATCH"}andmajor_version_zero = true, no explicitbump_map_major_version_zerocz bump --dry-runreportsincrement detected: PATCH— user's map is honouredbump_map_major_version_zeroexplicitly setcz_customizewith nobump_mapand nobump_map_major_version_zerodefaults.BUMP_MAP_MAJOR_VERSION_ZERO— identical to current behaviourcz_customizewithbump_mapcontaining aMAJORentry andmajor_version_zero = trueMAJORtoMINOR; no version crossing from0.xto1.0.0Steps to Test This Pull Request
Additional Context
This fix was identified during the issue audit in #1964. The root cause — the gap between
bump_mapandbump_map_major_version_zeroincz_customize— was acknowledged in the triage comment on #1728. The workaround documented there (set both keys explicitly) remains valid for users on older releases; this PR makes the no-explicit-bump_map_major_version_zeropath do the obvious thing automatically.