Skip to content

Commit 4ae8913

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

File tree

5 files changed

+297
-14
lines changed

5 files changed

+297
-14
lines changed

commitizen/version_schemes.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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,20 @@ 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/
351+
"""
352+
353+
@property
354+
def prerelease(self) -> str | None:
355+
if self.is_prerelease and self.pre:
356+
return f"{self.pre[0]}.{self.pre[1]}"
357+
return None
358+
359+
346360
DEFAULT_SCHEME: VersionScheme = Pep440
347361

348362
SCHEMES_ENTRYPOINT = "commitizen.scheme"

docs/bump.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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` |
624-
625-
Options: `semver`, `pep440`
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-a.0` |
622+
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` | `0.1.1-dev1` |
623+
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` | `1.0.0-a.3-dev1` |
624+
625+
Options: `pep440`, `semver`, `semver2`
626626
627627
Defaults to: `pep440`
628628

docs/config.md

Lines changed: 2 additions & 1 deletion
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

pyproject.toml

Lines changed: 1 addition & 2 deletions
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]

tests/test_version_scheme_semver2.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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-dev1"),
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-dev1"),
14+
(("0.3.0", "PATCH", None, 0, None), "0.3.1"),
15+
(("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-a.0"),
16+
(("0.3.1a0", None, "alpha", 0, None), "0.3.1-a.1"),
17+
(("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-a.1"),
18+
(("0.3.1a0", None, "alpha", 1, None), "0.3.1-a.1"),
19+
(("0.3.1a0", 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-a.0"),
22+
(("1.0.0a0", None, "alpha", 0, None), "1.0.0-a.1"),
23+
(("1.0.0a1", None, "alpha", 0, None), "1.0.0-a.2"),
24+
(("1.0.0a1", None, "alpha", 0, 1), "1.0.0-a.2-dev1"),
25+
(("1.0.0a2.dev0", None, "alpha", 0, 1), "1.0.0-a.3-dev1"),
26+
(("1.0.0a2.dev0", None, "alpha", 0, 0), "1.0.0-a.3-dev0"),
27+
(("1.0.0a1", None, "beta", 0, None), "1.0.0-b.0"),
28+
(("1.0.0b0", None, "beta", 0, None), "1.0.0-b.1"),
29+
(("1.0.0b1", None, "rc", 0, None), "1.0.0-rc.0"),
30+
(("1.0.0rc0", None, "rc", 0, None), "1.0.0-rc.1"),
31+
(("1.0.0rc0", None, "rc", 0, 1), "1.0.0-rc.1-dev1"),
32+
(("1.0.0rc0", "PATCH", None, 0, None), "1.0.0"),
33+
(("1.0.0a3.dev0", None, "beta", 0, None), "1.0.0-b.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.1b1", None, "alpha", 0, None), "0.1.1-b.2"),
51+
(("0.1.1rc0", None, "alpha", 0, None), "0.1.1-rc.1"),
52+
(("0.1.1rc0", 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+
(("1a0", None, "alpha", 0, None), "1.0.0-a.1"),
60+
(("1a0", None, "alpha", 1, None), "1.0.0-a.1"),
61+
(("1", None, "beta", 0, None), "1.0.0-b.0"),
62+
(("1", None, "beta", 1, None), "1.0.0-b.1"),
63+
(("1beta", None, "beta", 0, None), "1.0.0-b.1"),
64+
(("1.0.0alpha1", None, "alpha", 0, None), "1.0.0-a.2"),
65+
(("1", None, "rc", 0, None), "1.0.0-rc.0"),
66+
(("1.0.0rc1+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-a.0"),
75+
(("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-a.0"),
76+
(("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-a.0"),
77+
(("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-a.1"),
78+
(("1.0.0a2", None, "beta", 0, None), "1.0.0-b.0"),
79+
(("1.0.0a2", None, "beta", 1, None), "1.0.0-b.1"),
80+
(("1.0.0beta1", None, "rc", 0, None), "1.0.0-rc.0"),
81+
(("1.0.0rc1", None, "rc", 0, None), "1.0.0-rc.2"),
82+
(("1.0.0-a0", None, "rc", 0, None), "1.0.0-rc.0"),
83+
(("1.0.0-alpha1", None, "alpha", 0, None), "1.0.0-a.2"),
84+
]
85+
86+
excact_cases = [
87+
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
88+
(("1.0.0", "MINOR", None, 0, None), "1.1.0"),
89+
# with exact_increment=False: "1.0.0-b0"
90+
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b.0"),
91+
# with exact_increment=False: "1.0.0-b1"
92+
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b.0"),
93+
# with exact_increment=False: "1.0.0-rc0"
94+
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc.0"),
95+
# with exact_increment=False: "1.0.0-rc1"
96+
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc.0"),
97+
# with exact_increment=False: "1.0.0-rc1-dev1"
98+
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc.0-dev1"),
99+
# with exact_increment=False: "1.0.0-b0"
100+
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b.0"),
101+
# with exact_increment=False: "1.0.0-b1"
102+
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b.0"),
103+
# with exact_increment=False: "1.0.0-rc0"
104+
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc.0"),
105+
# with exact_increment=False: "1.0.0-rc1"
106+
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc.0"),
107+
# with exact_increment=False: "1.0.0-rc1-dev1"
108+
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc.0-dev1"),
109+
# with exact_increment=False: "2.0.0"
110+
(("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
111+
# with exact_increment=False: "2.0.0"
112+
(("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
113+
# with exact_increment=False: "2.0.0"
114+
(("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
115+
# same with exact_increment=False
116+
(("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0-a.0"),
117+
# with exact_increment=False: "2.0.0b1"
118+
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0-a.0"),
119+
# with exact_increment=False: "2.0.0b1"
120+
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1-a.0"),
121+
]
122+
123+
124+
@pytest.mark.parametrize(
125+
"test_input, expected",
126+
itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases),
127+
)
128+
def test_bump_semver_version(test_input, expected):
129+
current_version = test_input[0]
130+
increment = test_input[1]
131+
prerelease = test_input[2]
132+
prerelease_offset = test_input[3]
133+
devrelease = test_input[4]
134+
assert (
135+
str(
136+
SemVer2(current_version).bump(
137+
increment=increment,
138+
prerelease=prerelease,
139+
prerelease_offset=prerelease_offset,
140+
devrelease=devrelease,
141+
)
142+
)
143+
== expected
144+
)
145+
146+
147+
@pytest.mark.parametrize("test_input, expected", excact_cases)
148+
def test_bump_semver_version_force(test_input, expected):
149+
current_version = test_input[0]
150+
increment = test_input[1]
151+
prerelease = test_input[2]
152+
prerelease_offset = test_input[3]
153+
devrelease = test_input[4]
154+
assert (
155+
str(
156+
SemVer2(current_version).bump(
157+
increment=increment,
158+
prerelease=prerelease,
159+
prerelease_offset=prerelease_offset,
160+
devrelease=devrelease,
161+
exact_increment=True,
162+
)
163+
)
164+
== expected
165+
)
166+
167+
168+
@pytest.mark.parametrize("test_input,expected", local_versions)
169+
def test_bump_semver_version_local(test_input, expected):
170+
current_version = test_input[0]
171+
increment = test_input[1]
172+
prerelease = test_input[2]
173+
prerelease_offset = test_input[3]
174+
devrelease = test_input[4]
175+
is_local_version = True
176+
assert (
177+
str(
178+
SemVer2(current_version).bump(
179+
increment=increment,
180+
prerelease=prerelease,
181+
prerelease_offset=prerelease_offset,
182+
devrelease=devrelease,
183+
is_local_version=is_local_version,
184+
)
185+
)
186+
== expected
187+
)
188+
189+
190+
def test_semver_scheme_property():
191+
version = SemVer2("0.0.1")
192+
assert version.scheme is SemVer2
193+
194+
195+
def test_semver_implement_version_protocol():
196+
assert isinstance(SemVer2("0.0.1"), VersionProtocol)
197+
198+
199+
def test_semver_sortable():
200+
test_input = [x[0][0] for x in simple_flow]
201+
test_input.extend([x[1] for x in simple_flow])
202+
# randomize
203+
random_input = [SemVer2(x) for x in random.sample(test_input, len(test_input))]
204+
assert len(random_input) == len(test_input)
205+
sorted_result = [str(x) for x in sorted(random_input)]
206+
assert sorted_result == [
207+
"0.1.0",
208+
"0.1.0",
209+
"0.1.1-dev1",
210+
"0.1.1",
211+
"0.1.1",
212+
"0.2.0",
213+
"0.2.0",
214+
"0.2.0",
215+
"0.3.0-dev1",
216+
"0.3.0",
217+
"0.3.0",
218+
"0.3.0",
219+
"0.3.0",
220+
"0.3.1-a.0",
221+
"0.3.1-a.0",
222+
"0.3.1-a.0",
223+
"0.3.1-a.0",
224+
"0.3.1-a.1",
225+
"0.3.1-a.1",
226+
"0.3.1-a.1",
227+
"0.3.1",
228+
"0.3.1",
229+
"0.3.1",
230+
"0.3.2",
231+
"0.4.2",
232+
"1.0.0-a.0",
233+
"1.0.0-a.0",
234+
"1.0.0-a.1",
235+
"1.0.0-a.1",
236+
"1.0.0-a.1",
237+
"1.0.0-a.1",
238+
"1.0.0-a.2-dev0",
239+
"1.0.0-a.2-dev0",
240+
"1.0.0-a.2-dev1",
241+
"1.0.0-a.2",
242+
"1.0.0-a.3-dev0",
243+
"1.0.0-a.3-dev0",
244+
"1.0.0-a.3-dev1",
245+
"1.0.0-b.0",
246+
"1.0.0-b.0",
247+
"1.0.0-b.0",
248+
"1.0.0-b.1",
249+
"1.0.0-b.1",
250+
"1.0.0-rc.0",
251+
"1.0.0-rc.0",
252+
"1.0.0-rc.0",
253+
"1.0.0-rc.0",
254+
"1.0.0-rc.1-dev1",
255+
"1.0.0-rc.1",
256+
"1.0.0",
257+
"1.0.0",
258+
"1.0.1",
259+
"1.0.1",
260+
"1.0.2",
261+
"1.0.2",
262+
"1.1.0",
263+
"1.1.0",
264+
"1.2.0",
265+
"1.2.0",
266+
"1.2.1",
267+
"1.2.1",
268+
"2.0.0",
269+
]

0 commit comments

Comments
 (0)