Skip to content

Commit c130f3e

Browse files
committed
no-mistakes(review): Fix invalid custom bump-map increment handling
1 parent 67dc6d5 commit c130f3e

3 files changed

Lines changed: 131 additions & 5 deletions

File tree

‎commitizen/version_increment.py‎

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
if TYPE_CHECKING:
88
from collections.abc import Iterable, Mapping
99

10+
RegexPattern = str | re.Pattern[str]
11+
1012

1113
class VersionIncrement(IntEnum):
1214
"""Semantic versioning bump increments.
@@ -39,6 +41,30 @@ def from_value(cls, value: object) -> VersionIncrement:
3941
except KeyError:
4042
return VersionIncrement.NONE
4143

44+
@classmethod
45+
def _compile_pattern(cls, regex: RegexPattern) -> re.Pattern[str]:
46+
"""Normalize a bump-pattern input into a compiled regular expression."""
47+
if isinstance(regex, re.Pattern):
48+
return regex
49+
return re.compile(regex)
50+
51+
@classmethod
52+
def _from_bump_map_value(cls, value: object) -> VersionIncrement:
53+
"""Resolve a configured bump-map value or reject invalid rule output.
54+
55+
This preserves the historical behavior where a matched custom bump-map
56+
entry with an unsupported increment fails immediately instead of being
57+
treated as a no-op.
58+
"""
59+
if value is None:
60+
return cls.NONE
61+
if not isinstance(value, str):
62+
raise ValueError(f"Invalid bump increment: {value!r}")
63+
try:
64+
return cls[value]
65+
except KeyError as exc:
66+
raise ValueError(f"Invalid bump increment: {value!r}") from exc
67+
4268
@classmethod
4369
def from_match(
4470
cls, matched_text: str, increments_map: Mapping[str, object]
@@ -50,20 +76,20 @@ def from_match(
5076
"""
5177
for match_pattern, increment in increments_map.items():
5278
if re.match(match_pattern, matched_text):
53-
return cls.from_value(increment)
79+
return cls._from_bump_map_value(increment)
5480
return cls.NONE
5581

5682
@classmethod
5783
def from_message(
58-
cls, message: str, regex: str, increments_map: Mapping[str, object]
84+
cls, message: str, regex: RegexPattern, increments_map: Mapping[str, object]
5985
) -> VersionIncrement:
6086
"""Compute the highest increment contributed by one commit message.
6187
6288
Commitizen historically evaluates each line of the full commit message
6389
against ``bump_pattern`` and then keeps the highest resulting bump.
6490
This method preserves that behavior while returning a comparable enum.
6591
"""
66-
select_pattern = re.compile(regex)
92+
select_pattern = cls._compile_pattern(regex)
6793
increments = []
6894

6995
for line in message.split("\n"):
@@ -75,15 +101,22 @@ def from_message(
75101

76102
@classmethod
77103
def get_highest_by_messages(
78-
cls, messages: Iterable[str], regex: str, increments_map: Mapping[str, object]
104+
cls,
105+
messages: Iterable[str],
106+
regex: RegexPattern,
107+
increments_map: Mapping[str, object],
79108
) -> VersionIncrement:
80109
"""Compute the highest increment across many commit messages.
81110
82111
An empty iterable yields ``NONE`` so callers can preserve the existing
83112
"no increment" behavior explicitly instead of relying on exceptions from
84113
``max()``.
85114
"""
115+
select_pattern = cls._compile_pattern(regex)
86116
return max(
87-
(cls.from_message(message, regex, increments_map) for message in messages),
117+
(
118+
cls.from_message(message, select_pattern, increments_map)
119+
for message in messages
120+
),
88121
default=cls.NONE,
89122
)

‎tests/commands/test_bump_command.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,45 @@ def test_bump_allow_no_commit_with_no_eligible_commit(
13301330
assert "bump: version 1.0.0 → 1.0.1" in out
13311331

13321332

1333+
1334+
def test_bump_allow_no_commit_with_invalid_custom_bump_map_raises(
1335+
tmp_commitizen_project: Path,
1336+
monkeypatch: pytest.MonkeyPatch,
1337+
util: UtilFixture,
1338+
) -> None:
1339+
"""An invalid matched custom bump-map value must fail before PATCH fallback."""
1340+
monkeypatch.chdir(tmp_commitizen_project)
1341+
(tmp_commitizen_project / "pyproject.toml").write_text(
1342+
dedent(
1343+
"""
1344+
[tool.commitizen]
1345+
name = "cz_customize"
1346+
version = "0.1.0"
1347+
1348+
[tool.commitizen.customize]
1349+
bump_pattern = "^(new|fix|docs)"
1350+
bump_map = { new = "MINORR", fix = "PATCH", docs = "NONE" }
1351+
change_type_map = { new = "Features", fix = "Bugs", docs = "Docs" }
1352+
changelog_pattern = "^(new|fix|docs)"
1353+
commit_parser = '^(?P<change_type>new|fix|docs):\\s(?P<message>.+)$'
1354+
schema_pattern = ".*"
1355+
questions = []
1356+
"""
1357+
).strip()
1358+
+ "\n",
1359+
encoding="utf-8",
1360+
)
1361+
1362+
util.create_file_and_commit("fix: seed release")
1363+
util.run_cli("bump", "--yes")
1364+
util.create_file_and_commit("new: add endpoint")
1365+
1366+
with pytest.raises(ValueError, match="MINORR"):
1367+
util.run_cli("bump", "--yes", "--allow-no-commit")
1368+
1369+
assert git.tag_exist("0.1.2") is False
1370+
1371+
13331372
def test_bump_allow_no_commit_with_increment(
13341373
tmp_commitizen_project, monkeypatch, util: UtilFixture, capsys
13351374
):

‎tests/test_version_increment.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for semantic-version increment ordering and extraction helpers."""
22

3+
import re
4+
35
import pytest
46

57
from commitizen.cz.conventional_commits import ConventionalCommitsCz
@@ -205,3 +207,55 @@ def test_from_message_uses_highest_matching_line() -> None:
205207
)
206208
== VersionIncrement.MAJOR
207209
)
210+
211+
212+
def test_get_highest_by_messages_supports_custom_none_mapping() -> None:
213+
"""A matched custom rule can still explicitly opt out of bumping."""
214+
assert (
215+
VersionIncrement.get_highest_by_messages(
216+
["docs: update guide"],
217+
r"^(docs)",
218+
{"docs": None},
219+
)
220+
== VersionIncrement.NONE
221+
)
222+
223+
224+
225+
def test_get_highest_by_messages_raises_for_invalid_bump_map_value() -> None:
226+
"""A matched custom bump-map value must fail instead of becoming NONE."""
227+
with pytest.raises(ValueError, match="MINORR"):
228+
VersionIncrement.get_highest_by_messages(
229+
["new: add endpoint"],
230+
r"^(new)",
231+
{"new": "MINORR"},
232+
)
233+
234+
235+
236+
def test_get_highest_by_messages_compiles_pattern_once(
237+
monkeypatch: pytest.MonkeyPatch,
238+
) -> None:
239+
"""Scanning many messages reuses one compiled bump pattern."""
240+
compile_calls = 0
241+
original_compile = re.compile
242+
243+
def counted_compile(pattern: str, flags: int = 0) -> re.Pattern[str]:
244+
"""Record each pattern compilation while preserving regex behavior."""
245+
nonlocal compile_calls
246+
compile_calls += 1
247+
return original_compile(pattern, flags)
248+
249+
monkeypatch.setattr("commitizen.version_increment.re.compile", counted_compile)
250+
251+
increment = VersionIncrement.get_highest_by_messages(
252+
[
253+
"fix: correct typo",
254+
"feat(api): add endpoint\n\nBREAKING CHANGE: remove fallback",
255+
],
256+
ConventionalCommitsCz.bump_pattern,
257+
ConventionalCommitsCz.bump_map,
258+
)
259+
260+
assert increment == VersionIncrement.MAJOR
261+
assert compile_calls == 1

0 commit comments

Comments
 (0)