Skip to content

Commit 1059556

Browse files
chadrikwoile
authored andcommitted
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 ddab546 commit 1059556

File tree

7 files changed

+213
-24
lines changed

7 files changed

+213
-24
lines changed

commitizen/cli.py

+10
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,16 @@ def __call__(
230230
"choices": ["MAJOR", "MINOR", "PATCH"],
231231
"type": str.upper,
232232
},
233+
{
234+
"name": ["--exact-increment"],
235+
"action": "store_true",
236+
"help": (
237+
"apply the exact changes that have been specified (or "
238+
"determined from the commit log), disabling logic that "
239+
"guesses the next version based on typical version "
240+
"progression when a prelease suffix is present."
241+
),
242+
},
233243
{
234244
"name": ["--check-consistency", "-cc"],
235245
"help": (

commitizen/commands/bump.py

+4
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_increment",
5556
"bump_message",
5657
"gpg_sign",
5758
"annotated_tag",
@@ -158,6 +159,7 @@ def __call__(self) -> None: # noqa: C901
158159
is_local_version: bool = self.arguments["local_version"]
159160
manual_version = self.arguments["manual_version"]
160161
build_metadata = self.arguments["build_metadata"]
162+
exact_increment: bool = self.arguments["exact_increment"]
161163

162164
if manual_version:
163165
if increment:
@@ -250,6 +252,7 @@ def __call__(self) -> None: # noqa: C901
250252
devrelease=devrelease,
251253
is_local_version=is_local_version,
252254
build_metadata=build_metadata,
255+
exact_increment=exact_increment,
253256
)
254257

255258
new_tag_version = bump.normalize_tag(
@@ -351,6 +354,7 @@ def __call__(self) -> None: # noqa: C901
351354
if is_files_only:
352355
raise ExpectedExit()
353356

357+
# FIXME: check if any changes have been staged
354358
c = git.commit(message, args=self._get_commit_args())
355359
if self.retry and c.return_code != 0 and self.changelog:
356360
# Maybe pre-commit reformatted some files? Retry once

commitizen/version_schemes.py

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

139146

@@ -239,7 +246,7 @@ def bump(
239246
devrelease: int | None = None,
240247
is_local_version: bool = False,
241248
build_metadata: str | None = None,
242-
force_bump: bool = False,
249+
exact_increment: bool = False,
243250
) -> Self:
244251
"""Based on the given increment a proper semver will be generated.
245252
@@ -259,7 +266,7 @@ def bump(
259266
else:
260267
if not self.is_prerelease:
261268
base = self.increment_base(increment)
262-
elif force_bump:
269+
elif exact_increment:
263270
base = self.increment_base(increment)
264271
else:
265272
base = f"{self.major}.{self.minor}.{self.micro}"

docs/bump.md

+21
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ options:
7979
specify non-negative integer for dev. release
8080
--increment {MAJOR,MINOR,PATCH}
8181
manually specify the desired increment
82+
--exact-increment apply the exact changes that have been specified (or determined from the commit log), disabling logic that guesses the next version based on typical version progression when a prelease suffix is present.
8283
--check-consistency, -cc
8384
check consistency among versions defined in commitizen configuration and version_files
8485
--annotated-tag, -at create annotated tag instead of lightweight one
@@ -142,6 +143,26 @@ by their precedence and showcase how a release might flow through a development
142143
Also note that bumping pre-releases _maintains linearity_: bumping of a pre-release with lower precedence than
143144
the current pre-release phase maintains the current phase of higher precedence. For example, if the current
144145
version is `1.0.0b1` then bumping with `--prerelease alpha` will continue to bump the “beta” phase.
146+
This behavior can be overridden by passing `--exact-increment` (see below).
147+
148+
### `--exact-increment`
149+
150+
The `--exact-increment` flag bypasses the logic that creates a best guess for the next version based on the
151+
principle of maintaining linearity when a pre-release is present (see above). Instead, `bump` will apply the
152+
exact changes that have been specified with `--increment` or determined from the commit log. For example,
153+
`--prerelease beta` will always result in a `b` tag, and `--increment PATCH` will always increase the patch component.
154+
155+
Below are some examples that illustrate the difference in behavior:
156+
157+
158+
| Increment | Pre-release | Start Version | Without `--exact-increment` | With `--exact-increment` |
159+
|-----------|-------------|---------------|-----------------------------|--------------------------|
160+
| `MAJOR` | | `2.0.0b0` | `2.0.0` | `3.0.0` |
161+
| `MINOR` | | `2.0.0b0` | `2.0.0` | `2.1.0` |
162+
| `PATCH` | | `2.0.0b0` | `2.0.0` | `2.0.1` |
163+
| `MAJOR` | `alpha` | `2.0.0b0` | `3.0.0a0` | `3.0.0a0` |
164+
| `MINOR` | `alpha` | `2.0.0b0` | `2.0.0b1` | `2.1.0a0` |
165+
| `PATCH` | `alpha` | `2.0.0b0` | `2.0.0b1` | `2.0.1a0` |
145166
146167
### `--check-consistency`
147168

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-increment"]
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-increment 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-increment"]
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-increment 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-increment",
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

+61-21
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@
115115
(("2.0.0a4", "PATCH", "alpha", 0, None), "2.0.0a5"),
116116
(("2.0.0a5", "MAJOR", "alpha", 0, None), "2.0.0a6"),
117117
#
118+
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.0.0b1"),
119+
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.0b1"),
120+
#
118121
(("1.0.1a0", "PATCH", None, 0, None), "1.0.1"),
119122
(("1.0.1a0", "MINOR", None, 0, None), "1.1.0"),
120123
(("1.0.1a0", "MAJOR", None, 0, None), "2.0.0"),
@@ -141,27 +144,43 @@
141144
(("3.1.4a0", "MAJOR", "alpha", 0, None), "4.0.0a0"),
142145
]
143146

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",
147+
excact_cases = [
148+
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
149+
(("1.0.0", "MINOR", None, 0, None), "1.1.0"),
150+
# with exact_increment=False: "1.0.0b0"
151+
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1b0"),
152+
# with exact_increment=False: "1.0.0b1"
153+
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1b0"),
154+
# with exact_increment=False: "1.0.0rc0"
155+
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1rc0"),
156+
# with exact_increment=False: "1.0.0-rc1"
157+
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1rc0"),
158+
# with exact_increment=False: "1.0.0rc1-dev1"
159+
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1rc0.dev1"),
160+
# with exact_increment=False: "1.0.0b0"
161+
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0b0"),
162+
# with exact_increment=False: "1.0.0b1"
163+
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0b0"),
164+
# with exact_increment=False: "1.0.0b1"
165+
(("1.0.0b0", "MINOR", "alpha", 0, None), "1.1.0a0"),
166+
# with exact_increment=False: "1.0.0rc0"
167+
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0rc0"),
168+
# with exact_increment=False: "1.0.0rc1"
169+
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0rc0"),
170+
# with exact_increment=False: "1.0.0rc1-dev1"
171+
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0rc0.dev1"),
172+
# with exact_increment=False: "2.0.0"
173+
(("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
174+
# with exact_increment=False: "2.0.0"
175+
(("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
176+
# with exact_increment=False: "2.0.0"
177+
(("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
178+
# same with exact_increment=False
179+
(("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0a0"),
180+
# with exact_increment=False: "2.0.0b1"
181+
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0a0"),
182+
# with exact_increment=False: "2.0.0b1"
183+
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1a0"),
165184
]
166185

167186

@@ -194,6 +213,27 @@ def test_bump_pep440_version(test_input, expected):
194213
)
195214

196215

216+
@pytest.mark.parametrize("test_input, expected", excact_cases)
217+
def test_bump_pep440_version_force(test_input, expected):
218+
current_version = test_input[0]
219+
increment = test_input[1]
220+
prerelease = test_input[2]
221+
prerelease_offset = test_input[3]
222+
devrelease = test_input[4]
223+
assert (
224+
str(
225+
Pep440(current_version).bump(
226+
increment=increment,
227+
prerelease=prerelease,
228+
prerelease_offset=prerelease_offset,
229+
devrelease=devrelease,
230+
exact_increment=True,
231+
)
232+
)
233+
== expected
234+
)
235+
236+
197237
@pytest.mark.parametrize("test_input,expected", local_versions)
198238
def test_bump_pep440_version_local(test_input, expected):
199239
current_version = test_input[0]

tests/test_version_scheme_semver.py

+58
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,43 @@
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_increment=False: "1.0.0-b0"
90+
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"),
91+
# with exact_increment=False: "1.0.0-b1"
92+
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"),
93+
# with exact_increment=False: "1.0.0-rc0"
94+
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"),
95+
# with exact_increment=False: "1.0.0-rc1"
96+
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"),
97+
# with exact_increment=False: "1.0.0-rc1-dev1"
98+
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"),
99+
# with exact_increment=False: "1.0.0-b0"
100+
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"),
101+
# with exact_increment=False: "1.0.0-b1"
102+
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"),
103+
# with exact_increment=False: "1.0.0-rc0"
104+
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"),
105+
# with exact_increment=False: "1.0.0-rc1"
106+
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"),
107+
# with exact_increment=False: "1.0.0-rc1-dev1"
108+
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"),
109+
# with exact_increment=False: "2.0.0"
110+
(("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
111+
# with exact_increment=False: "2.0.0"
112+
(("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
113+
# with exact_increment=False: "2.0.0"
114+
(("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
115+
# same with exact_increment=False
116+
(("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0-a0"),
117+
# with exact_increment=False: "2.0.0b1"
118+
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0-a0"),
119+
# with exact_increment=False: "2.0.0b1"
120+
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1-a0"),
121+
]
122+
86123

87124
@pytest.mark.parametrize(
88125
"test_input, expected",
@@ -107,6 +144,27 @@ def test_bump_semver_version(test_input, expected):
107144
)
108145

109146

147+
@pytest.mark.parametrize("test_input, expected", excact_cases)
148+
def test_bump_semver_version_force(test_input, expected):
149+
current_version = test_input[0]
150+
increment = test_input[1]
151+
prerelease = test_input[2]
152+
prerelease_offset = test_input[3]
153+
devrelease = test_input[4]
154+
assert (
155+
str(
156+
SemVer(current_version).bump(
157+
increment=increment,
158+
prerelease=prerelease,
159+
prerelease_offset=prerelease_offset,
160+
devrelease=devrelease,
161+
exact_increment=True,
162+
)
163+
)
164+
== expected
165+
)
166+
167+
110168
@pytest.mark.parametrize("test_input,expected", local_versions)
111169
def test_bump_semver_version_local(test_input, expected):
112170
current_version = test_input[0]

0 commit comments

Comments
 (0)