From 66896bccf77860f24840eb578fa12714e7ebdea5 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Tue, 30 Jan 2024 09:39:24 -0800 Subject: [PATCH] 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 --- commitizen/cli.py | 9 ++++ commitizen/commands/bump.py | 6 +++ commitizen/version_schemes.py | 13 ++++-- tests/commands/test_bump_command.py | 49 ++++++++++++++++++++++ tests/test_version_scheme_pep440.py | 65 +++++++++++++++++++---------- tests/test_version_scheme_semver.py | 46 ++++++++++++++++++++ 6 files changed, 164 insertions(+), 24 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 511f74565..14bd844c9 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -230,6 +230,15 @@ def __call__( "choices": ["MAJOR", "MINOR", "PATCH"], "type": str.upper, }, + { + "name": ["--exact"], + "action": "store_true", + "help": ( + "treat the increment and prerelease arguments " + "explicitly. Disables logic that attempts to deduce " + "the correct increment when a prelease suffix is present." + ), + }, { "name": ["--check-consistency", "-cc"], "help": ( diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 12a753ef0..bc68e447c 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -52,6 +52,7 @@ def __init__(self, config: BaseConfig, arguments: dict): "tag_format", "prerelease", "increment", + "exact", "bump_message", "gpg_sign", "annotated_tag", @@ -157,6 +158,7 @@ def __call__(self) -> None: # noqa: C901 is_files_only: bool | None = self.arguments["files_only"] is_local_version: bool = self.arguments["local_version"] manual_version = self.arguments["manual_version"] + exact_mode: bool = self.arguments["exact"] if manual_version: if increment: @@ -183,6 +185,9 @@ def __call__(self) -> None: # noqa: C901 "--prerelease-offset cannot be combined with MANUAL_VERSION" ) + if not prerelease and exact_mode: + raise NotAllowed("--exact is only valid with --prerelease") + if major_version_zero: if not current_version.release[0] == 0: raise NotAllowed( @@ -237,6 +242,7 @@ def __call__(self) -> None: # noqa: C901 prerelease_offset=prerelease_offset, devrelease=devrelease, is_local_version=is_local_version, + exact_mode=exact_mode, ) new_tag_version = bump.normalize_tag( diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index fe19d0d62..58847a5e8 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -129,10 +129,17 @@ def bump( prerelease_offset: int = 0, devrelease: int | None = None, is_local_version: bool = False, - force_bump: bool = False, + exact_mode: bool = False, ) -> Self: """ Based on the given increment, generate the next bumped version according to the version scheme + + Args: + increment: The component to increase + prerelease: The type of prerelease, if Any + is_local_version: Whether to increment the local version instead + exact_mode: Treat the increment and prerelease arguments explicitly. Disables logic + that attempts to deduce the correct increment when a prelease suffix is present. """ @@ -226,7 +233,7 @@ def bump( prerelease_offset: int = 0, devrelease: int | None = None, is_local_version: bool = False, - force_bump: bool = False, + exact_mode: bool = False, ) -> Self: """Based on the given increment a proper semver will be generated. @@ -246,7 +253,7 @@ def bump( else: if not self.is_prerelease: base = self.increment_base(increment) - elif force_bump: + elif exact_mode: base = self.increment_base(increment) else: base = f"{self.major}.{self.minor}.{self.micro}" diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index d05c8f330..2e07528da 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -314,6 +314,55 @@ def test_bump_command_prelease_increment(mocker: MockFixture): assert git.tag_exist("1.0.0a0") +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_prelease_exact_mode(mocker: MockFixture): + # PRERELEASE + create_file_and_commit("feat: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a0") + assert tag_exists is True + + # PRERELEASE + PATCH BUMP + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes", "--exact"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a1") + assert tag_exists is True + + # PRERELEASE + MINOR BUMP + # --exact allows the minor version to bump, and restart the prerelease + create_file_and_commit("feat: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes", "--exact"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.3.0a0") + assert tag_exists is True + + # PRERELEASE + MAJOR BUMP + # --exact allows the major version to bump, and restart the prerelease + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--increment=MAJOR", + "--exact", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("1.0.0a0") + assert tag_exists is True + + @pytest.mark.usefixtures("tmp_commitizen_project") def test_bump_on_git_with_hooks_no_verify_disabled(mocker: MockFixture): """Bump commit without --no-verify""" diff --git a/tests/test_version_scheme_pep440.py b/tests/test_version_scheme_pep440.py index ac9945065..7b3c9d62e 100644 --- a/tests/test_version_scheme_pep440.py +++ b/tests/test_version_scheme_pep440.py @@ -141,27 +141,29 @@ (("3.1.4a0", "MAJOR", "alpha", 0, None), "4.0.0a0"), ] - -# test driven development -sortability = [ - "0.10.0a0", - "0.1.1", - "0.1.2", - "2.1.1", - "3.0.0", - "0.9.1a0", - "1.0.0a1", - "1.0.0b1", - "1.0.0a1", - "1.0.0a2.dev1", - "1.0.0rc2", - "1.0.0a3.dev0", - "1.0.0a2.dev0", - "1.0.0a3.dev1", - "1.0.0a2.dev0", - "1.0.0b0", - "1.0.0rc0", - "1.0.0rc1", +excact_cases = [ + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.0", "MINOR", None, 0, None), "1.1.0"), + # with exact_mode=False: "1.0.0b0" + (("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1b0"), + # with exact_mode=False: "1.0.0b1" + (("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1b0"), + # with exact_mode=False: "1.0.0rc0" + (("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1rc0"), + # with exact_mode=False: "1.0.0-rc1" + (("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1rc0"), + # with exact_mode=False: "1.0.0rc1-dev1" + (("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1rc0.dev1"), + # with exact_mode=False: "1.0.0b0" + (("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0b0"), + # with exact_mode=False: "1.0.0b1" + (("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0b0"), + # with exact_mode=False: "1.0.0rc0" + (("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0rc0"), + # with exact_mode=False: "1.0.0rc1" + (("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0rc0"), + # with exact_mode=False: "1.0.0rc1-dev1" + (("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0rc0.dev1"), ] @@ -194,6 +196,27 @@ def test_bump_pep440_version(test_input, expected): ) +@pytest.mark.parametrize("test_input, expected", excact_cases) +def test_bump_pep440_version_force(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + Pep440(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + exact_mode=True, + ) + ) + == expected + ) + + @pytest.mark.parametrize("test_input,expected", local_versions) def test_bump_pep440_version_local(test_input, expected): current_version = test_input[0] diff --git a/tests/test_version_scheme_semver.py b/tests/test_version_scheme_semver.py index a0d6e14b5..f6a4fd134 100644 --- a/tests/test_version_scheme_semver.py +++ b/tests/test_version_scheme_semver.py @@ -83,6 +83,31 @@ (("1.0.0-alpha1", None, "alpha", 0, None), "1.0.0-a2"), ] +excact_cases = [ + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.0", "MINOR", None, 0, None), "1.1.0"), + # with exact_mode=False: "1.0.0-b0" + (("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"), + # with exact_mode=False: "1.0.0-b1" + (("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"), + # with exact_mode=False: "1.0.0-rc0" + (("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"), + # with exact_mode=False: "1.0.0-rc1" + (("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"), + # with exact_mode=False: "1.0.0-rc1-dev1" + (("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"), + # with exact_mode=False: "1.0.0-b0" + (("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"), + # with exact_mode=False: "1.0.0-b1" + (("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"), + # with exact_mode=False: "1.0.0-rc0" + (("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"), + # with exact_mode=False: "1.0.0-rc1" + (("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"), + # with exact_mode=False: "1.0.0-rc1-dev1" + (("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"), +] + @pytest.mark.parametrize( "test_input, expected", @@ -107,6 +132,27 @@ def test_bump_semver_version(test_input, expected): ) +@pytest.mark.parametrize("test_input, expected", excact_cases) +def test_bump_semver_version_force(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + SemVer(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + exact_mode=True, + ) + ) + == expected + ) + + @pytest.mark.parametrize("test_input,expected", local_versions) def test_bump_semver_version_local(test_input, expected): current_version = test_input[0]