Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8c7e2d3

Browse files
committedJun 8, 2025·
feat(bump_rule): add BumpRule, VersionIncrement, Prerelease Enum
Closes #129
1 parent d968f26 commit 8c7e2d3

14 files changed

+1342
-512
lines changed
 

‎commitizen/bump.py

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

33
import os
44
import re
5-
from collections import OrderedDict
65
from collections.abc import Iterable
76
from glob import iglob
8-
from logging import getLogger
97
from string import Template
10-
from typing import cast
118

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

6114

6215
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+
) -> None:
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: 16 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,
@@ -51,7 +52,7 @@ class BumpArgs(Settings, total=False):
5152
get_next: bool
5253
git_output_to_stderr: bool
5354
increment_mode: str
54-
increment: Increment | None
55+
increment: VersionIncrement | None
5556
local_version: bool
5657
manual_version: str | None
5758
no_verify: bool
@@ -144,21 +145,14 @@ def _is_initial_tag(
144145
)
145146
return bool(questionary.confirm("Is this the first tag created?").ask())
146147

147-
def _find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
148+
def _find_increment(self, commits: list[git.GitCommit]) -> VersionIncrement | None:
148149
# Update the bump map to ensure major version doesn't increment.
149-
# self.cz.bump_map = defaults.bump_map_major_version_zero
150-
bump_map = (
151-
self.cz.bump_map_major_version_zero
152-
if self.bump_settings["major_version_zero"]
153-
else self.cz.bump_map
154-
)
155-
bump_pattern = self.cz.bump_pattern
150+
is_major_version_zero = self.bump_settings["major_version_zero"]
156151

157-
if not bump_map or not bump_pattern:
158-
raise NoPatternMapError(
159-
f"'{self.config.settings['name']}' rule does not support bump"
160-
)
161-
return bump.find_increment(commits, regex=bump_pattern, increments_map=bump_map)
152+
return VersionIncrement.get_highest_by_messages(
153+
(commit.message for commit in commits),
154+
lambda x: self.cz.bump_rule.get_increment(x, is_major_version_zero),
155+
)
162156

163157
def __call__(self) -> None:
164158
"""Steps executed to bump."""
@@ -169,10 +163,11 @@ def __call__(self) -> None:
169163
except TypeError:
170164
raise NoVersionSpecifiedError()
171165

172-
increment = self.arguments["increment"]
173-
prerelease = self.arguments["prerelease"]
174-
devrelease = self.arguments["devrelease"]
175-
is_local_version = self.arguments["local_version"]
166+
dry_run: bool = self.arguments["dry_run"]
167+
increment = VersionIncrement.safe_cast(self.arguments["increment"])
168+
prerelease = Prerelease.safe_cast(self.arguments["prerelease"])
169+
devrelease: int | None = self.arguments["devrelease"]
170+
is_local_version: bool = self.arguments["local_version"]
176171
manual_version = self.arguments["manual_version"]
177172
build_metadata = self.arguments["build_metadata"]
178173
get_next = self.arguments["get_next"]
@@ -275,7 +270,7 @@ def __call__(self) -> None:
275270

276271
# we create an empty PATCH increment for empty tag
277272
if increment is None and allow_no_commit:
278-
increment = "PATCH"
273+
increment = VersionIncrement.PATCH
279274

280275
new_version = current_version.bump(
281276
increment,

‎commitizen/cz/base.py

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

33
from abc import ABCMeta, abstractmethod
44
from collections.abc import Iterable, Mapping
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
14+
from commitizen.exceptions import NoPatternMapError
1215
from commitizen.question import CzQuestion
1316

1417

@@ -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) -> Style:
8491
]
8592
) # type: ignore[return-value]
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
from typing import TypedDict
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.question import CzQuestion
@@ -27,6 +28,8 @@ class ConventionalCommitsAnswers(TypedDict):
2728

2829

2930
class ConventionalCommitsCz(BaseCommitizen):
31+
_bump_rule = ConventionalCommitBumpRule()
32+
3033
bump_pattern = defaults.BUMP_PATTERN
3134
bump_map = defaults.BUMP_MAP
3235
bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO

‎commitizen/defaults.py

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

9+
from commitizen.bump_rule import VersionIncrement
910
from commitizen.question import CzQuestion
1011

1112
# Type
@@ -113,31 +114,27 @@ class Settings(TypedDict, total=False):
113114
"extras": {},
114115
}
115116

116-
MAJOR = "MAJOR"
117-
MINOR = "MINOR"
118-
PATCH = "PATCH"
119-
120117
CHANGELOG_FORMAT = "markdown"
121118

122119
BUMP_PATTERN = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
123-
BUMP_MAP = OrderedDict(
120+
BUMP_MAP = dict(
124121
(
125-
(r"^.+!$", MAJOR),
126-
(r"^BREAKING[\-\ ]CHANGE", MAJOR),
127-
(r"^feat", MINOR),
128-
(r"^fix", PATCH),
129-
(r"^refactor", PATCH),
130-
(r"^perf", PATCH),
122+
(r"^.+!$", str(VersionIncrement.MAJOR)),
123+
(r"^BREAKING[\-\ ]CHANGE", str(VersionIncrement.MAJOR)),
124+
(r"^feat", str(VersionIncrement.MINOR)),
125+
(r"^fix", str(VersionIncrement.PATCH)),
126+
(r"^refactor", str(VersionIncrement.PATCH)),
127+
(r"^perf", str(VersionIncrement.PATCH)),
131128
)
132129
)
133-
BUMP_MAP_MAJOR_VERSION_ZERO = OrderedDict(
130+
BUMP_MAP_MAJOR_VERSION_ZERO = dict(
134131
(
135-
(r"^.+!$", MINOR),
136-
(r"^BREAKING[\-\ ]CHANGE", MINOR),
137-
(r"^feat", MINOR),
138-
(r"^fix", PATCH),
139-
(r"^refactor", PATCH),
140-
(r"^perf", PATCH),
132+
(r"^.+!$", str(VersionIncrement.MINOR)),
133+
(r"^BREAKING[\-\ ]CHANGE", str(VersionIncrement.MINOR)),
134+
(r"^feat", str(VersionIncrement.MINOR)),
135+
(r"^fix", str(VersionIncrement.PATCH)),
136+
(r"^refactor", str(VersionIncrement.PATCH)),
137+
(r"^perf", str(VersionIncrement.PATCH)),
141138
)
142139
)
143140
CHANGE_TYPE_ORDER = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"]

‎commitizen/version_schemes.py

Lines changed: 57 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 = re.compile(
4560
r"v?(?P<version>([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.]+)?(\w+)?)"
4661
)
@@ -128,7 +143,7 @@ def __ne__(self, other: object) -> bool:
128143

129144
def bump(
130145
self,
131-
increment: Increment | None,
146+
increment: VersionIncrement | None,
132147
prerelease: Prerelease | None = None,
133148
prerelease_offset: int = 0,
134149
devrelease: int | None = None,
@@ -173,7 +188,7 @@ def prerelease(self) -> str | None:
173188
return None
174189

175190
def generate_prerelease(
176-
self, prerelease: str | None = None, offset: int = 0
191+
self, prerelease: Prerelease | None = None, offset: int = 0
177192
) -> str:
178193
"""Generate prerelease
179194
@@ -188,20 +203,18 @@ def generate_prerelease(
188203
if not prerelease:
189204
return ""
190205

206+
prerelease_value = prerelease.value
207+
new_prerelease_number = offset
208+
191209
# prevent down-bumping the pre-release phase, e.g. from 'b1' to 'a2'
192210
# https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases
193211
# https://semver.org/#spec-item-11
194212
if self.is_prerelease and self.pre:
195-
prerelease = max(prerelease, self.pre[0])
213+
prerelease_value = max(prerelease_value, self.pre[0])
214+
if prerelease_value.startswith(self.pre[0]):
215+
new_prerelease_number = self.pre[1] + 1
196216

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

206219
def generate_devrelease(self, devrelease: int | None) -> str:
207220
"""Generate devrelease
@@ -225,26 +238,34 @@ def generate_build_metadata(self, build_metadata: str | None) -> str:
225238

226239
return f"+{build_metadata}"
227240

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

233-
if increment == MAJOR:
234-
base[MAJOR] += 1
235-
base[MINOR] = 0
236-
base[PATCH] = 0
237-
elif increment == MINOR:
238-
base[MINOR] += 1
239-
base[PATCH] = 0
240-
elif increment == PATCH:
241-
base[PATCH] += 1
254+
if increment == VersionIncrement.MAJOR:
255+
base[VersionIncrement.MAJOR] += 1
256+
base[VersionIncrement.MINOR] = 0
257+
base[VersionIncrement.PATCH] = 0
258+
elif increment == VersionIncrement.MINOR:
259+
base[VersionIncrement.MINOR] += 1
260+
base[VersionIncrement.PATCH] = 0
261+
elif increment == VersionIncrement.PATCH:
262+
base[VersionIncrement.PATCH] += 1
242263

243-
return f"{base[MAJOR]}.{base[MINOR]}.{base[PATCH]}"
264+
return f"{base[VersionIncrement.MAJOR]}.{base[VersionIncrement.MINOR]}.{base[VersionIncrement.PATCH]}"
244265

245266
def bump(
246267
self,
247-
increment: Increment | None,
268+
increment: VersionIncrement | None,
248269
prerelease: Prerelease | None = None,
249270
prerelease_offset: int = 0,
250271
devrelease: int | None = None,
@@ -286,13 +307,16 @@ def bump(
286307
) # type: ignore
287308

288309
def _get_increment_base(
289-
self, increment: Increment | None, exact_increment: bool
310+
self, increment: VersionIncrement | None, exact_increment: bool
290311
) -> str:
291312
if (
292313
not self.is_prerelease
293314
or exact_increment
294-
or (increment == MINOR and self.micro != 0)
295-
or (increment == MAJOR and (self.minor != 0 or self.micro != 0))
315+
or (increment == VersionIncrement.MINOR and self.micro != 0)
316+
or (
317+
increment == VersionIncrement.MAJOR
318+
and (self.minor != 0 or self.micro != 0)
319+
)
296320
):
297321
return self.increment_base(increment)
298322
return f"{self.major}.{self.minor}.{self.micro}"

‎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, defaults, git, hooks
16+
from commitizen.bump_rule import VersionIncrement
1617
from commitizen.changelog_formats import ChangelogFormat
1718
from commitizen.config.base_config import BaseConfig
1819
from commitizen.cz.base import BaseCommitizen
@@ -1001,7 +1002,7 @@ def test_bump_with_pre_bump_hooks(
10011002
new_version="0.2.0",
10021003
new_tag_version="0.2.0",
10031004
message="bump: version 0.1.0 → 0.2.0",
1004-
increment="MINOR",
1005+
increment=VersionIncrement.MINOR,
10051006
changelog_file_name=None,
10061007
),
10071008
call(
@@ -1013,7 +1014,7 @@ def test_bump_with_pre_bump_hooks(
10131014
current_version="0.2.0",
10141015
current_tag_version="0.2.0",
10151016
message="bump: version 0.1.0 → 0.2.0",
1016-
increment="MINOR",
1017+
increment=VersionIncrement.MINOR,
10171018
changelog_file_name=None,
10181019
),
10191020
]

‎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.