Skip to content

Commit

Permalink
feat(schemes): adds support for SemVer 2.0 (dot in pre-releases) (fix #…
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre authored Apr 18, 2024
1 parent aad0602 commit 2ad26e0
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 15 deletions.
61 changes: 57 additions & 4 deletions commitizen/version_schemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ class SemVer(BaseVersion):
"""
Semantic Versioning (SemVer) scheme
See: https://semver.org/
See: https://semver.org/spec/v1.0.0.html
"""

def __str__(self) -> str:
Expand All @@ -324,9 +324,8 @@ def __str__(self) -> str:
parts.append(".".join(str(x) for x in self.release))

# Pre-release
if self.pre:
pre = "".join(str(x) for x in self.pre)
parts.append(f"-{pre}")
if self.prerelease:
parts.append(f"-{self.prerelease}")

# Post-release
if self.post is not None:
Expand All @@ -343,6 +342,60 @@ def __str__(self) -> str:
return "".join(parts)


class SemVer2(SemVer):
"""
Semantic Versioning 2.0 (SemVer2) schema
See: https://semver.org/spec/v2.0.0.html
"""

_STD_PRELEASES = {
"a": "alpha",
"b": "beta",
}

@property
def prerelease(self) -> str | None:
if self.is_prerelease and self.pre:
prerelease_type = self._STD_PRELEASES.get(self.pre[0], self.pre[0])
return f"{prerelease_type}.{self.pre[1]}"
return None

def __str__(self) -> str:
parts = []

# Epoch
if self.epoch != 0:
parts.append(f"{self.epoch}!")

# Release segment
parts.append(".".join(str(x) for x in self.release))

# Pre-release identifiers
# See: https://semver.org/spec/v2.0.0.html#spec-item-9
prerelease_parts = []
if self.prerelease:
prerelease_parts.append(f"{self.prerelease}")

# Post-release
if self.post is not None:
prerelease_parts.append(f"post.{self.post}")

# Development release
if self.dev is not None:
prerelease_parts.append(f"dev.{self.dev}")

if prerelease_parts:
parts.append("-")
parts.append(".".join(prerelease_parts))

# Local version segment
if self.local:
parts.append(f"+{self.local}")

return "".join(parts)


DEFAULT_SCHEME: VersionScheme = Pep440

SCHEMES_ENTRYPOINT = "commitizen.scheme"
Expand Down
20 changes: 10 additions & 10 deletions docs/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ $ cz bump --help
usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog] [--no-verify] [--yes] [--tag-format TAG_FORMAT]
[--bump-message BUMP_MESSAGE] [--prerelease {alpha,beta,rc}] [--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}]
[--check-consistency] [--annotated-tag] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero]
[--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {semver,pep440}] [--version-type {semver,pep440}] [--build-metadata BUILD_METADATA]
[--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA]
[MANUAL_VERSION]

positional arguments:
Expand Down Expand Up @@ -97,9 +97,9 @@ options:
--major-version-zero keep major version at zero, even for breaking changes
--prerelease-offset PRERELEASE_OFFSET
start pre-releases with this offset
--version-scheme {semver,pep440}
--version-scheme {pep440,semver,semver2}
choose version scheme
--version-type {semver,pep440}
--version-type {pep440,semver,semver2}
Deprecated, use --version-scheme
--build-metadata {BUILD_METADATA}
additional metadata in the version string
Expand Down Expand Up @@ -619,14 +619,14 @@ prerelease_offset = 1
Choose version scheme
| schemes | pep440 | semver |
| -------------- | -------------- | --------------- |
| non-prerelease | `0.1.0` | `0.1.0` |
| prerelease | `0.3.1a0` | `0.3.1-a0` |
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` |
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` |
| schemes | pep440 | semver | semver2 |
| -------------- | -------------- | --------------- | --------------------- |
| non-prerelease | `0.1.0` | `0.1.0` | `0.1.0` |
| prerelease | `0.3.1a0` | `0.3.1-a0` | `0.3.1-alpha.0` |
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` | `0.1.1-dev.1` |
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` | `1.0.0-alpha.3.dev.1` |
Options: `semver`, `pep440`
Options: `pep440`, `semver`, `semver2`
Defaults to: `pep440`
Expand Down
3 changes: 2 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ Type: `str`

Default: `pep440`

Select a version scheme from the following options [`pep440`, `semver`]. Useful for non-python projects. [Read more][version-scheme]
Select a version scheme from the following options [`pep440`, `semver`, `semver2`].
Useful for non-python projects. [Read more][version-scheme]

### `tag_format`

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ scm = "commitizen.providers:ScmProvider"
[tool.poetry.plugins."commitizen.scheme"]
pep440 = "commitizen.version_schemes:Pep440"
semver = "commitizen.version_schemes:SemVer"
semver2 = "commitizen.version_schemes:SemVer2"

[tool.coverage]
[tool.coverage.report]
Expand Down
211 changes: 211 additions & 0 deletions tests/test_version_scheme_semver2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import itertools
import random

import pytest

from commitizen.version_schemes import SemVer2, VersionProtocol

simple_flow = [
(("0.1.0", "PATCH", None, 0, None), "0.1.1"),
(("0.1.0", "PATCH", None, 0, 1), "0.1.1-dev.1"),
(("0.1.1", "MINOR", None, 0, None), "0.2.0"),
(("0.2.0", "MINOR", None, 0, None), "0.3.0"),
(("0.2.0", "MINOR", None, 0, 1), "0.3.0-dev.1"),
(("0.3.0", "PATCH", None, 0, None), "0.3.1"),
(("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-alpha.0"),
(("0.3.1-alpha.0", None, "alpha", 0, None), "0.3.1-alpha.1"),
(("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-alpha.1"),
(("0.3.1-alpha.0", None, "alpha", 1, None), "0.3.1-alpha.1"),
(("0.3.1-alpha.0", None, None, 0, None), "0.3.1"),
(("0.3.1", "PATCH", None, 0, None), "0.3.2"),
(("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"),
(("1.0.0-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"),
(("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
(("1.0.0-alpha.1", None, "alpha", 0, 1), "1.0.0-alpha.2.dev.1"),
(("1.0.0-alpha.2.dev.0", None, "alpha", 0, 1), "1.0.0-alpha.3.dev.1"),
(("1.0.0-alpha.2.dev.0", None, "alpha", 0, 0), "1.0.0-alpha.3.dev.0"),
(("1.0.0-alpha.1", None, "beta", 0, None), "1.0.0-beta.0"),
(("1.0.0-beta.0", None, "beta", 0, None), "1.0.0-beta.1"),
(("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"),
(("1.0.0-rc.0", None, "rc", 0, None), "1.0.0-rc.1"),
(("1.0.0-rc.0", None, "rc", 0, 1), "1.0.0-rc.1.dev.1"),
(("1.0.0-rc.0", "PATCH", None, 0, None), "1.0.0"),
(("1.0.0-alpha.3.dev.0", None, "beta", 0, None), "1.0.0-beta.0"),
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
(("1.0.1", "PATCH", None, 0, None), "1.0.2"),
(("1.0.2", "MINOR", None, 0, None), "1.1.0"),
(("1.1.0", "MINOR", None, 0, None), "1.2.0"),
(("1.2.0", "PATCH", None, 0, None), "1.2.1"),
(("1.2.1", "MAJOR", None, 0, None), "2.0.0"),
]

local_versions = [
(("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"),
(("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"),
(("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"),
]

# never bump backwards on pre-releases
linear_prerelease_cases = [
(("0.1.1-beta.1", None, "alpha", 0, None), "0.1.1-beta.2"),
(("0.1.1-rc.0", None, "alpha", 0, None), "0.1.1-rc.1"),
(("0.1.1-rc.0", None, "beta", 0, None), "0.1.1-rc.1"),
]

weird_cases = [
(("1.1", "PATCH", None, 0, None), "1.1.1"),
(("1", "MINOR", None, 0, None), "1.1.0"),
(("1", "MAJOR", None, 0, None), "2.0.0"),
(("1-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"),
(("1-alpha.0", None, "alpha", 1, None), "1.0.0-alpha.1"),
(("1", None, "beta", 0, None), "1.0.0-beta.0"),
(("1", None, "beta", 1, None), "1.0.0-beta.1"),
(("1-beta", None, "beta", 0, None), "1.0.0-beta.1"),
(("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
(("1", None, "rc", 0, None), "1.0.0-rc.0"),
(("1.0.0-rc.1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"),
]

# test driven development
tdd_cases = [
(("0.1.1", "PATCH", None, 0, None), "0.1.2"),
(("0.1.1", "MINOR", None, 0, None), "0.2.0"),
(("2.1.1", "MAJOR", None, 0, None), "3.0.0"),
(("0.9.0", "PATCH", "alpha", 0, None), "0.9.1-alpha.0"),
(("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-alpha.0"),
(("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"),
(("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-alpha.1"),
(("1.0.0-alpha.2", None, "beta", 0, None), "1.0.0-beta.0"),
(("1.0.0-alpha.2", None, "beta", 1, None), "1.0.0-beta.1"),
(("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"),
(("1.0.0-rc.1", None, "rc", 0, None), "1.0.0-rc.2"),
(("1.0.0-alpha.0", None, "rc", 0, None), "1.0.0-rc.0"),
(("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
]


@pytest.mark.parametrize(
"test_input, expected",
itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases),
)
def test_bump_semver_version(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(
SemVer2(current_version).bump(
increment=increment,
prerelease=prerelease,
prerelease_offset=prerelease_offset,
devrelease=devrelease,
)
)
== expected
)


@pytest.mark.parametrize("test_input,expected", local_versions)
def test_bump_semver_version_local(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]
is_local_version = True
assert (
str(
SemVer2(current_version).bump(
increment=increment,
prerelease=prerelease,
prerelease_offset=prerelease_offset,
devrelease=devrelease,
is_local_version=is_local_version,
)
)
== expected
)


def test_semver_scheme_property():
version = SemVer2("0.0.1")
assert version.scheme is SemVer2


def test_semver_implement_version_protocol():
assert isinstance(SemVer2("0.0.1"), VersionProtocol)


def test_semver_sortable():
test_input = [x[0][0] for x in simple_flow]
test_input.extend([x[1] for x in simple_flow])
# randomize
random_input = [SemVer2(x) for x in random.sample(test_input, len(test_input))]
assert len(random_input) == len(test_input)
sorted_result = [str(x) for x in sorted(random_input)]
assert sorted_result == [
"0.1.0",
"0.1.0",
"0.1.1-dev.1",
"0.1.1",
"0.1.1",
"0.2.0",
"0.2.0",
"0.2.0",
"0.3.0-dev.1",
"0.3.0",
"0.3.0",
"0.3.0",
"0.3.0",
"0.3.1-alpha.0",
"0.3.1-alpha.0",
"0.3.1-alpha.0",
"0.3.1-alpha.0",
"0.3.1-alpha.1",
"0.3.1-alpha.1",
"0.3.1-alpha.1",
"0.3.1",
"0.3.1",
"0.3.1",
"0.3.2",
"0.4.2",
"1.0.0-alpha.0",
"1.0.0-alpha.0",
"1.0.0-alpha.1",
"1.0.0-alpha.1",
"1.0.0-alpha.1",
"1.0.0-alpha.1",
"1.0.0-alpha.2.dev.0",
"1.0.0-alpha.2.dev.0",
"1.0.0-alpha.2.dev.1",
"1.0.0-alpha.2",
"1.0.0-alpha.3.dev.0",
"1.0.0-alpha.3.dev.0",
"1.0.0-alpha.3.dev.1",
"1.0.0-beta.0",
"1.0.0-beta.0",
"1.0.0-beta.0",
"1.0.0-beta.1",
"1.0.0-beta.1",
"1.0.0-rc.0",
"1.0.0-rc.0",
"1.0.0-rc.0",
"1.0.0-rc.0",
"1.0.0-rc.1.dev.1",
"1.0.0-rc.1",
"1.0.0",
"1.0.0",
"1.0.1",
"1.0.1",
"1.0.2",
"1.0.2",
"1.1.0",
"1.1.0",
"1.2.0",
"1.2.0",
"1.2.1",
"1.2.1",
"2.0.0",
]

0 comments on commit 2ad26e0

Please sign in to comment.