From 2ad26e08e471ddb4943c6d84d1fc5c2e7327a8b3 Mon Sep 17 00:00:00 2001 From: Axel H Date: Thu, 18 Apr 2024 12:14:09 +0200 Subject: [PATCH] feat(schemes): adds support for SemVer 2.0 (dot in pre-releases) (fix #1025) (#1072) --- commitizen/version_schemes.py | 61 +++++++- docs/bump.md | 20 +-- docs/config.md | 3 +- pyproject.toml | 1 + tests/test_version_scheme_semver2.py | 211 +++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 15 deletions(-) create mode 100644 tests/test_version_scheme_semver2.py diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index ec04fde1e..596aa94b2 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -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: @@ -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: @@ -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" diff --git a/docs/bump.md b/docs/bump.md index 05843eeab..6dad38219 100644 --- a/docs/bump.md +++ b/docs/bump.md @@ -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: @@ -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 @@ -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` diff --git a/docs/config.md b/docs/config.md index d6ab12abc..7ef8644fa 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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` diff --git a/pyproject.toml b/pyproject.toml index 66a73a310..96f630e86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/tests/test_version_scheme_semver2.py b/tests/test_version_scheme_semver2.py new file mode 100644 index 000000000..d18a058a7 --- /dev/null +++ b/tests/test_version_scheme_semver2.py @@ -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", + ]