Skip to content

fix(cz_customize): derive bump_map_major_version_zero from custom bump_map#1977

Open
bearomorphism wants to merge 1 commit intocommitizen-tools:masterfrom
bearomorphism:fix/1728-customize-bump-map-derive
Open

fix(cz_customize): derive bump_map_major_version_zero from custom bump_map#1977
bearomorphism wants to merge 1 commit intocommitizen-tools:masterfrom
bearomorphism:fix/1728-customize-bump-map-derive

Conversation

@bearomorphism
Copy link
Copy Markdown
Collaborator

@bearomorphism bearomorphism commented May 9, 2026

Description

Closes #1728.

Why

When cz_customize is active and the user sets major_version_zero = true, commitizen's bump command selects self.cz.bump_map_major_version_zero instead of self.cz.bump_map to determine the version increment (see commitizen/commands/bump.py:150-154). CustomizeCommitsCz inherits bump_map_major_version_zero as a class attribute pointing at defaults.BUMP_MAP_MAJOR_VERSION_ZERO (line 30 of commitizen/cz/customize/customize.py). The __init__ attribute-copy loop (lines 40-50) only overwrites it if the user explicitly sets bump_map_major_version_zero in their [tool.commitizen.customize] section.

The result is that a user who carefully crafts a bump_map — mapping feat and docs to PATCH instead of the conventional MINOR — finds that their mapping is completely ignored whenever major_version_zero = true is set. The bump falls through to defaults.BUMP_MAP_MAJOR_VERSION_ZERO, which maps feat to MINOR. The user's custom rule produces a MINOR increment when they expected PATCH, 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 derive bump_map_major_version_zero from the user's own bump_map whenever the former is absent: copy every pattern from bump_map and demote any MAJOR rule to MINOR (since the semantic purpose of major_version_zero is to prevent 1.0.0 from being crossed). Users who want a completely different mapping for the 0.x series can still set bump_map_major_version_zero explicitly; that value takes unconditional precedence.

What changed

File Change
commitizen/cz/customize/customize.py Add _derive_major_version_zero() module-level helper; extend CustomizeCommitsCz.__init__ to call it when bump_map is set in customize settings but bump_map_major_version_zero is not
tests/test_cz_customize.py Three new tests: derivation from bump_map (regression for #1728), explicit user value wins, and neither-set fallback to class default

How it works

  • MAJOR → MINOR demotion is the right semantic. The entire point of major_version_zero is that a project still in 0.x treats breaking changes as MINOR rather than MAJOR (to avoid leaving 0.x prematurely). Keeping a user's MAJOR rule as-is in the derived map would defeat that intent, so every MAJOR entry is demoted to MINOR while all MINOR and PATCH entries pass through unchanged.

  • OrderedDict is used deliberately. commitizen/bump.py:find_increment (line 43) iterates the bump map in insertion order to find the first matching pattern. A plain dict would be fine on Python 3.7+ (insertion-ordered), but the rest of the codebase types bump_map_major_version_zero as OrderedDict[str, str] (see commitizen/defaults.py:137 and commitizen/defaults.py:17). Using OrderedDict in 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 AND self.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 inherit defaults.BUMP_MAP_MAJOR_VERSION_ZERO unchanged.

  • self.bump_map is read after the attribute-copy loop. Because the loop at lines 40-50 runs first, self.bump_map already holds the user's custom map (not the class-level default) by the time the derivation check runs. The derived bump_map_major_version_zero is therefore always consistent with whatever bump_map the bump command will actually use.

Backward compatibility

  • Configs that set both bump_map and bump_map_major_version_zero explicitly are unaffected — the not self.custom_settings.get("bump_map_major_version_zero") guard short-circuits derivation.
  • Configs that set neither key continue to use defaults.BUMP_MAP_MAJOR_VERSION_ZERO as the class attribute — the derivation branch is never entered.
  • All 68 existing cz_customize tests (as of master v4.15.1) continue to pass.
  • No CLI flags, config keys, or public APIs are added or removed.

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce
  • Run uv run poe all locally to ensure this change passes linter check and tests
  • Manually test the changes (see "Steps to Test" below)
  • Update the documentation for the changes

Expected Behavior

Scenario Outcome
cz_customize with custom bump_map = {feat = "PATCH"} and major_version_zero = true, no explicit bump_map_major_version_zero cz bump --dry-run reports increment detected: PATCH — user's map is honoured
Same config but with bump_map_major_version_zero explicitly set Explicit value wins unchanged — derivation does not run
cz_customize with no bump_map and no bump_map_major_version_zero Falls back to defaults.BUMP_MAP_MAJOR_VERSION_ZERO — identical to current behaviour
cz_customize with bump_map containing a MAJOR entry and major_version_zero = true Derived map demotes MAJOR to MINOR; no version crossing from 0.x to 1.0.0

Steps to Test This Pull Request

git fetch fork fix/1728-customize-bump-map-major-zero
git checkout fork/fix/1728-customize-bump-map-major-zero

# 1. Targeted regression tests.
uv run pytest tests/test_cz_customize.py::test_bump_map_major_version_zero_is_derived_from_bump_map \
               tests/test_cz_customize.py::test_bump_map_major_version_zero_explicit_user_value_wins \
               tests/test_cz_customize.py::test_bump_map_major_version_zero_falls_back_to_defaults_without_bump_map \
               -v

# 2. Reproduce-the-bug-then-verify-the-fix sequence (exact reproducer from #1728).
mkdir cz1728 && cd cz1728
git init -b main
git config user.name test && git config user.email test@example.com
cat > pyproject.toml << 'EOF'
[tool.commitizen]
name = "cz_customize"
version = "0.1.0"
major_version_zero = true

[tool.commitizen.customize]
bump_pattern = "^(feat|fix|docs)"
bump_map = {feat = "PATCH", docs = "PATCH"}
EOF
git add pyproject.toml && git commit -m "feat: initial setup"

# Before fix: reports "increment detected: MINOR" (defaults map used).
# After fix:  reports "increment detected: PATCH" (user's map honoured).
cz bump --dry-run --yes

# Verify explicit override still wins (both maps set):
sed -i 's/bump_map = .*/bump_map = {feat = "PATCH"}\nbump_map_major_version_zero = {feat = "MAJOR"}/' pyproject.toml
cz bump --dry-run --yes   # expect MAJOR (explicit value, not derived)

Additional Context

This fix was identified during the issue audit in #1964. The root cause — the gap between bump_map and bump_map_major_version_zero in cz_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_zero path do the obvious thing automatically.

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
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.24%. Comparing base (4b93a50) to head (ef4650f).
⚠️ Report is 3 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 build bump_map_major_version_zero from bump_map (with MAJOR → MINOR demotion).
  • Update CustomizeCommitsCz.__init__ to derive bump_map_major_version_zero when bump_map is set but bump_map_major_version_zero is 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bump_map in pyproject.toml seems to be ignored

2 participants