Skip to content

Commit 2ad26e0

Browse files
authored
feat(schemes): adds support for SemVer 2.0 (dot in pre-releases) (fix #1025) (#1072)
1 parent aad0602 commit 2ad26e0

File tree

5 files changed

+281
-15
lines changed

5 files changed

+281
-15
lines changed

Diff for: commitizen/version_schemes.py

+57-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,8 @@ 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+
parts.append(f"-{self.prerelease}")
330329

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

345344

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

348401
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
@@ -619,14 +619,14 @@ prerelease_offset = 1
619619
620620
Choose version scheme
621621
622-
| schemes | pep440 | semver |
623-
| -------------- | -------------- | --------------- |
624-
| non-prerelease | `0.1.0` | `0.1.0` |
625-
| prerelease | `0.3.1a0` | `0.3.1-a0` |
626-
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` |
627-
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` |
622+
| schemes | pep440 | semver | semver2 |
623+
| -------------- | -------------- | --------------- | --------------------- |
624+
| non-prerelease | `0.1.0` | `0.1.0` | `0.1.0` |
625+
| prerelease | `0.3.1a0` | `0.3.1-a0` | `0.3.1-alpha.0` |
626+
| devrelease | `0.1.1.dev1` | `0.1.1-dev1` | `0.1.1-dev.1` |
627+
| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` | `1.0.0-alpha.3.dev.1` |
628628
629-
Options: `semver`, `pep440`
629+
Options: `pep440`, `semver`, `semver2`
630630
631631
Defaults to: `pep440`
632632

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
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ scm = "commitizen.providers:ScmProvider"
103103
[tool.poetry.plugins."commitizen.scheme"]
104104
pep440 = "commitizen.version_schemes:Pep440"
105105
semver = "commitizen.version_schemes:SemVer"
106+
semver2 = "commitizen.version_schemes:SemVer2"
106107

107108
[tool.coverage]
108109
[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)