Skip to content

Commit 6492bad

Browse files
committed
feat(ConventionalCommitsCZ): add support for customizable change type choices and bump map overrides
1 parent a523e55 commit 6492bad

3 files changed

Lines changed: 203 additions & 61 deletions

File tree

commitizen/cz/conventional_commits/conventional_commits.py

Lines changed: 112 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from __future__ import annotations
22

3+
from collections import OrderedDict
34
from pathlib import Path
45
from typing import TYPE_CHECKING, TypedDict
56

67
from commitizen import defaults
78
from commitizen.cz.base import BaseCommitizen
89
from commitizen.cz.utils import multiple_line_breaker, required_validator
10+
from commitizen.question import Choice
911

1012
if TYPE_CHECKING:
13+
from commitizen.config import BaseConfig
1114
from commitizen.question import CzQuestion
1215

1316
__all__ = ["ConventionalCommitsCz"]
@@ -41,74 +44,123 @@ class ConventionalCommitsCz(BaseCommitizen):
4144
"refactor": "Refactor",
4245
"perf": "Perf",
4346
}
47+
change_type_choices = [
48+
Choice(
49+
value="fix",
50+
name=("fix: A bug fix. Correlates with PATCH in SemVer"),
51+
key="x",
52+
),
53+
Choice(
54+
value="feat",
55+
name="feat: A new feature. Correlates with MINOR in SemVer",
56+
key="f",
57+
),
58+
Choice(
59+
value="docs",
60+
name="docs: Documentation only changes",
61+
key="d",
62+
),
63+
Choice(
64+
value="style",
65+
name=(
66+
"style: Changes that do not affect the "
67+
"meaning of the code (white-space, formatting,"
68+
" missing semi-colons, etc)"
69+
),
70+
key="s",
71+
),
72+
Choice(
73+
value="refactor",
74+
name=(
75+
"refactor: A code change that neither fixes a bug nor adds a feature"
76+
),
77+
key="r",
78+
),
79+
Choice(
80+
value="perf",
81+
name="perf: A code change that improves performance",
82+
key="p",
83+
),
84+
Choice(
85+
value="test",
86+
name="test: Adding missing or correcting existing tests",
87+
key="t",
88+
),
89+
Choice(
90+
value="build",
91+
name=(
92+
"build: Changes that affect the build system or "
93+
"external dependencies (example scopes: pip, docker, npm)"
94+
),
95+
key="b",
96+
),
97+
Choice(
98+
value="ci",
99+
name=(
100+
"ci: Changes to CI configuration files and "
101+
"scripts (example scopes: GitLabCI)"
102+
),
103+
key="c",
104+
),
105+
]
106+
44107
changelog_pattern = defaults.BUMP_PATTERN
45108

109+
def __init__(self, config: BaseConfig) -> None:
110+
super().__init__(config)
111+
self._isolate_mutable_defaults()
112+
113+
if override_settings := self.config.settings.get("override"):
114+
self._apply_override_settings(override_settings)
115+
elif extend_settings := self.config.settings.get("extend"):
116+
self._apply_extend_settings(extend_settings)
117+
118+
def _isolate_mutable_defaults(self) -> None:
119+
# Keep mutable class defaults isolated per instance.
120+
self.bump_map = OrderedDict(self.bump_map)
121+
self.bump_map_major_version_zero = OrderedDict(self.bump_map_major_version_zero)
122+
self.change_type_map = dict(self.change_type_map)
123+
self.change_type_choices = [*self.change_type_choices]
124+
125+
def _apply_override_settings(self, settings: defaults.CzOverrideSettings) -> None:
126+
if bump_pattern := settings.get("bump_pattern"):
127+
self.bump_pattern = bump_pattern
128+
if bump_map := settings.get("bump_map"):
129+
self.bump_map = OrderedDict(bump_map)
130+
if bump_map_major_version_zero := settings.get("bump_map_major_version_zero"):
131+
self.bump_map_major_version_zero = OrderedDict(bump_map_major_version_zero)
132+
if commit_parser := settings.get("commit_parser"):
133+
self.commit_parser = commit_parser
134+
if changelog_pattern := settings.get("changelog_pattern"):
135+
self.changelog_pattern = changelog_pattern
136+
if change_type_map := settings.get("change_type_map"):
137+
self.change_type_map = dict(change_type_map)
138+
if change_type_choices := settings.get("change_type_choices"):
139+
self.change_type_choices = [*change_type_choices]
140+
141+
def _apply_extend_settings(self, settings: defaults.CzExtendSettings) -> None:
142+
if bump_pattern := settings.get("bump_pattern"):
143+
self.bump_pattern = bump_pattern
144+
if bump_map := settings.get("bump_map"):
145+
self.bump_map.update(bump_map)
146+
if bump_map_major_version_zero := settings.get("bump_map_major_version_zero"):
147+
self.bump_map_major_version_zero.update(bump_map_major_version_zero)
148+
if commit_parser := settings.get("commit_parser"):
149+
self.commit_parser = commit_parser
150+
if changelog_pattern := settings.get("changelog_pattern"):
151+
self.changelog_pattern = changelog_pattern
152+
if change_type_map := settings.get("change_type_map"):
153+
self.change_type_map.update(change_type_map)
154+
if change_type_choices := settings.get("change_type_choices"):
155+
self.change_type_choices.extend(change_type_choices)
156+
46157
def questions(self) -> list[CzQuestion]:
47158
return [
48159
{
49160
"type": "list",
50161
"name": "prefix",
51162
"message": "Select the type of change you are committing",
52-
"choices": [
53-
{
54-
"value": "fix",
55-
"name": "fix: A bug fix. Correlates with PATCH in SemVer",
56-
"key": "x",
57-
},
58-
{
59-
"value": "feat",
60-
"name": "feat: A new feature. Correlates with MINOR in SemVer",
61-
"key": "f",
62-
},
63-
{
64-
"value": "docs",
65-
"name": "docs: Documentation only changes",
66-
"key": "d",
67-
},
68-
{
69-
"value": "style",
70-
"name": (
71-
"style: Changes that do not affect the "
72-
"meaning of the code (white-space, formatting,"
73-
" missing semi-colons, etc)"
74-
),
75-
"key": "s",
76-
},
77-
{
78-
"value": "refactor",
79-
"name": (
80-
"refactor: A code change that neither fixes "
81-
"a bug nor adds a feature"
82-
),
83-
"key": "r",
84-
},
85-
{
86-
"value": "perf",
87-
"name": "perf: A code change that improves performance",
88-
"key": "p",
89-
},
90-
{
91-
"value": "test",
92-
"name": ("test: Adding missing or correcting existing tests"),
93-
"key": "t",
94-
},
95-
{
96-
"value": "build",
97-
"name": (
98-
"build: Changes that affect the build system or "
99-
"external dependencies (example scopes: pip, docker, npm)"
100-
),
101-
"key": "b",
102-
},
103-
{
104-
"value": "ci",
105-
"name": (
106-
"ci: Changes to CI configuration files and "
107-
"scripts (example scopes: GitLabCI)"
108-
),
109-
"key": "c",
110-
},
111-
],
163+
"choices": self.change_type_choices,
112164
},
113165
{
114166
"type": "input",

commitizen/defaults.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
if TYPE_CHECKING:
99
import pathlib
1010

11-
from commitizen.question import CzQuestion
11+
from commitizen.question import Choice, CzQuestion
1212

1313

1414
class CzSettings(TypedDict, total=False):
@@ -29,6 +29,14 @@ class CzSettings(TypedDict, total=False):
2929
change_type_map: dict[str, str] | None
3030

3131

32+
class CzOverrideSettings(CzSettings, total=False):
33+
change_type_choices: list[Choice]
34+
35+
36+
class CzExtendSettings(CzSettings, total=False):
37+
change_type_choices: list[Choice]
38+
39+
3240
class Settings(TypedDict, total=False):
3341
allow_abort: bool
3442
allowed_prefixes: list[str]
@@ -43,13 +51,15 @@ class Settings(TypedDict, total=False):
4351
changelog_start_rev: str | None
4452
customize: CzSettings
4553
encoding: str
54+
extend: CzExtendSettings
4655
extras: dict[str, Any]
4756
gpg_sign: bool
4857
ignored_tag_formats: Sequence[str]
4958
legacy_tag_formats: Sequence[str]
5059
major_version_zero: bool
5160
message_length_limit: int
5261
name: str
62+
override: CzOverrideSettings
5363
post_bump_hooks: list[str] | None
5464
pre_bump_hooks: list[str] | None
5565
prerelease_offset: int

tests/test_cz_conventional_commits.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,83 @@ def test_info(config):
173173
conventional_commits = ConventionalCommitsCz(config)
174174
info = conventional_commits.info()
175175
assert isinstance(info, str)
176+
177+
178+
def test_override_change_type_choices(config):
179+
config.settings["override"] = {
180+
"change_type_choices": [
181+
{
182+
"value": "foo",
183+
"name": "foo: non standard change",
184+
"key": "o",
185+
}
186+
]
187+
}
188+
189+
conventional_commits = ConventionalCommitsCz(config)
190+
questions = conventional_commits.questions()
191+
change_type_question = next(q for q in questions if q["name"] == "prefix")
192+
choices = change_type_question["choices"]
193+
assert choices == [
194+
{
195+
"value": "foo",
196+
"name": "foo: non standard change",
197+
"key": "o",
198+
}
199+
]
200+
201+
202+
def test_extend_bump_map_merges(config, monkeypatch):
203+
monkeypatch.setattr(
204+
ConventionalCommitsCz, "bump_map", dict(ConventionalCommitsCz.bump_map)
205+
)
206+
config.settings["extend"] = {
207+
"bump_map": {
208+
"^foo": "MINOR",
209+
}
210+
}
211+
212+
conventional_commits = ConventionalCommitsCz(config)
213+
assert conventional_commits.bump_map["^foo"] == "MINOR"
214+
assert r"^feat" in conventional_commits.bump_map
215+
216+
217+
def test_extend_change_type_choices_appends(config, monkeypatch):
218+
monkeypatch.setattr(
219+
ConventionalCommitsCz,
220+
"change_type_choices",
221+
[*ConventionalCommitsCz.change_type_choices],
222+
)
223+
config.settings["extend"] = {
224+
"change_type_choices": [
225+
{
226+
"value": "foo",
227+
"name": "foo: non standard change",
228+
"key": "o",
229+
}
230+
]
231+
}
232+
233+
conventional_commits = ConventionalCommitsCz(config)
234+
questions = conventional_commits.questions()
235+
change_type_question = next(q for q in questions if q["name"] == "prefix")
236+
choice_values = {choice["value"] for choice in change_type_question["choices"]}
237+
238+
assert "fix" in choice_values
239+
assert "foo" in choice_values
240+
241+
242+
def test_override_takes_precedence_over_extend(config):
243+
config.settings["override"] = {
244+
"bump_map": {
245+
"^bar": "PATCH",
246+
}
247+
}
248+
config.settings["extend"] = {
249+
"bump_map": {
250+
"^foo": "MINOR",
251+
}
252+
}
253+
254+
conventional_commits = ConventionalCommitsCz(config)
255+
assert conventional_commits.bump_map == {"^bar": "PATCH"}

0 commit comments

Comments
 (0)