From 03aa5968363d683c4beaca849de83e4d46258e2c Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 01:25:19 +0800
Subject: [PATCH 01/11] refactor(bump_rule): add bump_rule interface,
 DefaultBumpRule and its tests

---
 commitizen/bump_rule.py                       | 63 ++++++++++++
 .../conventional_commits.py                   |  8 +-
 commitizen/cz/customize/customize.py          |  6 +-
 commitizen/defaults.py                        |  6 +-
 tests/test_bump_rule.py                       | 95 +++++++++++++++++++
 5 files changed, 168 insertions(+), 10 deletions(-)
 create mode 100644 commitizen/bump_rule.py
 create mode 100644 tests/test_bump_rule.py

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
new file mode 100644
index 000000000..d9fb473b9
--- /dev/null
+++ b/commitizen/bump_rule.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import re
+from functools import cached_property
+from typing import Protocol
+
+from commitizen.version_schemes import Increment
+
+
+class BumpRule(Protocol):
+    def get_increment(
+        self, commit_message: str, major_version_zero: bool
+    ) -> Increment | None: ...
+
+
+class DefaultBumpRule(BumpRule):
+    _PATCH_CHANGE_TYPES = set(["fix", "perf", "refactor"])
+
+    def get_increment(
+        self, commit_message: str, major_version_zero: bool
+    ) -> Increment | None:
+        if not (m := self._head_pattern.match(commit_message)):
+            return None
+
+        change_type = m.group("change_type")
+        if m.group("bang") or change_type == "BREAKING CHANGE":
+            return "MAJOR" if major_version_zero else "MINOR"
+
+        if change_type == "feat":
+            return "MINOR"
+
+        if change_type in self._PATCH_CHANGE_TYPES:
+            return "PATCH"
+
+        return None
+
+    @cached_property
+    def _head_pattern(self) -> re.Pattern:
+        change_types = [
+            r"BREAKING[\-\ ]CHANGE",
+            "fix",
+            "feat",
+            "docs",
+            "style",
+            "refactor",
+            "perf",
+            "test",
+            "build",
+            "ci",
+        ]
+        re_change_type = r"(?P<change_type>" + "|".join(change_types) + r")"
+        re_scope = r"(?P<scope>\(.+\))?"
+        re_bang = r"(?P<bang>!)?"
+        return re.compile(f"^{re_change_type}{re_scope}{re_bang}:")
+
+
+class CustomBumpRule(BumpRule):
+    """TODO: Implement"""
+
+    def get_increment(
+        self, commit_message: str, major_version_zero: bool
+    ) -> Increment | None:
+        return None
diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py
index c7b88258c..af29a209f 100644
--- a/commitizen/cz/conventional_commits/conventional_commits.py
+++ b/commitizen/cz/conventional_commits/conventional_commits.py
@@ -28,9 +28,9 @@ def parse_subject(text):
 
 
 class ConventionalCommitsCz(BaseCommitizen):
-    bump_pattern = defaults.bump_pattern
-    bump_map = defaults.bump_map
-    bump_map_major_version_zero = defaults.bump_map_major_version_zero
+    bump_pattern = defaults.BUMP_PATTERN
+    bump_map = defaults.BUMP_MAP
+    bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO
     commit_parser = r"^((?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):\s(?P<message>.*)?"  # noqa
     change_type_map = {
         "feat": "Feat",
@@ -38,7 +38,7 @@ class ConventionalCommitsCz(BaseCommitizen):
         "refactor": "Refactor",
         "perf": "Perf",
     }
-    changelog_pattern = defaults.bump_pattern
+    changelog_pattern = defaults.BUMP_PATTERN
 
     def questions(self) -> Questions:
         questions: Questions = [
diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py
index d53ae29f1..de29fa984 100644
--- a/commitizen/cz/customize/customize.py
+++ b/commitizen/cz/customize/customize.py
@@ -21,9 +21,9 @@
 
 
 class CustomizeCommitsCz(BaseCommitizen):
-    bump_pattern = defaults.bump_pattern
-    bump_map = defaults.bump_map
-    bump_map_major_version_zero = defaults.bump_map_major_version_zero
+    bump_pattern = defaults.BUMP_PATTERN
+    bump_map = defaults.BUMP_MAP
+    bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO
     change_type_order = defaults.change_type_order
 
     def __init__(self, config: BaseConfig):
diff --git a/commitizen/defaults.py b/commitizen/defaults.py
index 0b78e1b0b..c61c54cbb 100644
--- a/commitizen/defaults.py
+++ b/commitizen/defaults.py
@@ -114,8 +114,8 @@ class Settings(TypedDict, total=False):
 
 CHANGELOG_FORMAT = "markdown"
 
-bump_pattern = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
-bump_map = OrderedDict(
+BUMP_PATTERN = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
+BUMP_MAP = OrderedDict(
     (
         (r"^.+!$", MAJOR),
         (r"^BREAKING[\-\ ]CHANGE", MAJOR),
@@ -125,7 +125,7 @@ class Settings(TypedDict, total=False):
         (r"^perf", PATCH),
     )
 )
-bump_map_major_version_zero = OrderedDict(
+BUMP_MAP_MAJOR_VERSION_ZERO = OrderedDict(
     (
         (r"^.+!$", MINOR),
         (r"^BREAKING[\-\ ]CHANGE", MINOR),
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
new file mode 100644
index 000000000..a47575d36
--- /dev/null
+++ b/tests/test_bump_rule.py
@@ -0,0 +1,95 @@
+import pytest
+
+from commitizen.bump_rule import DefaultBumpRule
+from commitizen.defaults import MAJOR, MINOR, PATCH
+
+
+@pytest.fixture
+def bump_rule():
+    return DefaultBumpRule()
+
+
+class TestDefaultBumpRule:
+    def test_feat_commit(self, bump_rule):
+        assert bump_rule.get_increment("feat: add new feature", False) == MINOR
+        assert bump_rule.get_increment("feat: add new feature", True) == MINOR
+
+    def test_fix_commit(self, bump_rule):
+        assert bump_rule.get_increment("fix: fix bug", False) == PATCH
+        assert bump_rule.get_increment("fix: fix bug", True) == PATCH
+
+    def test_perf_commit(self, bump_rule):
+        assert bump_rule.get_increment("perf: improve performance", False) == PATCH
+        assert bump_rule.get_increment("perf: improve performance", True) == PATCH
+
+    def test_refactor_commit(self, bump_rule):
+        assert bump_rule.get_increment("refactor: restructure code", False) == PATCH
+        assert bump_rule.get_increment("refactor: restructure code", True) == PATCH
+
+    def test_breaking_change_with_bang(self, bump_rule):
+        assert bump_rule.get_increment("feat!: breaking change", False) == MINOR
+        assert bump_rule.get_increment("feat!: breaking change", True) == MAJOR
+
+    def test_breaking_change_type(self, bump_rule):
+        assert bump_rule.get_increment("BREAKING CHANGE: major change", False) == MINOR
+        assert bump_rule.get_increment("BREAKING CHANGE: major change", True) == MAJOR
+
+    def test_commit_with_scope(self, bump_rule):
+        assert bump_rule.get_increment("feat(api): add new endpoint", False) == MINOR
+        assert bump_rule.get_increment("fix(ui): fix button alignment", False) == PATCH
+
+    def test_commit_with_complex_scopes(self, bump_rule):
+        # Test with multiple word scopes
+        assert (
+            bump_rule.get_increment("feat(user_management): add user roles", False)
+            == MINOR
+        )
+        assert (
+            bump_rule.get_increment("fix(database_connection): handle timeout", False)
+            == PATCH
+        )
+
+        # Test with nested scopes
+        assert (
+            bump_rule.get_increment("feat(api/auth): implement OAuth", False) == MINOR
+        )
+        assert (
+            bump_rule.get_increment("fix(ui/components): fix dropdown", False) == PATCH
+        )
+
+        # Test with breaking changes and scopes
+        assert (
+            bump_rule.get_increment("feat(api)!: remove deprecated endpoints", False)
+            == MINOR
+        )
+        assert (
+            bump_rule.get_increment("feat(api)!: remove deprecated endpoints", True)
+            == MAJOR
+        )
+
+        # Test with BREAKING CHANGE and scopes
+        assert (
+            bump_rule.get_increment(
+                "BREAKING CHANGE(api): remove deprecated endpoints", False
+            )
+            == MINOR
+        )
+        assert (
+            bump_rule.get_increment(
+                "BREAKING CHANGE(api): remove deprecated endpoints", True
+            )
+            == MAJOR
+        )
+
+    def test_invalid_commit_message(self, bump_rule):
+        assert bump_rule.get_increment("invalid commit message", False) is None
+        assert bump_rule.get_increment("", False) is None
+        assert bump_rule.get_increment("feat", False) is None
+
+    def test_other_commit_types(self, bump_rule):
+        # These commit types should not trigger any version bump
+        assert bump_rule.get_increment("docs: update documentation", False) is None
+        assert bump_rule.get_increment("style: format code", False) is None
+        assert bump_rule.get_increment("test: add unit tests", False) is None
+        assert bump_rule.get_increment("build: update build config", False) is None
+        assert bump_rule.get_increment("ci: update CI pipeline", False) is None

From 4dea3fa25f8552bfd278a01705f2a27270a54de0 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 02:27:39 +0800
Subject: [PATCH 02/11] refactor(bump_rule): add find_increment_by_callable

---
 commitizen/bump.py      |  1 +
 commitizen/bump_rule.py | 33 +++++++++++++++++-
 tests/test_bump_rule.py | 74 ++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 106 insertions(+), 2 deletions(-)

diff --git a/commitizen/bump.py b/commitizen/bump.py
index adfab64cb..6412df6f0 100644
--- a/commitizen/bump.py
+++ b/commitizen/bump.py
@@ -18,6 +18,7 @@
 logger = getLogger("commitizen")
 
 
+# TODO: replace this with find_increment_by_callable?
 def find_increment(
     commits: list[GitCommit], regex: str, increments_map: dict | OrderedDict
 ) -> Increment | None:
diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index d9fb473b9..9b42db805 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -2,10 +2,41 @@
 
 import re
 from functools import cached_property
-from typing import Protocol
+from typing import Callable, Protocol
 
 from commitizen.version_schemes import Increment
 
+_VERSION_ORDERING = dict(zip((None, "PATCH", "MINOR", "MAJOR"), range(4)))
+
+
+def find_increment_by_callable(
+    commit_messages: list[str], get_increment: Callable[[str], Increment | None]
+) -> Increment | None:
+    """Find the highest version increment from a list of messages.
+
+    This function processes a list of messages and determines the highest version
+    increment needed based on the commit messages. It splits multi-line commit messages
+    and evaluates each line using the provided get_increment callable.
+
+    Args:
+        commit_messages: A list of messages to analyze.
+        get_increment: A callable that takes a commit message string and returns an
+            Increment value (MAJOR, MINOR, PATCH) or None if no increment is needed.
+
+    Returns:
+        The highest version increment needed (MAJOR, MINOR, PATCH) or None if no
+        increment is needed. The order of precedence is MAJOR > MINOR > PATCH.
+
+    Example:
+        >>> commit_messages = ["feat: new feature", "fix: bug fix"]
+        >>> rule = DefaultBumpRule()
+        >>> find_increment_by_callable(commit_messages, lambda x: rule.get_increment(x, False))
+        'MINOR'
+    """
+    lines = (line for message in commit_messages for line in message.split("\n"))
+    increments = map(get_increment, lines)
+    return max(increments, key=lambda x: _VERSION_ORDERING[x], default=None)
+
 
 class BumpRule(Protocol):
     def get_increment(
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index a47575d36..920ef6c3b 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -1,6 +1,6 @@
 import pytest
 
-from commitizen.bump_rule import DefaultBumpRule
+from commitizen.bump_rule import DefaultBumpRule, find_increment_by_callable
 from commitizen.defaults import MAJOR, MINOR, PATCH
 
 
@@ -93,3 +93,75 @@ def test_other_commit_types(self, bump_rule):
         assert bump_rule.get_increment("test: add unit tests", False) is None
         assert bump_rule.get_increment("build: update build config", False) is None
         assert bump_rule.get_increment("ci: update CI pipeline", False) is None
+
+
+class TestFindIncrementByCallable:
+    @pytest.fixture
+    def get_increment(self, bump_rule):
+        return lambda x: bump_rule.get_increment(x, False)
+
+    def test_single_commit(self, get_increment):
+        commit_messages = ["feat: add new feature"]
+        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+
+    def test_multiple_commits(self, get_increment):
+        commit_messages = [
+            "feat: new feature",
+            "fix: bug fix",
+            "docs: update readme",
+        ]
+        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+
+    def test_breaking_change(self, get_increment):
+        commit_messages = [
+            "feat: new feature",
+            "feat!: breaking change",
+        ]
+        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+
+    def test_multi_line_commit(self, get_increment):
+        commit_messages = [
+            "feat: new feature\n\nBREAKING CHANGE: major change",
+        ]
+        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+
+    def test_no_increment_needed(self, get_increment):
+        commit_messages = [
+            "docs: update documentation",
+            "style: format code",
+        ]
+        assert find_increment_by_callable(commit_messages, get_increment) is None
+
+    def test_empty_commits(self, get_increment):
+        commit_messages = []
+        assert find_increment_by_callable(commit_messages, get_increment) is None
+
+    def test_major_version_zero(self):
+        bump_rule = DefaultBumpRule()
+
+        commit_messages = [
+            "feat!: breaking change",
+            "BREAKING CHANGE: major change",
+        ]
+        assert (
+            find_increment_by_callable(
+                commit_messages, lambda x: bump_rule.get_increment(x, True)
+            )
+            == MAJOR
+        )
+
+    def test_mixed_commit_types(self, get_increment):
+        commit_messages = [
+            "feat: new feature",
+            "fix: bug fix",
+            "perf: improve performance",
+            "refactor: restructure code",
+        ]
+        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+
+    def test_commit_with_scope(self, get_increment):
+        commit_messages = [
+            "feat(api): add new endpoint",
+            "fix(ui): fix button alignment",
+        ]
+        assert find_increment_by_callable(commit_messages, get_increment) == MINOR

From 57205e4762b64d50ec8525c15e04b12a54c54f09 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 02:59:37 +0800
Subject: [PATCH 03/11] refactor(bump_rule): deprecate wip

---
 commitizen/bump_rule.py                           | 15 +++++----------
 commitizen/commands/bump.py                       |  9 ++++++++-
 commitizen/cz/base.py                             |  5 +++++
 .../conventional_commits/conventional_commits.py  |  3 +++
 tests/test_bump_rule.py                           |  8 ++++----
 5 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index 9b42db805..67c147424 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import re
+from collections.abc import Iterable
 from functools import cached_property
 from typing import Callable, Protocol
 
@@ -10,7 +11,7 @@
 
 
 def find_increment_by_callable(
-    commit_messages: list[str], get_increment: Callable[[str], Increment | None]
+    commit_messages: Iterable[str], get_increment: Callable[[str], Increment | None]
 ) -> Increment | None:
     """Find the highest version increment from a list of messages.
 
@@ -29,7 +30,7 @@ def find_increment_by_callable(
 
     Example:
         >>> commit_messages = ["feat: new feature", "fix: bug fix"]
-        >>> rule = DefaultBumpRule()
+        >>> rule = ConventionalCommitBumpRule()
         >>> find_increment_by_callable(commit_messages, lambda x: rule.get_increment(x, False))
         'MINOR'
     """
@@ -44,7 +45,7 @@ def get_increment(
     ) -> Increment | None: ...
 
 
-class DefaultBumpRule(BumpRule):
+class ConventionalCommitBumpRule(BumpRule):
     _PATCH_CHANGE_TYPES = set(["fix", "perf", "refactor"])
 
     def get_increment(
@@ -85,10 +86,4 @@ def _head_pattern(self) -> re.Pattern:
         return re.compile(f"^{re_change_type}{re_scope}{re_bang}:")
 
 
-class CustomBumpRule(BumpRule):
-    """TODO: Implement"""
-
-    def get_increment(
-        self, commit_message: str, major_version_zero: bool
-    ) -> Increment | None:
-        return None
+# TODO: Implement CustomBumpRule
diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py
index 0a2bbe37f..cf588e87d 100644
--- a/commitizen/commands/bump.py
+++ b/commitizen/commands/bump.py
@@ -7,6 +7,7 @@
 import questionary
 
 from commitizen import bump, factory, git, hooks, out
+from commitizen.bump_rule import find_increment_by_callable
 from commitizen.changelog_formats import get_changelog_format
 from commitizen.commands.changelog import Changelog
 from commitizen.config import BaseConfig
@@ -122,7 +123,13 @@ def is_initial_tag(
     def find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
         # Update the bump map to ensure major version doesn't increment.
         is_major_version_zero: bool = self.bump_settings["major_version_zero"]
-        # self.cz.bump_map = defaults.bump_map_major_version_zero
+
+        if rule := self.cz.bump_rule:
+            return find_increment_by_callable(
+                (commit.message for commit in commits),
+                lambda x: rule.get_increment(x, is_major_version_zero),
+            )
+
         bump_map = (
             self.cz.bump_map_major_version_zero
             if is_major_version_zero
diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py
index 43455a74c..dd48dd718 100644
--- a/commitizen/cz/base.py
+++ b/commitizen/cz/base.py
@@ -8,6 +8,7 @@
 from prompt_toolkit.styles import Style, merge_styles
 
 from commitizen import git
+from commitizen.bump_rule import BumpRule
 from commitizen.config.base_config import BaseConfig
 from commitizen.defaults import Questions
 
@@ -25,9 +26,13 @@ def __call__(
 
 
 class BaseCommitizen(metaclass=ABCMeta):
+    bump_rule: BumpRule | None = None
+
+    # TODO: deprecate these
     bump_pattern: str | None = None
     bump_map: dict[str, str] | None = None
     bump_map_major_version_zero: dict[str, str] | None = None
+
     default_style_config: list[tuple[str, str]] = [
         ("qmark", "fg:#ff9d00 bold"),
         ("question", "bold"),
diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py
index af29a209f..9c696bc39 100644
--- a/commitizen/cz/conventional_commits/conventional_commits.py
+++ b/commitizen/cz/conventional_commits/conventional_commits.py
@@ -2,6 +2,7 @@
 import re
 
 from commitizen import defaults
+from commitizen.bump_rule import ConventionalCommitBumpRule
 from commitizen.cz.base import BaseCommitizen
 from commitizen.cz.utils import multiple_line_breaker, required_validator
 from commitizen.defaults import Questions
@@ -28,6 +29,8 @@ def parse_subject(text):
 
 
 class ConventionalCommitsCz(BaseCommitizen):
+    bump_rule = ConventionalCommitBumpRule()
+
     bump_pattern = defaults.BUMP_PATTERN
     bump_map = defaults.BUMP_MAP
     bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index 920ef6c3b..dae8942f1 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -1,15 +1,15 @@
 import pytest
 
-from commitizen.bump_rule import DefaultBumpRule, find_increment_by_callable
+from commitizen.bump_rule import ConventionalCommitBumpRule, find_increment_by_callable
 from commitizen.defaults import MAJOR, MINOR, PATCH
 
 
 @pytest.fixture
 def bump_rule():
-    return DefaultBumpRule()
+    return ConventionalCommitBumpRule()
 
 
-class TestDefaultBumpRule:
+class TestConventionalCommitBumpRule:
     def test_feat_commit(self, bump_rule):
         assert bump_rule.get_increment("feat: add new feature", False) == MINOR
         assert bump_rule.get_increment("feat: add new feature", True) == MINOR
@@ -137,7 +137,7 @@ def test_empty_commits(self, get_increment):
         assert find_increment_by_callable(commit_messages, get_increment) is None
 
     def test_major_version_zero(self):
-        bump_rule = DefaultBumpRule()
+        bump_rule = ConventionalCommitBumpRule()
 
         commit_messages = [
             "feat!: breaking change",

From afa0063d00592c716ada2ca834ae53ed2c4509b7 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 03:23:56 +0800
Subject: [PATCH 04/11] refactor(bump_rule): add old school bump rule for
 backward compatibility

---
 commitizen/bump_rule.py |  36 ++++++++
 tests/test_bump_rule.py | 197 +++++++++++++++++++++++++++++++++++++++-
 2 files changed, 231 insertions(+), 2 deletions(-)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index 67c147424..934cf22b2 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -5,6 +5,7 @@
 from functools import cached_property
 from typing import Callable, Protocol
 
+from commitizen.exceptions import NoPatternMapError
 from commitizen.version_schemes import Increment
 
 _VERSION_ORDERING = dict(zip((None, "PATCH", "MINOR", "MAJOR"), range(4)))
@@ -86,4 +87,39 @@ def _head_pattern(self) -> re.Pattern:
         return re.compile(f"^{re_change_type}{re_scope}{re_bang}:")
 
 
+class OldSchoolBumpRule(BumpRule):
+    """TODO: rename?"""
+
+    def __init__(
+        self,
+        bump_pattern: str,
+        bump_map: dict[str, Increment],
+        bump_map_major_version_zero: dict[str, Increment],
+    ):
+        if not bump_map or not bump_pattern:
+            raise NoPatternMapError(
+                f"Invalid bump rule: {bump_pattern=} and {bump_map=}"
+            )
+
+        self.bump_pattern = re.compile(bump_pattern)
+        self.bump_map = bump_map
+        self.bump_map_major_version_zero = bump_map_major_version_zero
+
+    def get_increment(
+        self, commit_message: str, major_version_zero: bool
+    ) -> Increment | None:
+        if not (m := self.bump_pattern.search(commit_message)):
+            return None
+
+        bump_map = (
+            self.bump_map_major_version_zero if major_version_zero else self.bump_map
+        )
+
+        found_keyword = m.group(1)
+        for match_pattern, increment in bump_map.items():
+            if re.match(match_pattern, found_keyword):
+                return increment
+        return None
+
+
 # TODO: Implement CustomBumpRule
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index dae8942f1..5eeab7497 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -1,7 +1,19 @@
 import pytest
 
-from commitizen.bump_rule import ConventionalCommitBumpRule, find_increment_by_callable
-from commitizen.defaults import MAJOR, MINOR, PATCH
+from commitizen.bump_rule import (
+    ConventionalCommitBumpRule,
+    OldSchoolBumpRule,
+    find_increment_by_callable,
+)
+from commitizen.defaults import (
+    BUMP_MAP,
+    BUMP_MAP_MAJOR_VERSION_ZERO,
+    BUMP_PATTERN,
+    MAJOR,
+    MINOR,
+    PATCH,
+)
+from commitizen.exceptions import NoPatternMapError
 
 
 @pytest.fixture
@@ -165,3 +177,184 @@ def test_commit_with_scope(self, get_increment):
             "fix(ui): fix button alignment",
         ]
         assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+
+
+class TestOldSchoolBumpRule:
+    @pytest.fixture
+    def bump_pattern(self):
+        return r"^.*?\[(.*?)\].*$"
+
+    @pytest.fixture
+    def bump_map(self):
+        return {
+            "MAJOR": MAJOR,
+            "MINOR": MINOR,
+            "PATCH": PATCH,
+        }
+
+    @pytest.fixture
+    def bump_map_major_version_zero(self):
+        return {
+            "MAJOR": MINOR,  # MAJOR becomes MINOR in version zero
+            "MINOR": MINOR,
+            "PATCH": PATCH,
+        }
+
+    @pytest.fixture
+    def old_school_rule(self, bump_pattern, bump_map, bump_map_major_version_zero):
+        return OldSchoolBumpRule(bump_pattern, bump_map, bump_map_major_version_zero)
+
+    def test_major_version(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("feat: add new feature [MAJOR]", False)
+            == MAJOR
+        )
+        assert old_school_rule.get_increment("fix: bug fix [MAJOR]", False) == MAJOR
+
+    def test_minor_version(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("feat: add new feature [MINOR]", False)
+            == MINOR
+        )
+        assert old_school_rule.get_increment("fix: bug fix [MINOR]", False) == MINOR
+
+    def test_patch_version(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("feat: add new feature [PATCH]", False)
+            == PATCH
+        )
+        assert old_school_rule.get_increment("fix: bug fix [PATCH]", False) == PATCH
+
+    def test_major_version_zero(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("feat: add new feature [MAJOR]", True)
+            == MINOR
+        )
+        assert old_school_rule.get_increment("fix: bug fix [MAJOR]", True) == MINOR
+
+    def test_no_match(self, old_school_rule):
+        assert old_school_rule.get_increment("feat: add new feature", False) is None
+        assert old_school_rule.get_increment("fix: bug fix", False) is None
+
+    def test_invalid_pattern(self, bump_map, bump_map_major_version_zero):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule("", bump_map, bump_map_major_version_zero)
+
+    def test_invalid_bump_map(self, bump_pattern):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule(bump_pattern, {}, {})
+
+    def test_complex_pattern(self):
+        pattern = r"^.*?\[(.*?)\].*?\[(.*?)\].*$"
+        bump_map = {
+            "MAJOR": MAJOR,
+            "MINOR": MINOR,
+            "PATCH": PATCH,
+        }
+        rule = OldSchoolBumpRule(pattern, bump_map, bump_map)
+
+        assert (
+            rule.get_increment("feat: add new feature [MAJOR] [MINOR]", False) == MAJOR
+        )
+        assert rule.get_increment("fix: bug fix [MINOR] [PATCH]", False) == MINOR
+
+    def test_with_find_increment_by_callable(self, old_school_rule):
+        commit_messages = [
+            "feat: add new feature [MAJOR]",
+            "fix: bug fix [PATCH]",
+            "docs: update readme [MINOR]",
+        ]
+        assert (
+            find_increment_by_callable(
+                commit_messages, lambda x: old_school_rule.get_increment(x, False)
+            )
+            == MAJOR
+        )
+
+
+class TestOldSchoolBumpRuleWithDefault:
+    @pytest.fixture
+    def old_school_rule(self):
+        return OldSchoolBumpRule(BUMP_PATTERN, BUMP_MAP, BUMP_MAP_MAJOR_VERSION_ZERO)
+
+    def test_breaking_change_with_bang(self, old_school_rule):
+        assert old_school_rule.get_increment("feat!: breaking change", False) == MAJOR
+        assert old_school_rule.get_increment("fix!: breaking change", False) == MAJOR
+        assert old_school_rule.get_increment("feat!: breaking change", True) == MINOR
+        assert old_school_rule.get_increment("fix!: breaking change", True) == MINOR
+
+    def test_breaking_change_type(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("BREAKING CHANGE: major change", False)
+            == MAJOR
+        )
+        assert (
+            old_school_rule.get_increment("BREAKING-CHANGE: major change", False)
+            == MAJOR
+        )
+        assert (
+            old_school_rule.get_increment("BREAKING CHANGE: major change", True)
+            == MINOR
+        )
+        assert (
+            old_school_rule.get_increment("BREAKING-CHANGE: major change", True)
+            == MINOR
+        )
+
+    def test_feat_commit(self, old_school_rule):
+        assert old_school_rule.get_increment("feat: add new feature", False) == MINOR
+        assert old_school_rule.get_increment("feat: add new feature", True) == MINOR
+
+    def test_fix_commit(self, old_school_rule):
+        assert old_school_rule.get_increment("fix: fix bug", False) == PATCH
+        assert old_school_rule.get_increment("fix: fix bug", True) == PATCH
+
+    def test_refactor_commit(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("refactor: restructure code", False) == PATCH
+        )
+        assert (
+            old_school_rule.get_increment("refactor: restructure code", True) == PATCH
+        )
+
+    def test_perf_commit(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("perf: improve performance", False) == PATCH
+        )
+        assert old_school_rule.get_increment("perf: improve performance", True) == PATCH
+
+    def test_commit_with_scope(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("feat(api): add new endpoint", False) == MINOR
+        )
+        assert (
+            old_school_rule.get_increment("fix(ui): fix button alignment", False)
+            == PATCH
+        )
+        assert (
+            old_school_rule.get_increment("refactor(core): restructure", False) == PATCH
+        )
+
+    def test_no_match(self, old_school_rule):
+        assert (
+            old_school_rule.get_increment("docs: update documentation", False) is None
+        )
+        assert old_school_rule.get_increment("style: format code", False) is None
+        assert old_school_rule.get_increment("test: add unit tests", False) is None
+        assert (
+            old_school_rule.get_increment("build: update build config", False) is None
+        )
+        assert old_school_rule.get_increment("ci: update CI pipeline", False) is None
+
+    def test_with_find_increment_by_callable(self, old_school_rule):
+        commit_messages = [
+            "feat!: breaking change",
+            "fix: bug fix",
+            "perf: improve performance",
+        ]
+        assert (
+            find_increment_by_callable(
+                commit_messages, lambda x: old_school_rule.get_increment(x, False)
+            )
+            == MAJOR
+        )

From facb51c1e8ea7fb6c5d64eabf926cedb368f87b4 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 03:30:44 +0800
Subject: [PATCH 05/11] test(bump_rule): raise error

---
 commitizen/bump_rule.py |  4 ++--
 tests/test_bump_rule.py | 20 ++++++++++++++++++++
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index 934cf22b2..c6865d3d6 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -96,9 +96,9 @@ def __init__(
         bump_map: dict[str, Increment],
         bump_map_major_version_zero: dict[str, Increment],
     ):
-        if not bump_map or not bump_pattern:
+        if not bump_map or not bump_pattern or not bump_map_major_version_zero:
             raise NoPatternMapError(
-                f"Invalid bump rule: {bump_pattern=} and {bump_map=}"
+                f"Invalid bump rule: {bump_pattern=} and {bump_map=} and {bump_map_major_version_zero=}"
             )
 
         self.bump_pattern = re.compile(bump_pattern)
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index 5eeab7497..c2bf76d7d 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -244,6 +244,26 @@ def test_invalid_bump_map(self, bump_pattern):
         with pytest.raises(NoPatternMapError):
             OldSchoolBumpRule(bump_pattern, {}, {})
 
+    def test_invalid_bump_map_major_version_zero(self, bump_pattern, bump_map):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule(bump_pattern, bump_map, {})
+
+    def test_all_invalid(self):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule("", {}, {})
+
+    def test_none_values(self):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule(None, {}, {})
+
+    def test_empty_pattern_with_valid_maps(self, bump_map, bump_map_major_version_zero):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule("", bump_map, bump_map_major_version_zero)
+
+    def test_empty_maps_with_valid_pattern(self, bump_pattern):
+        with pytest.raises(NoPatternMapError):
+            OldSchoolBumpRule(bump_pattern, {}, {})
+
     def test_complex_pattern(self):
         pattern = r"^.*?\[(.*?)\].*?\[(.*?)\].*$"
         bump_map = {

From be7cb23ba0330f52738dfbed16cdc95ca39e27a4 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 03:57:39 +0800
Subject: [PATCH 06/11] refactor(command_bump): use bump rules

---
 commitizen/bump.py                |  47 +----------
 commitizen/bump_rule.py           |   8 +-
 commitizen/commands/bump.py       |  36 +++++----
 tests/test_bump_find_increment.py | 124 ------------------------------
 tests/test_bump_rule.py           |  22 +++---
 5 files changed, 38 insertions(+), 199 deletions(-)
 delete mode 100644 tests/test_bump_find_increment.py

diff --git a/commitizen/bump.py b/commitizen/bump.py
index 6412df6f0..b3b782fa1 100644
--- a/commitizen/bump.py
+++ b/commitizen/bump.py
@@ -2,63 +2,20 @@
 
 import os
 import re
-from collections import OrderedDict
 from glob import iglob
 from logging import getLogger
 from string import Template
-from typing import cast
 
 from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message, encoding
 from commitizen.exceptions import CurrentVersionNotFoundError
-from commitizen.git import GitCommit, smart_open
-from commitizen.version_schemes import Increment, Version
+from commitizen.git import smart_open
+from commitizen.version_schemes import Version
 
 VERSION_TYPES = [None, PATCH, MINOR, MAJOR]
 
 logger = getLogger("commitizen")
 
 
-# TODO: replace this with find_increment_by_callable?
-def find_increment(
-    commits: list[GitCommit], regex: str, increments_map: dict | OrderedDict
-) -> Increment | None:
-    if isinstance(increments_map, dict):
-        increments_map = OrderedDict(increments_map)
-
-    # Most important cases are major and minor.
-    # Everything else will be considered patch.
-    select_pattern = re.compile(regex)
-    increment: str | None = None
-
-    for commit in commits:
-        for message in commit.message.split("\n"):
-            result = select_pattern.search(message)
-
-            if result:
-                found_keyword = result.group(1)
-                new_increment = None
-                for match_pattern in increments_map.keys():
-                    if re.match(match_pattern, found_keyword):
-                        new_increment = increments_map[match_pattern]
-                        break
-
-                if new_increment is None:
-                    logger.debug(
-                        f"no increment needed for '{found_keyword}' in '{message}'"
-                    )
-
-                if VERSION_TYPES.index(increment) < VERSION_TYPES.index(new_increment):
-                    logger.debug(
-                        f"increment detected is '{new_increment}' due to '{found_keyword}' in '{message}'"
-                    )
-                    increment = new_increment
-
-                if increment == MAJOR:
-                    break
-
-    return cast(Increment, increment)
-
-
 def update_version_in_files(
     current_version: str,
     new_version: str,
diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index c6865d3d6..0aa5663ed 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -48,6 +48,8 @@ def get_increment(
 
 class ConventionalCommitBumpRule(BumpRule):
     _PATCH_CHANGE_TYPES = set(["fix", "perf", "refactor"])
+    _BREAKING_CHANGE = r"BREAKING[\-\ ]CHANGE"
+    _RE_BREAKING_CHANGE = re.compile(_BREAKING_CHANGE)
 
     def get_increment(
         self, commit_message: str, major_version_zero: bool
@@ -56,8 +58,8 @@ def get_increment(
             return None
 
         change_type = m.group("change_type")
-        if m.group("bang") or change_type == "BREAKING CHANGE":
-            return "MAJOR" if major_version_zero else "MINOR"
+        if m.group("bang") or self._RE_BREAKING_CHANGE.match(change_type):
+            return "MINOR" if major_version_zero else "MAJOR"
 
         if change_type == "feat":
             return "MINOR"
@@ -70,7 +72,7 @@ def get_increment(
     @cached_property
     def _head_pattern(self) -> re.Pattern:
         change_types = [
-            r"BREAKING[\-\ ]CHANGE",
+            self._BREAKING_CHANGE,
             "fix",
             "feat",
             "docs",
diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py
index cf588e87d..a18db544f 100644
--- a/commitizen/commands/bump.py
+++ b/commitizen/commands/bump.py
@@ -7,7 +7,7 @@
 import questionary
 
 from commitizen import bump, factory, git, hooks, out
-from commitizen.bump_rule import find_increment_by_callable
+from commitizen.bump_rule import OldSchoolBumpRule, find_increment_by_callable
 from commitizen.changelog_formats import get_changelog_format
 from commitizen.commands.changelog import Changelog
 from commitizen.config import BaseConfig
@@ -124,27 +124,31 @@ def find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
         # Update the bump map to ensure major version doesn't increment.
         is_major_version_zero: bool = self.bump_settings["major_version_zero"]
 
-        if rule := self.cz.bump_rule:
-            return find_increment_by_callable(
-                (commit.message for commit in commits),
-                lambda x: rule.get_increment(x, is_major_version_zero),
-            )
-
-        bump_map = (
-            self.cz.bump_map_major_version_zero
-            if is_major_version_zero
-            else self.cz.bump_map
+        # Fallback to old school bump rule if no bump rule is provided
+        rule = self.cz.bump_rule or OldSchoolBumpRule(
+            *self._get_validated_cz_bump(),
+        )
+        return find_increment_by_callable(
+            (commit.message for commit in commits),
+            lambda x: rule.get_increment(x, is_major_version_zero),
         )
-        bump_pattern = self.cz.bump_pattern
 
-        if not bump_map or not bump_pattern:
+    def _get_validated_cz_bump(
+        self,
+    ) -> tuple[str, dict[str, Increment], dict[str, Increment]]:
+        """For fixing the type errors"""
+        bump_pattern = self.cz.bump_pattern
+        bump_map = self.cz.bump_map
+        bump_map_major_version_zero = self.cz.bump_map_major_version_zero
+        if not bump_pattern or not bump_map or not bump_map_major_version_zero:
             raise NoPatternMapError(
                 f"'{self.config.settings['name']}' rule does not support bump"
             )
-        increment = bump.find_increment(
-            commits, regex=bump_pattern, increments_map=bump_map
+
+        return cast(
+            tuple[str, dict[str, Increment], dict[str, Increment]],
+            (bump_pattern, bump_map, bump_map_major_version_zero),
         )
-        return increment
 
     def __call__(self) -> None:  # noqa: C901
         """Steps executed to bump."""
diff --git a/tests/test_bump_find_increment.py b/tests/test_bump_find_increment.py
deleted file mode 100644
index ff24ff17a..000000000
--- a/tests/test_bump_find_increment.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""
-CC: Conventional commits
-SVE: Semantic version at the end
-"""
-
-import pytest
-
-from commitizen import bump
-from commitizen.cz.conventional_commits import ConventionalCommitsCz
-from commitizen.git import GitCommit
-
-NONE_INCREMENT_CC = [
-    "docs(README): motivation",
-    "ci: added travis",
-    "performance. Remove or disable the reimplemented linters",
-    "refactor that how this line starts",
-]
-
-PATCH_INCREMENTS_CC = [
-    "fix(setup.py): future is now required for every python version",
-    "docs(README): motivation",
-]
-
-MINOR_INCREMENTS_CC = [
-    "feat(cli): added version",
-    "docs(README): motivation",
-    "fix(setup.py): future is now required for every python version",
-    "perf: app is much faster",
-    "refactor: app is much faster",
-]
-
-MAJOR_INCREMENTS_BREAKING_CHANGE_CC = [
-    "feat(cli): added version",
-    "docs(README): motivation",
-    "BREAKING CHANGE: `extends` key in config file is now used for extending other config files",  # noqa
-    "fix(setup.py): future is now required for every python version",
-]
-
-MAJOR_INCREMENTS_BREAKING_CHANGE_ALT_CC = [
-    "feat(cli): added version",
-    "docs(README): motivation",
-    "BREAKING-CHANGE: `extends` key in config file is now used for extending other config files",  # noqa
-    "fix(setup.py): future is now required for every python version",
-]
-
-MAJOR_INCREMENTS_EXCLAMATION_CC = [
-    "feat(cli)!: added version",
-    "docs(README): motivation",
-    "fix(setup.py): future is now required for every python version",
-]
-
-MAJOR_INCREMENTS_EXCLAMATION_CC_SAMPLE_2 = [
-    "feat(pipeline)!: some text with breaking change"
-]
-
-MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_CC = [
-    "chore!: drop support for Python 3.9",
-    "docs(README): motivation",
-    "fix(setup.py): future is now required for every python version",
-]
-
-MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_WITH_SCOPE_CC = [
-    "chore(deps)!: drop support for Python 3.9",
-    "docs(README): motivation",
-    "fix(setup.py): future is now required for every python version",
-]
-
-PATCH_INCREMENTS_SVE = ["readme motivation PATCH", "fix setup.py PATCH"]
-
-MINOR_INCREMENTS_SVE = [
-    "readme motivation PATCH",
-    "fix setup.py PATCH",
-    "added version to cli MINOR",
-]
-
-MAJOR_INCREMENTS_SVE = [
-    "readme motivation PATCH",
-    "fix setup.py PATCH",
-    "added version to cli MINOR",
-    "extends key is used for other config files MAJOR",
-]
-
-semantic_version_pattern = r"(MAJOR|MINOR|PATCH)"
-semantic_version_map = {"MAJOR": "MAJOR", "MINOR": "MINOR", "PATCH": "PATCH"}
-
-
-@pytest.mark.parametrize(
-    "messages, expected_type",
-    (
-        (PATCH_INCREMENTS_CC, "PATCH"),
-        (MINOR_INCREMENTS_CC, "MINOR"),
-        (MAJOR_INCREMENTS_BREAKING_CHANGE_CC, "MAJOR"),
-        (MAJOR_INCREMENTS_BREAKING_CHANGE_ALT_CC, "MAJOR"),
-        (MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_CC, "MAJOR"),
-        (MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_WITH_SCOPE_CC, "MAJOR"),
-        (MAJOR_INCREMENTS_EXCLAMATION_CC, "MAJOR"),
-        (MAJOR_INCREMENTS_EXCLAMATION_CC_SAMPLE_2, "MAJOR"),
-        (NONE_INCREMENT_CC, None),
-    ),
-)
-def test_find_increment(messages, expected_type):
-    commits = [GitCommit(rev="test", title=message) for message in messages]
-    increment_type = bump.find_increment(
-        commits,
-        regex=ConventionalCommitsCz.bump_pattern,
-        increments_map=ConventionalCommitsCz.bump_map,
-    )
-    assert increment_type == expected_type
-
-
-@pytest.mark.parametrize(
-    "messages, expected_type",
-    (
-        (PATCH_INCREMENTS_SVE, "PATCH"),
-        (MINOR_INCREMENTS_SVE, "MINOR"),
-        (MAJOR_INCREMENTS_SVE, "MAJOR"),
-    ),
-)
-def test_find_increment_sve(messages, expected_type):
-    commits = [GitCommit(rev="test", title=message) for message in messages]
-    increment_type = bump.find_increment(
-        commits, regex=semantic_version_pattern, increments_map=semantic_version_map
-    )
-    assert increment_type == expected_type
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index c2bf76d7d..1f7ba4f8c 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -39,12 +39,12 @@ def test_refactor_commit(self, bump_rule):
         assert bump_rule.get_increment("refactor: restructure code", True) == PATCH
 
     def test_breaking_change_with_bang(self, bump_rule):
-        assert bump_rule.get_increment("feat!: breaking change", False) == MINOR
-        assert bump_rule.get_increment("feat!: breaking change", True) == MAJOR
+        assert bump_rule.get_increment("feat!: breaking change", False) == MAJOR
+        assert bump_rule.get_increment("feat!: breaking change", True) == MINOR
 
     def test_breaking_change_type(self, bump_rule):
-        assert bump_rule.get_increment("BREAKING CHANGE: major change", False) == MINOR
-        assert bump_rule.get_increment("BREAKING CHANGE: major change", True) == MAJOR
+        assert bump_rule.get_increment("BREAKING CHANGE: major change", False) == MAJOR
+        assert bump_rule.get_increment("BREAKING CHANGE: major change", True) == MINOR
 
     def test_commit_with_scope(self, bump_rule):
         assert bump_rule.get_increment("feat(api): add new endpoint", False) == MINOR
@@ -72,11 +72,11 @@ def test_commit_with_complex_scopes(self, bump_rule):
         # Test with breaking changes and scopes
         assert (
             bump_rule.get_increment("feat(api)!: remove deprecated endpoints", False)
-            == MINOR
+            == MAJOR
         )
         assert (
             bump_rule.get_increment("feat(api)!: remove deprecated endpoints", True)
-            == MAJOR
+            == MINOR
         )
 
         # Test with BREAKING CHANGE and scopes
@@ -84,13 +84,13 @@ def test_commit_with_complex_scopes(self, bump_rule):
             bump_rule.get_increment(
                 "BREAKING CHANGE(api): remove deprecated endpoints", False
             )
-            == MINOR
+            == MAJOR
         )
         assert (
             bump_rule.get_increment(
                 "BREAKING CHANGE(api): remove deprecated endpoints", True
             )
-            == MAJOR
+            == MINOR
         )
 
     def test_invalid_commit_message(self, bump_rule):
@@ -129,13 +129,13 @@ def test_breaking_change(self, get_increment):
             "feat: new feature",
             "feat!: breaking change",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+        assert find_increment_by_callable(commit_messages, get_increment) == MAJOR
 
     def test_multi_line_commit(self, get_increment):
         commit_messages = [
             "feat: new feature\n\nBREAKING CHANGE: major change",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+        assert find_increment_by_callable(commit_messages, get_increment) == MAJOR
 
     def test_no_increment_needed(self, get_increment):
         commit_messages = [
@@ -159,7 +159,7 @@ def test_major_version_zero(self):
             find_increment_by_callable(
                 commit_messages, lambda x: bump_rule.get_increment(x, True)
             )
-            == MAJOR
+            == MINOR
         )
 
     def test_mixed_commit_types(self, get_increment):

From 88efb6171596521fb941b2c4b0f018500032612f Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 04:28:40 +0800
Subject: [PATCH 07/11] docs(bump_rule): docstring

---
 commitizen/bump_rule.py | 24 ++++++++++++++++++++----
 1 file changed, 20 insertions(+), 4 deletions(-)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index 0aa5663ed..b84eb4e12 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -43,7 +43,26 @@ def find_increment_by_callable(
 class BumpRule(Protocol):
     def get_increment(
         self, commit_message: str, major_version_zero: bool
-    ) -> Increment | None: ...
+    ) -> Increment | None:
+        """Determine the version increment based on a commit message.
+
+        This method analyzes a commit message to determine what kind of version increment
+        is needed according to the Conventional Commits specification. It handles special
+        cases for breaking changes and respects the major_version_zero flag.
+
+        Args:
+            commit_message: The commit message to analyze. Should follow conventional commit format.
+            major_version_zero: If True, breaking changes will result in a MINOR version bump
+                              instead of MAJOR. This is useful for projects in 0.x.x versions.
+
+        Returns:
+            Increment | None: The type of version increment needed:
+                - "MAJOR": For breaking changes when major_version_zero is False
+                - "MINOR": For breaking changes when major_version_zero is True, or for new features
+                - "PATCH": For bug fixes, performance improvements, or refactors
+                - None: For commits that don't require a version bump (docs, style, etc.)
+        """
+        ...
 
 
 class ConventionalCommitBumpRule(BumpRule):
@@ -122,6 +141,3 @@ def get_increment(
             if re.match(match_pattern, found_keyword):
                 return increment
         return None
-
-
-# TODO: Implement CustomBumpRule

From a4d6eaa1e55bd7001130f8681a3789596bc941b4 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 05:04:58 +0800
Subject: [PATCH 08/11] docs(bump_rule): add todo

---
 commitizen/bump_rule.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index b84eb4e12..0699ee2d8 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -136,6 +136,17 @@ def get_increment(
             self.bump_map_major_version_zero if major_version_zero else self.bump_map
         )
 
+        # TODO: need more flexibility for the pattern
+        # "refactor!: drop support for Python 2.7" => MAJOR
+        # bump_pattern = r"^((?P<major>major)|(?P<minor>minor)|(?P<patch>patch))(?P<scope>\(.+\))?(?P<bang>!)?:"
+        # bump_map = {
+        #     "major": "MAJOR",
+        #     "bang": "MAJOR",
+        #     "minor": "MINOR",
+        #     "patch": "PATCH",
+        # }
+        # bump_map_major_version_zero = { ... }
+
         found_keyword = m.group(1)
         for match_pattern, increment in bump_map.items():
             if re.match(match_pattern, found_keyword):

From 39f8bc7a5742b7476c1e314cd0818c994aaa67d9 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 05:24:41 +0800
Subject: [PATCH 09/11] fix(bump_rule): support flexible bump map

---
 commitizen/bump_rule.py | 23 +++++-----
 tests/test_bump_rule.py | 95 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+), 10 deletions(-)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index 0699ee2d8..149b9cc48 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -37,6 +37,10 @@ def find_increment_by_callable(
     """
     lines = (line for message in commit_messages for line in message.split("\n"))
     increments = map(get_increment, lines)
+    return _find_highest_increment(increments)
+
+
+def _find_highest_increment(increments: Iterable[Increment | None]) -> Increment | None:
     return max(increments, key=lambda x: _VERSION_ORDERING[x], default=None)
 
 
@@ -136,17 +140,16 @@ def get_increment(
             self.bump_map_major_version_zero if major_version_zero else self.bump_map
         )
 
-        # TODO: need more flexibility for the pattern
-        # "refactor!: drop support for Python 2.7" => MAJOR
-        # bump_pattern = r"^((?P<major>major)|(?P<minor>minor)|(?P<patch>patch))(?P<scope>\(.+\))?(?P<bang>!)?:"
-        # bump_map = {
-        #     "major": "MAJOR",
-        #     "bang": "MAJOR",
-        #     "minor": "MINOR",
-        #     "patch": "PATCH",
-        # }
-        # bump_map_major_version_zero = { ... }
+        try:
+            if ret := _find_highest_increment(
+                (increment for name, increment in bump_map.items() if m.group(name))
+            ):
+                return ret
+        except IndexError:
+            # Fallback to old school bump rule
+            pass
 
+        # Fallback to old school bump rule
         found_keyword = m.group(1)
         for match_pattern, increment in bump_map.items():
             if re.match(match_pattern, found_keyword):
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index 1f7ba4f8c..9be38545f 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -3,6 +3,7 @@
 from commitizen.bump_rule import (
     ConventionalCommitBumpRule,
     OldSchoolBumpRule,
+    _find_highest_increment,
     find_increment_by_callable,
 )
 from commitizen.defaults import (
@@ -106,6 +107,38 @@ def test_other_commit_types(self, bump_rule):
         assert bump_rule.get_increment("build: update build config", False) is None
         assert bump_rule.get_increment("ci: update CI pipeline", False) is None
 
+    def test_breaking_change_with_refactor(self, bump_rule):
+        """Test breaking changes with refactor type commit messages."""
+        # Breaking change with refactor type
+        assert (
+            bump_rule.get_increment("refactor!: drop support for Python 2.7", False)
+            == MAJOR
+        )
+        assert (
+            bump_rule.get_increment("refactor!: drop support for Python 2.7", True)
+            == MINOR
+        )
+
+        # Breaking change with refactor type and scope
+        assert (
+            bump_rule.get_increment(
+                "refactor(api)!: remove deprecated endpoints", False
+            )
+            == MAJOR
+        )
+        assert (
+            bump_rule.get_increment("refactor(api)!: remove deprecated endpoints", True)
+            == MINOR
+        )
+
+        # Regular refactor (should be PATCH)
+        assert (
+            bump_rule.get_increment("refactor: improve code structure", False) == PATCH
+        )
+        assert (
+            bump_rule.get_increment("refactor: improve code structure", True) == PATCH
+        )
+
 
 class TestFindIncrementByCallable:
     @pytest.fixture
@@ -291,6 +324,38 @@ def test_with_find_increment_by_callable(self, old_school_rule):
             == MAJOR
         )
 
+    def test_flexible_bump_map(self, old_school_rule):
+        """Test that _find_highest_increment is used correctly in bump map processing."""
+        # Test with multiple matching patterns
+        pattern = r"^((?P<major>major)|(?P<minor>minor)|(?P<patch>patch))(?P<scope>\(.+\))?(?P<bang>!)?:"
+        bump_map = {
+            "major": MAJOR,
+            "bang": MAJOR,
+            "minor": MINOR,
+            "patch": PATCH,
+        }
+        bump_map_major_version_zero = {
+            "major": MINOR,
+            "bang": MINOR,
+            "minor": MINOR,
+            "patch": PATCH,
+        }
+        rule = OldSchoolBumpRule(pattern, bump_map, bump_map_major_version_zero)
+
+        # Test with multiple version tags
+        assert rule.get_increment("major!: drop support for Python 2.7", False) == MAJOR
+        assert rule.get_increment("major!: drop support for Python 2.7", True) == MINOR
+        assert rule.get_increment("major: drop support for Python 2.7", False) == MAJOR
+        assert rule.get_increment("major: drop support for Python 2.7", True) == MINOR
+        assert rule.get_increment("patch!: drop support for Python 2.7", False) == MAJOR
+        assert rule.get_increment("patch!: drop support for Python 2.7", True) == MINOR
+        assert rule.get_increment("patch: drop support for Python 2.7", False) == PATCH
+        assert rule.get_increment("patch: drop support for Python 2.7", True) == PATCH
+        assert rule.get_increment("minor: add new feature", False) == MINOR
+        assert rule.get_increment("minor: add new feature", True) == MINOR
+        assert rule.get_increment("patch: fix bug", False) == PATCH
+        assert rule.get_increment("patch: fix bug", True) == PATCH
+
 
 class TestOldSchoolBumpRuleWithDefault:
     @pytest.fixture
@@ -378,3 +443,33 @@ def test_with_find_increment_by_callable(self, old_school_rule):
             )
             == MAJOR
         )
+
+
+def test_find_highest_increment():
+    """Test the _find_highest_increment function."""
+    # Test with single increment
+    assert _find_highest_increment([MAJOR]) == MAJOR
+    assert _find_highest_increment([MINOR]) == MINOR
+    assert _find_highest_increment([PATCH]) == PATCH
+
+    # Test with multiple increments
+    assert _find_highest_increment([PATCH, MINOR, MAJOR]) == MAJOR
+    assert _find_highest_increment([PATCH, MINOR]) == MINOR
+    assert _find_highest_increment([PATCH, PATCH]) == PATCH
+
+    # Test with None values
+    assert _find_highest_increment([None, PATCH]) == PATCH
+    assert _find_highest_increment([None, None]) is None
+    assert _find_highest_increment([]) is None
+
+    # Test with mixed values
+    assert _find_highest_increment([None, PATCH, MINOR, MAJOR]) == MAJOR
+    assert _find_highest_increment([None, PATCH, MINOR]) == MINOR
+    assert _find_highest_increment([None, PATCH]) == PATCH
+
+    # Test with empty iterator
+    assert _find_highest_increment(iter([])) is None
+
+    # Test with generator expression
+    assert _find_highest_increment(x for x in [PATCH, MINOR, MAJOR]) == MAJOR
+    assert _find_highest_increment(x for x in [None, PATCH, MINOR]) == MINOR

From 42d965e953065d72a0a79295f95df0b71fb61766 Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 14:45:13 +0800
Subject: [PATCH 10/11] refactor(bump): remove unused var

---
 commitizen/bump.py | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/commitizen/bump.py b/commitizen/bump.py
index b3b782fa1..454505333 100644
--- a/commitizen/bump.py
+++ b/commitizen/bump.py
@@ -3,18 +3,13 @@
 import os
 import re
 from glob import iglob
-from logging import getLogger
 from string import Template
 
-from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message, encoding
+from commitizen.defaults import bump_message, encoding
 from commitizen.exceptions import CurrentVersionNotFoundError
 from commitizen.git import smart_open
 from commitizen.version_schemes import Version
 
-VERSION_TYPES = [None, PATCH, MINOR, MAJOR]
-
-logger = getLogger("commitizen")
-
 
 def update_version_in_files(
     current_version: str,

From 166f768776f759f4c0e3590557c4ebc153c3f25d Mon Sep 17 00:00:00 2001
From: Yu-Ting Hsiung <bear890707@gmail.com>
Date: Sun, 18 May 2025 15:36:31 +0800
Subject: [PATCH 11/11] refactor(bump_rule): use enum on increment

---
 commitizen/bump_rule.py              |  69 ++++-
 commitizen/commands/bump.py          |  25 +-
 commitizen/defaults.py               |  34 +--
 commitizen/version_schemes.py        |  43 +--
 tests/test_bump_rule.py              | 418 +++++++++++++++++++--------
 tests/test_version_scheme_pep440.py  | 183 ++++++------
 tests/test_version_scheme_semver.py  |  99 +++----
 tests/test_version_scheme_semver2.py |  63 ++--
 8 files changed, 583 insertions(+), 351 deletions(-)

diff --git a/commitizen/bump_rule.py b/commitizen/bump_rule.py
index 149b9cc48..35addf6b3 100644
--- a/commitizen/bump_rule.py
+++ b/commitizen/bump_rule.py
@@ -2,18 +2,51 @@
 
 import re
 from collections.abc import Iterable
+from enum import Enum, auto
 from functools import cached_property
-from typing import Callable, Protocol
+from typing import Any, Callable, Protocol
 
 from commitizen.exceptions import NoPatternMapError
-from commitizen.version_schemes import Increment
 
-_VERSION_ORDERING = dict(zip((None, "PATCH", "MINOR", "MAJOR"), range(4)))
+
+class SemVerIncrement(Enum):
+    MAJOR = auto()
+    MINOR = auto()
+    PATCH = auto()
+
+    def __str__(self) -> str:
+        return self.name
+
+    @classmethod
+    def safe_cast(cls, value: str | None) -> SemVerIncrement | None:
+        if value is None:
+            return None
+        try:
+            return cls[value]
+        except ValueError:
+            return None
+
+    @classmethod
+    def safe_cast_dict(cls, d: dict[str, Any]) -> dict[str, SemVerIncrement]:
+        return {
+            k: v
+            for k, v in ((k, SemVerIncrement.safe_cast(v)) for k, v in d.items())
+            if v is not None
+        }
+
+
+_VERSION_ORDERING = dict(
+    zip(
+        (None, SemVerIncrement.PATCH, SemVerIncrement.MINOR, SemVerIncrement.MAJOR),
+        range(4),
+    )
+)
 
 
 def find_increment_by_callable(
-    commit_messages: Iterable[str], get_increment: Callable[[str], Increment | None]
-) -> Increment | None:
+    commit_messages: Iterable[str],
+    get_increment: Callable[[str], SemVerIncrement | None],
+) -> SemVerIncrement | None:
     """Find the highest version increment from a list of messages.
 
     This function processes a list of messages and determines the highest version
@@ -23,7 +56,7 @@ def find_increment_by_callable(
     Args:
         commit_messages: A list of messages to analyze.
         get_increment: A callable that takes a commit message string and returns an
-            Increment value (MAJOR, MINOR, PATCH) or None if no increment is needed.
+            SemVerIncrement value (MAJOR, MINOR, PATCH) or None if no increment is needed.
 
     Returns:
         The highest version increment needed (MAJOR, MINOR, PATCH) or None if no
@@ -40,14 +73,16 @@ def find_increment_by_callable(
     return _find_highest_increment(increments)
 
 
-def _find_highest_increment(increments: Iterable[Increment | None]) -> Increment | None:
+def _find_highest_increment(
+    increments: Iterable[SemVerIncrement | None],
+) -> SemVerIncrement | None:
     return max(increments, key=lambda x: _VERSION_ORDERING[x], default=None)
 
 
 class BumpRule(Protocol):
     def get_increment(
         self, commit_message: str, major_version_zero: bool
-    ) -> Increment | None:
+    ) -> SemVerIncrement | None:
         """Determine the version increment based on a commit message.
 
         This method analyzes a commit message to determine what kind of version increment
@@ -60,7 +95,7 @@ def get_increment(
                               instead of MAJOR. This is useful for projects in 0.x.x versions.
 
         Returns:
-            Increment | None: The type of version increment needed:
+            SemVerIncrement | None: The type of version increment needed:
                 - "MAJOR": For breaking changes when major_version_zero is False
                 - "MINOR": For breaking changes when major_version_zero is True, or for new features
                 - "PATCH": For bug fixes, performance improvements, or refactors
@@ -76,19 +111,21 @@ class ConventionalCommitBumpRule(BumpRule):
 
     def get_increment(
         self, commit_message: str, major_version_zero: bool
-    ) -> Increment | None:
+    ) -> SemVerIncrement | None:
         if not (m := self._head_pattern.match(commit_message)):
             return None
 
         change_type = m.group("change_type")
         if m.group("bang") or self._RE_BREAKING_CHANGE.match(change_type):
-            return "MINOR" if major_version_zero else "MAJOR"
+            return (
+                SemVerIncrement.MINOR if major_version_zero else SemVerIncrement.MAJOR
+            )
 
         if change_type == "feat":
-            return "MINOR"
+            return SemVerIncrement.MINOR
 
         if change_type in self._PATCH_CHANGE_TYPES:
-            return "PATCH"
+            return SemVerIncrement.PATCH
 
         return None
 
@@ -118,8 +155,8 @@ class OldSchoolBumpRule(BumpRule):
     def __init__(
         self,
         bump_pattern: str,
-        bump_map: dict[str, Increment],
-        bump_map_major_version_zero: dict[str, Increment],
+        bump_map: dict[str, SemVerIncrement],
+        bump_map_major_version_zero: dict[str, SemVerIncrement],
     ):
         if not bump_map or not bump_pattern or not bump_map_major_version_zero:
             raise NoPatternMapError(
@@ -132,7 +169,7 @@ def __init__(
 
     def get_increment(
         self, commit_message: str, major_version_zero: bool
-    ) -> Increment | None:
+    ) -> SemVerIncrement | None:
         if not (m := self.bump_pattern.search(commit_message)):
             return None
 
diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py
index a18db544f..551145fd1 100644
--- a/commitizen/commands/bump.py
+++ b/commitizen/commands/bump.py
@@ -7,7 +7,11 @@
 import questionary
 
 from commitizen import bump, factory, git, hooks, out
-from commitizen.bump_rule import OldSchoolBumpRule, find_increment_by_callable
+from commitizen.bump_rule import (
+    OldSchoolBumpRule,
+    SemVerIncrement,
+    find_increment_by_callable,
+)
 from commitizen.changelog_formats import get_changelog_format
 from commitizen.commands.changelog import Changelog
 from commitizen.config import BaseConfig
@@ -29,7 +33,6 @@
 from commitizen.providers import get_provider
 from commitizen.tags import TagRules
 from commitizen.version_schemes import (
-    Increment,
     InvalidVersion,
     Prerelease,
     get_version_scheme,
@@ -120,7 +123,7 @@ def is_initial_tag(
                 is_initial = questionary.confirm("Is this the first tag created?").ask()
         return is_initial
 
-    def find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
+    def find_increment(self, commits: list[git.GitCommit]) -> SemVerIncrement | None:
         # Update the bump map to ensure major version doesn't increment.
         is_major_version_zero: bool = self.bump_settings["major_version_zero"]
 
@@ -128,6 +131,7 @@ def find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
         rule = self.cz.bump_rule or OldSchoolBumpRule(
             *self._get_validated_cz_bump(),
         )
+
         return find_increment_by_callable(
             (commit.message for commit in commits),
             lambda x: rule.get_increment(x, is_major_version_zero),
@@ -135,7 +139,7 @@ def find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
 
     def _get_validated_cz_bump(
         self,
-    ) -> tuple[str, dict[str, Increment], dict[str, Increment]]:
+    ) -> tuple[str, dict[str, SemVerIncrement], dict[str, SemVerIncrement]]:
         """For fixing the type errors"""
         bump_pattern = self.cz.bump_pattern
         bump_map = self.cz.bump_map
@@ -145,9 +149,10 @@ def _get_validated_cz_bump(
                 f"'{self.config.settings['name']}' rule does not support bump"
             )
 
-        return cast(
-            tuple[str, dict[str, Increment], dict[str, Increment]],
-            (bump_pattern, bump_map, bump_map_major_version_zero),
+        return (
+            bump_pattern,
+            SemVerIncrement.safe_cast_dict(bump_map),
+            SemVerIncrement.safe_cast_dict(bump_map_major_version_zero),
         )
 
     def __call__(self) -> None:  # noqa: C901
@@ -166,7 +171,9 @@ def __call__(self) -> None:  # noqa: C901
 
         dry_run: bool = self.arguments["dry_run"]
         is_yes: bool = self.arguments["yes"]
-        increment: Increment | None = self.arguments["increment"]
+        increment: SemVerIncrement | None = SemVerIncrement.safe_cast(
+            self.arguments["increment"]
+        )
         prerelease: Prerelease | None = self.arguments["prerelease"]
         devrelease: int | None = self.arguments["devrelease"]
         is_files_only: bool | None = self.arguments["files_only"]
@@ -283,7 +290,7 @@ def __call__(self) -> None:  # noqa: C901
 
             # we create an empty PATCH increment for empty tag
             if increment is None and allow_no_commit:
-                increment = "PATCH"
+                increment = SemVerIncrement.PATCH
 
             new_version = current_version.bump(
                 increment,
diff --git a/commitizen/defaults.py b/commitizen/defaults.py
index c61c54cbb..3e6841b05 100644
--- a/commitizen/defaults.py
+++ b/commitizen/defaults.py
@@ -5,6 +5,8 @@
 from collections.abc import Iterable, MutableMapping, Sequence
 from typing import Any, TypedDict
 
+from commitizen.bump_rule import SemVerIncrement
+
 # Type
 Questions = Iterable[MutableMapping[str, Any]]
 
@@ -108,31 +110,27 @@ class Settings(TypedDict, total=False):
     "extras": {},
 }
 
-MAJOR = "MAJOR"
-MINOR = "MINOR"
-PATCH = "PATCH"
-
 CHANGELOG_FORMAT = "markdown"
 
 BUMP_PATTERN = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):"
-BUMP_MAP = OrderedDict(
+BUMP_MAP = dict(
     (
-        (r"^.+!$", MAJOR),
-        (r"^BREAKING[\-\ ]CHANGE", MAJOR),
-        (r"^feat", MINOR),
-        (r"^fix", PATCH),
-        (r"^refactor", PATCH),
-        (r"^perf", PATCH),
+        (r"^.+!$", str(SemVerIncrement.MAJOR)),
+        (r"^BREAKING[\-\ ]CHANGE", str(SemVerIncrement.MAJOR)),
+        (r"^feat", str(SemVerIncrement.MINOR)),
+        (r"^fix", str(SemVerIncrement.PATCH)),
+        (r"^refactor", str(SemVerIncrement.PATCH)),
+        (r"^perf", str(SemVerIncrement.PATCH)),
     )
 )
-BUMP_MAP_MAJOR_VERSION_ZERO = OrderedDict(
+BUMP_MAP_MAJOR_VERSION_ZERO = dict(
     (
-        (r"^.+!$", MINOR),
-        (r"^BREAKING[\-\ ]CHANGE", MINOR),
-        (r"^feat", MINOR),
-        (r"^fix", PATCH),
-        (r"^refactor", PATCH),
-        (r"^perf", PATCH),
+        (r"^.+!$", str(SemVerIncrement.MINOR)),
+        (r"^BREAKING[\-\ ]CHANGE", str(SemVerIncrement.MINOR)),
+        (r"^feat", str(SemVerIncrement.MINOR)),
+        (r"^fix", str(SemVerIncrement.PATCH)),
+        (r"^refactor", str(SemVerIncrement.PATCH)),
+        (r"^perf", str(SemVerIncrement.PATCH)),
     )
 )
 change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"]
diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py
index 84ded9316..14bed686c 100644
--- a/commitizen/version_schemes.py
+++ b/commitizen/version_schemes.py
@@ -14,6 +14,8 @@
     runtime_checkable,
 )
 
+from commitizen.bump_rule import SemVerIncrement
+
 if sys.version_info >= (3, 10):
     from importlib import metadata
 else:
@@ -22,7 +24,7 @@
 from packaging.version import InvalidVersion  # noqa: F401: expose the common exception
 from packaging.version import Version as _BaseVersion
 
-from commitizen.defaults import MAJOR, MINOR, PATCH, Settings
+from commitizen.defaults import Settings
 from commitizen.exceptions import VersionSchemeUnknown
 
 if TYPE_CHECKING:
@@ -39,7 +41,6 @@
         from typing import Self
 
 
-Increment: TypeAlias = Literal["MAJOR", "MINOR", "PATCH"]
 Prerelease: TypeAlias = Literal["alpha", "beta", "rc"]
 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+)?)"
 
@@ -126,7 +127,7 @@ def __ne__(self, other: object) -> bool:
 
     def bump(
         self,
-        increment: Increment | None,
+        increment: SemVerIncrement | None,
         prerelease: Prerelease | None = None,
         prerelease_offset: int = 0,
         devrelease: int | None = None,
@@ -223,26 +224,30 @@ def generate_build_metadata(self, build_metadata: str | None) -> str:
 
         return f"+{build_metadata}"
 
-    def increment_base(self, increment: Increment | None = None) -> str:
+    def increment_base(self, increment: SemVerIncrement | None = None) -> str:
         prev_release = list(self.release)
-        increments = [MAJOR, MINOR, PATCH]
+        increments = [
+            SemVerIncrement.MAJOR,
+            SemVerIncrement.MINOR,
+            SemVerIncrement.PATCH,
+        ]
         base = dict(zip_longest(increments, prev_release, fillvalue=0))
 
-        if increment == MAJOR:
-            base[MAJOR] += 1
-            base[MINOR] = 0
-            base[PATCH] = 0
-        elif increment == MINOR:
-            base[MINOR] += 1
-            base[PATCH] = 0
-        elif increment == PATCH:
-            base[PATCH] += 1
+        if increment == SemVerIncrement.MAJOR:
+            base[SemVerIncrement.MAJOR] += 1
+            base[SemVerIncrement.MINOR] = 0
+            base[SemVerIncrement.PATCH] = 0
+        elif increment == SemVerIncrement.MINOR:
+            base[SemVerIncrement.MINOR] += 1
+            base[SemVerIncrement.PATCH] = 0
+        elif increment == SemVerIncrement.PATCH:
+            base[SemVerIncrement.PATCH] += 1
 
-        return f"{base[MAJOR]}.{base[MINOR]}.{base[PATCH]}"
+        return f"{base[SemVerIncrement.MAJOR]}.{base[SemVerIncrement.MINOR]}.{base[SemVerIncrement.PATCH]}"
 
     def bump(
         self,
-        increment: Increment | None,
+        increment: SemVerIncrement | None,
         prerelease: Prerelease | None = None,
         prerelease_offset: int = 0,
         devrelease: int | None = None,
@@ -272,12 +277,12 @@ def bump(
                 base = self.increment_base(increment)
             else:
                 base = f"{self.major}.{self.minor}.{self.micro}"
-                if increment == PATCH:
+                if increment == SemVerIncrement.PATCH:
                     pass
-                elif increment == MINOR:
+                elif increment == SemVerIncrement.MINOR:
                     if self.micro != 0:
                         base = self.increment_base(increment)
-                elif increment == MAJOR:
+                elif increment == SemVerIncrement.MAJOR:
                     if self.minor != 0 or self.micro != 0:
                         base = self.increment_base(increment)
             dev_version = self.generate_devrelease(devrelease)
diff --git a/tests/test_bump_rule.py b/tests/test_bump_rule.py
index 9be38545f..0c8974bcb 100644
--- a/tests/test_bump_rule.py
+++ b/tests/test_bump_rule.py
@@ -3,6 +3,7 @@
 from commitizen.bump_rule import (
     ConventionalCommitBumpRule,
     OldSchoolBumpRule,
+    SemVerIncrement,
     _find_highest_increment,
     find_increment_by_callable,
 )
@@ -10,9 +11,6 @@
     BUMP_MAP,
     BUMP_MAP_MAJOR_VERSION_ZERO,
     BUMP_PATTERN,
-    MAJOR,
-    MINOR,
-    PATCH,
 )
 from commitizen.exceptions import NoPatternMapError
 
@@ -24,60 +22,98 @@ def bump_rule():
 
 class TestConventionalCommitBumpRule:
     def test_feat_commit(self, bump_rule):
-        assert bump_rule.get_increment("feat: add new feature", False) == MINOR
-        assert bump_rule.get_increment("feat: add new feature", True) == MINOR
+        assert (
+            bump_rule.get_increment("feat: add new feature", False)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            bump_rule.get_increment("feat: add new feature", True)
+            == SemVerIncrement.MINOR
+        )
 
     def test_fix_commit(self, bump_rule):
-        assert bump_rule.get_increment("fix: fix bug", False) == PATCH
-        assert bump_rule.get_increment("fix: fix bug", True) == PATCH
+        assert bump_rule.get_increment("fix: fix bug", False) == SemVerIncrement.PATCH
+        assert bump_rule.get_increment("fix: fix bug", True) == SemVerIncrement.PATCH
 
     def test_perf_commit(self, bump_rule):
-        assert bump_rule.get_increment("perf: improve performance", False) == PATCH
-        assert bump_rule.get_increment("perf: improve performance", True) == PATCH
+        assert (
+            bump_rule.get_increment("perf: improve performance", False)
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            bump_rule.get_increment("perf: improve performance", True)
+            == SemVerIncrement.PATCH
+        )
 
     def test_refactor_commit(self, bump_rule):
-        assert bump_rule.get_increment("refactor: restructure code", False) == PATCH
-        assert bump_rule.get_increment("refactor: restructure code", True) == PATCH
+        assert (
+            bump_rule.get_increment("refactor: restructure code", False)
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            bump_rule.get_increment("refactor: restructure code", True)
+            == SemVerIncrement.PATCH
+        )
 
     def test_breaking_change_with_bang(self, bump_rule):
-        assert bump_rule.get_increment("feat!: breaking change", False) == MAJOR
-        assert bump_rule.get_increment("feat!: breaking change", True) == MINOR
+        assert (
+            bump_rule.get_increment("feat!: breaking change", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            bump_rule.get_increment("feat!: breaking change", True)
+            == SemVerIncrement.MINOR
+        )
 
     def test_breaking_change_type(self, bump_rule):
-        assert bump_rule.get_increment("BREAKING CHANGE: major change", False) == MAJOR
-        assert bump_rule.get_increment("BREAKING CHANGE: major change", True) == MINOR
+        assert (
+            bump_rule.get_increment("BREAKING CHANGE: major change", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            bump_rule.get_increment("BREAKING CHANGE: major change", True)
+            == SemVerIncrement.MINOR
+        )
 
     def test_commit_with_scope(self, bump_rule):
-        assert bump_rule.get_increment("feat(api): add new endpoint", False) == MINOR
-        assert bump_rule.get_increment("fix(ui): fix button alignment", False) == PATCH
+        assert (
+            bump_rule.get_increment("feat(api): add new endpoint", False)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            bump_rule.get_increment("fix(ui): fix button alignment", False)
+            == SemVerIncrement.PATCH
+        )
 
     def test_commit_with_complex_scopes(self, bump_rule):
         # Test with multiple word scopes
         assert (
             bump_rule.get_increment("feat(user_management): add user roles", False)
-            == MINOR
+            == SemVerIncrement.MINOR
         )
         assert (
             bump_rule.get_increment("fix(database_connection): handle timeout", False)
-            == PATCH
+            == SemVerIncrement.PATCH
         )
 
         # Test with nested scopes
         assert (
-            bump_rule.get_increment("feat(api/auth): implement OAuth", False) == MINOR
+            bump_rule.get_increment("feat(api/auth): implement OAuth", False)
+            == SemVerIncrement.MINOR
         )
         assert (
-            bump_rule.get_increment("fix(ui/components): fix dropdown", False) == PATCH
+            bump_rule.get_increment("fix(ui/components): fix dropdown", False)
+            == SemVerIncrement.PATCH
         )
 
         # Test with breaking changes and scopes
         assert (
             bump_rule.get_increment("feat(api)!: remove deprecated endpoints", False)
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
         assert (
             bump_rule.get_increment("feat(api)!: remove deprecated endpoints", True)
-            == MINOR
+            == SemVerIncrement.MINOR
         )
 
         # Test with BREAKING CHANGE and scopes
@@ -85,13 +121,13 @@ def test_commit_with_complex_scopes(self, bump_rule):
             bump_rule.get_increment(
                 "BREAKING CHANGE(api): remove deprecated endpoints", False
             )
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
         assert (
             bump_rule.get_increment(
                 "BREAKING CHANGE(api): remove deprecated endpoints", True
             )
-            == MINOR
+            == SemVerIncrement.MINOR
         )
 
     def test_invalid_commit_message(self, bump_rule):
@@ -112,11 +148,11 @@ def test_breaking_change_with_refactor(self, bump_rule):
         # Breaking change with refactor type
         assert (
             bump_rule.get_increment("refactor!: drop support for Python 2.7", False)
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
         assert (
             bump_rule.get_increment("refactor!: drop support for Python 2.7", True)
-            == MINOR
+            == SemVerIncrement.MINOR
         )
 
         # Breaking change with refactor type and scope
@@ -124,19 +160,21 @@ def test_breaking_change_with_refactor(self, bump_rule):
             bump_rule.get_increment(
                 "refactor(api)!: remove deprecated endpoints", False
             )
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
         assert (
             bump_rule.get_increment("refactor(api)!: remove deprecated endpoints", True)
-            == MINOR
+            == SemVerIncrement.MINOR
         )
 
-        # Regular refactor (should be PATCH)
+        # Regular refactor (should be SemVerIncrement.PATCH)
         assert (
-            bump_rule.get_increment("refactor: improve code structure", False) == PATCH
+            bump_rule.get_increment("refactor: improve code structure", False)
+            == SemVerIncrement.PATCH
         )
         assert (
-            bump_rule.get_increment("refactor: improve code structure", True) == PATCH
+            bump_rule.get_increment("refactor: improve code structure", True)
+            == SemVerIncrement.PATCH
         )
 
 
@@ -147,7 +185,10 @@ def get_increment(self, bump_rule):
 
     def test_single_commit(self, get_increment):
         commit_messages = ["feat: add new feature"]
-        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+        assert (
+            find_increment_by_callable(commit_messages, get_increment)
+            == SemVerIncrement.MINOR
+        )
 
     def test_multiple_commits(self, get_increment):
         commit_messages = [
@@ -155,20 +196,29 @@ def test_multiple_commits(self, get_increment):
             "fix: bug fix",
             "docs: update readme",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+        assert (
+            find_increment_by_callable(commit_messages, get_increment)
+            == SemVerIncrement.MINOR
+        )
 
     def test_breaking_change(self, get_increment):
         commit_messages = [
             "feat: new feature",
             "feat!: breaking change",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MAJOR
+        assert (
+            find_increment_by_callable(commit_messages, get_increment)
+            == SemVerIncrement.MAJOR
+        )
 
     def test_multi_line_commit(self, get_increment):
         commit_messages = [
             "feat: new feature\n\nBREAKING CHANGE: major change",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MAJOR
+        assert (
+            find_increment_by_callable(commit_messages, get_increment)
+            == SemVerIncrement.MAJOR
+        )
 
     def test_no_increment_needed(self, get_increment):
         commit_messages = [
@@ -192,7 +242,7 @@ def test_major_version_zero(self):
             find_increment_by_callable(
                 commit_messages, lambda x: bump_rule.get_increment(x, True)
             )
-            == MINOR
+            == SemVerIncrement.MINOR
         )
 
     def test_mixed_commit_types(self, get_increment):
@@ -202,14 +252,20 @@ def test_mixed_commit_types(self, get_increment):
             "perf: improve performance",
             "refactor: restructure code",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+        assert (
+            find_increment_by_callable(commit_messages, get_increment)
+            == SemVerIncrement.MINOR
+        )
 
     def test_commit_with_scope(self, get_increment):
         commit_messages = [
             "feat(api): add new endpoint",
             "fix(ui): fix button alignment",
         ]
-        assert find_increment_by_callable(commit_messages, get_increment) == MINOR
+        assert (
+            find_increment_by_callable(commit_messages, get_increment)
+            == SemVerIncrement.MINOR
+        )
 
 
 class TestOldSchoolBumpRule:
@@ -220,17 +276,17 @@ def bump_pattern(self):
     @pytest.fixture
     def bump_map(self):
         return {
-            "MAJOR": MAJOR,
-            "MINOR": MINOR,
-            "PATCH": PATCH,
+            "SemVerIncrement.MAJOR": SemVerIncrement.MAJOR,
+            "SemVerIncrement.MINOR": SemVerIncrement.MINOR,
+            "SemVerIncrement.PATCH": SemVerIncrement.PATCH,
         }
 
     @pytest.fixture
     def bump_map_major_version_zero(self):
         return {
-            "MAJOR": MINOR,  # MAJOR becomes MINOR in version zero
-            "MINOR": MINOR,
-            "PATCH": PATCH,
+            "SemVerIncrement.MAJOR": SemVerIncrement.MINOR,  # SemVerIncrement.MAJOR becomes SemVerIncrement.MINOR in version zero
+            "SemVerIncrement.MINOR": SemVerIncrement.MINOR,
+            "SemVerIncrement.PATCH": SemVerIncrement.PATCH,
         }
 
     @pytest.fixture
@@ -239,31 +295,51 @@ def old_school_rule(self, bump_pattern, bump_map, bump_map_major_version_zero):
 
     def test_major_version(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("feat: add new feature [MAJOR]", False)
-            == MAJOR
+            old_school_rule.get_increment(
+                "feat: add new feature [SemVerIncrement.MAJOR]", False
+            )
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            old_school_rule.get_increment("fix: bug fix [SemVerIncrement.MAJOR]", False)
+            == SemVerIncrement.MAJOR
         )
-        assert old_school_rule.get_increment("fix: bug fix [MAJOR]", False) == MAJOR
 
     def test_minor_version(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("feat: add new feature [MINOR]", False)
-            == MINOR
+            old_school_rule.get_increment(
+                "feat: add new feature [SemVerIncrement.MINOR]", False
+            )
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            old_school_rule.get_increment("fix: bug fix [SemVerIncrement.MINOR]", False)
+            == SemVerIncrement.MINOR
         )
-        assert old_school_rule.get_increment("fix: bug fix [MINOR]", False) == MINOR
 
     def test_patch_version(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("feat: add new feature [PATCH]", False)
-            == PATCH
+            old_school_rule.get_increment(
+                "feat: add new feature [SemVerIncrement.PATCH]", False
+            )
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            old_school_rule.get_increment("fix: bug fix [SemVerIncrement.PATCH]", False)
+            == SemVerIncrement.PATCH
         )
-        assert old_school_rule.get_increment("fix: bug fix [PATCH]", False) == PATCH
 
     def test_major_version_zero(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("feat: add new feature [MAJOR]", True)
-            == MINOR
+            old_school_rule.get_increment(
+                "feat: add new feature [SemVerIncrement.MAJOR]", True
+            )
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            old_school_rule.get_increment("fix: bug fix [SemVerIncrement.MAJOR]", True)
+            == SemVerIncrement.MINOR
         )
-        assert old_school_rule.get_increment("fix: bug fix [MAJOR]", True) == MINOR
 
     def test_no_match(self, old_school_rule):
         assert old_school_rule.get_increment("feat: add new feature", False) is None
@@ -300,28 +376,37 @@ def test_empty_maps_with_valid_pattern(self, bump_pattern):
     def test_complex_pattern(self):
         pattern = r"^.*?\[(.*?)\].*?\[(.*?)\].*$"
         bump_map = {
-            "MAJOR": MAJOR,
-            "MINOR": MINOR,
-            "PATCH": PATCH,
+            "SemVerIncrement.MAJOR": SemVerIncrement.MAJOR,
+            "SemVerIncrement.MINOR": SemVerIncrement.MINOR,
+            "SemVerIncrement.PATCH": SemVerIncrement.PATCH,
         }
         rule = OldSchoolBumpRule(pattern, bump_map, bump_map)
 
         assert (
-            rule.get_increment("feat: add new feature [MAJOR] [MINOR]", False) == MAJOR
+            rule.get_increment(
+                "feat: add new feature [SemVerIncrement.MAJOR] [SemVerIncrement.MINOR]",
+                False,
+            )
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            rule.get_increment(
+                "fix: bug fix [SemVerIncrement.MINOR] [SemVerIncrement.PATCH]", False
+            )
+            == SemVerIncrement.MINOR
         )
-        assert rule.get_increment("fix: bug fix [MINOR] [PATCH]", False) == MINOR
 
     def test_with_find_increment_by_callable(self, old_school_rule):
         commit_messages = [
-            "feat: add new feature [MAJOR]",
-            "fix: bug fix [PATCH]",
-            "docs: update readme [MINOR]",
+            "feat: add new feature [SemVerIncrement.MAJOR]",
+            "fix: bug fix [SemVerIncrement.PATCH]",
+            "docs: update readme [SemVerIncrement.MINOR]",
         ]
         assert (
             find_increment_by_callable(
                 commit_messages, lambda x: old_school_rule.get_increment(x, False)
             )
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
 
     def test_flexible_bump_map(self, old_school_rule):
@@ -329,32 +414,60 @@ def test_flexible_bump_map(self, old_school_rule):
         # Test with multiple matching patterns
         pattern = r"^((?P<major>major)|(?P<minor>minor)|(?P<patch>patch))(?P<scope>\(.+\))?(?P<bang>!)?:"
         bump_map = {
-            "major": MAJOR,
-            "bang": MAJOR,
-            "minor": MINOR,
-            "patch": PATCH,
+            "major": SemVerIncrement.MAJOR,
+            "bang": SemVerIncrement.MAJOR,
+            "minor": SemVerIncrement.MINOR,
+            "patch": SemVerIncrement.PATCH,
         }
         bump_map_major_version_zero = {
-            "major": MINOR,
-            "bang": MINOR,
-            "minor": MINOR,
-            "patch": PATCH,
+            "major": SemVerIncrement.MINOR,
+            "bang": SemVerIncrement.MINOR,
+            "minor": SemVerIncrement.MINOR,
+            "patch": SemVerIncrement.PATCH,
         }
         rule = OldSchoolBumpRule(pattern, bump_map, bump_map_major_version_zero)
 
         # Test with multiple version tags
-        assert rule.get_increment("major!: drop support for Python 2.7", False) == MAJOR
-        assert rule.get_increment("major!: drop support for Python 2.7", True) == MINOR
-        assert rule.get_increment("major: drop support for Python 2.7", False) == MAJOR
-        assert rule.get_increment("major: drop support for Python 2.7", True) == MINOR
-        assert rule.get_increment("patch!: drop support for Python 2.7", False) == MAJOR
-        assert rule.get_increment("patch!: drop support for Python 2.7", True) == MINOR
-        assert rule.get_increment("patch: drop support for Python 2.7", False) == PATCH
-        assert rule.get_increment("patch: drop support for Python 2.7", True) == PATCH
-        assert rule.get_increment("minor: add new feature", False) == MINOR
-        assert rule.get_increment("minor: add new feature", True) == MINOR
-        assert rule.get_increment("patch: fix bug", False) == PATCH
-        assert rule.get_increment("patch: fix bug", True) == PATCH
+        assert (
+            rule.get_increment("major!: drop support for Python 2.7", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            rule.get_increment("major!: drop support for Python 2.7", True)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            rule.get_increment("major: drop support for Python 2.7", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            rule.get_increment("major: drop support for Python 2.7", True)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            rule.get_increment("patch!: drop support for Python 2.7", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            rule.get_increment("patch!: drop support for Python 2.7", True)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            rule.get_increment("patch: drop support for Python 2.7", False)
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            rule.get_increment("patch: drop support for Python 2.7", True)
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            rule.get_increment("minor: add new feature", False) == SemVerIncrement.MINOR
+        )
+        assert (
+            rule.get_increment("minor: add new feature", True) == SemVerIncrement.MINOR
+        )
+        assert rule.get_increment("patch: fix bug", False) == SemVerIncrement.PATCH
+        assert rule.get_increment("patch: fix bug", True) == SemVerIncrement.PATCH
 
 
 class TestOldSchoolBumpRuleWithDefault:
@@ -363,61 +476,92 @@ def old_school_rule(self):
         return OldSchoolBumpRule(BUMP_PATTERN, BUMP_MAP, BUMP_MAP_MAJOR_VERSION_ZERO)
 
     def test_breaking_change_with_bang(self, old_school_rule):
-        assert old_school_rule.get_increment("feat!: breaking change", False) == MAJOR
-        assert old_school_rule.get_increment("fix!: breaking change", False) == MAJOR
-        assert old_school_rule.get_increment("feat!: breaking change", True) == MINOR
-        assert old_school_rule.get_increment("fix!: breaking change", True) == MINOR
+        assert (
+            old_school_rule.get_increment("feat!: breaking change", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            old_school_rule.get_increment("fix!: breaking change", False)
+            == SemVerIncrement.MAJOR
+        )
+        assert (
+            old_school_rule.get_increment("feat!: breaking change", True)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            old_school_rule.get_increment("fix!: breaking change", True)
+            == SemVerIncrement.MINOR
+        )
 
     def test_breaking_change_type(self, old_school_rule):
         assert (
             old_school_rule.get_increment("BREAKING CHANGE: major change", False)
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
         assert (
             old_school_rule.get_increment("BREAKING-CHANGE: major change", False)
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
         assert (
             old_school_rule.get_increment("BREAKING CHANGE: major change", True)
-            == MINOR
+            == SemVerIncrement.MINOR
         )
         assert (
             old_school_rule.get_increment("BREAKING-CHANGE: major change", True)
-            == MINOR
+            == SemVerIncrement.MINOR
         )
 
     def test_feat_commit(self, old_school_rule):
-        assert old_school_rule.get_increment("feat: add new feature", False) == MINOR
-        assert old_school_rule.get_increment("feat: add new feature", True) == MINOR
+        assert (
+            old_school_rule.get_increment("feat: add new feature", False)
+            == SemVerIncrement.MINOR
+        )
+        assert (
+            old_school_rule.get_increment("feat: add new feature", True)
+            == SemVerIncrement.MINOR
+        )
 
     def test_fix_commit(self, old_school_rule):
-        assert old_school_rule.get_increment("fix: fix bug", False) == PATCH
-        assert old_school_rule.get_increment("fix: fix bug", True) == PATCH
+        assert (
+            old_school_rule.get_increment("fix: fix bug", False)
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            old_school_rule.get_increment("fix: fix bug", True) == SemVerIncrement.PATCH
+        )
 
     def test_refactor_commit(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("refactor: restructure code", False) == PATCH
+            old_school_rule.get_increment("refactor: restructure code", False)
+            == SemVerIncrement.PATCH
         )
         assert (
-            old_school_rule.get_increment("refactor: restructure code", True) == PATCH
+            old_school_rule.get_increment("refactor: restructure code", True)
+            == SemVerIncrement.PATCH
         )
 
     def test_perf_commit(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("perf: improve performance", False) == PATCH
+            old_school_rule.get_increment("perf: improve performance", False)
+            == SemVerIncrement.PATCH
+        )
+        assert (
+            old_school_rule.get_increment("perf: improve performance", True)
+            == SemVerIncrement.PATCH
         )
-        assert old_school_rule.get_increment("perf: improve performance", True) == PATCH
 
     def test_commit_with_scope(self, old_school_rule):
         assert (
-            old_school_rule.get_increment("feat(api): add new endpoint", False) == MINOR
+            old_school_rule.get_increment("feat(api): add new endpoint", False)
+            == SemVerIncrement.MINOR
         )
         assert (
             old_school_rule.get_increment("fix(ui): fix button alignment", False)
-            == PATCH
+            == SemVerIncrement.PATCH
         )
         assert (
-            old_school_rule.get_increment("refactor(core): restructure", False) == PATCH
+            old_school_rule.get_increment("refactor(core): restructure", False)
+            == SemVerIncrement.PATCH
         )
 
     def test_no_match(self, old_school_rule):
@@ -441,35 +585,73 @@ def test_with_find_increment_by_callable(self, old_school_rule):
             find_increment_by_callable(
                 commit_messages, lambda x: old_school_rule.get_increment(x, False)
             )
-            == MAJOR
+            == SemVerIncrement.MAJOR
         )
 
 
 def test_find_highest_increment():
     """Test the _find_highest_increment function."""
     # Test with single increment
-    assert _find_highest_increment([MAJOR]) == MAJOR
-    assert _find_highest_increment([MINOR]) == MINOR
-    assert _find_highest_increment([PATCH]) == PATCH
+    assert _find_highest_increment([SemVerIncrement.MAJOR]) == SemVerIncrement.MAJOR
+    assert _find_highest_increment([SemVerIncrement.MINOR]) == SemVerIncrement.MINOR
+    assert _find_highest_increment([SemVerIncrement.PATCH]) == SemVerIncrement.PATCH
 
     # Test with multiple increments
-    assert _find_highest_increment([PATCH, MINOR, MAJOR]) == MAJOR
-    assert _find_highest_increment([PATCH, MINOR]) == MINOR
-    assert _find_highest_increment([PATCH, PATCH]) == PATCH
+    assert (
+        _find_highest_increment(
+            [SemVerIncrement.PATCH, SemVerIncrement.MINOR, SemVerIncrement.MAJOR]
+        )
+        == SemVerIncrement.MAJOR
+    )
+    assert (
+        _find_highest_increment([SemVerIncrement.PATCH, SemVerIncrement.MINOR])
+        == SemVerIncrement.MINOR
+    )
+    assert (
+        _find_highest_increment([SemVerIncrement.PATCH, SemVerIncrement.PATCH])
+        == SemVerIncrement.PATCH
+    )
 
     # Test with None values
-    assert _find_highest_increment([None, PATCH]) == PATCH
+    assert (
+        _find_highest_increment([None, SemVerIncrement.PATCH]) == SemVerIncrement.PATCH
+    )
     assert _find_highest_increment([None, None]) is None
     assert _find_highest_increment([]) is None
 
     # Test with mixed values
-    assert _find_highest_increment([None, PATCH, MINOR, MAJOR]) == MAJOR
-    assert _find_highest_increment([None, PATCH, MINOR]) == MINOR
-    assert _find_highest_increment([None, PATCH]) == PATCH
+    assert (
+        _find_highest_increment(
+            [None, SemVerIncrement.PATCH, SemVerIncrement.MINOR, SemVerIncrement.MAJOR]
+        )
+        == SemVerIncrement.MAJOR
+    )
+    assert (
+        _find_highest_increment([None, SemVerIncrement.PATCH, SemVerIncrement.MINOR])
+        == SemVerIncrement.MINOR
+    )
+    assert (
+        _find_highest_increment([None, SemVerIncrement.PATCH]) == SemVerIncrement.PATCH
+    )
 
     # Test with empty iterator
     assert _find_highest_increment(iter([])) is None
 
     # Test with generator expression
-    assert _find_highest_increment(x for x in [PATCH, MINOR, MAJOR]) == MAJOR
-    assert _find_highest_increment(x for x in [None, PATCH, MINOR]) == MINOR
+    assert (
+        _find_highest_increment(
+            x
+            for x in [
+                SemVerIncrement.PATCH,
+                SemVerIncrement.MINOR,
+                SemVerIncrement.MAJOR,
+            ]
+        )
+        == SemVerIncrement.MAJOR
+    )
+    assert (
+        _find_highest_increment(
+            x for x in [None, SemVerIncrement.PATCH, SemVerIncrement.MINOR]
+        )
+        == SemVerIncrement.MINOR
+    )
diff --git a/tests/test_version_scheme_pep440.py b/tests/test_version_scheme_pep440.py
index a983dad14..54fd15229 100644
--- a/tests/test_version_scheme_pep440.py
+++ b/tests/test_version_scheme_pep440.py
@@ -3,22 +3,23 @@
 
 import pytest
 
+from commitizen.bump_rule import SemVerIncrement
 from commitizen.version_schemes import Pep440, VersionProtocol
 
 simple_flow = [
-    (("0.1.0", "PATCH", None, 0, None), "0.1.1"),
-    (("0.1.0", "PATCH", None, 0, 1), "0.1.1.dev1"),
-    (("0.1.1", "MINOR", None, 0, None), "0.2.0"),
-    (("0.2.0", "MINOR", None, 0, None), "0.3.0"),
-    (("0.2.0", "MINOR", None, 0, 1), "0.3.0.dev1"),
-    (("0.3.0", "PATCH", None, 0, None), "0.3.1"),
-    (("0.3.0", "PATCH", "alpha", 0, None), "0.3.1a0"),
+    (("0.1.0", SemVerIncrement.PATCH, None, 0, None), "0.1.1"),
+    (("0.1.0", SemVerIncrement.PATCH, None, 0, 1), "0.1.1.dev1"),
+    (("0.1.1", SemVerIncrement.MINOR, None, 0, None), "0.2.0"),
+    (("0.2.0", SemVerIncrement.MINOR, None, 0, None), "0.3.0"),
+    (("0.2.0", SemVerIncrement.MINOR, None, 0, 1), "0.3.0.dev1"),
+    (("0.3.0", SemVerIncrement.PATCH, None, 0, None), "0.3.1"),
+    (("0.3.0", SemVerIncrement.PATCH, "alpha", 0, None), "0.3.1a0"),
     (("0.3.1a0", None, "alpha", 0, None), "0.3.1a1"),
-    (("0.3.0", "PATCH", "alpha", 1, None), "0.3.1a1"),
+    (("0.3.0", SemVerIncrement.PATCH, "alpha", 1, None), "0.3.1a1"),
     (("0.3.1a0", None, "alpha", 1, None), "0.3.1a1"),
     (("0.3.1a0", None, None, 0, None), "0.3.1"),
-    (("0.3.1", "PATCH", None, 0, None), "0.3.2"),
-    (("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0a0"),
+    (("0.3.1", SemVerIncrement.PATCH, None, 0, None), "0.3.2"),
+    (("0.4.2", SemVerIncrement.MAJOR, "alpha", 0, None), "1.0.0a0"),
     (("1.0.0a0", None, "alpha", 0, None), "1.0.0a1"),
     (("1.0.0a1", None, "alpha", 0, None), "1.0.0a2"),
     (("1.0.0a1", None, "alpha", 0, 1), "1.0.0a2.dev1"),
@@ -29,20 +30,20 @@
     (("1.0.0b1", None, "rc", 0, None), "1.0.0rc0"),
     (("1.0.0rc0", None, "rc", 0, None), "1.0.0rc1"),
     (("1.0.0rc0", None, "rc", 0, 1), "1.0.0rc1.dev1"),
-    (("1.0.0rc0", "PATCH", None, 0, None), "1.0.0"),
+    (("1.0.0rc0", SemVerIncrement.PATCH, None, 0, None), "1.0.0"),
     (("1.0.0a3.dev0", None, "beta", 0, None), "1.0.0b0"),
-    (("1.0.0", "PATCH", None, 0, None), "1.0.1"),
-    (("1.0.1", "PATCH", None, 0, None), "1.0.2"),
-    (("1.0.2", "MINOR", None, 0, None), "1.1.0"),
-    (("1.1.0", "MINOR", None, 0, None), "1.2.0"),
-    (("1.2.0", "PATCH", None, 0, None), "1.2.1"),
-    (("1.2.1", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.0.0", SemVerIncrement.PATCH, None, 0, None), "1.0.1"),
+    (("1.0.1", SemVerIncrement.PATCH, None, 0, None), "1.0.2"),
+    (("1.0.2", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1.1.0", SemVerIncrement.MINOR, None, 0, None), "1.2.0"),
+    (("1.2.0", SemVerIncrement.PATCH, None, 0, None), "1.2.1"),
+    (("1.2.1", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
 ]
 
 local_versions = [
-    (("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"),
-    (("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"),
-    (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"),
+    (("4.5.0+0.1.0", SemVerIncrement.PATCH, None, 0, None), "4.5.0+0.1.1"),
+    (("4.5.0+0.1.1", SemVerIncrement.MINOR, None, 0, None), "4.5.0+0.2.0"),
+    (("4.5.0+0.2.0", SemVerIncrement.MAJOR, None, 0, None), "4.5.0+1.0.0"),
 ]
 
 # never bump backwards on pre-releases
@@ -53,9 +54,9 @@
 ]
 
 weird_cases = [
-    (("1.1", "PATCH", None, 0, None), "1.1.1"),
-    (("1", "MINOR", None, 0, None), "1.1.0"),
-    (("1", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.1", SemVerIncrement.PATCH, None, 0, None), "1.1.1"),
+    (("1", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
     (("1a0", None, "alpha", 0, None), "1.0.0a1"),
     (("1a0", None, "alpha", 1, None), "1.0.0a1"),
     (("1", None, "beta", 0, None), "1.0.0b0"),
@@ -63,18 +64,18 @@
     (("1beta", None, "beta", 0, None), "1.0.0b1"),
     (("1.0.0alpha1", None, "alpha", 0, None), "1.0.0a2"),
     (("1", None, "rc", 0, None), "1.0.0rc0"),
-    (("1.0.0rc1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"),
+    (("1.0.0rc1+e20d7b57f3eb", SemVerIncrement.PATCH, None, 0, None), "1.0.0"),
 ]
 
 # test driven development
 tdd_cases = [
-    (("0.1.1", "PATCH", None, 0, None), "0.1.2"),
-    (("0.1.1", "MINOR", None, 0, None), "0.2.0"),
-    (("2.1.1", "MAJOR", None, 0, None), "3.0.0"),
-    (("0.9.0", "PATCH", "alpha", 0, None), "0.9.1a0"),
-    (("0.9.0", "MINOR", "alpha", 0, None), "0.10.0a0"),
-    (("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0a0"),
-    (("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0a1"),
+    (("0.1.1", SemVerIncrement.PATCH, None, 0, None), "0.1.2"),
+    (("0.1.1", SemVerIncrement.MINOR, None, 0, None), "0.2.0"),
+    (("2.1.1", SemVerIncrement.MAJOR, None, 0, None), "3.0.0"),
+    (("0.9.0", SemVerIncrement.PATCH, "alpha", 0, None), "0.9.1a0"),
+    (("0.9.0", SemVerIncrement.MINOR, "alpha", 0, None), "0.10.0a0"),
+    (("0.9.0", SemVerIncrement.MAJOR, "alpha", 0, None), "1.0.0a0"),
+    (("0.9.0", SemVerIncrement.MAJOR, "alpha", 1, None), "1.0.0a1"),
     (("1.0.0a2", None, "beta", 0, None), "1.0.0b0"),
     (("1.0.0a2", None, "beta", 1, None), "1.0.0b1"),
     (("1.0.0beta1", None, "rc", 0, None), "1.0.0rc0"),
@@ -84,51 +85,51 @@
 # additional pre-release tests run through various release scenarios
 prerelease_cases = [
     #
-    (("3.3.3", "PATCH", "alpha", 0, None), "3.3.4a0"),
-    (("3.3.4a0", "PATCH", "alpha", 0, None), "3.3.4a1"),
-    (("3.3.4a1", "MINOR", "alpha", 0, None), "3.4.0a0"),
-    (("3.4.0a0", "PATCH", "alpha", 0, None), "3.4.0a1"),
-    (("3.4.0a1", "MINOR", "alpha", 0, None), "3.4.0a2"),
-    (("3.4.0a2", "MAJOR", "alpha", 0, None), "4.0.0a0"),
-    (("4.0.0a0", "PATCH", "alpha", 0, None), "4.0.0a1"),
-    (("4.0.0a1", "MINOR", "alpha", 0, None), "4.0.0a2"),
-    (("4.0.0a2", "MAJOR", "alpha", 0, None), "4.0.0a3"),
+    (("3.3.3", SemVerIncrement.PATCH, "alpha", 0, None), "3.3.4a0"),
+    (("3.3.4a0", SemVerIncrement.PATCH, "alpha", 0, None), "3.3.4a1"),
+    (("3.3.4a1", SemVerIncrement.MINOR, "alpha", 0, None), "3.4.0a0"),
+    (("3.4.0a0", SemVerIncrement.PATCH, "alpha", 0, None), "3.4.0a1"),
+    (("3.4.0a1", SemVerIncrement.MINOR, "alpha", 0, None), "3.4.0a2"),
+    (("3.4.0a2", SemVerIncrement.MAJOR, "alpha", 0, None), "4.0.0a0"),
+    (("4.0.0a0", SemVerIncrement.PATCH, "alpha", 0, None), "4.0.0a1"),
+    (("4.0.0a1", SemVerIncrement.MINOR, "alpha", 0, None), "4.0.0a2"),
+    (("4.0.0a2", SemVerIncrement.MAJOR, "alpha", 0, None), "4.0.0a3"),
     #
-    (("1.0.0", "PATCH", "alpha", 0, None), "1.0.1a0"),
-    (("1.0.1a0", "PATCH", "alpha", 0, None), "1.0.1a1"),
-    (("1.0.1a1", "MINOR", "alpha", 0, None), "1.1.0a0"),
-    (("1.1.0a0", "PATCH", "alpha", 0, None), "1.1.0a1"),
-    (("1.1.0a1", "MINOR", "alpha", 0, None), "1.1.0a2"),
-    (("1.1.0a2", "MAJOR", "alpha", 0, None), "2.0.0a0"),
+    (("1.0.0", SemVerIncrement.PATCH, "alpha", 0, None), "1.0.1a0"),
+    (("1.0.1a0", SemVerIncrement.PATCH, "alpha", 0, None), "1.0.1a1"),
+    (("1.0.1a1", SemVerIncrement.MINOR, "alpha", 0, None), "1.1.0a0"),
+    (("1.1.0a0", SemVerIncrement.PATCH, "alpha", 0, None), "1.1.0a1"),
+    (("1.1.0a1", SemVerIncrement.MINOR, "alpha", 0, None), "1.1.0a2"),
+    (("1.1.0a2", SemVerIncrement.MAJOR, "alpha", 0, None), "2.0.0a0"),
     #
-    (("1.0.0", "MINOR", "alpha", 0, None), "1.1.0a0"),
-    (("1.1.0a0", "PATCH", "alpha", 0, None), "1.1.0a1"),
-    (("1.1.0a1", "MINOR", "alpha", 0, None), "1.1.0a2"),
-    (("1.1.0a2", "PATCH", "alpha", 0, None), "1.1.0a3"),
-    (("1.1.0a3", "MAJOR", "alpha", 0, None), "2.0.0a0"),
+    (("1.0.0", SemVerIncrement.MINOR, "alpha", 0, None), "1.1.0a0"),
+    (("1.1.0a0", SemVerIncrement.PATCH, "alpha", 0, None), "1.1.0a1"),
+    (("1.1.0a1", SemVerIncrement.MINOR, "alpha", 0, None), "1.1.0a2"),
+    (("1.1.0a2", SemVerIncrement.PATCH, "alpha", 0, None), "1.1.0a3"),
+    (("1.1.0a3", SemVerIncrement.MAJOR, "alpha", 0, None), "2.0.0a0"),
     #
-    (("1.0.0", "MAJOR", "alpha", 0, None), "2.0.0a0"),
-    (("2.0.0a0", "MINOR", "alpha", 0, None), "2.0.0a1"),
-    (("2.0.0a1", "PATCH", "alpha", 0, None), "2.0.0a2"),
-    (("2.0.0a2", "MAJOR", "alpha", 0, None), "2.0.0a3"),
-    (("2.0.0a3", "MINOR", "alpha", 0, None), "2.0.0a4"),
-    (("2.0.0a4", "PATCH", "alpha", 0, None), "2.0.0a5"),
-    (("2.0.0a5", "MAJOR", "alpha", 0, None), "2.0.0a6"),
+    (("1.0.0", SemVerIncrement.MAJOR, "alpha", 0, None), "2.0.0a0"),
+    (("2.0.0a0", SemVerIncrement.MINOR, "alpha", 0, None), "2.0.0a1"),
+    (("2.0.0a1", SemVerIncrement.PATCH, "alpha", 0, None), "2.0.0a2"),
+    (("2.0.0a2", SemVerIncrement.MAJOR, "alpha", 0, None), "2.0.0a3"),
+    (("2.0.0a3", SemVerIncrement.MINOR, "alpha", 0, None), "2.0.0a4"),
+    (("2.0.0a4", SemVerIncrement.PATCH, "alpha", 0, None), "2.0.0a5"),
+    (("2.0.0a5", SemVerIncrement.MAJOR, "alpha", 0, None), "2.0.0a6"),
     #
-    (("2.0.0b0", "MINOR", "alpha", 0, None), "2.0.0b1"),
-    (("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.0b1"),
+    (("2.0.0b0", SemVerIncrement.MINOR, "alpha", 0, None), "2.0.0b1"),
+    (("2.0.0b0", SemVerIncrement.PATCH, "alpha", 0, None), "2.0.0b1"),
     #
-    (("1.0.1a0", "PATCH", None, 0, None), "1.0.1"),
-    (("1.0.1a0", "MINOR", None, 0, None), "1.1.0"),
-    (("1.0.1a0", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.0.1a0", SemVerIncrement.PATCH, None, 0, None), "1.0.1"),
+    (("1.0.1a0", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1.0.1a0", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
     #
-    (("1.1.0a0", "PATCH", None, 0, None), "1.1.0"),
-    (("1.1.0a0", "MINOR", None, 0, None), "1.1.0"),
-    (("1.1.0a0", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.1.0a0", SemVerIncrement.PATCH, None, 0, None), "1.1.0"),
+    (("1.1.0a0", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1.1.0a0", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
     #
-    (("2.0.0a0", "MINOR", None, 0, None), "2.0.0"),
-    (("2.0.0a0", "MAJOR", None, 0, None), "2.0.0"),
-    (("2.0.0a0", "PATCH", None, 0, None), "2.0.0"),
+    (("2.0.0a0", SemVerIncrement.MINOR, None, 0, None), "2.0.0"),
+    (("2.0.0a0", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
+    (("2.0.0a0", SemVerIncrement.PATCH, None, 0, None), "2.0.0"),
     #
     (("3.0.0a1", None, None, 0, None), "3.0.0"),
     (("3.0.0b1", None, None, 0, None), "3.0.0"),
@@ -139,48 +140,48 @@
     (("3.1.4", None, "rc", 0, None), "3.1.4rc0"),
     #
     (("3.1.4", None, "alpha", 0, None), "3.1.4a0"),
-    (("3.1.4a0", "PATCH", "alpha", 0, None), "3.1.4a1"),  # UNEXPECTED!
-    (("3.1.4a0", "MINOR", "alpha", 0, None), "3.2.0a0"),
-    (("3.1.4a0", "MAJOR", "alpha", 0, None), "4.0.0a0"),
+    (("3.1.4a0", SemVerIncrement.PATCH, "alpha", 0, None), "3.1.4a1"),  # UNEXPECTED!
+    (("3.1.4a0", SemVerIncrement.MINOR, "alpha", 0, None), "3.2.0a0"),
+    (("3.1.4a0", SemVerIncrement.MAJOR, "alpha", 0, None), "4.0.0a0"),
 ]
 
 exact_cases = [
-    (("1.0.0", "PATCH", None, 0, None), "1.0.1"),
-    (("1.0.0", "MINOR", None, 0, None), "1.1.0"),
+    (("1.0.0", SemVerIncrement.PATCH, None, 0, None), "1.0.1"),
+    (("1.0.0", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
     # with exact_increment=False: "1.0.0b0"
-    (("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1b0"),
+    (("1.0.0a1", SemVerIncrement.PATCH, "beta", 0, None), "1.0.1b0"),
     # with exact_increment=False: "1.0.0b1"
-    (("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1b0"),
+    (("1.0.0b0", SemVerIncrement.PATCH, "beta", 0, None), "1.0.1b0"),
     # with exact_increment=False: "1.0.0rc0"
-    (("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1rc0"),
+    (("1.0.0b1", SemVerIncrement.PATCH, "rc", 0, None), "1.0.1rc0"),
     # with exact_increment=False: "1.0.0-rc1"
-    (("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1rc0"),
+    (("1.0.0rc0", SemVerIncrement.PATCH, "rc", 0, None), "1.0.1rc0"),
     # with exact_increment=False: "1.0.0rc1-dev1"
-    (("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1rc0.dev1"),
+    (("1.0.0rc0", SemVerIncrement.PATCH, "rc", 0, 1), "1.0.1rc0.dev1"),
     # with exact_increment=False: "1.0.0b0"
-    (("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0b0"),
+    (("1.0.0a1", SemVerIncrement.MINOR, "beta", 0, None), "1.1.0b0"),
     # with exact_increment=False: "1.0.0b1"
-    (("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0b0"),
+    (("1.0.0b0", SemVerIncrement.MINOR, "beta", 0, None), "1.1.0b0"),
     # with exact_increment=False: "1.0.0b1"
-    (("1.0.0b0", "MINOR", "alpha", 0, None), "1.1.0a0"),
+    (("1.0.0b0", SemVerIncrement.MINOR, "alpha", 0, None), "1.1.0a0"),
     # with exact_increment=False: "1.0.0rc0"
-    (("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0rc0"),
+    (("1.0.0b1", SemVerIncrement.MINOR, "rc", 0, None), "1.1.0rc0"),
     # with exact_increment=False: "1.0.0rc1"
-    (("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0rc0"),
+    (("1.0.0rc0", SemVerIncrement.MINOR, "rc", 0, None), "1.1.0rc0"),
     # with exact_increment=False: "1.0.0rc1-dev1"
-    (("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0rc0.dev1"),
+    (("1.0.0rc0", SemVerIncrement.MINOR, "rc", 0, 1), "1.1.0rc0.dev1"),
     # with exact_increment=False: "2.0.0"
-    (("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
+    (("2.0.0b0", SemVerIncrement.MAJOR, None, 0, None), "3.0.0"),
     # with exact_increment=False: "2.0.0"
-    (("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
+    (("2.0.0b0", SemVerIncrement.MINOR, None, 0, None), "2.1.0"),
     # with exact_increment=False: "2.0.0"
-    (("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
+    (("2.0.0b0", SemVerIncrement.PATCH, None, 0, None), "2.0.1"),
     # same with exact_increment=False
-    (("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0a0"),
+    (("2.0.0b0", SemVerIncrement.MAJOR, "alpha", 0, None), "3.0.0a0"),
     # with exact_increment=False: "2.0.0b1"
-    (("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0a0"),
+    (("2.0.0b0", SemVerIncrement.MINOR, "alpha", 0, None), "2.1.0a0"),
     # with exact_increment=False: "2.0.0b1"
-    (("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1a0"),
+    (("2.0.0b0", SemVerIncrement.PATCH, "alpha", 0, None), "2.0.1a0"),
 ]
 
 
diff --git a/tests/test_version_scheme_semver.py b/tests/test_version_scheme_semver.py
index 8785717a3..fa1b2fd72 100644
--- a/tests/test_version_scheme_semver.py
+++ b/tests/test_version_scheme_semver.py
@@ -3,22 +3,23 @@
 
 import pytest
 
+from commitizen.bump_rule import SemVerIncrement
 from commitizen.version_schemes import SemVer, VersionProtocol
 
 simple_flow = [
-    (("0.1.0", "PATCH", None, 0, None), "0.1.1"),
-    (("0.1.0", "PATCH", None, 0, 1), "0.1.1-dev1"),
-    (("0.1.1", "MINOR", None, 0, None), "0.2.0"),
-    (("0.2.0", "MINOR", None, 0, None), "0.3.0"),
-    (("0.2.0", "MINOR", None, 0, 1), "0.3.0-dev1"),
-    (("0.3.0", "PATCH", None, 0, None), "0.3.1"),
-    (("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-a0"),
+    (("0.1.0", SemVerIncrement.PATCH, None, 0, None), "0.1.1"),
+    (("0.1.0", SemVerIncrement.PATCH, None, 0, 1), "0.1.1-dev1"),
+    (("0.1.1", SemVerIncrement.MINOR, None, 0, None), "0.2.0"),
+    (("0.2.0", SemVerIncrement.MINOR, None, 0, None), "0.3.0"),
+    (("0.2.0", SemVerIncrement.MINOR, None, 0, 1), "0.3.0-dev1"),
+    (("0.3.0", SemVerIncrement.PATCH, None, 0, None), "0.3.1"),
+    (("0.3.0", SemVerIncrement.PATCH, "alpha", 0, None), "0.3.1-a0"),
     (("0.3.1a0", None, "alpha", 0, None), "0.3.1-a1"),
-    (("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-a1"),
+    (("0.3.0", SemVerIncrement.PATCH, "alpha", 1, None), "0.3.1-a1"),
     (("0.3.1a0", None, "alpha", 1, None), "0.3.1-a1"),
     (("0.3.1a0", None, None, 0, None), "0.3.1"),
-    (("0.3.1", "PATCH", None, 0, None), "0.3.2"),
-    (("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0-a0"),
+    (("0.3.1", SemVerIncrement.PATCH, None, 0, None), "0.3.2"),
+    (("0.4.2", SemVerIncrement.MAJOR, "alpha", 0, None), "1.0.0-a0"),
     (("1.0.0a0", None, "alpha", 0, None), "1.0.0-a1"),
     (("1.0.0a1", None, "alpha", 0, None), "1.0.0-a2"),
     (("1.0.0a1", None, "alpha", 0, 1), "1.0.0-a2-dev1"),
@@ -29,20 +30,20 @@
     (("1.0.0b1", None, "rc", 0, None), "1.0.0-rc0"),
     (("1.0.0rc0", None, "rc", 0, None), "1.0.0-rc1"),
     (("1.0.0rc0", None, "rc", 0, 1), "1.0.0-rc1-dev1"),
-    (("1.0.0rc0", "PATCH", None, 0, None), "1.0.0"),
+    (("1.0.0rc0", SemVerIncrement.PATCH, None, 0, None), "1.0.0"),
     (("1.0.0a3.dev0", None, "beta", 0, None), "1.0.0-b0"),
-    (("1.0.0", "PATCH", None, 0, None), "1.0.1"),
-    (("1.0.1", "PATCH", None, 0, None), "1.0.2"),
-    (("1.0.2", "MINOR", None, 0, None), "1.1.0"),
-    (("1.1.0", "MINOR", None, 0, None), "1.2.0"),
-    (("1.2.0", "PATCH", None, 0, None), "1.2.1"),
-    (("1.2.1", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.0.0", SemVerIncrement.PATCH, None, 0, None), "1.0.1"),
+    (("1.0.1", SemVerIncrement.PATCH, None, 0, None), "1.0.2"),
+    (("1.0.2", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1.1.0", SemVerIncrement.MINOR, None, 0, None), "1.2.0"),
+    (("1.2.0", SemVerIncrement.PATCH, None, 0, None), "1.2.1"),
+    (("1.2.1", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
 ]
 
 local_versions = [
-    (("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"),
-    (("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"),
-    (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"),
+    (("4.5.0+0.1.0", SemVerIncrement.PATCH, None, 0, None), "4.5.0+0.1.1"),
+    (("4.5.0+0.1.1", SemVerIncrement.MINOR, None, 0, None), "4.5.0+0.2.0"),
+    (("4.5.0+0.2.0", SemVerIncrement.MAJOR, None, 0, None), "4.5.0+1.0.0"),
 ]
 
 # never bump backwards on pre-releases
@@ -53,9 +54,9 @@
 ]
 
 weird_cases = [
-    (("1.1", "PATCH", None, 0, None), "1.1.1"),
-    (("1", "MINOR", None, 0, None), "1.1.0"),
-    (("1", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.1", SemVerIncrement.PATCH, None, 0, None), "1.1.1"),
+    (("1", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
     (("1a0", None, "alpha", 0, None), "1.0.0-a1"),
     (("1a0", None, "alpha", 1, None), "1.0.0-a1"),
     (("1", None, "beta", 0, None), "1.0.0-b0"),
@@ -63,18 +64,18 @@
     (("1beta", None, "beta", 0, None), "1.0.0-b1"),
     (("1.0.0alpha1", None, "alpha", 0, None), "1.0.0-a2"),
     (("1", None, "rc", 0, None), "1.0.0-rc0"),
-    (("1.0.0rc1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"),
+    (("1.0.0rc1+e20d7b57f3eb", SemVerIncrement.PATCH, None, 0, None), "1.0.0"),
 ]
 
 # test driven development
 tdd_cases = [
-    (("0.1.1", "PATCH", None, 0, None), "0.1.2"),
-    (("0.1.1", "MINOR", None, 0, None), "0.2.0"),
-    (("2.1.1", "MAJOR", None, 0, None), "3.0.0"),
-    (("0.9.0", "PATCH", "alpha", 0, None), "0.9.1-a0"),
-    (("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-a0"),
-    (("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-a0"),
-    (("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-a1"),
+    (("0.1.1", SemVerIncrement.PATCH, None, 0, None), "0.1.2"),
+    (("0.1.1", SemVerIncrement.MINOR, None, 0, None), "0.2.0"),
+    (("2.1.1", SemVerIncrement.MAJOR, None, 0, None), "3.0.0"),
+    (("0.9.0", SemVerIncrement.PATCH, "alpha", 0, None), "0.9.1-a0"),
+    (("0.9.0", SemVerIncrement.MINOR, "alpha", 0, None), "0.10.0-a0"),
+    (("0.9.0", SemVerIncrement.MAJOR, "alpha", 0, None), "1.0.0-a0"),
+    (("0.9.0", SemVerIncrement.MAJOR, "alpha", 1, None), "1.0.0-a1"),
     (("1.0.0a2", None, "beta", 0, None), "1.0.0-b0"),
     (("1.0.0a2", None, "beta", 1, None), "1.0.0-b1"),
     (("1.0.0beta1", None, "rc", 0, None), "1.0.0-rc0"),
@@ -84,40 +85,40 @@
 ]
 
 exact_cases = [
-    (("1.0.0", "PATCH", None, 0, None), "1.0.1"),
-    (("1.0.0", "MINOR", None, 0, None), "1.1.0"),
+    (("1.0.0", SemVerIncrement.PATCH, None, 0, None), "1.0.1"),
+    (("1.0.0", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
     # with exact_increment=False: "1.0.0-b0"
-    (("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"),
+    (("1.0.0a1", SemVerIncrement.PATCH, "beta", 0, None), "1.0.1-b0"),
     # with exact_increment=False: "1.0.0-b1"
-    (("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"),
+    (("1.0.0b0", SemVerIncrement.PATCH, "beta", 0, None), "1.0.1-b0"),
     # with exact_increment=False: "1.0.0-rc0"
-    (("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"),
+    (("1.0.0b1", SemVerIncrement.PATCH, "rc", 0, None), "1.0.1-rc0"),
     # with exact_increment=False: "1.0.0-rc1"
-    (("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"),
+    (("1.0.0rc0", SemVerIncrement.PATCH, "rc", 0, None), "1.0.1-rc0"),
     # with exact_increment=False: "1.0.0-rc1-dev1"
-    (("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"),
+    (("1.0.0rc0", SemVerIncrement.PATCH, "rc", 0, 1), "1.0.1-rc0-dev1"),
     # with exact_increment=False: "1.0.0-b0"
-    (("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"),
+    (("1.0.0a1", SemVerIncrement.MINOR, "beta", 0, None), "1.1.0-b0"),
     # with exact_increment=False: "1.0.0-b1"
-    (("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"),
+    (("1.0.0b0", SemVerIncrement.MINOR, "beta", 0, None), "1.1.0-b0"),
     # with exact_increment=False: "1.0.0-rc0"
-    (("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"),
+    (("1.0.0b1", SemVerIncrement.MINOR, "rc", 0, None), "1.1.0-rc0"),
     # with exact_increment=False: "1.0.0-rc1"
-    (("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"),
+    (("1.0.0rc0", SemVerIncrement.MINOR, "rc", 0, None), "1.1.0-rc0"),
     # with exact_increment=False: "1.0.0-rc1-dev1"
-    (("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"),
+    (("1.0.0rc0", SemVerIncrement.MINOR, "rc", 0, 1), "1.1.0-rc0-dev1"),
     # with exact_increment=False: "2.0.0"
-    (("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"),
+    (("2.0.0b0", SemVerIncrement.MAJOR, None, 0, None), "3.0.0"),
     # with exact_increment=False: "2.0.0"
-    (("2.0.0b0", "MINOR", None, 0, None), "2.1.0"),
+    (("2.0.0b0", SemVerIncrement.MINOR, None, 0, None), "2.1.0"),
     # with exact_increment=False: "2.0.0"
-    (("2.0.0b0", "PATCH", None, 0, None), "2.0.1"),
+    (("2.0.0b0", SemVerIncrement.PATCH, None, 0, None), "2.0.1"),
     # same with exact_increment=False
-    (("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0-a0"),
+    (("2.0.0b0", SemVerIncrement.MAJOR, "alpha", 0, None), "3.0.0-a0"),
     # with exact_increment=False: "2.0.0b1"
-    (("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0-a0"),
+    (("2.0.0b0", SemVerIncrement.MINOR, "alpha", 0, None), "2.1.0-a0"),
     # with exact_increment=False: "2.0.0b1"
-    (("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1-a0"),
+    (("2.0.0b0", SemVerIncrement.PATCH, "alpha", 0, None), "2.0.1-a0"),
 ]
 
 
diff --git a/tests/test_version_scheme_semver2.py b/tests/test_version_scheme_semver2.py
index d18a058a7..9f47ed256 100644
--- a/tests/test_version_scheme_semver2.py
+++ b/tests/test_version_scheme_semver2.py
@@ -3,22 +3,23 @@
 
 import pytest
 
+from commitizen.bump_rule import SemVerIncrement
 from commitizen.version_schemes import SemVer2, VersionProtocol
 
 simple_flow = [
-    (("0.1.0", "PATCH", None, 0, None), "0.1.1"),
-    (("0.1.0", "PATCH", None, 0, 1), "0.1.1-dev.1"),
-    (("0.1.1", "MINOR", None, 0, None), "0.2.0"),
-    (("0.2.0", "MINOR", None, 0, None), "0.3.0"),
-    (("0.2.0", "MINOR", None, 0, 1), "0.3.0-dev.1"),
-    (("0.3.0", "PATCH", None, 0, None), "0.3.1"),
-    (("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-alpha.0"),
+    (("0.1.0", SemVerIncrement.PATCH, None, 0, None), "0.1.1"),
+    (("0.1.0", SemVerIncrement.PATCH, None, 0, 1), "0.1.1-dev.1"),
+    (("0.1.1", SemVerIncrement.MINOR, None, 0, None), "0.2.0"),
+    (("0.2.0", SemVerIncrement.MINOR, None, 0, None), "0.3.0"),
+    (("0.2.0", SemVerIncrement.MINOR, None, 0, 1), "0.3.0-dev.1"),
+    (("0.3.0", SemVerIncrement.PATCH, None, 0, None), "0.3.1"),
+    (("0.3.0", SemVerIncrement.PATCH, "alpha", 0, None), "0.3.1-alpha.0"),
     (("0.3.1-alpha.0", None, "alpha", 0, None), "0.3.1-alpha.1"),
-    (("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-alpha.1"),
+    (("0.3.0", SemVerIncrement.PATCH, "alpha", 1, None), "0.3.1-alpha.1"),
     (("0.3.1-alpha.0", None, "alpha", 1, None), "0.3.1-alpha.1"),
     (("0.3.1-alpha.0", None, None, 0, None), "0.3.1"),
-    (("0.3.1", "PATCH", None, 0, None), "0.3.2"),
-    (("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"),
+    (("0.3.1", SemVerIncrement.PATCH, None, 0, None), "0.3.2"),
+    (("0.4.2", SemVerIncrement.MAJOR, "alpha", 0, None), "1.0.0-alpha.0"),
     (("1.0.0-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"),
     (("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
     (("1.0.0-alpha.1", None, "alpha", 0, 1), "1.0.0-alpha.2.dev.1"),
@@ -29,20 +30,20 @@
     (("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"),
     (("1.0.0-rc.0", None, "rc", 0, None), "1.0.0-rc.1"),
     (("1.0.0-rc.0", None, "rc", 0, 1), "1.0.0-rc.1.dev.1"),
-    (("1.0.0-rc.0", "PATCH", None, 0, None), "1.0.0"),
+    (("1.0.0-rc.0", SemVerIncrement.PATCH, None, 0, None), "1.0.0"),
     (("1.0.0-alpha.3.dev.0", None, "beta", 0, None), "1.0.0-beta.0"),
-    (("1.0.0", "PATCH", None, 0, None), "1.0.1"),
-    (("1.0.1", "PATCH", None, 0, None), "1.0.2"),
-    (("1.0.2", "MINOR", None, 0, None), "1.1.0"),
-    (("1.1.0", "MINOR", None, 0, None), "1.2.0"),
-    (("1.2.0", "PATCH", None, 0, None), "1.2.1"),
-    (("1.2.1", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.0.0", SemVerIncrement.PATCH, None, 0, None), "1.0.1"),
+    (("1.0.1", SemVerIncrement.PATCH, None, 0, None), "1.0.2"),
+    (("1.0.2", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1.1.0", SemVerIncrement.MINOR, None, 0, None), "1.2.0"),
+    (("1.2.0", SemVerIncrement.PATCH, None, 0, None), "1.2.1"),
+    (("1.2.1", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
 ]
 
 local_versions = [
-    (("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"),
-    (("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"),
-    (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"),
+    (("4.5.0+0.1.0", SemVerIncrement.PATCH, None, 0, None), "4.5.0+0.1.1"),
+    (("4.5.0+0.1.1", SemVerIncrement.MINOR, None, 0, None), "4.5.0+0.2.0"),
+    (("4.5.0+0.2.0", SemVerIncrement.MAJOR, None, 0, None), "4.5.0+1.0.0"),
 ]
 
 # never bump backwards on pre-releases
@@ -53,9 +54,9 @@
 ]
 
 weird_cases = [
-    (("1.1", "PATCH", None, 0, None), "1.1.1"),
-    (("1", "MINOR", None, 0, None), "1.1.0"),
-    (("1", "MAJOR", None, 0, None), "2.0.0"),
+    (("1.1", SemVerIncrement.PATCH, None, 0, None), "1.1.1"),
+    (("1", SemVerIncrement.MINOR, None, 0, None), "1.1.0"),
+    (("1", SemVerIncrement.MAJOR, None, 0, None), "2.0.0"),
     (("1-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"),
     (("1-alpha.0", None, "alpha", 1, None), "1.0.0-alpha.1"),
     (("1", None, "beta", 0, None), "1.0.0-beta.0"),
@@ -63,18 +64,18 @@
     (("1-beta", None, "beta", 0, None), "1.0.0-beta.1"),
     (("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"),
     (("1", None, "rc", 0, None), "1.0.0-rc.0"),
-    (("1.0.0-rc.1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"),
+    (("1.0.0-rc.1+e20d7b57f3eb", SemVerIncrement.PATCH, None, 0, None), "1.0.0"),
 ]
 
 # test driven development
 tdd_cases = [
-    (("0.1.1", "PATCH", None, 0, None), "0.1.2"),
-    (("0.1.1", "MINOR", None, 0, None), "0.2.0"),
-    (("2.1.1", "MAJOR", None, 0, None), "3.0.0"),
-    (("0.9.0", "PATCH", "alpha", 0, None), "0.9.1-alpha.0"),
-    (("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-alpha.0"),
-    (("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"),
-    (("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-alpha.1"),
+    (("0.1.1", SemVerIncrement.PATCH, None, 0, None), "0.1.2"),
+    (("0.1.1", SemVerIncrement.MINOR, None, 0, None), "0.2.0"),
+    (("2.1.1", SemVerIncrement.MAJOR, None, 0, None), "3.0.0"),
+    (("0.9.0", SemVerIncrement.PATCH, "alpha", 0, None), "0.9.1-alpha.0"),
+    (("0.9.0", SemVerIncrement.MINOR, "alpha", 0, None), "0.10.0-alpha.0"),
+    (("0.9.0", SemVerIncrement.MAJOR, "alpha", 0, None), "1.0.0-alpha.0"),
+    (("0.9.0", SemVerIncrement.MAJOR, "alpha", 1, None), "1.0.0-alpha.1"),
     (("1.0.0-alpha.2", None, "beta", 0, None), "1.0.0-beta.0"),
     (("1.0.0-alpha.2", None, "beta", 1, None), "1.0.0-beta.1"),
     (("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"),