Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d2377dd

Browse files
eduardocardosojenstroeger
andauthored
feat: properly bump versions between prereleases (#799)
* fix: properly bump versions between prereleases * refactor: incorporate PR feedback * refactor: lower version bump into BaseVersion class and simplify callers of BaseVersion.bump() * feat: preserve prerelease linearity * docs: document the `--prerelease` option --------- Co-authored-by: Jens Troeger <jens.troeger@light-speed.de>
1 parent db97a5f commit d2377dd

9 files changed

+277
-37
lines changed

‎commitizen/commands/bump.py‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
)
2525
from commitizen.changelog_formats import get_changelog_format
2626
from commitizen.providers import get_provider
27-
from commitizen.version_schemes import InvalidVersion, get_version_scheme
27+
from commitizen.version_schemes import (
28+
get_version_scheme,
29+
InvalidVersion,
30+
VersionProtocol,
31+
)
2832

2933
logger = getLogger("commitizen")
3034

@@ -226,11 +230,6 @@ def __call__(self): # noqa: C901
226230
"To avoid this error, manually specify the type of increment with `--increment`"
227231
)
228232

229-
# Increment is removed when current and next version
230-
# are expected to be prereleases.
231-
if prerelease and current_version.is_prerelease:
232-
increment = None
233-
234233
new_version = current_version.bump(
235234
increment,
236235
prerelease=prerelease,
@@ -398,3 +397,33 @@ def _get_commit_args(self):
398397
if self.no_verify:
399398
commit_args.append("--no-verify")
400399
return " ".join(commit_args)
400+
401+
def find_previous_final_version(
402+
self, current_version: VersionProtocol
403+
) -> VersionProtocol | None:
404+
tag_format: str = self.bump_settings["tag_format"]
405+
current = bump.normalize_tag(
406+
current_version,
407+
tag_format=tag_format,
408+
scheme=self.scheme,
409+
)
410+
411+
final_versions = []
412+
for tag in git.get_tag_names():
413+
assert tag
414+
try:
415+
version = self.scheme(tag)
416+
if not version.is_prerelease or tag == current:
417+
final_versions.append(version)
418+
except InvalidVersion:
419+
continue
420+
421+
if not final_versions:
422+
return None
423+
424+
final_versions = sorted(final_versions) # type: ignore [type-var]
425+
current_index = final_versions.index(current_version)
426+
previous_index = current_index - 1
427+
if previous_index < 0:
428+
return None
429+
return final_versions[previous_index]

‎commitizen/version_schemes.py‎

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def bump(
100100
prerelease_offset: int = 0,
101101
devrelease: int | None = None,
102102
is_local_version: bool = False,
103+
force_bump: bool = False,
103104
) -> Self:
104105
"""
105106
Based on the given increment, generate the next bumped version according to the version scheme
@@ -146,6 +147,12 @@ def generate_prerelease(
146147
if not prerelease:
147148
return ""
148149

150+
# prevent down-bumping the pre-release phase, e.g. from 'b1' to 'a2'
151+
# https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases
152+
# https://semver.org/#spec-item-11
153+
if self.is_prerelease and self.pre:
154+
prerelease = max(prerelease, self.pre[0])
155+
149156
# version.pre is needed for mypy check
150157
if self.is_prerelease and self.pre and prerelease.startswith(self.pre[0]):
151158
prev_prerelease: int = self.pre[1]
@@ -171,20 +178,15 @@ def increment_base(self, increment: str | None = None) -> str:
171178
increments = [MAJOR, MINOR, PATCH]
172179
base = dict(zip_longest(increments, prev_release, fillvalue=0))
173180

174-
# This flag means that current version
175-
# must remove its prerelease tag,
176-
# so it doesn't matter the increment.
177-
# Example: 1.0.0a0 with PATCH/MINOR -> 1.0.0
178-
if not self.is_prerelease:
179-
if increment == MAJOR:
180-
base[MAJOR] += 1
181-
base[MINOR] = 0
182-
base[PATCH] = 0
183-
elif increment == MINOR:
184-
base[MINOR] += 1
185-
base[PATCH] = 0
186-
elif increment == PATCH:
187-
base[PATCH] += 1
181+
if increment == MAJOR:
182+
base[MAJOR] += 1
183+
base[MINOR] = 0
184+
base[PATCH] = 0
185+
elif increment == MINOR:
186+
base[MINOR] += 1
187+
base[PATCH] = 0
188+
elif increment == PATCH:
189+
base[PATCH] += 1
188190

189191
return f"{base[MAJOR]}.{base[MINOR]}.{base[PATCH]}"
190192

@@ -195,6 +197,7 @@ def bump(
195197
prerelease_offset: int = 0,
196198
devrelease: int | None = None,
197199
is_local_version: bool = False,
200+
force_bump: bool = False,
198201
) -> Self:
199202
"""Based on the given increment a proper semver will be generated.
200203
@@ -212,9 +215,34 @@ def bump(
212215
local_version = self.scheme(self.local).bump(increment)
213216
return self.scheme(f"{self.public}+{local_version}") # type: ignore
214217
else:
215-
base = self.increment_base(increment)
218+
if not self.is_prerelease:
219+
base = self.increment_base(increment)
220+
elif force_bump:
221+
base = self.increment_base(increment)
222+
else:
223+
base = f"{self.major}.{self.minor}.{self.micro}"
224+
if increment == PATCH:
225+
pass
226+
elif increment == MINOR:
227+
if self.micro != 0:
228+
base = self.increment_base(increment)
229+
elif increment == MAJOR:
230+
if self.minor != 0 or self.micro != 0:
231+
base = self.increment_base(increment)
216232
dev_version = self.generate_devrelease(devrelease)
217-
pre_version = self.generate_prerelease(prerelease, offset=prerelease_offset)
233+
release = list(self.release)
234+
if len(release) < 3:
235+
release += [0] * (3 - len(release))
236+
current_base = ".".join(str(part) for part in release)
237+
if base == current_base:
238+
pre_version = self.generate_prerelease(
239+
prerelease, offset=prerelease_offset
240+
)
241+
else:
242+
base_version = cast(BaseVersion, self.scheme(base))
243+
pre_version = base_version.generate_prerelease(
244+
prerelease, offset=prerelease_offset
245+
)
218246
# TODO: post version
219247
return self.scheme(f"{base}{pre_version}{dev_version}") # type: ignore
220248

‎docs/bump.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,34 @@ Generate a **changelog** along with the new version and tag when bumping.
113113
cz bump --changelog
114114
```
115115
116+
### `--prerelease`
117+
118+
The bump is a pre-release bump, meaning that in addition to a possible version bump the new version receives a
119+
pre-release segment compatible with the bump’s version scheme, where the segment consist of a _phase_ and a
120+
non-negative number. Supported options for `--prerelease` are the following phase names `alpha`, `beta`, or
121+
`rc` (release candidate). For more details, refer to the
122+
[Python Packaging User Guide](https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases).
123+
124+
Note that as per [semantic versioning spec](https://semver.org/#spec-item-9)
125+
126+
> Pre-release versions have a lower precedence than the associated normal version. A pre-release version
127+
> indicates that the version is unstable and might not satisfy the intended compatibility requirements
128+
> as denoted by its associated normal version.
129+
130+
For example, the following versions (using the [PEP 440](https://peps.python.org/pep-0440/) scheme) are ordered
131+
by their precedence and showcase how a release might flow through a development cycle:
132+
133+
- `1.0.0` is the current published version
134+
- `1.0.1a0` after committing a `fix:` for pre-release
135+
- `1.1.0a1` after committing an additional `feat:` for pre-release
136+
- `1.1.0b0` after bumping a beta release
137+
- `1.1.0rc0` after bumping the release candidate
138+
- `1.1.0` next feature release
139+
140+
Also note that bumping pre-releases _maintains linearity_: bumping of a pre-release with lower precedence than
141+
the current pre-release phase maintains the current phase of higher precedence. For example, if the current
142+
version is `1.0.0b1` then bumping with `--prerelease alpha` will continue to bump the "beta" phase.
143+
116144
### `--check-consistency`
117145
118146
Check whether the versions defined in `version_files` and the version in commitizen

‎tests/commands/test_bump_command.py‎

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,17 +208,68 @@ def test_bump_command_increment_option(
208208

209209
@pytest.mark.usefixtures("tmp_commitizen_project")
210210
def test_bump_command_prelease(mocker: MockFixture):
211-
# PRERELEASE
212211
create_file_and_commit("feat: location")
213212

213+
# Create an alpha pre-release.
214214
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
215215
mocker.patch.object(sys, "argv", testargs)
216216
cli.main()
217217

218218
tag_exists = git.tag_exist("0.2.0a0")
219219
assert tag_exists is True
220220

221-
# PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE
221+
# Create a beta pre-release.
222+
testargs = ["cz", "bump", "--prerelease", "beta", "--yes"]
223+
mocker.patch.object(sys, "argv", testargs)
224+
cli.main()
225+
226+
tag_exists = git.tag_exist("0.2.0b0")
227+
assert tag_exists is True
228+
229+
# With a current beta pre-release, bumping alpha must bump beta
230+
# because we can't bump "backwards".
231+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
232+
mocker.patch.object(sys, "argv", testargs)
233+
cli.main()
234+
235+
tag_exists = git.tag_exist("0.2.0a1")
236+
assert tag_exists is False
237+
tag_exists = git.tag_exist("0.2.0b1")
238+
assert tag_exists is True
239+
240+
# Create a rc pre-release.
241+
testargs = ["cz", "bump", "--prerelease", "rc", "--yes"]
242+
mocker.patch.object(sys, "argv", testargs)
243+
cli.main()
244+
245+
tag_exists = git.tag_exist("0.2.0rc0")
246+
assert tag_exists is True
247+
248+
# With a current rc pre-release, bumping alpha must bump rc.
249+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
250+
mocker.patch.object(sys, "argv", testargs)
251+
cli.main()
252+
253+
tag_exists = git.tag_exist("0.2.0a1")
254+
assert tag_exists is False
255+
tag_exists = git.tag_exist("0.2.0b2")
256+
assert tag_exists is False
257+
tag_exists = git.tag_exist("0.2.0rc1")
258+
assert tag_exists is True
259+
260+
# With a current rc pre-release, bumping beta must bump rc.
261+
testargs = ["cz", "bump", "--prerelease", "beta", "--yes"]
262+
mocker.patch.object(sys, "argv", testargs)
263+
cli.main()
264+
265+
tag_exists = git.tag_exist("0.2.0a2")
266+
assert tag_exists is False
267+
tag_exists = git.tag_exist("0.2.0b2")
268+
assert tag_exists is False
269+
tag_exists = git.tag_exist("0.2.0rc2")
270+
assert tag_exists is True
271+
272+
# Create a final release from the current pre-release.
222273
testargs = ["cz", "bump"]
223274
mocker.patch.object(sys, "argv", testargs)
224275
cli.main()
@@ -227,6 +278,42 @@ def test_bump_command_prelease(mocker: MockFixture):
227278
assert tag_exists is True
228279

229280

281+
@pytest.mark.usefixtures("tmp_commitizen_project")
282+
def test_bump_command_prelease_increment(mocker: MockFixture):
283+
# FINAL RELEASE
284+
create_file_and_commit("fix: location")
285+
286+
testargs = ["cz", "bump", "--yes"]
287+
mocker.patch.object(sys, "argv", testargs)
288+
cli.main()
289+
assert git.tag_exist("0.1.1")
290+
291+
# PRERELEASE
292+
create_file_and_commit("fix: location")
293+
294+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
295+
mocker.patch.object(sys, "argv", testargs)
296+
cli.main()
297+
298+
assert git.tag_exist("0.1.2a0")
299+
300+
create_file_and_commit("feat: location")
301+
302+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
303+
mocker.patch.object(sys, "argv", testargs)
304+
cli.main()
305+
306+
assert git.tag_exist("0.2.0a0")
307+
308+
create_file_and_commit("feat!: breaking")
309+
310+
testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"]
311+
mocker.patch.object(sys, "argv", testargs)
312+
cli.main()
313+
314+
assert git.tag_exist("1.0.0a0")
315+
316+
230317
@pytest.mark.usefixtures("tmp_commitizen_project")
231318
def test_bump_on_git_with_hooks_no_verify_disabled(mocker: MockFixture):
232319
"""Bump commit without --no-verify"""

‎tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## 0.2.0a0 (2021年06月11日)
7+
## 0.2.0b1 (2021年06月11日)
88

99
## 0.2.0b0 (2021年06月11日)
1010

‎tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## 0.2.0a0 (2021年06月11日)
7+
## 0.2.0rc1 (2021年06月11日)
88

99
## 0.2.0rc0 (2021年06月11日)
1010

‎tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## 0.2.0b0 (2021年06月11日)
7+
## 0.2.0rc1 (2021年06月11日)
88

99
## 0.2.0rc0 (2021年06月11日)
1010

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /