Skip to content

Commit 66896bc

Browse files
committed
feat(commands): add bump --exact
When bumping a prerelease to a new prerelease, honor the detected increment and preserve the prerelease suffix, rather than bumping to the next non-prerelease version
1 parent 12b7158 commit 66896bc

File tree

6 files changed

+164
-24
lines changed

6 files changed

+164
-24
lines changed

commitizen/cli.py

+9
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,15 @@ def __call__(
230230
"choices": ["MAJOR", "MINOR", "PATCH"],
231231
"type": str.upper,
232232
},
233+
{
234+
"name": ["--exact"],
235+
"action": "store_true",
236+
"help": (
237+
"treat the increment and prerelease arguments "
238+
"explicitly. Disables logic that attempts to deduce "
239+
"the correct increment when a prelease suffix is present."
240+
),
241+
},
233242
{
234243
"name": ["--check-consistency", "-cc"],
235244
"help": (

commitizen/commands/bump.py

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self, config: BaseConfig, arguments: dict):
5252
"tag_format",
5353
"prerelease",
5454
"increment",
55+
"exact",
5556
"bump_message",
5657
"gpg_sign",
5758
"annotated_tag",
@@ -157,6 +158,7 @@ def __call__(self) -> None: # noqa: C901
157158
is_files_only: bool | None = self.arguments["files_only"]
158159
is_local_version: bool = self.arguments["local_version"]
159160
manual_version = self.arguments["manual_version"]
161+
exact_mode: bool = self.arguments["exact"]
160162

161163
if manual_version:
162164
if increment:
@@ -183,6 +185,9 @@ def __call__(self) -> None: # noqa: C901
183185
"--prerelease-offset cannot be combined with MANUAL_VERSION"
184186
)
185187

188+
if not prerelease and exact_mode:
189+
raise NotAllowed("--exact is only valid with --prerelease")
190+
186191
if major_version_zero:
187192
if not current_version.release[0] == 0:
188193
raise NotAllowed(
@@ -237,6 +242,7 @@ def __call__(self) -> None: # noqa: C901
237242
prerelease_offset=prerelease_offset,
238243
devrelease=devrelease,
239244
is_local_version=is_local_version,
245+
exact_mode=exact_mode,
240246
)
241247

242248
new_tag_version = bump.normalize_tag(

commitizen/version_schemes.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,17 @@ def bump(
129129
prerelease_offset: int = 0,
130130
devrelease: int | None = None,
131131
is_local_version: bool = False,
132-
force_bump: bool = False,
132+
exact_mode: bool = False,
133133
) -> Self:
134134
"""
135135
Based on the given increment, generate the next bumped version according to the version scheme
136+
137+
Args:
138+
increment: The component to increase
139+
prerelease: The type of prerelease, if Any
140+
is_local_version: Whether to increment the local version instead
141+
exact_mode: Treat the increment and prerelease arguments explicitly. Disables logic
142+
that attempts to deduce the correct increment when a prelease suffix is present.
136143
"""
137144

138145

@@ -226,7 +233,7 @@ def bump(
226233
prerelease_offset: int = 0,
227234
devrelease: int | None = None,
228235
is_local_version: bool = False,
229-
force_bump: bool = False,
236+
exact_mode: bool = False,
230237
) -> Self:
231238
"""Based on the given increment a proper semver will be generated.
232239
@@ -246,7 +253,7 @@ def bump(
246253
else:
247254
if not self.is_prerelease:
248255
base = self.increment_base(increment)
249-
elif force_bump:
256+
elif exact_mode:
250257
base = self.increment_base(increment)
251258
else:
252259
base = f"{self.major}.{self.minor}.{self.micro}"

tests/commands/test_bump_command.py

+49
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,55 @@ def test_bump_command_prelease_increment(mocker: MockFixture):
314314
assert git.tag_exist("1.0.0a0")
315315

316316

317+
@pytest.mark.usefixtures("tmp_commitizen_project")
318+
def test_bump_command_prelease_exact_mode(mocker: MockFixture):
319+
# PRERELEASE
320+
create_file_and_commit("feat: location")
321+
322+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
323+
mocker.patch.object(sys, "argv", testargs)
324+
cli.main()
325+
326+
tag_exists = git.tag_exist("0.2.0a0")
327+
assert tag_exists is True
328+
329+
# PRERELEASE + PATCH BUMP
330+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes", "--exact"]
331+
mocker.patch.object(sys, "argv", testargs)
332+
cli.main()
333+
334+
tag_exists = git.tag_exist("0.2.0a1")
335+
assert tag_exists is True
336+
337+
# PRERELEASE + MINOR BUMP
338+
# --exact allows the minor version to bump, and restart the prerelease
339+
create_file_and_commit("feat: location")
340+
341+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes", "--exact"]
342+
mocker.patch.object(sys, "argv", testargs)
343+
cli.main()
344+
345+
tag_exists = git.tag_exist("0.3.0a0")
346+
assert tag_exists is True
347+
348+
# PRERELEASE + MAJOR BUMP
349+
# --exact allows the major version to bump, and restart the prerelease
350+
testargs = [
351+
"cz",
352+
"bump",
353+
"--prerelease",
354+
"alpha",
355+
"--yes",
356+
"--increment=MAJOR",
357+
"--exact",
358+
]
359+
mocker.patch.object(sys, "argv", testargs)
360+
cli.main()
361+
362+
tag_exists = git.tag_exist("1.0.0a0")
363+
assert tag_exists is True
364+
365+
317366
@pytest.mark.usefixtures("tmp_commitizen_project")
318367
def test_bump_on_git_with_hooks_no_verify_disabled(mocker: MockFixture):
319368
"""Bump commit without --no-verify"""

tests/test_version_scheme_pep440.py

+44-21
Original file line numberDiff line numberDiff line change
@@ -141,27 +141,29 @@
141141
(("3.1.4a0", "MAJOR", "alpha", 0, None), "4.0.0a0"),
142142
]
143143

144-
145-
# test driven development
146-
sortability = [
147-
"0.10.0a0",
148-
"0.1.1",
149-
"0.1.2",
150-
"2.1.1",
151-
"3.0.0",
152-
"0.9.1a0",
153-
"1.0.0a1",
154-
"1.0.0b1",
155-
"1.0.0a1",
156-
"1.0.0a2.dev1",
157-
"1.0.0rc2",
158-
"1.0.0a3.dev0",
159-
"1.0.0a2.dev0",
160-
"1.0.0a3.dev1",
161-
"1.0.0a2.dev0",
162-
"1.0.0b0",
163-
"1.0.0rc0",
164-
"1.0.0rc1",
144+
excact_cases = [
145+
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
146+
(("1.0.0", "MINOR", None, 0, None), "1.1.0"),
147+
# with exact_mode=False: "1.0.0b0"
148+
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1b0"),
149+
# with exact_mode=False: "1.0.0b1"
150+
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1b0"),
151+
# with exact_mode=False: "1.0.0rc0"
152+
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1rc0"),
153+
# with exact_mode=False: "1.0.0-rc1"
154+
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1rc0"),
155+
# with exact_mode=False: "1.0.0rc1-dev1"
156+
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1rc0.dev1"),
157+
# with exact_mode=False: "1.0.0b0"
158+
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0b0"),
159+
# with exact_mode=False: "1.0.0b1"
160+
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0b0"),
161+
# with exact_mode=False: "1.0.0rc0"
162+
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0rc0"),
163+
# with exact_mode=False: "1.0.0rc1"
164+
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0rc0"),
165+
# with exact_mode=False: "1.0.0rc1-dev1"
166+
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0rc0.dev1"),
165167
]
166168

167169

@@ -194,6 +196,27 @@ def test_bump_pep440_version(test_input, expected):
194196
)
195197

196198

199+
@pytest.mark.parametrize("test_input, expected", excact_cases)
200+
def test_bump_pep440_version_force(test_input, expected):
201+
current_version = test_input[0]
202+
increment = test_input[1]
203+
prerelease = test_input[2]
204+
prerelease_offset = test_input[3]
205+
devrelease = test_input[4]
206+
assert (
207+
str(
208+
Pep440(current_version).bump(
209+
increment=increment,
210+
prerelease=prerelease,
211+
prerelease_offset=prerelease_offset,
212+
devrelease=devrelease,
213+
exact_mode=True,
214+
)
215+
)
216+
== expected
217+
)
218+
219+
197220
@pytest.mark.parametrize("test_input,expected", local_versions)
198221
def test_bump_pep440_version_local(test_input, expected):
199222
current_version = test_input[0]

tests/test_version_scheme_semver.py

+46
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,31 @@
8383
(("1.0.0-alpha1", None, "alpha", 0, None), "1.0.0-a2"),
8484
]
8585

86+
excact_cases = [
87+
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
88+
(("1.0.0", "MINOR", None, 0, None), "1.1.0"),
89+
# with exact_mode=False: "1.0.0-b0"
90+
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"),
91+
# with exact_mode=False: "1.0.0-b1"
92+
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"),
93+
# with exact_mode=False: "1.0.0-rc0"
94+
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"),
95+
# with exact_mode=False: "1.0.0-rc1"
96+
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"),
97+
# with exact_mode=False: "1.0.0-rc1-dev1"
98+
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"),
99+
# with exact_mode=False: "1.0.0-b0"
100+
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"),
101+
# with exact_mode=False: "1.0.0-b1"
102+
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"),
103+
# with exact_mode=False: "1.0.0-rc0"
104+
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"),
105+
# with exact_mode=False: "1.0.0-rc1"
106+
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"),
107+
# with exact_mode=False: "1.0.0-rc1-dev1"
108+
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"),
109+
]
110+
86111

87112
@pytest.mark.parametrize(
88113
"test_input, expected",
@@ -107,6 +132,27 @@ def test_bump_semver_version(test_input, expected):
107132
)
108133

109134

135+
@pytest.mark.parametrize("test_input, expected", excact_cases)
136+
def test_bump_semver_version_force(test_input, expected):
137+
current_version = test_input[0]
138+
increment = test_input[1]
139+
prerelease = test_input[2]
140+
prerelease_offset = test_input[3]
141+
devrelease = test_input[4]
142+
assert (
143+
str(
144+
SemVer(current_version).bump(
145+
increment=increment,
146+
prerelease=prerelease,
147+
prerelease_offset=prerelease_offset,
148+
devrelease=devrelease,
149+
exact_mode=True,
150+
)
151+
)
152+
== expected
153+
)
154+
155+
110156
@pytest.mark.parametrize("test_input,expected", local_versions)
111157
def test_bump_semver_version_local(test_input, expected):
112158
current_version = test_input[0]

0 commit comments

Comments
 (0)