Skip to content

Commit

Permalink
feat(commands): add bump --exact
Browse files Browse the repository at this point in the history
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
  • Loading branch information
chadrik authored and woile committed Feb 26, 2024
1 parent ddab546 commit 1059556
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 24 deletions.
10 changes: 10 additions & 0 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,16 @@ def __call__(
"choices": ["MAJOR", "MINOR", "PATCH"],
"type": str.upper,
},
{
"name": ["--exact-increment"],
"action": "store_true",
"help": (
"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."
),
},
{
"name": ["--check-consistency", "-cc"],
"help": (
Expand Down
4 changes: 4 additions & 0 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def __init__(self, config: BaseConfig, arguments: dict):
"tag_format",
"prerelease",
"increment",
"exact_increment",
"bump_message",
"gpg_sign",
"annotated_tag",
Expand Down Expand Up @@ -158,6 +159,7 @@ def __call__(self) -> None: # noqa: C901
is_local_version: bool = self.arguments["local_version"]
manual_version = self.arguments["manual_version"]
build_metadata = self.arguments["build_metadata"]
exact_increment: bool = self.arguments["exact_increment"]

if manual_version:
if increment:
Expand Down Expand Up @@ -250,6 +252,7 @@ def __call__(self) -> None: # noqa: C901
devrelease=devrelease,
is_local_version=is_local_version,
build_metadata=build_metadata,
exact_increment=exact_increment,
)

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

# FIXME: check if any changes have been staged
c = git.commit(message, args=self._get_commit_args())
if self.retry and c.return_code != 0 and self.changelog:
# Maybe pre-commit reformatted some files? Retry once
Expand Down
13 changes: 10 additions & 3 deletions commitizen/version_schemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,17 @@ def bump(
devrelease: int | None = None,
is_local_version: bool = False,
build_metadata: str | None = None,
force_bump: bool = False,
exact_increment: 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_increment: Treat the increment and prerelease arguments explicitly. Disables logic
that attempts to deduce the correct increment when a prelease suffix is present.
"""


Expand Down Expand Up @@ -239,7 +246,7 @@ def bump(
devrelease: int | None = None,
is_local_version: bool = False,
build_metadata: str | None = None,
force_bump: bool = False,
exact_increment: bool = False,
) -> Self:
"""Based on the given increment a proper semver will be generated.
Expand All @@ -259,7 +266,7 @@ def bump(
else:
if not self.is_prerelease:
base = self.increment_base(increment)
elif force_bump:
elif exact_increment:
base = self.increment_base(increment)
else:
base = f"{self.major}.{self.minor}.{self.micro}"
Expand Down
21 changes: 21 additions & 0 deletions docs/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ options:
specify non-negative integer for dev. release
--increment {MAJOR,MINOR,PATCH}
manually specify the desired increment
--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.
--check-consistency, -cc
check consistency among versions defined in commitizen configuration and version_files
--annotated-tag, -at create annotated tag instead of lightweight one
Expand Down Expand Up @@ -142,6 +143,26 @@ by their precedence and showcase how a release might flow through a development
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.
This behavior can be overridden by passing `--exact-increment` (see below).
### `--exact-increment`
The `--exact-increment` flag bypasses the logic that creates a best guess for the next version based on the
principle of maintaining linearity when a pre-release is present (see above). Instead, `bump` will apply the
exact changes that have been specified with `--increment` or determined from the commit log. For example,
`--prerelease beta` will always result in a `b` tag, and `--increment PATCH` will always increase the patch component.
Below are some examples that illustrate the difference in behavior:
| Increment | Pre-release | Start Version | Without `--exact-increment` | With `--exact-increment` |
|-----------|-------------|---------------|-----------------------------|--------------------------|
| `MAJOR` | | `2.0.0b0` | `2.0.0` | `3.0.0` |
| `MINOR` | | `2.0.0b0` | `2.0.0` | `2.1.0` |
| `PATCH` | | `2.0.0b0` | `2.0.0` | `2.0.1` |
| `MAJOR` | `alpha` | `2.0.0b0` | `3.0.0a0` | `3.0.0a0` |
| `MINOR` | `alpha` | `2.0.0b0` | `2.0.0b1` | `2.1.0a0` |
| `PATCH` | `alpha` | `2.0.0b0` | `2.0.0b1` | `2.0.1a0` |
### `--check-consistency`
Expand Down
49 changes: 49 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-increment"]
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-increment allows the minor version to bump, and restart the prerelease
create_file_and_commit("feat: location")

testargs = ["cz", "bump", "--prerelease", "alpha", "--yes", "--exact-increment"]
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-increment allows the major version to bump, and restart the prerelease
testargs = [
"cz",
"bump",
"--prerelease",
"alpha",
"--yes",
"--increment=MAJOR",
"--exact-increment",
]
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"""
Expand Down
82 changes: 61 additions & 21 deletions tests/test_version_scheme_pep440.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
(("2.0.0a4", "PATCH", "alpha", 0, None), "2.0.0a5"),
(("2.0.0a5", "MAJOR", "alpha", 0, None), "2.0.0a6"),
#
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.0.0b1"),
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.0b1"),
#
(("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"),
Expand All @@ -141,27 +144,43 @@
(("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_increment=False: "1.0.0b0"
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1b0"),
# with exact_increment=False: "1.0.0b1"
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1b0"),
# with exact_increment=False: "1.0.0rc0"
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1rc0"),
# with exact_increment=False: "1.0.0-rc1"
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1rc0"),
# with exact_increment=False: "1.0.0rc1-dev1"
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1rc0.dev1"),
# with exact_increment=False: "1.0.0b0"
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0b0"),
# with exact_increment=False: "1.0.0b1"
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0b0"),
# with exact_increment=False: "1.0.0b1"
(("1.0.0b0", "MINOR", "alpha", 0, None), "1.1.0a0"),
# with exact_increment=False: "1.0.0rc0"
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0rc0"),
# with exact_increment=False: "1.0.0rc1"
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0rc0"),
# with exact_increment=False: "1.0.0rc1-dev1"
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0rc0.dev1"),
# with exact_increment=False: "2.0.0"
(("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
# with exact_increment=False: "2.0.0"
(("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
# with exact_increment=False: "2.0.0"
(("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
# same with exact_increment=False
(("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0a0"),
# with exact_increment=False: "2.0.0b1"
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0a0"),
# with exact_increment=False: "2.0.0b1"
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1a0"),
]


Expand Down Expand Up @@ -194,6 +213,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_increment=True,
)
)
== expected
)


@pytest.mark.parametrize("test_input,expected", local_versions)
def test_bump_pep440_version_local(test_input, expected):
current_version = test_input[0]
Expand Down
58 changes: 58 additions & 0 deletions tests/test_version_scheme_semver.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,43 @@
(("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_increment=False: "1.0.0-b0"
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"),
# with exact_increment=False: "1.0.0-b1"
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"),
# with exact_increment=False: "1.0.0-rc0"
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"),
# with exact_increment=False: "1.0.0-rc1"
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"),
# with exact_increment=False: "1.0.0-rc1-dev1"
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"),
# with exact_increment=False: "1.0.0-b0"
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"),
# with exact_increment=False: "1.0.0-b1"
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"),
# with exact_increment=False: "1.0.0-rc0"
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"),
# with exact_increment=False: "1.0.0-rc1"
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"),
# with exact_increment=False: "1.0.0-rc1-dev1"
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"),
# with exact_increment=False: "2.0.0"
(("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
# with exact_increment=False: "2.0.0"
(("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
# with exact_increment=False: "2.0.0"
(("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
# same with exact_increment=False
(("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0-a0"),
# with exact_increment=False: "2.0.0b1"
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0-a0"),
# with exact_increment=False: "2.0.0b1"
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1-a0"),
]


@pytest.mark.parametrize(
"test_input, expected",
Expand All @@ -107,6 +144,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_increment=True,
)
)
== expected
)


@pytest.mark.parametrize("test_input,expected", local_versions)
def test_bump_semver_version_local(test_input, expected):
current_version = test_input[0]
Expand Down

0 comments on commit 1059556

Please sign in to comment.