Skip to content

Commit 03aa596

Browse files
committed
refactor(bump_rule): add bump_rule interface, DefaultBumpRule and its tests
1 parent a0cc490 commit 03aa596

File tree

5 files changed

+168
-10
lines changed

5 files changed

+168
-10
lines changed

commitizen/bump_rule.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from functools import cached_property
5+
from typing import Protocol
6+
7+
from commitizen.version_schemes import Increment
8+
9+
10+
class BumpRule(Protocol):
11+
def get_increment(
12+
self, commit_message: str, major_version_zero: bool
13+
) -> Increment | None: ...
14+
15+
16+
class DefaultBumpRule(BumpRule):
17+
_PATCH_CHANGE_TYPES = set(["fix", "perf", "refactor"])
18+
19+
def get_increment(
20+
self, commit_message: str, major_version_zero: bool
21+
) -> Increment | None:
22+
if not (m := self._head_pattern.match(commit_message)):
23+
return None
24+
25+
change_type = m.group("change_type")
26+
if m.group("bang") or change_type == "BREAKING CHANGE":
27+
return "MAJOR" if major_version_zero else "MINOR"
28+
29+
if change_type == "feat":
30+
return "MINOR"
31+
32+
if change_type in self._PATCH_CHANGE_TYPES:
33+
return "PATCH"
34+
35+
return None
36+
37+
@cached_property
38+
def _head_pattern(self) -> re.Pattern:
39+
change_types = [
40+
r"BREAKING[\-\ ]CHANGE",
41+
"fix",
42+
"feat",
43+
"docs",
44+
"style",
45+
"refactor",
46+
"perf",
47+
"test",
48+
"build",
49+
"ci",
50+
]
51+
re_change_type = r"(?P<change_type>" + "|".join(change_types) + r")"
52+
re_scope = r"(?P<scope>\(.+\))?"
53+
re_bang = r"(?P<bang>!)?"
54+
return re.compile(f"^{re_change_type}{re_scope}{re_bang}:")
55+
56+
57+
class CustomBumpRule(BumpRule):
58+
"""TODO: Implement"""
59+
60+
def get_increment(
61+
self, commit_message: str, major_version_zero: bool
62+
) -> Increment | None:
63+
return None

commitizen/cz/conventional_commits/conventional_commits.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ def parse_subject(text):
2828

2929

3030
class ConventionalCommitsCz(BaseCommitizen):
31-
bump_pattern = defaults.bump_pattern
32-
bump_map = defaults.bump_map
33-
bump_map_major_version_zero = defaults.bump_map_major_version_zero
31+
bump_pattern = defaults.BUMP_PATTERN
32+
bump_map = defaults.BUMP_MAP
33+
bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO
3434
commit_parser = r"^((?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):\s(?P<message>.*)?" # noqa
3535
change_type_map = {
3636
"feat": "Feat",
3737
"fix": "Fix",
3838
"refactor": "Refactor",
3939
"perf": "Perf",
4040
}
41-
changelog_pattern = defaults.bump_pattern
41+
changelog_pattern = defaults.BUMP_PATTERN
4242

4343
def questions(self) -> Questions:
4444
questions: Questions = [

commitizen/cz/customize/customize.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222

2323
class CustomizeCommitsCz(BaseCommitizen):
24-
bump_pattern = defaults.bump_pattern
25-
bump_map = defaults.bump_map
26-
bump_map_major_version_zero = defaults.bump_map_major_version_zero
24+
bump_pattern = defaults.BUMP_PATTERN
25+
bump_map = defaults.BUMP_MAP
26+
bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO
2727
change_type_order = defaults.change_type_order
2828

2929
def __init__(self, config: BaseConfig):

commitizen/defaults.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ class Settings(TypedDict, total=False):
114114

115115
CHANGELOG_FORMAT = "markdown"
116116

117-
bump_pattern = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
118-
bump_map = OrderedDict(
117+
BUMP_PATTERN = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
118+
BUMP_MAP = OrderedDict(
119119
(
120120
(r"^.+!$", MAJOR),
121121
(r"^BREAKING[\-\ ]CHANGE", MAJOR),
@@ -125,7 +125,7 @@ class Settings(TypedDict, total=False):
125125
(r"^perf", PATCH),
126126
)
127127
)
128-
bump_map_major_version_zero = OrderedDict(
128+
BUMP_MAP_MAJOR_VERSION_ZERO = OrderedDict(
129129
(
130130
(r"^.+!$", MINOR),
131131
(r"^BREAKING[\-\ ]CHANGE", MINOR),

tests/test_bump_rule.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import pytest
2+
3+
from commitizen.bump_rule import DefaultBumpRule
4+
from commitizen.defaults import MAJOR, MINOR, PATCH
5+
6+
7+
@pytest.fixture
8+
def bump_rule():
9+
return DefaultBumpRule()
10+
11+
12+
class TestDefaultBumpRule:
13+
def test_feat_commit(self, bump_rule):
14+
assert bump_rule.get_increment("feat: add new feature", False) == MINOR
15+
assert bump_rule.get_increment("feat: add new feature", True) == MINOR
16+
17+
def test_fix_commit(self, bump_rule):
18+
assert bump_rule.get_increment("fix: fix bug", False) == PATCH
19+
assert bump_rule.get_increment("fix: fix bug", True) == PATCH
20+
21+
def test_perf_commit(self, bump_rule):
22+
assert bump_rule.get_increment("perf: improve performance", False) == PATCH
23+
assert bump_rule.get_increment("perf: improve performance", True) == PATCH
24+
25+
def test_refactor_commit(self, bump_rule):
26+
assert bump_rule.get_increment("refactor: restructure code", False) == PATCH
27+
assert bump_rule.get_increment("refactor: restructure code", True) == PATCH
28+
29+
def test_breaking_change_with_bang(self, bump_rule):
30+
assert bump_rule.get_increment("feat!: breaking change", False) == MINOR
31+
assert bump_rule.get_increment("feat!: breaking change", True) == MAJOR
32+
33+
def test_breaking_change_type(self, bump_rule):
34+
assert bump_rule.get_increment("BREAKING CHANGE: major change", False) == MINOR
35+
assert bump_rule.get_increment("BREAKING CHANGE: major change", True) == MAJOR
36+
37+
def test_commit_with_scope(self, bump_rule):
38+
assert bump_rule.get_increment("feat(api): add new endpoint", False) == MINOR
39+
assert bump_rule.get_increment("fix(ui): fix button alignment", False) == PATCH
40+
41+
def test_commit_with_complex_scopes(self, bump_rule):
42+
# Test with multiple word scopes
43+
assert (
44+
bump_rule.get_increment("feat(user_management): add user roles", False)
45+
== MINOR
46+
)
47+
assert (
48+
bump_rule.get_increment("fix(database_connection): handle timeout", False)
49+
== PATCH
50+
)
51+
52+
# Test with nested scopes
53+
assert (
54+
bump_rule.get_increment("feat(api/auth): implement OAuth", False) == MINOR
55+
)
56+
assert (
57+
bump_rule.get_increment("fix(ui/components): fix dropdown", False) == PATCH
58+
)
59+
60+
# Test with breaking changes and scopes
61+
assert (
62+
bump_rule.get_increment("feat(api)!: remove deprecated endpoints", False)
63+
== MINOR
64+
)
65+
assert (
66+
bump_rule.get_increment("feat(api)!: remove deprecated endpoints", True)
67+
== MAJOR
68+
)
69+
70+
# Test with BREAKING CHANGE and scopes
71+
assert (
72+
bump_rule.get_increment(
73+
"BREAKING CHANGE(api): remove deprecated endpoints", False
74+
)
75+
== MINOR
76+
)
77+
assert (
78+
bump_rule.get_increment(
79+
"BREAKING CHANGE(api): remove deprecated endpoints", True
80+
)
81+
== MAJOR
82+
)
83+
84+
def test_invalid_commit_message(self, bump_rule):
85+
assert bump_rule.get_increment("invalid commit message", False) is None
86+
assert bump_rule.get_increment("", False) is None
87+
assert bump_rule.get_increment("feat", False) is None
88+
89+
def test_other_commit_types(self, bump_rule):
90+
# These commit types should not trigger any version bump
91+
assert bump_rule.get_increment("docs: update documentation", False) is None
92+
assert bump_rule.get_increment("style: format code", False) is None
93+
assert bump_rule.get_increment("test: add unit tests", False) is None
94+
assert bump_rule.get_increment("build: update build config", False) is None
95+
assert bump_rule.get_increment("ci: update CI pipeline", False) is None

0 commit comments

Comments
 (0)