Skip to content

Commit ffdaa62

Browse files
committedMay 29, 2025·
feat(bump_rule): add BumpRule, VersionIncrement, Prerelease Enum
Closes #129
1 parent 4b6b3fb commit ffdaa62

14 files changed

+1335
-512
lines changed
 

‎commitizen/bump.py

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,13 @@
22

33
import os
44
import re
5-
from collections import OrderedDict
65
from glob import iglob
7-
from logging import getLogger
86
from string import Template
9-
from typing import cast
107

11-
from commitizen.defaults import BUMP_MESSAGE, ENCODING, MAJOR, MINOR, PATCH
8+
from commitizen.defaults import BUMP_MESSAGE, ENCODING
129
from commitizen.exceptions import CurrentVersionNotFoundError
13-
from commitizen.git import GitCommit, smart_open
14-
from commitizen.version_schemes import Increment, Version
15-
16-
VERSION_TYPES = [None, PATCH, MINOR, MAJOR]
17-
18-
logger = getLogger("commitizen")
19-
20-
21-
def find_increment(
22-
commits: list[GitCommit], regex: str, increments_map: dict | OrderedDict
23-
) -> Increment | None:
24-
if isinstance(increments_map, dict):
25-
increments_map = OrderedDict(increments_map)
26-
27-
# Most important cases are major and minor.
28-
# Everything else will be considered patch.
29-
select_pattern = re.compile(regex)
30-
increment: str | None = None
31-
32-
for commit in commits:
33-
for message in commit.message.split("\n"):
34-
result = select_pattern.search(message)
35-
36-
if result:
37-
found_keyword = result.group(1)
38-
new_increment = None
39-
for match_pattern in increments_map.keys():
40-
if re.match(match_pattern, found_keyword):
41-
new_increment = increments_map[match_pattern]
42-
break
43-
44-
if new_increment is None:
45-
logger.debug(
46-
f"no increment needed for '{found_keyword}' in '{message}'"
47-
)
48-
49-
if VERSION_TYPES.index(increment) < VERSION_TYPES.index(new_increment):
50-
logger.debug(
51-
f"increment detected is '{new_increment}' due to '{found_keyword}' in '{message}'"
52-
)
53-
increment = new_increment
54-
55-
if increment == MAJOR:
56-
break
57-
58-
return cast(Increment, increment)
10+
from commitizen.git import smart_open
11+
from commitizen.version_schemes import Version
5912

6013

6114
def update_version_in_files(

‎commitizen/bump_rule.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from collections.abc import Iterable, Mapping
5+
from enum import IntEnum, auto
6+
from functools import cached_property
7+
from typing import Callable, Protocol
8+
9+
from commitizen.exceptions import NoPatternMapError
10+
11+
12+
class VersionIncrement(IntEnum):
13+
"""An enumeration representing semantic versioning increments.
14+
15+
This class defines the three types of version increments according to semantic versioning:
16+
- PATCH: For backwards-compatible bug fixes
17+
- MINOR: For backwards-compatible functionality additions
18+
- MAJOR: For incompatible API changes
19+
"""
20+
21+
PATCH = auto()
22+
MINOR = auto()
23+
MAJOR = auto()
24+
25+
def __str__(self) -> str:
26+
return self.name
27+
28+
@classmethod
29+
def safe_cast(cls, value: object) -> VersionIncrement | None:
30+
if not isinstance(value, str):
31+
return None
32+
try:
33+
return cls[value]
34+
except KeyError:
35+
return None
36+
37+
@classmethod
38+
def safe_cast_dict(cls, d: Mapping[str, object]) -> dict[str, VersionIncrement]:
39+
return {
40+
k: v
41+
for k, v in ((k, VersionIncrement.safe_cast(v)) for k, v in d.items())
42+
if v is not None
43+
}
44+
45+
@staticmethod
46+
def get_highest_by_messages(
47+
commit_messages: Iterable[str],
48+
get_increment: Callable[[str], VersionIncrement | None],
49+
) -> VersionIncrement | None:
50+
"""Find the highest version increment from a list of messages.
51+
52+
This function processes a list of messages and determines the highest version
53+
increment needed based on the commit messages. It splits multi-line commit messages
54+
and evaluates each line using the provided get_increment callable.
55+
56+
Args:
57+
commit_messages: A list of messages to analyze.
58+
get_increment: A callable that takes a commit message string and returns an
59+
VersionIncrement value (MAJOR, MINOR, PATCH) or None if no increment is needed.
60+
61+
Returns:
62+
The highest version increment needed (MAJOR, MINOR, PATCH) or None if no
63+
increment is needed. The order of precedence is MAJOR > MINOR > PATCH.
64+
65+
Example:
66+
>>> commit_messages = ["feat: new feature", "fix: bug fix"]
67+
>>> rule = ConventionalCommitBumpRule()
68+
>>> VersionIncrement.get_highest_by_messages(commit_messages, lambda x: rule.get_increment(x, False))
69+
'MINOR'
70+
"""
71+
return VersionIncrement.get_highest(
72+
get_increment(line)
73+
for message in commit_messages
74+
for line in message.split("\n")
75+
)
76+
77+
@staticmethod
78+
def get_highest(
79+
increments: Iterable[VersionIncrement | None],
80+
) -> VersionIncrement | None:
81+
return max(filter(None, increments), default=None)
82+
83+
84+
class BumpRule(Protocol):
85+
"""A protocol defining the interface for version bump rules.
86+
87+
This protocol specifies the contract that all version bump rule implementations must follow.
88+
It defines how commit messages should be analyzed to determine the appropriate semantic
89+
version increment.
90+
91+
The protocol is used to ensure consistent behavior across different bump rule implementations,
92+
such as conventional commits or custom rules.
93+
"""
94+
95+
def get_increment(
96+
self, commit_message: str, major_version_zero: bool
97+
) -> VersionIncrement | None:
98+
"""Determine the version increment based on a commit message.
99+
100+
This method analyzes a commit message to determine what kind of version increment
101+
is needed according to the Conventional Commits specification. It handles special
102+
cases for breaking changes and respects the major_version_zero flag.
103+
104+
Args:
105+
commit_message: The commit message to analyze. Should follow conventional commit format.
106+
major_version_zero: If True, breaking changes will result in a MINOR version bump
107+
instead of MAJOR. This is useful for projects in 0.x.x versions.
108+
109+
Returns:
110+
VersionIncrement | None: The type of version increment needed:
111+
- MAJOR: For breaking changes when major_version_zero is False
112+
- MINOR: For breaking changes when major_version_zero is True, or for new features
113+
- PATCH: For bug fixes, performance improvements, or refactors
114+
- None: For commits that don't require a version bump (docs, style, etc.)
115+
"""
116+
117+
118+
class ConventionalCommitBumpRule(BumpRule):
119+
_BREAKING_CHANGE_TYPES = set(["BREAKING CHANGE", "BREAKING-CHANGE"])
120+
_MINOR_CHANGE_TYPES = set(["feat"])
121+
_PATCH_CHANGE_TYPES = set(["fix", "perf", "refactor"])
122+
123+
def get_increment(
124+
self, commit_message: str, major_version_zero: bool
125+
) -> VersionIncrement | None:
126+
if not (m := self._head_pattern.match(commit_message)):
127+
return None
128+
129+
change_type = m.group("change_type")
130+
if m.group("bang") or change_type in self._BREAKING_CHANGE_TYPES:
131+
return (
132+
VersionIncrement.MINOR if major_version_zero else VersionIncrement.MAJOR
133+
)
134+
135+
if change_type in self._MINOR_CHANGE_TYPES:
136+
return VersionIncrement.MINOR
137+
138+
if change_type in self._PATCH_CHANGE_TYPES:
139+
return VersionIncrement.PATCH
140+
141+
return None
142+
143+
@cached_property
144+
def _head_pattern(self) -> re.Pattern:
145+
change_types = [
146+
*self._BREAKING_CHANGE_TYPES,
147+
*self._PATCH_CHANGE_TYPES,
148+
*self._MINOR_CHANGE_TYPES,
149+
"docs",
150+
"style",
151+
"test",
152+
"build",
153+
"ci",
154+
]
155+
re_change_type = r"(?P<change_type>" + "|".join(change_types) + r")"
156+
re_scope = r"(?P<scope>\(.+\))?"
157+
re_bang = r"(?P<bang>!)?"
158+
return re.compile(f"^{re_change_type}{re_scope}{re_bang}:")
159+
160+
161+
class CustomBumpRule(BumpRule):
162+
def __init__(
163+
self,
164+
bump_pattern: str,
165+
bump_map: Mapping[str, VersionIncrement],
166+
bump_map_major_version_zero: Mapping[str, VersionIncrement],
167+
):
168+
"""Initialize a custom bump rule for version incrementing.
169+
170+
This constructor creates a rule that determines how version numbers should be
171+
incremented based on commit messages. It validates and compiles the provided
172+
pattern and maps for use in version bumping.
173+
174+
The fallback logic is used for backward compatibility.
175+
176+
Args:
177+
bump_pattern: A regex pattern string used to match commit messages.
178+
Example: r"^((?P<major>major)|(?P<minor>minor)|(?P<patch>patch))(?P<scope>\(.+\))?(?P<bang>!)?:"
179+
Or with fallback regex: r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):" # First group is type
180+
bump_map: A mapping of commit types to their corresponding version increments.
181+
Example: {
182+
"major": VersionIncrement.MAJOR,
183+
"bang": VersionIncrement.MAJOR,
184+
"minor": VersionIncrement.MINOR,
185+
"patch": VersionIncrement.PATCH
186+
}
187+
Or with fallback: {
188+
(r"^.+!$", VersionIncrement.MAJOR),
189+
(r"^BREAKING[\-\ ]CHANGE", VersionIncrement.MAJOR),
190+
(r"^feat", VersionIncrement.MINOR),
191+
(r"^fix", VersionIncrement.PATCH),
192+
(r"^refactor", VersionIncrement.PATCH),
193+
(r"^perf", VersionIncrement.PATCH),
194+
}
195+
bump_map_major_version_zero: A mapping of commit types to version increments
196+
specifically for when the major version is 0. This allows for different
197+
versioning behavior during initial development.
198+
The format is the same as bump_map.
199+
Example: {
200+
"major": VersionIncrement.MINOR, # MAJOR becomes MINOR in version zero
201+
"bang": VersionIncrement.MINOR, # Breaking changes become MINOR in version zero
202+
"minor": VersionIncrement.MINOR,
203+
"patch": VersionIncrement.PATCH
204+
}
205+
Or with fallback: {
206+
(r"^.+!$", VersionIncrement.MINOR),
207+
(r"^BREAKING[\-\ ]CHANGE", VersionIncrement.MINOR),
208+
(r"^feat", VersionIncrement.MINOR),
209+
(r"^fix", VersionIncrement.PATCH),
210+
(r"^refactor", VersionIncrement.PATCH),
211+
(r"^perf", VersionIncrement.PATCH),
212+
}
213+
214+
Raises:
215+
NoPatternMapError: If any of the required parameters are empty or None
216+
"""
217+
if not bump_map or not bump_pattern or not bump_map_major_version_zero:
218+
raise NoPatternMapError(
219+
f"Invalid bump rule: {bump_pattern=} and {bump_map=} and {bump_map_major_version_zero=}"
220+
)
221+
222+
self.bump_pattern = re.compile(bump_pattern)
223+
self.bump_map = bump_map
224+
self.bump_map_major_version_zero = bump_map_major_version_zero
225+
226+
def get_increment(
227+
self, commit_message: str, major_version_zero: bool
228+
) -> VersionIncrement | None:
229+
if not (m := self.bump_pattern.search(commit_message)):
230+
return None
231+
232+
effective_bump_map = (
233+
self.bump_map_major_version_zero if major_version_zero else self.bump_map
234+
)
235+
236+
try:
237+
if ret := VersionIncrement.get_highest(
238+
(
239+
increment
240+
for name, increment in effective_bump_map.items()
241+
if m.group(name)
242+
),
243+
):
244+
return ret
245+
except IndexError:
246+
pass
247+
248+
# Fallback to legacy bump rule, for backward compatibility
249+
found_keyword = m.group(1)
250+
for match_pattern, increment in effective_bump_map.items():
251+
if re.match(match_pattern, found_keyword):
252+
return increment
253+
return None

‎commitizen/commands/bump.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import questionary
88

99
from commitizen import bump, factory, git, hooks, out
10+
from commitizen.bump_rule import (
11+
VersionIncrement,
12+
)
1013
from commitizen.changelog_formats import get_changelog_format
1114
from commitizen.commands.changelog import Changelog
1215
from commitizen.config import BaseConfig
@@ -20,15 +23,13 @@
2023
InvalidManualVersion,
2124
NoCommitsFoundError,
2225
NoneIncrementExit,
23-
NoPatternMapError,
2426
NotAGitProjectError,
2527
NotAllowed,
2628
NoVersionSpecifiedError,
2729
)
2830
from commitizen.providers import get_provider
2931
from commitizen.tags import TagRules
3032
from commitizen.version_schemes import (
31-
Increment,
3233
InvalidVersion,
3334
Prerelease,
3435
get_version_scheme,
@@ -119,25 +120,14 @@ def is_initial_tag(
119120
is_initial = questionary.confirm("Is this the first tag created?").ask()
120121
return is_initial
121122

122-
def find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
123+
def find_increment(self, commits: list[git.GitCommit]) -> VersionIncrement | None:
123124
# Update the bump map to ensure major version doesn't increment.
124-
is_major_version_zero: bool = self.bump_settings["major_version_zero"]
125-
# self.cz.bump_map = defaults.bump_map_major_version_zero
126-
bump_map = (
127-
self.cz.bump_map_major_version_zero
128-
if is_major_version_zero
129-
else self.cz.bump_map
130-
)
131-
bump_pattern = self.cz.bump_pattern
125+
is_major_version_zero = bool(self.bump_settings["major_version_zero"])
132126

133-
if not bump_map or not bump_pattern:
134-
raise NoPatternMapError(
135-
f"'{self.config.settings['name']}' rule does not support bump"
136-
)
137-
increment = bump.find_increment(
138-
commits, regex=bump_pattern, increments_map=bump_map
127+
return VersionIncrement.get_highest_by_messages(
128+
(commit.message for commit in commits),
129+
lambda x: self.cz.bump_rule.get_increment(x, is_major_version_zero),
139130
)
140-
return increment
141131

142132
def __call__(self) -> None: # noqa: C901
143133
"""Steps executed to bump."""
@@ -155,8 +145,8 @@ def __call__(self) -> None: # noqa: C901
155145

156146
dry_run: bool = self.arguments["dry_run"]
157147
is_yes: bool = self.arguments["yes"]
158-
increment: Increment | None = self.arguments["increment"]
159-
prerelease: Prerelease | None = self.arguments["prerelease"]
148+
increment = VersionIncrement.safe_cast(self.arguments["increment"])
149+
prerelease = Prerelease.safe_cast(self.arguments["prerelease"])
160150
devrelease: int | None = self.arguments["devrelease"]
161151
is_files_only: bool | None = self.arguments["files_only"]
162152
is_local_version: bool = self.arguments["local_version"]
@@ -272,7 +262,7 @@ def __call__(self) -> None: # noqa: C901
272262

273263
# we create an empty PATCH increment for empty tag
274264
if increment is None and allow_no_commit:
275-
increment = "PATCH"
265+
increment = VersionIncrement.PATCH
276266

277267
new_version = current_version.bump(
278268
increment,

‎commitizen/cz/base.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
from abc import ABCMeta, abstractmethod
44
from collections.abc import Iterable
5+
from functools import cached_property
56
from typing import Any, Callable, Protocol
67

78
from jinja2 import BaseLoader, PackageLoader
89
from prompt_toolkit.styles import Style, merge_styles
910

1011
from commitizen import git
12+
from commitizen.bump_rule import BumpRule, CustomBumpRule, VersionIncrement
1113
from commitizen.config.base_config import BaseConfig
1214
from commitizen.defaults import Questions
15+
from commitizen.exceptions import NoPatternMapError
1316

1417

1518
class MessageBuilderHook(Protocol):
@@ -25,9 +28,13 @@ def __call__(
2528

2629

2730
class BaseCommitizen(metaclass=ABCMeta):
31+
_bump_rule: BumpRule | None = None
32+
33+
# TODO: decide if these should be removed
2834
bump_pattern: str | None = None
2935
bump_map: dict[str, str] | None = None
3036
bump_map_major_version_zero: dict[str, str] | None = None
37+
3138
default_style_config: list[tuple[str, str]] = [
3239
("qmark", "fg:#ff9d00 bold"),
3340
("question", "bold"),
@@ -84,6 +91,44 @@ def style(self):
8491
]
8592
)
8693

94+
@cached_property
95+
def bump_rule(self) -> BumpRule:
96+
"""Get the bump rule for version incrementing.
97+
98+
This property returns a BumpRule instance that determines how version numbers
99+
should be incremented based on commit messages. It first checks if a custom
100+
bump rule was set via `_bump_rule`. If not, it falls back to creating a
101+
CustomBumpRule using the class's bump pattern and maps.
102+
103+
The CustomBumpRule requires three components to be defined:
104+
- bump_pattern: A regex pattern to match commit messages
105+
- bump_map: A mapping of commit types to version increments
106+
- bump_map_major_version_zero: A mapping for version increments when major version is 0
107+
108+
Returns:
109+
BumpRule: A rule instance that determines version increments
110+
111+
Raises:
112+
NoPatternMapError: If the required bump pattern or maps are not defined
113+
"""
114+
if self._bump_rule:
115+
return self._bump_rule
116+
117+
# Fallback to custom bump rule if no bump rule is provided
118+
if (
119+
not self.bump_pattern
120+
or not self.bump_map
121+
or not self.bump_map_major_version_zero
122+
):
123+
raise NoPatternMapError(
124+
f"'{self.config.settings['name']}' rule does not support bump: {self.bump_pattern=}, {self.bump_map=}, {self.bump_map_major_version_zero=}"
125+
)
126+
return CustomBumpRule(
127+
self.bump_pattern,
128+
VersionIncrement.safe_cast_dict(self.bump_map),
129+
VersionIncrement.safe_cast_dict(self.bump_map_major_version_zero),
130+
)
131+
87132
def example(self) -> str:
88133
"""Example of the commit message."""
89134
raise NotImplementedError("Not Implemented yet")

‎commitizen/cz/conventional_commits/conventional_commits.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33

44
from commitizen import defaults
5+
from commitizen.bump_rule import ConventionalCommitBumpRule
56
from commitizen.cz.base import BaseCommitizen
67
from commitizen.cz.utils import multiple_line_breaker, required_validator
78
from commitizen.defaults import Questions
@@ -28,6 +29,8 @@ def parse_subject(text):
2829

2930

3031
class ConventionalCommitsCz(BaseCommitizen):
32+
_bump_rule = ConventionalCommitBumpRule()
33+
3134
bump_pattern = defaults.BUMP_PATTERN
3235
bump_map = defaults.BUMP_MAP
3336
bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO

‎commitizen/defaults.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from collections.abc import Iterable, MutableMapping, Sequence
66
from typing import Any, TypedDict
77

8+
from commitizen.bump_rule import VersionIncrement
9+
810
# Type
911
Questions = Iterable[MutableMapping[str, Any]]
1012

@@ -107,31 +109,27 @@ class Settings(TypedDict, total=False):
107109
"extras": {},
108110
}
109111

110-
MAJOR = "MAJOR"
111-
MINOR = "MINOR"
112-
PATCH = "PATCH"
113-
114112
CHANGELOG_FORMAT = "markdown"
115113

116114
BUMP_PATTERN = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
117-
BUMP_MAP = OrderedDict(
115+
BUMP_MAP = dict(
118116
(
119-
(r"^.+!$", MAJOR),
120-
(r"^BREAKING[\-\ ]CHANGE", MAJOR),
121-
(r"^feat", MINOR),
122-
(r"^fix", PATCH),
123-
(r"^refactor", PATCH),
124-
(r"^perf", PATCH),
117+
(r"^.+!$", str(VersionIncrement.MAJOR)),
118+
(r"^BREAKING[\-\ ]CHANGE", str(VersionIncrement.MAJOR)),
119+
(r"^feat", str(VersionIncrement.MINOR)),
120+
(r"^fix", str(VersionIncrement.PATCH)),
121+
(r"^refactor", str(VersionIncrement.PATCH)),
122+
(r"^perf", str(VersionIncrement.PATCH)),
125123
)
126124
)
127-
BUMP_MAP_MAJOR_VERSION_ZERO = OrderedDict(
125+
BUMP_MAP_MAJOR_VERSION_ZERO = dict(
128126
(
129-
(r"^.+!$", MINOR),
130-
(r"^BREAKING[\-\ ]CHANGE", MINOR),
131-
(r"^feat", MINOR),
132-
(r"^fix", PATCH),
133-
(r"^refactor", PATCH),
134-
(r"^perf", PATCH),
127+
(r"^.+!$", str(VersionIncrement.MINOR)),
128+
(r"^BREAKING[\-\ ]CHANGE", str(VersionIncrement.MINOR)),
129+
(r"^feat", str(VersionIncrement.MINOR)),
130+
(r"^fix", str(VersionIncrement.PATCH)),
131+
(r"^refactor", str(VersionIncrement.PATCH)),
132+
(r"^perf", str(VersionIncrement.PATCH)),
135133
)
136134
)
137135
CHANGE_TYPE_ORDER = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"]

‎commitizen/version_schemes.py

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
import re
44
import sys
55
import warnings
6+
from enum import Enum
67
from itertools import zip_longest
78
from typing import (
89
TYPE_CHECKING,
910
Any,
1011
ClassVar,
11-
Literal,
1212
Protocol,
1313
cast,
1414
runtime_checkable,
1515
)
1616

17+
from commitizen.bump_rule import VersionIncrement
18+
1719
if sys.version_info >= (3, 10):
1820
from importlib import metadata
1921
else:
@@ -22,7 +24,7 @@
2224
from packaging.version import InvalidVersion # noqa: F401: expose the common exception
2325
from packaging.version import Version as _BaseVersion
2426

25-
from commitizen.defaults import MAJOR, MINOR, PATCH, Settings
27+
from commitizen.defaults import Settings
2628
from commitizen.exceptions import VersionSchemeUnknown
2729

2830
if TYPE_CHECKING:
@@ -39,8 +41,21 @@
3941
from typing import Self
4042

4143

42-
Increment: TypeAlias = Literal["MAJOR", "MINOR", "PATCH"]
43-
Prerelease: TypeAlias = Literal["alpha", "beta", "rc"]
44+
class Prerelease(Enum):
45+
ALPHA = "alpha"
46+
BETA = "beta"
47+
RC = "rc"
48+
49+
@classmethod
50+
def safe_cast(cls, value: object) -> Prerelease | None:
51+
if not isinstance(value, str):
52+
return None
53+
try:
54+
return cls[value.upper()]
55+
except KeyError:
56+
return None
57+
58+
4459
DEFAULT_VERSION_PARSER = r"v?(?P<version>([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.]+)?(\w+)?)"
4560

4661

@@ -126,7 +141,7 @@ def __ne__(self, other: object) -> bool:
126141

127142
def bump(
128143
self,
129-
increment: Increment | None,
144+
increment: VersionIncrement | None,
130145
prerelease: Prerelease | None = None,
131146
prerelease_offset: int = 0,
132147
devrelease: int | None = None,
@@ -171,7 +186,7 @@ def prerelease(self) -> str | None:
171186
return None
172187

173188
def generate_prerelease(
174-
self, prerelease: str | None = None, offset: int = 0
189+
self, prerelease: Prerelease | None = None, offset: int = 0
175190
) -> str:
176191
"""Generate prerelease
177192
@@ -186,20 +201,18 @@ def generate_prerelease(
186201
if not prerelease:
187202
return ""
188203

204+
prerelease_value = prerelease.value
205+
new_prerelease_number = offset
206+
189207
# prevent down-bumping the pre-release phase, e.g. from 'b1' to 'a2'
190208
# https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases
191209
# https://semver.org/#spec-item-11
192210
if self.is_prerelease and self.pre:
193-
prerelease = max(prerelease, self.pre[0])
211+
prerelease_value = max(prerelease_value, self.pre[0])
212+
if prerelease_value.startswith(self.pre[0]):
213+
new_prerelease_number = self.pre[1] + 1
194214

195-
# version.pre is needed for mypy check
196-
if self.is_prerelease and self.pre and prerelease.startswith(self.pre[0]):
197-
prev_prerelease: int = self.pre[1]
198-
new_prerelease_number = prev_prerelease + 1
199-
else:
200-
new_prerelease_number = offset
201-
pre_version = f"{prerelease}{new_prerelease_number}"
202-
return pre_version
215+
return f"{prerelease_value}{new_prerelease_number}"
203216

204217
def generate_devrelease(self, devrelease: int | None) -> str:
205218
"""Generate devrelease
@@ -223,26 +236,34 @@ def generate_build_metadata(self, build_metadata: str | None) -> str:
223236

224237
return f"+{build_metadata}"
225238

226-
def increment_base(self, increment: Increment | None = None) -> str:
227-
prev_release = list(self.release)
228-
increments = [MAJOR, MINOR, PATCH]
229-
base = dict(zip_longest(increments, prev_release, fillvalue=0))
239+
def increment_base(self, increment: VersionIncrement | None = None) -> str:
240+
base = dict(
241+
zip_longest(
242+
(
243+
VersionIncrement.MAJOR,
244+
VersionIncrement.MINOR,
245+
VersionIncrement.PATCH,
246+
),
247+
self.release,
248+
fillvalue=0,
249+
)
250+
)
230251

231-
if increment == MAJOR:
232-
base[MAJOR] += 1
233-
base[MINOR] = 0
234-
base[PATCH] = 0
235-
elif increment == MINOR:
236-
base[MINOR] += 1
237-
base[PATCH] = 0
238-
elif increment == PATCH:
239-
base[PATCH] += 1
252+
if increment == VersionIncrement.MAJOR:
253+
base[VersionIncrement.MAJOR] += 1
254+
base[VersionIncrement.MINOR] = 0
255+
base[VersionIncrement.PATCH] = 0
256+
elif increment == VersionIncrement.MINOR:
257+
base[VersionIncrement.MINOR] += 1
258+
base[VersionIncrement.PATCH] = 0
259+
elif increment == VersionIncrement.PATCH:
260+
base[VersionIncrement.PATCH] += 1
240261

241-
return f"{base[MAJOR]}.{base[MINOR]}.{base[PATCH]}"
262+
return f"{base[VersionIncrement.MAJOR]}.{base[VersionIncrement.MINOR]}.{base[VersionIncrement.PATCH]}"
242263

243264
def bump(
244265
self,
245-
increment: Increment | None,
266+
increment: VersionIncrement | None,
246267
prerelease: Prerelease | None = None,
247268
prerelease_offset: int = 0,
248269
devrelease: int | None = None,
@@ -272,12 +293,12 @@ def bump(
272293
base = self.increment_base(increment)
273294
else:
274295
base = f"{self.major}.{self.minor}.{self.micro}"
275-
if increment == PATCH:
296+
if increment == VersionIncrement.PATCH:
276297
pass
277-
elif increment == MINOR:
298+
elif increment == VersionIncrement.MINOR:
278299
if self.micro != 0:
279300
base = self.increment_base(increment)
280-
elif increment == MAJOR:
301+
elif increment == VersionIncrement.MAJOR:
281302
if self.minor != 0 or self.micro != 0:
282303
base = self.increment_base(increment)
283304
dev_version = self.generate_devrelease(devrelease)

‎tests/commands/test_bump_command.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import commitizen.commands.bump as bump
1515
from commitizen import cli, cmd, git, hooks
16+
from commitizen.bump_rule import VersionIncrement
1617
from commitizen.changelog_formats import ChangelogFormat
1718
from commitizen.cz.base import BaseCommitizen
1819
from commitizen.exceptions import (
@@ -1000,7 +1001,7 @@ def test_bump_with_pre_bump_hooks(
10001001
new_version="0.2.0",
10011002
new_tag_version="0.2.0",
10021003
message="bump: version 0.1.0 → 0.2.0",
1003-
increment="MINOR",
1004+
increment=VersionIncrement.MINOR,
10041005
changelog_file_name=None,
10051006
),
10061007
call(
@@ -1012,7 +1013,7 @@ def test_bump_with_pre_bump_hooks(
10121013
current_version="0.2.0",
10131014
current_tag_version="0.2.0",
10141015
message="bump: version 0.1.0 → 0.2.0",
1015-
increment="MINOR",
1016+
increment=VersionIncrement.MINOR,
10161017
changelog_file_name=None,
10171018
),
10181019
]

‎tests/test_bump_find_increment.py

Lines changed: 0 additions & 124 deletions
This file was deleted.

‎tests/test_bump_rule.py

Lines changed: 649 additions & 0 deletions
Large diffs are not rendered by default.

‎tests/test_version_scheme_pep440.py

Lines changed: 127 additions & 123 deletions
Large diffs are not rendered by default.

‎tests/test_version_scheme_semver.py

Lines changed: 80 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,121 +3,122 @@
33

44
import pytest
55

6-
from commitizen.version_schemes import SemVer, VersionProtocol
6+
from commitizen.bump_rule import VersionIncrement
7+
from commitizen.version_schemes import Prerelease, SemVer, VersionProtocol
78

89
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-a0"),
16-
(("0.3.1a0", None, "alpha", 0, None), "0.3.1-a1"),
17-
(("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-a1"),
18-
(("0.3.1a0", None, "alpha", 1, None), "0.3.1-a1"),
10+
(("0.1.0", VersionIncrement.PATCH, None, 0, None), "0.1.1"),
11+
(("0.1.0", VersionIncrement.PATCH, None, 0, 1), "0.1.1-dev1"),
12+
(("0.1.1", VersionIncrement.MINOR, None, 0, None), "0.2.0"),
13+
(("0.2.0", VersionIncrement.MINOR, None, 0, None), "0.3.0"),
14+
(("0.2.0", VersionIncrement.MINOR, None, 0, 1), "0.3.0-dev1"),
15+
(("0.3.0", VersionIncrement.PATCH, None, 0, None), "0.3.1"),
16+
(("0.3.0", VersionIncrement.PATCH, Prerelease.ALPHA, 0, None), "0.3.1-a0"),
17+
(("0.3.1a0", None, Prerelease.ALPHA, 0, None), "0.3.1-a1"),
18+
(("0.3.0", VersionIncrement.PATCH, Prerelease.ALPHA, 1, None), "0.3.1-a1"),
19+
(("0.3.1a0", None, Prerelease.ALPHA, 1, None), "0.3.1-a1"),
1920
(("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-a0"),
22-
(("1.0.0a0", None, "alpha", 0, None), "1.0.0-a1"),
23-
(("1.0.0a1", None, "alpha", 0, None), "1.0.0-a2"),
24-
(("1.0.0a1", None, "alpha", 0, 1), "1.0.0-a2-dev1"),
25-
(("1.0.0a2.dev0", None, "alpha", 0, 1), "1.0.0-a3-dev1"),
26-
(("1.0.0a2.dev0", None, "alpha", 0, 0), "1.0.0-a3-dev0"),
27-
(("1.0.0a1", None, "beta", 0, None), "1.0.0-b0"),
28-
(("1.0.0b0", None, "beta", 0, None), "1.0.0-b1"),
29-
(("1.0.0b1", None, "rc", 0, None), "1.0.0-rc0"),
30-
(("1.0.0rc0", None, "rc", 0, None), "1.0.0-rc1"),
31-
(("1.0.0rc0", None, "rc", 0, 1), "1.0.0-rc1-dev1"),
32-
(("1.0.0rc0", "PATCH", None, 0, None), "1.0.0"),
33-
(("1.0.0a3.dev0", None, "beta", 0, None), "1.0.0-b0"),
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"),
21+
(("0.3.1", VersionIncrement.PATCH, None, 0, None), "0.3.2"),
22+
(("0.4.2", VersionIncrement.MAJOR, Prerelease.ALPHA, 0, None), "1.0.0-a0"),
23+
(("1.0.0a0", None, Prerelease.ALPHA, 0, None), "1.0.0-a1"),
24+
(("1.0.0a1", None, Prerelease.ALPHA, 0, None), "1.0.0-a2"),
25+
(("1.0.0a1", None, Prerelease.ALPHA, 0, 1), "1.0.0-a2-dev1"),
26+
(("1.0.0a2.dev0", None, Prerelease.ALPHA, 0, 1), "1.0.0-a3-dev1"),
27+
(("1.0.0a2.dev0", None, Prerelease.ALPHA, 0, 0), "1.0.0-a3-dev0"),
28+
(("1.0.0a1", None, Prerelease.BETA, 0, None), "1.0.0-b0"),
29+
(("1.0.0b0", None, Prerelease.BETA, 0, None), "1.0.0-b1"),
30+
(("1.0.0b1", None, Prerelease.RC, 0, None), "1.0.0-rc0"),
31+
(("1.0.0rc0", None, Prerelease.RC, 0, None), "1.0.0-rc1"),
32+
(("1.0.0rc0", None, Prerelease.RC, 0, 1), "1.0.0-rc1-dev1"),
33+
(("1.0.0rc0", VersionIncrement.PATCH, None, 0, None), "1.0.0"),
34+
(("1.0.0a3.dev0", None, Prerelease.BETA, 0, None), "1.0.0-b0"),
35+
(("1.0.0", VersionIncrement.PATCH, None, 0, None), "1.0.1"),
36+
(("1.0.1", VersionIncrement.PATCH, None, 0, None), "1.0.2"),
37+
(("1.0.2", VersionIncrement.MINOR, None, 0, None), "1.1.0"),
38+
(("1.1.0", VersionIncrement.MINOR, None, 0, None), "1.2.0"),
39+
(("1.2.0", VersionIncrement.PATCH, None, 0, None), "1.2.1"),
40+
(("1.2.1", VersionIncrement.MAJOR, None, 0, None), "2.0.0"),
4041
]
4142

4243
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"),
44+
(("4.5.0+0.1.0", VersionIncrement.PATCH, None, 0, None), "4.5.0+0.1.1"),
45+
(("4.5.0+0.1.1", VersionIncrement.MINOR, None, 0, None), "4.5.0+0.2.0"),
46+
(("4.5.0+0.2.0", VersionIncrement.MAJOR, None, 0, None), "4.5.0+1.0.0"),
4647
]
4748

4849
# never bump backwards on pre-releases
4950
linear_prerelease_cases = [
50-
(("0.1.1b1", None, "alpha", 0, None), "0.1.1-b2"),
51-
(("0.1.1rc0", None, "alpha", 0, None), "0.1.1-rc1"),
52-
(("0.1.1rc0", None, "beta", 0, None), "0.1.1-rc1"),
51+
(("0.1.1b1", None, Prerelease.ALPHA, 0, None), "0.1.1-b2"),
52+
(("0.1.1rc0", None, Prerelease.ALPHA, 0, None), "0.1.1-rc1"),
53+
(("0.1.1rc0", None, Prerelease.BETA, 0, None), "0.1.1-rc1"),
5354
]
5455

5556
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-a1"),
60-
(("1a0", None, "alpha", 1, None), "1.0.0-a1"),
61-
(("1", None, "beta", 0, None), "1.0.0-b0"),
62-
(("1", None, "beta", 1, None), "1.0.0-b1"),
63-
(("1beta", None, "beta", 0, None), "1.0.0-b1"),
64-
(("1.0.0alpha1", None, "alpha", 0, None), "1.0.0-a2"),
65-
(("1", None, "rc", 0, None), "1.0.0-rc0"),
66-
(("1.0.0rc1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"),
57+
(("1.1", VersionIncrement.PATCH, None, 0, None), "1.1.1"),
58+
(("1", VersionIncrement.MINOR, None, 0, None), "1.1.0"),
59+
(("1", VersionIncrement.MAJOR, None, 0, None), "2.0.0"),
60+
(("1a0", None, Prerelease.ALPHA, 0, None), "1.0.0-a1"),
61+
(("1a0", None, Prerelease.ALPHA, 1, None), "1.0.0-a1"),
62+
(("1", None, Prerelease.BETA, 0, None), "1.0.0-b0"),
63+
(("1", None, Prerelease.BETA, 1, None), "1.0.0-b1"),
64+
(("1beta", None, Prerelease.BETA, 0, None), "1.0.0-b1"),
65+
(("1.0.0alpha1", None, Prerelease.ALPHA, 0, None), "1.0.0-a2"),
66+
(("1", None, Prerelease.RC, 0, None), "1.0.0-rc0"),
67+
(("1.0.0rc1+e20d7b57f3eb", VersionIncrement.PATCH, None, 0, None), "1.0.0"),
6768
]
6869

6970
# test driven development
7071
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-a0"),
75-
(("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-a0"),
76-
(("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-a0"),
77-
(("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-a1"),
78-
(("1.0.0a2", None, "beta", 0, None), "1.0.0-b0"),
79-
(("1.0.0a2", None, "beta", 1, None), "1.0.0-b1"),
80-
(("1.0.0beta1", None, "rc", 0, None), "1.0.0-rc0"),
81-
(("1.0.0rc1", None, "rc", 0, None), "1.0.0-rc2"),
82-
(("1.0.0-a0", None, "rc", 0, None), "1.0.0-rc0"),
83-
(("1.0.0-alpha1", None, "alpha", 0, None), "1.0.0-a2"),
72+
(("0.1.1", VersionIncrement.PATCH, None, 0, None), "0.1.2"),
73+
(("0.1.1", VersionIncrement.MINOR, None, 0, None), "0.2.0"),
74+
(("2.1.1", VersionIncrement.MAJOR, None, 0, None), "3.0.0"),
75+
(("0.9.0", VersionIncrement.PATCH, Prerelease.ALPHA, 0, None), "0.9.1-a0"),
76+
(("0.9.0", VersionIncrement.MINOR, Prerelease.ALPHA, 0, None), "0.10.0-a0"),
77+
(("0.9.0", VersionIncrement.MAJOR, Prerelease.ALPHA, 0, None), "1.0.0-a0"),
78+
(("0.9.0", VersionIncrement.MAJOR, Prerelease.ALPHA, 1, None), "1.0.0-a1"),
79+
(("1.0.0a2", None, Prerelease.BETA, 0, None), "1.0.0-b0"),
80+
(("1.0.0a2", None, Prerelease.BETA, 1, None), "1.0.0-b1"),
81+
(("1.0.0beta1", None, Prerelease.RC, 0, None), "1.0.0-rc0"),
82+
(("1.0.0rc1", None, Prerelease.RC, 0, None), "1.0.0-rc2"),
83+
(("1.0.0-a0", None, Prerelease.RC, 0, None), "1.0.0-rc0"),
84+
(("1.0.0-alpha1", None, Prerelease.ALPHA, 0, None), "1.0.0-a2"),
8485
]
8586

8687
exact_cases = [
87-
(("1.0.0", "PATCH", None, 0, None), "1.0.1"),
88-
(("1.0.0", "MINOR", None, 0, None), "1.1.0"),
88+
(("1.0.0", VersionIncrement.PATCH, None, 0, None), "1.0.1"),
89+
(("1.0.0", VersionIncrement.MINOR, None, 0, None), "1.1.0"),
8990
# with exact_increment=False: "1.0.0-b0"
90-
(("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"),
91+
(("1.0.0a1", VersionIncrement.PATCH, Prerelease.BETA, 0, None), "1.0.1-b0"),
9192
# with exact_increment=False: "1.0.0-b1"
92-
(("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"),
93+
(("1.0.0b0", VersionIncrement.PATCH, Prerelease.BETA, 0, None), "1.0.1-b0"),
9394
# with exact_increment=False: "1.0.0-rc0"
94-
(("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"),
95+
(("1.0.0b1", VersionIncrement.PATCH, Prerelease.RC, 0, None), "1.0.1-rc0"),
9596
# with exact_increment=False: "1.0.0-rc1"
96-
(("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"),
97+
(("1.0.0rc0", VersionIncrement.PATCH, Prerelease.RC, 0, None), "1.0.1-rc0"),
9798
# with exact_increment=False: "1.0.0-rc1-dev1"
98-
(("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"),
99+
(("1.0.0rc0", VersionIncrement.PATCH, Prerelease.RC, 0, 1), "1.0.1-rc0-dev1"),
99100
# with exact_increment=False: "1.0.0-b0"
100-
(("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"),
101+
(("1.0.0a1", VersionIncrement.MINOR, Prerelease.BETA, 0, None), "1.1.0-b0"),
101102
# with exact_increment=False: "1.0.0-b1"
102-
(("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"),
103+
(("1.0.0b0", VersionIncrement.MINOR, Prerelease.BETA, 0, None), "1.1.0-b0"),
103104
# with exact_increment=False: "1.0.0-rc0"
104-
(("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"),
105+
(("1.0.0b1", VersionIncrement.MINOR, Prerelease.RC, 0, None), "1.1.0-rc0"),
105106
# with exact_increment=False: "1.0.0-rc1"
106-
(("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"),
107+
(("1.0.0rc0", VersionIncrement.MINOR, Prerelease.RC, 0, None), "1.1.0-rc0"),
107108
# with exact_increment=False: "1.0.0-rc1-dev1"
108-
(("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"),
109+
(("1.0.0rc0", VersionIncrement.MINOR, Prerelease.RC, 0, 1), "1.1.0-rc0-dev1"),
109110
# with exact_increment=False: "2.0.0"
110-
(("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
111+
(("2.0.0b0", VersionIncrement.MAJOR, None, 0, None), "3.0.0"),
111112
# with exact_increment=False: "2.0.0"
112-
(("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
113+
(("2.0.0b0", VersionIncrement.MINOR, None, 0, None), "2.1.0"),
113114
# with exact_increment=False: "2.0.0"
114-
(("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
115+
(("2.0.0b0", VersionIncrement.PATCH, None, 0, None), "2.0.1"),
115116
# same with exact_increment=False
116-
(("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0-a0"),
117+
(("2.0.0b0", VersionIncrement.MAJOR, Prerelease.ALPHA, 0, None), "3.0.0-a0"),
117118
# with exact_increment=False: "2.0.0b1"
118-
(("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0-a0"),
119+
(("2.0.0b0", VersionIncrement.MINOR, Prerelease.ALPHA, 0, None), "2.1.0-a0"),
119120
# with exact_increment=False: "2.0.0b1"
120-
(("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1-a0"),
121+
(("2.0.0b0", VersionIncrement.PATCH, Prerelease.ALPHA, 0, None), "2.0.1-a0"),
121122
]
122123

123124

‎tests/test_version_scheme_semver2.py

Lines changed: 62 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,85 @@
33

44
import pytest
55

6-
from commitizen.version_schemes import SemVer2, VersionProtocol
6+
from commitizen.bump_rule import VersionIncrement
7+
from commitizen.version_schemes import Prerelease, SemVer2, VersionProtocol
78

89
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"),
10+
(("0.1.0", VersionIncrement.PATCH, None, 0, None), "0.1.1"),
11+
(("0.1.0", VersionIncrement.PATCH, None, 0, 1), "0.1.1-dev.1"),
12+
(("0.1.1", VersionIncrement.MINOR, None, 0, None), "0.2.0"),
13+
(("0.2.0", VersionIncrement.MINOR, None, 0, None), "0.3.0"),
14+
(("0.2.0", VersionIncrement.MINOR, None, 0, 1), "0.3.0-dev.1"),
15+
(("0.3.0", VersionIncrement.PATCH, None, 0, None), "0.3.1"),
16+
(("0.3.0", VersionIncrement.PATCH, Prerelease.ALPHA, 0, None), "0.3.1-alpha.0"),
17+
(("0.3.1-alpha.0", None, Prerelease.ALPHA, 0, None), "0.3.1-alpha.1"),
18+
(("0.3.0", VersionIncrement.PATCH, Prerelease.ALPHA, 1, None), "0.3.1-alpha.1"),
19+
(("0.3.1-alpha.0", None, Prerelease.ALPHA, 1, None), "0.3.1-alpha.1"),
1920
(("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"),
21+
(("0.3.1", VersionIncrement.PATCH, None, 0, None), "0.3.2"),
22+
(("0.4.2", VersionIncrement.MAJOR, Prerelease.ALPHA, 0, None), "1.0.0-alpha.0"),
23+
(("1.0.0-alpha.0", None, Prerelease.ALPHA, 0, None), "1.0.0-alpha.1"),
24+
(("1.0.0-alpha.1", None, Prerelease.ALPHA, 0, None), "1.0.0-alpha.2"),
25+
(("1.0.0-alpha.1", None, Prerelease.ALPHA, 0, 1), "1.0.0-alpha.2.dev.1"),
26+
(("1.0.0-alpha.2.dev.0", None, Prerelease.ALPHA, 0, 1), "1.0.0-alpha.3.dev.1"),
27+
(("1.0.0-alpha.2.dev.0", None, Prerelease.ALPHA, 0, 0), "1.0.0-alpha.3.dev.0"),
28+
(("1.0.0-alpha.1", None, Prerelease.BETA, 0, None), "1.0.0-beta.0"),
29+
(("1.0.0-beta.0", None, Prerelease.BETA, 0, None), "1.0.0-beta.1"),
30+
(("1.0.0-beta.1", None, Prerelease.RC, 0, None), "1.0.0-rc.0"),
31+
(("1.0.0-rc.0", None, Prerelease.RC, 0, None), "1.0.0-rc.1"),
32+
(("1.0.0-rc.0", None, Prerelease.RC, 0, 1), "1.0.0-rc.1.dev.1"),
33+
(("1.0.0-rc.0", VersionIncrement.PATCH, None, 0, None), "1.0.0"),
34+
(("1.0.0-alpha.3.dev.0", None, Prerelease.BETA, 0, None), "1.0.0-beta.0"),
35+
(("1.0.0", VersionIncrement.PATCH, None, 0, None), "1.0.1"),
36+
(("1.0.1", VersionIncrement.PATCH, None, 0, None), "1.0.2"),
37+
(("1.0.2", VersionIncrement.MINOR, None, 0, None), "1.1.0"),
38+
(("1.1.0", VersionIncrement.MINOR, None, 0, None), "1.2.0"),
39+
(("1.2.0", VersionIncrement.PATCH, None, 0, None), "1.2.1"),
40+
(("1.2.1", VersionIncrement.MAJOR, None, 0, None), "2.0.0"),
4041
]
4142

4243
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"),
44+
(("4.5.0+0.1.0", VersionIncrement.PATCH, None, 0, None), "4.5.0+0.1.1"),
45+
(("4.5.0+0.1.1", VersionIncrement.MINOR, None, 0, None), "4.5.0+0.2.0"),
46+
(("4.5.0+0.2.0", VersionIncrement.MAJOR, None, 0, None), "4.5.0+1.0.0"),
4647
]
4748

4849
# never bump backwards on pre-releases
4950
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"),
51+
(("0.1.1-beta.1", None, Prerelease.ALPHA, 0, None), "0.1.1-beta.2"),
52+
(("0.1.1-rc.0", None, Prerelease.ALPHA, 0, None), "0.1.1-rc.1"),
53+
(("0.1.1-rc.0", None, Prerelease.BETA, 0, None), "0.1.1-rc.1"),
5354
]
5455

5556
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"),
57+
(("1.1", VersionIncrement.PATCH, None, 0, None), "1.1.1"),
58+
(("1", VersionIncrement.MINOR, None, 0, None), "1.1.0"),
59+
(("1", VersionIncrement.MAJOR, None, 0, None), "2.0.0"),
60+
(("1-alpha.0", None, Prerelease.ALPHA, 0, None), "1.0.0-alpha.1"),
61+
(("1-alpha.0", None, Prerelease.ALPHA, 1, None), "1.0.0-alpha.1"),
62+
(("1", None, Prerelease.BETA, 0, None), "1.0.0-beta.0"),
63+
(("1", None, Prerelease.BETA, 1, None), "1.0.0-beta.1"),
64+
(("1-beta", None, Prerelease.BETA, 0, None), "1.0.0-beta.1"),
65+
(("1.0.0-alpha.1", None, Prerelease.ALPHA, 0, None), "1.0.0-alpha.2"),
66+
(("1", None, Prerelease.RC, 0, None), "1.0.0-rc.0"),
67+
(("1.0.0-rc.1+e20d7b57f3eb", VersionIncrement.PATCH, None, 0, None), "1.0.0"),
6768
]
6869

6970
# test driven development
7071
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"),
72+
(("0.1.1", VersionIncrement.PATCH, None, 0, None), "0.1.2"),
73+
(("0.1.1", VersionIncrement.MINOR, None, 0, None), "0.2.0"),
74+
(("2.1.1", VersionIncrement.MAJOR, None, 0, None), "3.0.0"),
75+
(("0.9.0", VersionIncrement.PATCH, Prerelease.ALPHA, 0, None), "0.9.1-alpha.0"),
76+
(("0.9.0", VersionIncrement.MINOR, Prerelease.ALPHA, 0, None), "0.10.0-alpha.0"),
77+
(("0.9.0", VersionIncrement.MAJOR, Prerelease.ALPHA, 0, None), "1.0.0-alpha.0"),
78+
(("0.9.0", VersionIncrement.MAJOR, Prerelease.ALPHA, 1, None), "1.0.0-alpha.1"),
79+
(("1.0.0-alpha.2", None, Prerelease.BETA, 0, None), "1.0.0-beta.0"),
80+
(("1.0.0-alpha.2", None, Prerelease.BETA, 1, None), "1.0.0-beta.1"),
81+
(("1.0.0-beta.1", None, Prerelease.RC, 0, None), "1.0.0-rc.0"),
82+
(("1.0.0-rc.1", None, Prerelease.RC, 0, None), "1.0.0-rc.2"),
83+
(("1.0.0-alpha.0", None, Prerelease.RC, 0, None), "1.0.0-rc.0"),
84+
(("1.0.0-alpha.1", None, Prerelease.ALPHA, 0, None), "1.0.0-alpha.2"),
8485
]
8586

8687

‎tests/test_version_schemes.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,35 @@
1212

1313
from commitizen.config.base_config import BaseConfig
1414
from commitizen.exceptions import VersionSchemeUnknown
15-
from commitizen.version_schemes import Pep440, SemVer, get_version_scheme
15+
from commitizen.version_schemes import Pep440, Prerelease, SemVer, get_version_scheme
16+
17+
18+
class TestPrereleaseSafeCast:
19+
def test_safe_cast_valid_strings(self):
20+
assert Prerelease.safe_cast("ALPHA") == Prerelease.ALPHA
21+
assert Prerelease.safe_cast("BETA") == Prerelease.BETA
22+
assert Prerelease.safe_cast("RC") == Prerelease.RC
23+
24+
def test_safe_cast_case_insensitive(self):
25+
assert Prerelease.safe_cast("alpha") == Prerelease.ALPHA
26+
assert Prerelease.safe_cast("beta") == Prerelease.BETA
27+
assert Prerelease.safe_cast("rc") == Prerelease.RC
28+
assert Prerelease.safe_cast("Alpha") == Prerelease.ALPHA
29+
assert Prerelease.safe_cast("Beta") == Prerelease.BETA
30+
assert Prerelease.safe_cast("Rc") == Prerelease.RC
31+
32+
def test_safe_cast_invalid_strings(self):
33+
assert Prerelease.safe_cast("invalid") is None
34+
assert Prerelease.safe_cast("") is None
35+
assert Prerelease.safe_cast("release") is None
36+
37+
def test_safe_cast_non_string_values(self):
38+
assert Prerelease.safe_cast(None) is None
39+
assert Prerelease.safe_cast(1) is None
40+
assert Prerelease.safe_cast(True) is None
41+
assert Prerelease.safe_cast([]) is None
42+
assert Prerelease.safe_cast({}) is None
43+
assert Prerelease.safe_cast(Prerelease.ALPHA) is None # enum value itself
1644

1745

1846
def test_default_version_scheme_is_pep440(config: BaseConfig):

0 commit comments

Comments
 (0)
Please sign in to comment.