From 7ff4fd91b2077348d821afd96abb65f51483d63d Mon Sep 17 00:00:00 2001 From: Eduardo Cardoso Date: Thu, 27 Jul 2023 14:06:41 -0300 Subject: [PATCH 1/5] fix: properly bump versions between prereleases --- commitizen/commands/bump.py | 52 +++++++++++++++++++++++++++-- commitizen/version_schemes.py | 24 ++++++++++--- tests/commands/test_bump_command.py | 36 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index af365ecd2..8eae93533 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -24,7 +24,11 @@ ) from commitizen.changelog_formats import get_changelog_format from commitizen.providers import get_provider -from commitizen.version_schemes import InvalidVersion, get_version_scheme +from commitizen.version_schemes import ( + get_version_scheme, + InvalidVersion, + VersionProtocol, +) logger = getLogger("commitizen") @@ -228,8 +232,19 @@ def __call__(self): # noqa: C901 # Increment is removed when current and next version # are expected to be prereleases. - if prerelease and current_version.is_prerelease: - increment = None + force_bump = False + if current_version.is_prerelease: + last_final = self.find_previous_final_version(current_version) + if last_final is not None: + commits = git.get_commits(last_final) + increment = self.find_increment(commits) + semver = last_final.increment_base( + increment=increment, force_bump=True + ) + if semver != current_version.base_version: + force_bump = True + elif prerelease: + increment = None new_version = current_version.bump( increment, @@ -237,6 +252,7 @@ def __call__(self): # noqa: C901 prerelease_offset=prerelease_offset, devrelease=devrelease, is_local_version=is_local_version, + force_bump=force_bump, ) new_tag_version = bump.normalize_tag( @@ -398,3 +414,33 @@ def _get_commit_args(self): if self.no_verify: commit_args.append("--no-verify") return " ".join(commit_args) + + def find_previous_final_version( + self, current_version: VersionProtocol + ) -> VersionProtocol | None: + tag_format: str = self.bump_settings["tag_format"] + current = bump.normalize_tag( + current_version, + tag_format=tag_format, + scheme=self.scheme, + ) + + final_versions = [] + for tag in git.get_tag_names(): + assert tag + try: + version = self.scheme(tag) + if not version.is_prerelease or tag == current: + final_versions.append(version) + except InvalidVersion: + continue + + if not final_versions: + return None + + final_versions = sorted(final_versions) # type: ignore [type-var] + current_index = final_versions.index(current_version) + previous_index = current_index - 1 + if previous_index < 0: + return None + return final_versions[previous_index] diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index d7967ae92..260523708 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -100,6 +100,7 @@ def bump( prerelease_offset: int = 0, devrelease: int | None = None, is_local_version: bool = False, + force_bump: bool = False, ) -> Self: """ Based on the given increment, generate the next bumped version according to the version scheme @@ -166,7 +167,9 @@ def generate_devrelease(self, devrelease: int | None) -> str: return f"dev{devrelease}" - def increment_base(self, increment: str | None = None) -> str: + def increment_base( + self, increment: str | None = None, force_bump: bool = False + ) -> str: prev_release = list(self.release) increments = [MAJOR, MINOR, PATCH] base = dict(zip_longest(increments, prev_release, fillvalue=0)) @@ -175,7 +178,7 @@ def increment_base(self, increment: str | None = None) -> str: # must remove its prerelease tag, # so it doesn't matter the increment. # Example: 1.0.0a0 with PATCH/MINOR -> 1.0.0 - if not self.is_prerelease: + if not self.is_prerelease or force_bump: if increment == MAJOR: base[MAJOR] += 1 base[MINOR] = 0 @@ -195,6 +198,7 @@ def bump( prerelease_offset: int = 0, devrelease: int | None = None, is_local_version: bool = False, + force_bump: bool = False, ) -> Self: """Based on the given increment a proper semver will be generated. @@ -212,9 +216,21 @@ def bump( local_version = self.scheme(self.local).bump(increment) return self.scheme(f"{self.public}+{local_version}") # type: ignore else: - base = self.increment_base(increment) + base = self.increment_base(increment, force_bump) dev_version = self.generate_devrelease(devrelease) - pre_version = self.generate_prerelease(prerelease, offset=prerelease_offset) + release = list(self.release) + if len(release) < 3: + release += [0] * (3 - len(release)) + current_semver = ".".join(str(part) for part in release) + if base == current_semver: + pre_version = self.generate_prerelease( + prerelease, offset=prerelease_offset + ) + else: + base_version = cast(BaseVersion, self.scheme(base)) + pre_version = base_version.generate_prerelease( + prerelease, offset=prerelease_offset + ) # TODO: post version return self.scheme(f"{base}{pre_version}{dev_version}") # type: ignore diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 774ec0a95..e8eb73c55 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -227,6 +227,42 @@ def test_bump_command_prelease(mocker: MockFixture): assert tag_exists is True +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_prelease_increment(mocker: MockFixture): + # FINAL RELEASE + create_file_and_commit("fix: location") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + assert git.tag_exist("0.1.1") + + # PRERELEASE + create_file_and_commit("fix: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("0.1.2a0") + + create_file_and_commit("feat: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("0.2.0a0") + + create_file_and_commit("feat!: breaking") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("1.0.0a0") + + @pytest.mark.usefixtures("tmp_commitizen_project") def test_bump_on_git_with_hooks_no_verify_disabled(mocker: MockFixture): """Bump commit without --no-verify""" From 9f0b2bcfc211cb5a9c9d7ee90269912a821cfa2d Mon Sep 17 00:00:00 2001 From: Jens Troeger Date: Sun, 12 Nov 2023 16:22:19 +1000 Subject: [PATCH 2/5] refactor: incorporate PR feedback --- commitizen/commands/bump.py | 4 ++-- commitizen/version_schemes.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 8eae93533..aa2f77a16 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -238,10 +238,10 @@ def __call__(self): # noqa: C901 if last_final is not None: commits = git.get_commits(last_final) increment = self.find_increment(commits) - semver = last_final.increment_base( + base = last_final.increment_base( increment=increment, force_bump=True ) - if semver != current_version.base_version: + if base != current_version.base_version: force_bump = True elif prerelease: increment = None diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index 260523708..c47c5fe69 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -221,8 +221,8 @@ def bump( release = list(self.release) if len(release) < 3: release += [0] * (3 - len(release)) - current_semver = ".".join(str(part) for part in release) - if base == current_semver: + current_base = ".".join(str(part) for part in release) + if base == current_base: pre_version = self.generate_prerelease( prerelease, offset=prerelease_offset ) From 53b0f09feaa85dfea67aec551f53cf3599ec18cf Mon Sep 17 00:00:00 2001 From: Jens Troeger Date: Mon, 27 Nov 2023 12:24:30 +1000 Subject: [PATCH 3/5] refactor: lower version bump into BaseVersion class and simplify callers of BaseVersion.bump() --- commitizen/commands/bump.py | 17 -------- commitizen/version_schemes.py | 42 ++++++++++-------- tests/test_version_scheme_pep440.py | 68 ++++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 36 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index aa2f77a16..b2ecd3130 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -230,29 +230,12 @@ def __call__(self): # noqa: C901 "To avoid this error, manually specify the type of increment with `--increment`" ) - # Increment is removed when current and next version - # are expected to be prereleases. - force_bump = False - if current_version.is_prerelease: - last_final = self.find_previous_final_version(current_version) - if last_final is not None: - commits = git.get_commits(last_final) - increment = self.find_increment(commits) - base = last_final.increment_base( - increment=increment, force_bump=True - ) - if base != current_version.base_version: - force_bump = True - elif prerelease: - increment = None - new_version = current_version.bump( increment, prerelease=prerelease, prerelease_offset=prerelease_offset, devrelease=devrelease, is_local_version=is_local_version, - force_bump=force_bump, ) new_tag_version = bump.normalize_tag( diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index c47c5fe69..f1272c4dd 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -167,27 +167,20 @@ def generate_devrelease(self, devrelease: int | None) -> str: return f"dev{devrelease}" - def increment_base( - self, increment: str | None = None, force_bump: bool = False - ) -> str: + def increment_base(self, increment: str | None = None) -> str: prev_release = list(self.release) increments = [MAJOR, MINOR, PATCH] base = dict(zip_longest(increments, prev_release, fillvalue=0)) - # This flag means that current version - # must remove its prerelease tag, - # so it doesn't matter the increment. - # Example: 1.0.0a0 with PATCH/MINOR -> 1.0.0 - if not self.is_prerelease or force_bump: - if increment == MAJOR: - base[MAJOR] += 1 - base[MINOR] = 0 - base[PATCH] = 0 - elif increment == MINOR: - base[MINOR] += 1 - base[PATCH] = 0 - elif increment == PATCH: - base[PATCH] += 1 + if increment == MAJOR: + base[MAJOR] += 1 + base[MINOR] = 0 + base[PATCH] = 0 + elif increment == MINOR: + base[MINOR] += 1 + base[PATCH] = 0 + elif increment == PATCH: + base[PATCH] += 1 return f"{base[MAJOR]}.{base[MINOR]}.{base[PATCH]}" @@ -216,7 +209,20 @@ def bump( local_version = self.scheme(self.local).bump(increment) return self.scheme(f"{self.public}+{local_version}") # type: ignore else: - base = self.increment_base(increment, force_bump) + if not self.is_prerelease: + base = self.increment_base(increment) + elif force_bump: + base = self.increment_base(increment) + else: + base = f"{self.major}.{self.minor}.{self.micro}" + if increment == PATCH: + pass + elif increment == MINOR: + if self.micro != 0: + base = self.increment_base(increment) + elif increment == MAJOR: + if self.minor != 0 or self.micro != 0: + base = self.increment_base(increment) dev_version = self.generate_devrelease(devrelease) release = list(self.release) if len(release) < 3: diff --git a/tests/test_version_scheme_pep440.py b/tests/test_version_scheme_pep440.py index 89f5a137a..2173cbcfb 100644 --- a/tests/test_version_scheme_pep440.py +++ b/tests/test_version_scheme_pep440.py @@ -78,10 +78,76 @@ (("1.0.0rc1", None, "rc", 0, None), "1.0.0rc2"), ] +# additional pre-release tests run through various release scenarios +prerelease_cases = [ + # + (("3.3.3", "PATCH", "alpha", 0, None), "3.3.4a0"), + (("3.3.4a0", "PATCH", "alpha", 0, None), "3.3.4a1"), + (("3.3.4a1", "MINOR", "alpha", 0, None), "3.4.0a0"), + (("3.4.0a0", "PATCH", "alpha", 0, None), "3.4.0a1"), + (("3.4.0a1", "MINOR", "alpha", 0, None), "3.4.0a2"), + (("3.4.0a2", "MAJOR", "alpha", 0, None), "4.0.0a0"), + (("4.0.0a0", "PATCH", "alpha", 0, None), "4.0.0a1"), + (("4.0.0a1", "MINOR", "alpha", 0, None), "4.0.0a2"), + (("4.0.0a2", "MAJOR", "alpha", 0, None), "4.0.0a3"), + # + (("1.0.0", "PATCH", "alpha", 0, None), "1.0.1a0"), + (("1.0.1a0", "PATCH", "alpha", 0, None), "1.0.1a1"), + (("1.0.1a1", "MINOR", "alpha", 0, None), "1.1.0a0"), + (("1.1.0a0", "PATCH", "alpha", 0, None), "1.1.0a1"), + (("1.1.0a1", "MINOR", "alpha", 0, None), "1.1.0a2"), + (("1.1.0a2", "MAJOR", "alpha", 0, None), "2.0.0a0"), + # + (("1.0.0", "MINOR", "alpha", 0, None), "1.1.0a0"), + (("1.1.0a0", "PATCH", "alpha", 0, None), "1.1.0a1"), + (("1.1.0a1", "MINOR", "alpha", 0, None), "1.1.0a2"), + (("1.1.0a2", "PATCH", "alpha", 0, None), "1.1.0a3"), + (("1.1.0a3", "MAJOR", "alpha", 0, None), "2.0.0a0"), + # + (("1.0.0", "MAJOR", "alpha", 0, None), "2.0.0a0"), + (("2.0.0a0", "MINOR", "alpha", 0, None), "2.0.0a1"), + (("2.0.0a1", "PATCH", "alpha", 0, None), "2.0.0a2"), + (("2.0.0a2", "MAJOR", "alpha", 0, None), "2.0.0a3"), + (("2.0.0a3", "MINOR", "alpha", 0, None), "2.0.0a4"), + (("2.0.0a4", "PATCH", "alpha", 0, None), "2.0.0a5"), + (("2.0.0a5", "MAJOR", "alpha", 0, None), "2.0.0a6"), + # + (("1.0.1a0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.1a0", "MINOR", None, 0, None), "1.1.0"), + (("1.0.1a0", "MAJOR", None, 0, None), "2.0.0"), + # + (("1.1.0a0", "PATCH", None, 0, None), "1.1.0"), + (("1.1.0a0", "MINOR", None, 0, None), "1.1.0"), + (("1.1.0a0", "MAJOR", None, 0, None), "2.0.0"), + # + (("2.0.0a0", "MINOR", None, 0, None), "2.0.0"), + (("2.0.0a0", "MAJOR", None, 0, None), "2.0.0"), + (("2.0.0a0", "PATCH", None, 0, None), "2.0.0"), + # + (("3.0.0a1", None, None, 0, None), "3.0.0"), + (("3.0.0b1", None, None, 0, None), "3.0.0"), + (("3.0.0rc1", None, None, 0, None), "3.0.0"), + # + (("3.1.4", None, "alpha", 0, None), "3.1.4a0"), + (("3.1.4", None, "beta", 0, None), "3.1.4b0"), + (("3.1.4", None, "rc", 0, None), "3.1.4rc0"), + # + (("3.1.4", None, "alpha", 0, None), "3.1.4a0"), + (("3.1.4a0", "PATCH", "alpha", 0, None), "3.1.4a1"), # UNEXPECTED! + (("3.1.4a0", "MINOR", "alpha", 0, None), "3.2.0a0"), + (("3.1.4a0", "MAJOR", "alpha", 0, None), "4.0.0a0"), +] + @pytest.mark.parametrize( "test_input,expected", - itertools.chain(tdd_cases, weird_cases, simple_flow, unexpected_cases), + itertools.chain( + tdd_cases, + weird_cases, + simple_flow, + unexpected_cases, + prerelease_cases, + ), ) def test_bump_pep440_version(test_input, expected): current_version = test_input[0] From b9e768ed093c06136f6ead06e13e2f74e8d31058 Mon Sep 17 00:00:00 2001 From: Jens Troeger Date: Mon, 11 Dec 2023 13:24:46 +1000 Subject: [PATCH 4/5] feat: preserve prerelease linearity --- commitizen/version_schemes.py | 6 ++ tests/commands/test_bump_command.py | 55 ++++++++++++++++++- ...rsion_to_prerelease_version_beta_alpha_.md | 2 +- ...version_to_prerelease_version_rc_alpha_.md | 2 +- ..._version_to_prerelease_version_rc_beta_.md | 2 +- tests/test_version_scheme_pep440.py | 11 ++-- tests/test_version_scheme_semver.py | 11 ++-- 7 files changed, 74 insertions(+), 15 deletions(-) diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index f1272c4dd..c277d2efc 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -147,6 +147,12 @@ def generate_prerelease( if not prerelease: return "" + # prevent down-bumping the pre-release phase, e.g. from 'b1' to 'a2' + # https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases + # https://semver.org/#spec-item-11 + if self.is_prerelease and self.pre: + prerelease = max(prerelease, self.pre[0]) + # version.pre is needed for mypy check if self.is_prerelease and self.pre and prerelease.startswith(self.pre[0]): prev_prerelease: int = self.pre[1] diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index e8eb73c55..d05c8f330 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -208,9 +208,9 @@ def test_bump_command_increment_option( @pytest.mark.usefixtures("tmp_commitizen_project") def test_bump_command_prelease(mocker: MockFixture): - # PRERELEASE create_file_and_commit("feat: location") + # Create an alpha pre-release. testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] mocker.patch.object(sys, "argv", testargs) cli.main() @@ -218,7 +218,58 @@ def test_bump_command_prelease(mocker: MockFixture): tag_exists = git.tag_exist("0.2.0a0") assert tag_exists is True - # PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE + # Create a beta pre-release. + testargs = ["cz", "bump", "--prerelease", "beta", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0b0") + assert tag_exists is True + + # With a current beta pre-release, bumping alpha must bump beta + # because we can't bump "backwards". + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a1") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0b1") + assert tag_exists is True + + # Create a rc pre-release. + testargs = ["cz", "bump", "--prerelease", "rc", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0rc0") + assert tag_exists is True + + # With a current rc pre-release, bumping alpha must bump rc. + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a1") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0b2") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0rc1") + assert tag_exists is True + + # With a current rc pre-release, bumping beta must bump rc. + testargs = ["cz", "bump", "--prerelease", "beta", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a2") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0b2") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0rc2") + assert tag_exists is True + + # Create a final release from the current pre-release. testargs = ["cz", "bump"] mocker.patch.object(sys, "argv", testargs) cli.main() diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md index 300ca9575..ec9ab11d7 100644 --- a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.2.0a0 (2021-06-11) +## 0.2.0b1 (2021-06-11) ## 0.2.0b0 (2021-06-11) diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md index 9a7e9f161..e1c20b7e0 100644 --- a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.2.0a0 (2021-06-11) +## 0.2.0rc1 (2021-06-11) ## 0.2.0rc0 (2021-06-11) diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md index b3be6f3c1..e1c20b7e0 100644 --- a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.2.0b0 (2021-06-11) +## 0.2.0rc1 (2021-06-11) ## 0.2.0rc0 (2021-06-11) diff --git a/tests/test_version_scheme_pep440.py b/tests/test_version_scheme_pep440.py index 2173cbcfb..aa2120fad 100644 --- a/tests/test_version_scheme_pep440.py +++ b/tests/test_version_scheme_pep440.py @@ -43,10 +43,11 @@ (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"), ] -# this cases should be handled gracefully -unexpected_cases = [ - (("0.1.1rc0", None, "alpha", 0, None), "0.1.1a0"), - (("0.1.1b1", None, "alpha", 0, None), "0.1.1a0"), +# never bump backwards on pre-releases +linear_prerelease_cases = [ + (("0.1.1b1", None, "alpha", 0, None), "0.1.1b2"), + (("0.1.1rc0", None, "alpha", 0, None), "0.1.1rc1"), + (("0.1.1rc0", None, "beta", 0, None), "0.1.1rc1"), ] weird_cases = [ @@ -145,7 +146,7 @@ tdd_cases, weird_cases, simple_flow, - unexpected_cases, + linear_prerelease_cases, prerelease_cases, ), ) diff --git a/tests/test_version_scheme_semver.py b/tests/test_version_scheme_semver.py index 82ed33c22..85cfcf5df 100644 --- a/tests/test_version_scheme_semver.py +++ b/tests/test_version_scheme_semver.py @@ -44,10 +44,11 @@ (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"), ] -# this cases should be handled gracefully -unexpected_cases = [ - (("0.1.1rc0", None, "alpha", 0, None), "0.1.1-a0"), - (("0.1.1b1", None, "alpha", 0, None), "0.1.1-a0"), +# never bump backwards on pre-releases +linear_prerelease_cases = [ + (("0.1.1b1", None, "alpha", 0, None), "0.1.1-b2"), + (("0.1.1rc0", None, "alpha", 0, None), "0.1.1-rc1"), + (("0.1.1rc0", None, "beta", 0, None), "0.1.1-rc1"), ] weird_cases = [ @@ -84,7 +85,7 @@ @pytest.mark.parametrize( "test_input, expected", - itertools.chain(tdd_cases, weird_cases, simple_flow, unexpected_cases), + itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases), ) def test_bump_semver_version(test_input, expected): current_version = test_input[0] From 7b4596a17f0943aac3a3374dacb498304ef9e4ea Mon Sep 17 00:00:00 2001 From: Jens Troeger Date: Mon, 11 Dec 2023 13:37:06 +1000 Subject: [PATCH 5/5] docs: document the `--prerelease` option --- docs/bump.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/bump.md b/docs/bump.md index 75d6eb7c1..1df5e227d 100644 --- a/docs/bump.md +++ b/docs/bump.md @@ -113,6 +113,34 @@ Generate a **changelog** along with the new version and tag when bumping. cz bump --changelog ``` +### `--prerelease` + +The bump is a pre-release bump, meaning that in addition to a possible version bump the new version receives a +pre-release segment compatible with the bump’s version scheme, where the segment consist of a _phase_ and a +non-negative number. Supported options for `--prerelease` are the following phase names `alpha`, `beta`, or +`rc` (release candidate). For more details, refer to the +[Python Packaging User Guide](https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases). + +Note that as per [semantic versioning spec](https://semver.org/#spec-item-9) + +> Pre-release versions have a lower precedence than the associated normal version. A pre-release version +> indicates that the version is unstable and might not satisfy the intended compatibility requirements +> as denoted by its associated normal version. + +For example, the following versions (using the [PEP 440](https://peps.python.org/pep-0440/) scheme) are ordered +by their precedence and showcase how a release might flow through a development cycle: + +- `1.0.0` is the current published version +- `1.0.1a0` after committing a `fix:` for pre-release +- `1.1.0a1` after committing an additional `feat:` for pre-release +- `1.1.0b0` after bumping a beta release +- `1.1.0rc0` after bumping the release candidate +- `1.1.0` next feature release + +Also note that bumping pre-releases _maintains linearity_: bumping of a pre-release with lower precedence than +the current pre-release phase maintains the current phase of higher precedence. For example, if the current +version is `1.0.0b1` then bumping with `--prerelease alpha` will continue to bump the “beta” phase. + ### `--check-consistency` Check whether the versions defined in `version_files` and the version in commitizen