Skip to content

Commit 0f5e8bf

Browse files
authored
Merge pull request #733 from noirbizarre/feature/schemes
Feature: version schemes endpoint
2 parents 3ececac + 49572ab commit 0f5e8bf

26 files changed

+644
-405
lines changed

commitizen/bump.py

Lines changed: 8 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,13 @@
22

33
import os
44
import re
5-
import sys
6-
import typing
75
from collections import OrderedDict
8-
from itertools import zip_longest
96
from string import Template
107

11-
from packaging.version import Version
12-
13-
from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message
8+
from commitizen.defaults import bump_message
149
from commitizen.exceptions import CurrentVersionNotFoundError
1510
from commitizen.git import GitCommit, smart_open
16-
17-
if sys.version_info >= (3, 8):
18-
from commitizen.version_types import VersionProtocol
19-
else:
20-
# workaround mypy issue for 3.7 python
21-
VersionProtocol = typing.Any
11+
from commitizen.version_schemes import DEFAULT_SCHEME, VersionScheme, Version
2212

2313

2414
def find_increment(
@@ -54,115 +44,6 @@ def find_increment(
5444
return increment
5545

5646

57-
def prerelease_generator(
58-
current_version: str, prerelease: str | None = None, offset: int = 0
59-
) -> str:
60-
"""Generate prerelease
61-
62-
X.YaN # Alpha release
63-
X.YbN # Beta release
64-
X.YrcN # Release Candidate
65-
X.Y # Final
66-
67-
This function might return something like 'alpha1'
68-
but it will be handled by Version.
69-
"""
70-
if not prerelease:
71-
return ""
72-
73-
version = Version(current_version)
74-
# version.pre is needed for mypy check
75-
if version.is_prerelease and version.pre and prerelease.startswith(version.pre[0]):
76-
prev_prerelease: int = version.pre[1]
77-
new_prerelease_number = prev_prerelease + 1
78-
else:
79-
new_prerelease_number = offset
80-
pre_version = f"{prerelease}{new_prerelease_number}"
81-
return pre_version
82-
83-
84-
def devrelease_generator(devrelease: int = None) -> str:
85-
"""Generate devrelease
86-
87-
The devrelease version should be passed directly and is not
88-
inferred based on the previous version.
89-
"""
90-
if devrelease is None:
91-
return ""
92-
93-
return f"dev{devrelease}"
94-
95-
96-
def semver_generator(current_version: str, increment: str = None) -> str:
97-
version = Version(current_version)
98-
prev_release = list(version.release)
99-
increments = [MAJOR, MINOR, PATCH]
100-
increments_version = dict(zip_longest(increments, prev_release, fillvalue=0))
101-
102-
# This flag means that current version
103-
# must remove its prerelease tag,
104-
# so it doesn't matter the increment.
105-
# Example: 1.0.0a0 with PATCH/MINOR -> 1.0.0
106-
if not version.is_prerelease:
107-
if increment == MAJOR:
108-
increments_version[MAJOR] += 1
109-
increments_version[MINOR] = 0
110-
increments_version[PATCH] = 0
111-
elif increment == MINOR:
112-
increments_version[MINOR] += 1
113-
increments_version[PATCH] = 0
114-
elif increment == PATCH:
115-
increments_version[PATCH] += 1
116-
117-
return str(
118-
f"{increments_version['MAJOR']}."
119-
f"{increments_version['MINOR']}."
120-
f"{increments_version['PATCH']}"
121-
)
122-
123-
124-
def generate_version(
125-
current_version: str,
126-
increment: str,
127-
prerelease: str | None = None,
128-
prerelease_offset: int = 0,
129-
devrelease: int | None = None,
130-
is_local_version: bool = False,
131-
version_type_cls: type[VersionProtocol] | None = None,
132-
) -> VersionProtocol:
133-
"""Based on the given increment a proper semver will be generated.
134-
135-
For now the rules and versioning scheme is based on
136-
python's PEP 0440.
137-
More info: https://www.python.org/dev/peps/pep-0440/
138-
139-
Example:
140-
PATCH 1.0.0 -> 1.0.1
141-
MINOR 1.0.0 -> 1.1.0
142-
MAJOR 1.0.0 -> 2.0.0
143-
"""
144-
if version_type_cls is None:
145-
version_type_cls = Version
146-
if is_local_version:
147-
version = version_type_cls(current_version)
148-
dev_version = devrelease_generator(devrelease=devrelease)
149-
pre_version = prerelease_generator(
150-
str(version.local), prerelease=prerelease, offset=prerelease_offset
151-
)
152-
semver = semver_generator(str(version.local), increment=increment)
153-
154-
return version_type_cls(f"{version.public}+{semver}{pre_version}{dev_version}")
155-
else:
156-
dev_version = devrelease_generator(devrelease=devrelease)
157-
pre_version = prerelease_generator(
158-
current_version, prerelease=prerelease, offset=prerelease_offset
159-
)
160-
semver = semver_generator(current_version, increment=increment)
161-
162-
# TODO: post version
163-
return version_type_cls(f"{semver}{pre_version}{dev_version}")
164-
165-
16647
def update_version_in_files(
16748
current_version: str, new_version: str, files: list[str], *, check_consistency=False
16849
) -> None:
@@ -219,9 +100,9 @@ def _version_to_regex(version: str) -> str:
219100

220101

221102
def normalize_tag(
222-
version: VersionProtocol | str,
103+
version: Version | str,
223104
tag_format: str | None = None,
224-
version_type_cls: type[VersionProtocol] | None = None,
105+
scheme: VersionScheme | None = None,
225106
) -> str:
226107
"""The tag and the software version might be different.
227108
@@ -234,19 +115,14 @@ def normalize_tag(
234115
| ver1.0.0 | 1.0.0 |
235116
| ver1.0.0.a0 | 1.0.0a0 |
236117
"""
237-
if version_type_cls is None:
238-
version_type_cls = Version
239-
if isinstance(version, str):
240-
version = version_type_cls(version)
118+
scheme = scheme or DEFAULT_SCHEME
119+
version = scheme(version) if isinstance(version, str) else version
241120

242121
if not tag_format:
243122
return str(version)
244123

245124
major, minor, patch = version.release
246-
prerelease = ""
247-
# version.pre is needed for mypy check
248-
if version.is_prerelease and version.pre:
249-
prerelease = f"{version.pre[0]}{version.pre[1]}"
125+
prerelease = version.prerelease or ""
250126

251127
t = Template(tag_format)
252128
return t.safe_substitute(
@@ -257,7 +133,7 @@ def normalize_tag(
257133
def create_commit_message(
258134
current_version: Version | str,
259135
new_version: Version | str,
260-
message_template: str = None,
136+
message_template: str | None = None,
261137
) -> str:
262138
if message_template is None:
263139
message_template = bump_message

commitizen/changelog.py

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,53 +24,41 @@
2424
- [x] hook after changelog is generated (api calls)
2525
- [x] add support for change_type maps
2626
"""
27-
2827
from __future__ import annotations
2928

3029
import os
3130
import re
32-
import sys
33-
import typing
3431
from collections import OrderedDict, defaultdict
3532
from datetime import date
36-
from typing import Callable, Iterable
33+
from typing import TYPE_CHECKING, Callable, Iterable, cast
3734

3835
from jinja2 import Environment, PackageLoader
39-
from packaging.version import InvalidVersion, Version
4036

41-
from commitizen import defaults
4237
from commitizen.bump import normalize_tag
4338
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
4439
from commitizen.git import GitCommit, GitTag
40+
from commitizen.version_schemes import DEFAULT_SCHEME, Pep440, InvalidVersion
4541

46-
if sys.version_info >= (3, 8):
47-
from commitizen.version_types import VersionProtocol
48-
else:
49-
# workaround mypy issue for 3.7 python
50-
VersionProtocol = typing.Any
42+
if TYPE_CHECKING:
43+
from commitizen.version_schemes import VersionScheme
5144

5245

5346
def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None:
5447
return next((tag for tag in tags if tag.rev == commit.rev), None)
5548

5649

57-
def get_version(tag: GitTag) -> Version | None:
58-
version = None
59-
try:
60-
version = Version(tag.name)
61-
except InvalidVersion:
62-
pass
63-
return version
64-
65-
6650
def tag_included_in_changelog(
67-
tag: GitTag, used_tags: list, merge_prerelease: bool
51+
tag: GitTag,
52+
used_tags: list,
53+
merge_prerelease: bool,
54+
scheme: VersionScheme = DEFAULT_SCHEME,
6855
) -> bool:
6956
if tag in used_tags:
7057
return False
7158

72-
version = get_version(tag)
73-
if version is None:
59+
try:
60+
version = scheme(tag.name)
61+
except InvalidVersion:
7462
return False
7563

7664
if merge_prerelease and version.is_prerelease:
@@ -88,6 +76,7 @@ def generate_tree_from_commits(
8876
change_type_map: dict[str, str] | None = None,
8977
changelog_message_builder_hook: Callable | None = None,
9078
merge_prerelease: bool = False,
79+
scheme: VersionScheme = DEFAULT_SCHEME,
9180
) -> Iterable[dict]:
9281
pat = re.compile(changelog_pattern)
9382
map_pat = re.compile(commit_parser, re.MULTILINE)
@@ -113,7 +102,7 @@ def generate_tree_from_commits(
113102
commit_tag = get_commit_tag(commit, tags)
114103

115104
if commit_tag is not None and tag_included_in_changelog(
116-
commit_tag, used_tags, merge_prerelease
105+
commit_tag, used_tags, merge_prerelease, scheme=scheme
117106
):
118107
used_tags.append(commit_tag)
119108
yield {
@@ -186,13 +175,15 @@ def render_changelog(tree: Iterable) -> str:
186175
return changelog
187176

188177

189-
def parse_version_from_markdown(value: str) -> str | None:
178+
def parse_version_from_markdown(
179+
value: str, scheme: VersionScheme = Pep440
180+
) -> str | None:
190181
if not value.startswith("#"):
191182
return None
192-
m = re.search(defaults.version_parser, value)
183+
m = scheme.parser.search(value)
193184
if not m:
194185
return None
195-
return m.groupdict().get("version")
186+
return cast(str, m.group("version"))
196187

197188

198189
def parse_title_type_of_line(value: str) -> str | None:
@@ -203,7 +194,7 @@ def parse_title_type_of_line(value: str) -> str | None:
203194
return m.groupdict().get("title")
204195

205196

206-
def get_metadata(filepath: str) -> dict:
197+
def get_metadata(filepath: str, scheme: VersionScheme = Pep440) -> dict:
207198
unreleased_start: int | None = None
208199
unreleased_end: int | None = None
209200
unreleased_title: str | None = None
@@ -236,7 +227,7 @@ def get_metadata(filepath: str) -> dict:
236227
unreleased_end = index
237228

238229
# Try to find the latest release done
239-
version = parse_version_from_markdown(line)
230+
version = parse_version_from_markdown(line, scheme)
240231
if version:
241232
latest_version = version
242233
latest_version_position = index
@@ -328,7 +319,7 @@ def get_oldest_and_newest_rev(
328319
tags: list[GitTag],
329320
version: str,
330321
tag_format: str,
331-
version_type_cls: type[VersionProtocol] | None = None,
322+
scheme: VersionScheme | None = None,
332323
) -> tuple[str | None, str | None]:
333324
"""Find the tags for the given version.
334325
@@ -343,15 +334,11 @@ def get_oldest_and_newest_rev(
343334
except ValueError:
344335
newest = version
345336

346-
newest_tag = normalize_tag(
347-
newest, tag_format=tag_format, version_type_cls=version_type_cls
348-
)
337+
newest_tag = normalize_tag(newest, tag_format=tag_format, scheme=scheme)
349338

350339
oldest_tag = None
351340
if oldest:
352-
oldest_tag = normalize_tag(
353-
oldest, tag_format=tag_format, version_type_cls=version_type_cls
354-
)
341+
oldest_tag = normalize_tag(oldest, tag_format=tag_format, scheme=scheme)
355342

356343
tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag)
357344
if not tags_range:

commitizen/cli.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import argcomplete
1111
from decli import cli
1212

13-
from commitizen import commands, config, out, version_types
13+
from commitizen import commands, config, out, version_schemes
1414
from commitizen.exceptions import (
1515
CommitizenException,
1616
ExitCode,
@@ -204,19 +204,25 @@
204204
"default": None,
205205
"help": "start pre-releases with this offset",
206206
},
207+
{
208+
"name": ["--version-scheme"],
209+
"help": "choose version scheme",
210+
"default": None,
211+
"choices": version_schemes.KNOWN_SCHEMES,
212+
},
213+
{
214+
"name": ["--version-type"],
215+
"help": "Deprecated, use --version-scheme",
216+
"default": None,
217+
"choices": version_schemes.KNOWN_SCHEMES,
218+
},
207219
{
208220
"name": "manual_version",
209221
"type": str,
210222
"nargs": "?",
211223
"help": "bump to the given version (e.g: 1.5.3)",
212224
"metavar": "MANUAL_VERSION",
213225
},
214-
{
215-
"name": ["--version-type"],
216-
"help": "choose version type",
217-
"default": None,
218-
"choices": version_types.VERSION_TYPES,
219-
},
220226
],
221227
},
222228
{
@@ -275,6 +281,12 @@
275281
"If not set, it will include prereleases in the changelog"
276282
),
277283
},
284+
{
285+
"name": ["--version-scheme"],
286+
"help": "choose version scheme",
287+
"default": None,
288+
"choices": version_schemes.KNOWN_SCHEMES,
289+
},
278290
],
279291
},
280292
{
@@ -354,7 +366,7 @@
354366

355367

356368
def commitizen_excepthook(
357-
type, value, traceback, debug=False, no_raise: list[int] = None
369+
type, value, traceback, debug=False, no_raise: list[int] | None = None
358370
):
359371
traceback = traceback if isinstance(traceback, TracebackType) else None
360372
if not no_raise:

0 commit comments

Comments
 (0)