Skip to content

Commit c29db83

Browse files
committed
feat(schemes): adds support for SemVer 2.0 (dot in pre-releases) (fix #1025)
1 parent 3015a76 commit c29db83

File tree

5 files changed

+282
-17
lines changed

5 files changed

+282
-17
lines changed

Diff for: commitizen/version_schemes.py

+58-4
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ class SemVer(BaseVersion):
310310
"""
311311
Semantic Versioning (SemVer) scheme
312312
313-
See: https://semver.org/
313+
See: https://semver.org/spec/v1.0.0.html
314314
"""
315315

316316
def __str__(self) -> str:
@@ -324,9 +324,9 @@ def __str__(self) -> str:
324324
parts.append(".".join(str(x) for x in self.release))
325325

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

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

345345

346+
class SemVer2(SemVer):
347+
"""
348+
Semantic Versioning 2.0 (SemVer2) schema
349+
350+
See: https://semver.org/spec/v2.0.0.html
351+
"""
352+
353+
_STD_PRELEASES = {
354+
"a": "alpha",
355+
"b": "beta",
356+
}
357+
358+
@property
359+
def prerelease(self) -> str | None:
360+
if self.is_prerelease and self.pre:
361+
prerelease_type = self._STD_PRELEASES.get(self.pre[0], self.pre[0])
362+
return f"{prerelease_type}.{self.pre[1]}"
363+
return None
364+
365+
def __str__(self) -> str:
366+
parts = []
367+
368+
# Epoch
369+
if self.epoch != 0:
370+
parts.append(f"{self.epoch}!")
371+
372+
# Release segment
373+
parts.append(".".join(str(x) for x in self.release))
374+
375+
# Pre-release identifiers
376+
# See: https://semver.org/spec/v2.0.0.html#spec-item-9
377+
prerelease_parts = []
378+
if self.prerelease:
379+
prerelease_parts.append(f"{self.prerelease}")
380+
381+
# Post-release
382+
if self.post is not None:
383+
prerelease_parts.append(f"post.{self.post}")
384+
385+
# Development release
386+
if self.dev is not None:
387+
prerelease_parts.append(f"dev.{self.dev}")
388+
389+
if prerelease_parts:
390+
parts.append("-")
391+
parts.append(".".join(prerelease_parts))
392+
393+
# Local version segment
394+
if self.local:
395+
parts.append(f"+{self.local}")
396+
397+
return "".join(parts)
398+
399+
346400
DEFAULT_SCHEME: VersionScheme = Pep440
347401

348402
SCHEMES_ENTRYPOINT = "commitizen.scheme"

Diff for: docs/bump.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ $ cz bump --help
5555
usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog] [--no-verify] [--yes] [--tag-format TAG_FORMAT]
5656
[--bump-message BUMP_MESSAGE] [--prerelease {alpha,beta,rc}] [--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}]
5757
[--check-consistency] [--annotated-tag] [--gpg-sign] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero]
58-
[--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {semver,pep440}] [--version-type {semver,pep440}] [--build-metadata BUILD_METADATA]
58+
[--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] [--build-metadata BUILD_METADATA]
5959
[MANUAL_VERSION]
6060

6161
positional arguments:
@@ -97,9 +97,9 @@ options:
9797
--major-version-zero keep major version at zero, even for breaking changes
9898
--prerelease-offset PRERELEASE_OFFSET
9999
start pre-releases with this offset
100-
--version-scheme {semver,pep440}
100+
--version-scheme {pep440,semver,semver2}
101101
choose version scheme
102-
--version-type {semver,pep440}
102+
--version-type {pep440,semver,semver2}
103103
Deprecated, use --version-scheme
104104
--build-metadata {BUILD_METADATA}
105105
additional metadata in the version string
@@ -615,14 +615,14 @@ prerelease_offset = 1
615615
616616
Choose version scheme
617617
618-
| schemes | pep440 | semver |
619-
| -------------- | -------------- | --------------- |
620-
| non-prerelease | `0.1.0` | `0.1.0` |
621-
| prerelease | `0.3.1a0` | `0.3.1-a0` |
622-
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` |
623-
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` |
618+
| schemes | pep440 | semver | semver2 |
619+
| -------------- | -------------- | --------------- | --------------------- |
620+
| non-prerelease | `0.1.0` | `0.1.0` | `0.1.0` |
621+
| prerelease | `0.3.1a0` | `0.3.1-a0` | `0.3.1-alpha.0` |
622+
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` | `0.1.1-dev.1` |
623+
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` | `1.0.0-alpha.3.dev.1` |
624624
625-
Options: `semver`, `pep440`
625+
Options: `pep440`, `semver`, `semver2`
626626
627627
Defaults to: `pep440`
628628

Diff for: docs/config.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ Type: `str`
4040

4141
Default: `pep440`
4242

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

4546
### `tag_format`
4647

Diff for: pyproject.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ deprecated = "^1.2.13"
7474
types-deprecated = "^1.2.9.2"
7575
types-python-dateutil = "^2.8.19.13"
7676
rich = "^13.7.1"
77-
78-
7977
[tool.poetry.scripts]
8078
cz = "commitizen.cli:main"
8179
git-cz = "commitizen.cli:main"
@@ -103,6 +101,7 @@ scm = "commitizen.providers:ScmProvider"
103101
[tool.poetry.plugins."commitizen.scheme"]
104102
pep440 = "commitizen.version_schemes:Pep440"
105103
semver = "commitizen.version_schemes:SemVer"
104+
semver2 = "commitizen.version_schemes:SemVer2"
106105

107106
[tool.coverage]
108107
[tool.coverage.report]

Diff for: tests/test_version_scheme_semver2.py

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import itertools
2+
import random
3+
4+
import pytest
5+
6+
from commitizen.version_schemes import SemVer2, VersionProtocol
7+
8+
simple_flow = [
9+
(("0.1.0", "PATCH", None, 0, None), "0.1.1"),
10+
(("0.1.0", "PATCH", None, 0, 1), "0.1.1-dev.1"),
11+
(("0.1.1", "MINOR", None, 0, None), "0.2.0"),
12+
(("0.2.0", "MINOR", None, 0, None), "0.3.0"),
13+
(("0.2.0", "MINOR", None, 0, 1), "0.3.0-dev.1"),
14+
(("0.3.0", "PATCH", None, 0, None), "0.3.1"),
15+
(("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-alpha.0"),
16+
(("0.3.1-alpha.0", None, "alpha", 0, None), "0.3.1-alpha.1"),
17+
(("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-alpha.1"),
18+
(("0.3.1-alpha.0", None, "alpha", 1, None), "0.3.1-alpha.1"),
19+
(("0.3.1-alpha.0", None, None, 0, None), "0.3.1"),
20+
(("0.3.1", "PATCH", None, 0, None), "0.3.2"),
21+
(("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"),
22+
(("1.0.0-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"),
23+
(("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
24+
(("1.0.0-alpha.1", None, "alpha", 0, 1), "1.0.0-alpha.2.dev.1"),
25+
(("1.0.0-alpha.2.dev.0", None, "alpha", 0, 1), "1.0.0-alpha.3.dev.1"),
26+
(("1.0.0-alpha.2.dev.0", None, "alpha", 0, 0), "1.0.0-alpha.3.dev.0"),
27+
(("1.0.0-alpha.1", None, "beta", 0, None), "1.0.0-beta.0"),
28+
(("1.0.0-beta.0", None, "beta", 0, None), "1.0.0-beta.1"),
29+
(("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"),
30+
(("1.0.0-rc.0", None, "rc", 0, None), "1.0.0-rc.1"),
31+
(("1.0.0-rc.0", None, "rc", 0, 1), "1.0.0-rc.1.dev.1"),
32+
(("1.0.0-rc.0", "PATCH", None, 0, None), "1.0.0"),
33+
(("1.0.0-alpha.3.dev.0", None, "beta", 0, None), "1.0.0-beta.0"),
34+
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
35+
(("1.0.1", "PATCH", None, 0, None), "1.0.2"),
36+
(("1.0.2", "MINOR", None, 0, None), "1.1.0"),
37+
(("1.1.0", "MINOR", None, 0, None), "1.2.0"),
38+
(("1.2.0", "PATCH", None, 0, None), "1.2.1"),
39+
(("1.2.1", "MAJOR", None, 0, None), "2.0.0"),
40+
]
41+
42+
local_versions = [
43+
(("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"),
44+
(("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"),
45+
(("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"),
46+
]
47+
48+
# never bump backwards on pre-releases
49+
linear_prerelease_cases = [
50+
(("0.1.1-beta.1", None, "alpha", 0, None), "0.1.1-beta.2"),
51+
(("0.1.1-rc.0", None, "alpha", 0, None), "0.1.1-rc.1"),
52+
(("0.1.1-rc.0", None, "beta", 0, None), "0.1.1-rc.1"),
53+
]
54+
55+
weird_cases = [
56+
(("1.1", "PATCH", None, 0, None), "1.1.1"),
57+
(("1", "MINOR", None, 0, None), "1.1.0"),
58+
(("1", "MAJOR", None, 0, None), "2.0.0"),
59+
(("1-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"),
60+
(("1-alpha.0", None, "alpha", 1, None), "1.0.0-alpha.1"),
61+
(("1", None, "beta", 0, None), "1.0.0-beta.0"),
62+
(("1", None, "beta", 1, None), "1.0.0-beta.1"),
63+
(("1-beta", None, "beta", 0, None), "1.0.0-beta.1"),
64+
(("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
65+
(("1", None, "rc", 0, None), "1.0.0-rc.0"),
66+
(("1.0.0-rc.1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"),
67+
]
68+
69+
# test driven development
70+
tdd_cases = [
71+
(("0.1.1", "PATCH", None, 0, None), "0.1.2"),
72+
(("0.1.1", "MINOR", None, 0, None), "0.2.0"),
73+
(("2.1.1", "MAJOR", None, 0, None), "3.0.0"),
74+
(("0.9.0", "PATCH", "alpha", 0, None), "0.9.1-alpha.0"),
75+
(("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-alpha.0"),
76+
(("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"),
77+
(("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-alpha.1"),
78+
(("1.0.0-alpha.2", None, "beta", 0, None), "1.0.0-beta.0"),
79+
(("1.0.0-alpha.2", None, "beta", 1, None), "1.0.0-beta.1"),
80+
(("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"),
81+
(("1.0.0-rc.1", None, "rc", 0, None), "1.0.0-rc.2"),
82+
(("1.0.0-alpha.0", None, "rc", 0, None), "1.0.0-rc.0"),
83+
(("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
84+
]
85+
86+
87+
@pytest.mark.parametrize(
88+
"test_input, expected",
89+
itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases),
90+
)
91+
def test_bump_semver_version(test_input, expected):
92+
current_version = test_input[0]
93+
increment = test_input[1]
94+
prerelease = test_input[2]
95+
prerelease_offset = test_input[3]
96+
devrelease = test_input[4]
97+
assert (
98+
str(
99+
SemVer2(current_version).bump(
100+
increment=increment,
101+
prerelease=prerelease,
102+
prerelease_offset=prerelease_offset,
103+
devrelease=devrelease,
104+
)
105+
)
106+
== expected
107+
)
108+
109+
110+
@pytest.mark.parametrize("test_input,expected", local_versions)
111+
def test_bump_semver_version_local(test_input, expected):
112+
current_version = test_input[0]
113+
increment = test_input[1]
114+
prerelease = test_input[2]
115+
prerelease_offset = test_input[3]
116+
devrelease = test_input[4]
117+
is_local_version = True
118+
assert (
119+
str(
120+
SemVer2(current_version).bump(
121+
increment=increment,
122+
prerelease=prerelease,
123+
prerelease_offset=prerelease_offset,
124+
devrelease=devrelease,
125+
is_local_version=is_local_version,
126+
)
127+
)
128+
== expected
129+
)
130+
131+
132+
def test_semver_scheme_property():
133+
version = SemVer2("0.0.1")
134+
assert version.scheme is SemVer2
135+
136+
137+
def test_semver_implement_version_protocol():
138+
assert isinstance(SemVer2("0.0.1"), VersionProtocol)
139+
140+
141+
def test_semver_sortable():
142+
test_input = [x[0][0] for x in simple_flow]
143+
test_input.extend([x[1] for x in simple_flow])
144+
# randomize
145+
random_input = [SemVer2(x) for x in random.sample(test_input, len(test_input))]
146+
assert len(random_input) == len(test_input)
147+
sorted_result = [str(x) for x in sorted(random_input)]
148+
assert sorted_result == [
149+
"0.1.0",
150+
"0.1.0",
151+
"0.1.1-dev.1",
152+
"0.1.1",
153+
"0.1.1",
154+
"0.2.0",
155+
"0.2.0",
156+
"0.2.0",
157+
"0.3.0-dev.1",
158+
"0.3.0",
159+
"0.3.0",
160+
"0.3.0",
161+
"0.3.0",
162+
"0.3.1-alpha.0",
163+
"0.3.1-alpha.0",
164+
"0.3.1-alpha.0",
165+
"0.3.1-alpha.0",
166+
"0.3.1-alpha.1",
167+
"0.3.1-alpha.1",
168+
"0.3.1-alpha.1",
169+
"0.3.1",
170+
"0.3.1",
171+
"0.3.1",
172+
"0.3.2",
173+
"0.4.2",
174+
"1.0.0-alpha.0",
175+
"1.0.0-alpha.0",
176+
"1.0.0-alpha.1",
177+
"1.0.0-alpha.1",
178+
"1.0.0-alpha.1",
179+
"1.0.0-alpha.1",
180+
"1.0.0-alpha.2.dev.0",
181+
"1.0.0-alpha.2.dev.0",
182+
"1.0.0-alpha.2.dev.1",
183+
"1.0.0-alpha.2",
184+
"1.0.0-alpha.3.dev.0",
185+
"1.0.0-alpha.3.dev.0",
186+
"1.0.0-alpha.3.dev.1",
187+
"1.0.0-beta.0",
188+
"1.0.0-beta.0",
189+
"1.0.0-beta.0",
190+
"1.0.0-beta.1",
191+
"1.0.0-beta.1",
192+
"1.0.0-rc.0",
193+
"1.0.0-rc.0",
194+
"1.0.0-rc.0",
195+
"1.0.0-rc.0",
196+
"1.0.0-rc.1.dev.1",
197+
"1.0.0-rc.1",
198+
"1.0.0",
199+
"1.0.0",
200+
"1.0.1",
201+
"1.0.1",
202+
"1.0.2",
203+
"1.0.2",
204+
"1.1.0",
205+
"1.1.0",
206+
"1.2.0",
207+
"1.2.0",
208+
"1.2.1",
209+
"1.2.1",
210+
"2.0.0",
211+
]

0 commit comments

Comments
 (0)