Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: properly bump versions between prereleases #799

Merged
merged 5 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -226,11 +230,6 @@
"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.
if prerelease and current_version.is_prerelease:
increment = None

new_version = current_version.bump(
increment,
prerelease=prerelease,
Expand Down Expand Up @@ -398,3 +397,33 @@
if self.no_verify:
commit_args.append("--no-verify")
return " ".join(commit_args)

def find_previous_final_version(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good API addition 👍🏼

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably rename the function to find_base_version() because “base version” seems to be the term used in other places.

self, current_version: VersionProtocol
) -> VersionProtocol | None:
tag_format: str = self.bump_settings["tag_format"]
current = bump.normalize_tag(

Check warning on line 405 in commitizen/commands/bump.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/bump.py#L404-L405

Added lines #L404 - L405 were not covered by tests
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

Check warning on line 419 in commitizen/commands/bump.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/bump.py#L411-L419

Added lines #L411 - L419 were not covered by tests

if not final_versions:
return None

Check warning on line 422 in commitizen/commands/bump.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/bump.py#L421-L422

Added lines #L421 - L422 were not covered by tests

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]

Check warning on line 429 in commitizen/commands/bump.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/bump.py#L424-L429

Added lines #L424 - L429 were not covered by tests
60 changes: 44 additions & 16 deletions commitizen/version_schemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
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
Expand Down Expand Up @@ -146,6 +147,12 @@
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]
Expand All @@ -171,20 +178,15 @@
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:
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]}"

Expand All @@ -195,6 +197,7 @@
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.

Expand All @@ -212,9 +215,34 @@
local_version = self.scheme(self.local).bump(increment)
return self.scheme(f"{self.public}+{local_version}") # type: ignore
else:
base = self.increment_base(increment)
if not self.is_prerelease:
base = self.increment_base(increment)
elif force_bump:
base = self.increment_base(increment)

Check warning on line 221 in commitizen/version_schemes.py

View check run for this annotation

Codecov / codecov/patch

commitizen/version_schemes.py#L221

Added line #L221 was not covered by tests
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)
pre_version = self.generate_prerelease(prerelease, offset=prerelease_offset)
release = list(self.release)
if len(release) < 3:
release += [0] * (3 - len(release))
current_base = ".".join(str(part) for part in release)
if base == current_base:
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

Expand Down
28 changes: 28 additions & 0 deletions docs/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 89 additions & 2 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,68 @@ 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()

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()
Expand All @@ -227,6 +278,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"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading
Loading