diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 13e30a90a..29b81f06f 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -23,7 +23,11 @@ NoVersionSpecifiedError, ) from commitizen.providers import get_provider -from commitizen.version_schemes import get_version_scheme, InvalidVersion +from commitizen.version_schemes import ( + get_version_scheme, + InvalidVersion, + VersionProtocol, +) logger = getLogger("commitizen") @@ -215,8 +219,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, @@ -224,6 +239,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( @@ -375,3 +391,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 a9936a597..1174c7553 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -105,6 +105,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 @@ -171,7 +172,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)) @@ -180,7 +183,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 @@ -200,6 +203,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. @@ -217,9 +221,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 acc2ad69a..43f32e297 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -223,6 +223,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"""