GeezerVison is showing github.com — refactor(bump): use VersionIncrement ordering for bump detection (#2097) · commitizen-tools/commitizen@667de74
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: commitizen/bump.py +4-50Lines changed: 4 additions & 50 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | import os | 4 | 4 | import re | 5 | | -from collections import OrderedDict | 6 | 5 | from glob import iglob | 7 | | -from logging import getLogger | 8 | 6 | from string import Template | 9 | | -from typing import TYPE_CHECKING, cast | | 7 | +from typing import TYPE_CHECKING | 10 | 8 |
| 11 | | -from commitizen.defaults import BUMP_MESSAGE, MAJOR, MINOR, PATCH | | 9 | +from commitizen.defaults import BUMP_MESSAGE | 12 | 10 | from commitizen.exceptions import CurrentVersionNotFoundError | 13 | | -from commitizen.git import GitCommit, smart_open | | 11 | +from commitizen.git import smart_open | 14 | 12 |
| 15 | 13 | if TYPE_CHECKING: | 16 | 14 | from collections.abc import Generator, Iterable | 17 | 15 |
| 18 | | - from commitizen.version_schemes import Increment, VersionProtocol | 19 | | - | 20 | | -VERSION_TYPES = [None, PATCH, MINOR, MAJOR] | 21 | | - | 22 | | -logger = getLogger("commitizen") | 23 | | - | 24 | | - | 25 | | -def find_increment( | 26 | | - commits: list[GitCommit], regex: str, increments_map: dict | OrderedDict | 27 | | -) -> Increment | None: | 28 | | - if isinstance(increments_map, dict): | 29 | | - increments_map = OrderedDict(increments_map) | 30 | | - | 31 | | - # Most important cases are major and minor. | 32 | | - # Everything else will be considered patch. | 33 | | - select_pattern = re.compile(regex) | 34 | | - increment: str | None = None | 35 | | - | 36 | | - for commit in commits: | 37 | | - for message in commit.message.split("\n"): | 38 | | - result = select_pattern.search(message) | 39 | | - | 40 | | - if result: | 41 | | - found_keyword = result.group(1) | 42 | | - new_increment = None | 43 | | - for match_pattern in increments_map.keys(): | 44 | | - if re.match(match_pattern, found_keyword): | 45 | | - new_increment = increments_map[match_pattern] | 46 | | - break | 47 | | - | 48 | | - if new_increment is None: | 49 | | - logger.debug( | 50 | | - f"no increment needed for '{found_keyword}' in '{message}'" | 51 | | - ) | 52 | | - | 53 | | - if VERSION_TYPES.index(increment) < VERSION_TYPES.index(new_increment): | 54 | | - logger.debug( | 55 | | - f"increment detected is '{new_increment}' due to '{found_keyword}' in '{message}'" | 56 | | - ) | 57 | | - increment = new_increment | 58 | | - | 59 | | - if increment == MAJOR: | 60 | | - break | 61 | | - | 62 | | - return cast("Increment", increment) | | 16 | + from commitizen.version_schemes import VersionProtocol | 63 | 17 |
| 64 | 18 |
| 65 | 19 | def update_version_in_files( | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/commands/bump.py +13-5Lines changed: 13 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 24 | 24 | ) | 25 | 25 | from commitizen.providers import get_provider | 26 | 26 | from commitizen.tags import TagRules | | 27 | +from commitizen.version_increment import VersionIncrement | 27 | 28 | from commitizen.version_schemes import ( | 28 | 29 | Increment, | 29 | 30 | InvalidVersion, | @@ -145,8 +146,8 @@ def _is_initial_tag( | 145 | 146 | return bool(questionary.confirm("Is this the first tag created?").ask()) | 146 | 147 |
| 147 | 148 | def _find_increment(self, commits: list[git.GitCommit]) -> Increment | None: | | 149 | + """Determine the highest configured increment across candidate commits.""" | 148 | 150 | # Update the bump map to ensure major version doesn't increment. | 149 | | - # self.cz.bump_map = defaults.bump_map_major_version_zero | 150 | 151 | bump_map = ( | 151 | 152 | self.cz.bump_map_major_version_zero | 152 | 153 | if self.bump_settings["major_version_zero"] | @@ -158,11 +159,18 @@ def _find_increment(self, commits: list[git.GitCommit]) -> Increment | None: | 158 | 159 | raise NoPatternMapError( | 159 | 160 | f"'{self.config.settings['name']}' rule does not support bump" | 160 | 161 | ) | 161 | | - return bump.find_increment( | 162 | | - self.cz.filter_commits_before_bump(commits), | 163 | | - regex=bump_pattern, | 164 | | - increments_map=bump_map, | | 162 | + | | 163 | + commit_messages = ( | | 164 | + commit.message for commit in self.cz.filter_commits_before_bump(commits) | | 165 | + ) | | 166 | + increment = VersionIncrement.get_highest_by_messages( | | 167 | + commit_messages, | | 168 | + bump_pattern, | | 169 | + bump_map, | 165 | 170 | ) | | 171 | + if increment == VersionIncrement.NONE: | | 172 | + return None | | 173 | + return cast("Increment", str(increment)) | 166 | 174 |
| 167 | 175 | def _validate_arguments(self, current_version: VersionProtocol) -> None: | 168 | 176 | errors: list[str] = [] | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/commands/version.py +14-5Lines changed: 14 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 4 | 4 |
| 5 | 5 | from packaging.version import InvalidVersion | 6 | 6 |
| 7 | | -from commitizen import bump, factory, git, out | | 7 | +from commitizen import factory, git, out | 8 | 8 | from commitizen.__version__ import __version__ | 9 | 9 | from commitizen.config import BaseConfig | 10 | 10 | from commitizen.exceptions import ( | @@ -180,11 +180,20 @@ def _get_next_git_version( | 180 | 180 | raise NoPatternMapError( | 181 | 181 | f"'{self.config.settings['name']}' rule does not support bump" | 182 | 182 | ) | 183 | | - increment = bump.find_increment( | 184 | | - self.cz.filter_commits_before_bump(commits), | 185 | | - regex=bump_pattern, | 186 | | - increments_map=bump_map, | | 183 | + next_increment = VersionIncrement.get_highest_by_messages( | | 184 | + (commit.message for commit in self.cz.filter_commits_before_bump(commits)), | | 185 | + bump_pattern, | | 186 | + bump_map, | 187 | 187 | ) | | 188 | + increment: Increment | None | | 189 | + if next_increment == VersionIncrement.NONE: | | 190 | + increment = None | | 191 | + elif next_increment == VersionIncrement.PATCH: | | 192 | + increment = "PATCH" | | 193 | + elif next_increment == VersionIncrement.MINOR: | | 194 | + increment = "MINOR" | | 195 | + else: | | 196 | + increment = "MAJOR" | 188 | 197 |
| 189 | 198 | # TODO: Consider adding all the parameters `.bump` supports: | 190 | 199 | # prerelease, prerelease_offset,exact_increment, etc.. | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/cz/base.py +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -37,8 +37,8 @@ class ValidationResult(NamedTuple): | 37 | 37 |
| 38 | 38 | class BaseCommitizen(metaclass=ABCMeta): | 39 | 39 | bump_pattern: str | None = None | 40 | | - bump_map: dict[str, str] | None = None | 41 | | - bump_map_major_version_zero: dict[str, str] | None = None | | 40 | + bump_map: Mapping[str, str | None] | None = None | | 41 | + bump_map_major_version_zero: Mapping[str, str | None] | None = None | 42 | 42 | default_style_config: list[tuple[str, str]] = [ | 43 | 43 | ("qmark", "fg:#ff9d00 bold"), | 44 | 44 | ("question", "bold"), | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/cz/conventional_commits/conventional_commits.py +6-2Lines changed: 6 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 8 | 8 | from commitizen.cz.utils import multiple_line_breaker, required_validator | 9 | 9 |
| 10 | 10 | if TYPE_CHECKING: | | 11 | + from collections.abc import Mapping | | 12 | + | 11 | 13 | from commitizen.question import CzQuestion | 12 | 14 |
| 13 | 15 | __all__ = ["ConventionalCommitsCz"] | @@ -32,8 +34,10 @@ class ConventionalCommitsAnswers(TypedDict): | 32 | 34 |
| 33 | 35 | class ConventionalCommitsCz(BaseCommitizen): | 34 | 36 | bump_pattern = defaults.BUMP_PATTERN | 35 | | - bump_map = defaults.BUMP_MAP | 36 | | - bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO | | 37 | + bump_map: Mapping[str, str | None] = defaults.BUMP_MAP | | 38 | + bump_map_major_version_zero: Mapping[str, str | None] = ( | | 39 | + defaults.BUMP_MAP_MAJOR_VERSION_ZERO | | 40 | + ) | 37 | 41 | commit_parser = r"^((?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):\s(?P<message>.*)?" | 38 | 42 | change_type_map = { | 39 | 43 | "feat": "Feat", | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/cz/customize/customize.py +6-4Lines changed: 6 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 27 | 27 |
| 28 | 28 |
| 29 | 29 | def _derive_major_version_zero( | 30 | | - bump_map: Mapping[str, str], | 31 | | -) -> OrderedDict[str, str]: | | 30 | + bump_map: Mapping[str, str | None], | | 31 | +) -> OrderedDict[str, str | None]: | 32 | 32 | """Derive a ``bump_map_major_version_zero`` from a user-supplied | 33 | 33 | ``bump_map`` by demoting any ``MAJOR`` rule to ``MINOR``. | 34 | 34 |
| @@ -45,8 +45,10 @@ def _derive_major_version_zero( | 45 | 45 |
| 46 | 46 | class CustomizeCommitsCz(BaseCommitizen): | 47 | 47 | bump_pattern = defaults.BUMP_PATTERN | 48 | | - bump_map = defaults.BUMP_MAP | 49 | | - bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO | | 48 | + bump_map: Mapping[str, str | None] = defaults.BUMP_MAP | | 49 | + bump_map_major_version_zero: Mapping[str, str | None] = ( | | 50 | + defaults.BUMP_MAP_MAJOR_VERSION_ZERO | | 51 | + ) | 50 | 52 | change_type_order = defaults.CHANGE_TYPE_ORDER | 51 | 53 |
| 52 | 54 | def __init__(self, config: BaseConfig) -> None: | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/defaults.py +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 13 | 13 |
| 14 | 14 | class CzSettings(TypedDict, total=False): | 15 | 15 | bump_pattern: str | 16 | | - bump_map: OrderedDict[str, str] | 17 | | - bump_map_major_version_zero: OrderedDict[str, str] | | 16 | + bump_map: OrderedDict[str, str | None] | | 17 | + bump_map_major_version_zero: OrderedDict[str, str | None] | 18 | 18 | change_type_order: list[str] | 19 | 19 |
| 20 | 20 | questions: Iterable[CzQuestion] | |
Collapse file Copy file name to clipboardExpand all lines: commitizen/version_increment.py +118Lines changed: 118 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +"""Helpers for comparing and deriving semantic-version increments. | | 2 | +
| | 3 | +This module centralizes the conversion between external increment labels and the | | 4 | +ordered enum used internally by bump-related commands. The distinction matters | | 5 | +because some callers intentionally coerce unknown values to ``NONE`` while | | 6 | +matched ``bump_map`` outputs must preserve the historical fail-fast behavior for | | 7 | +invalid custom rule values. | | 8 | +""" | | 9 | + | 1 | 10 | from __future__ import annotations | 2 | 11 |
| | 12 | +import re | 3 | 13 | from enum import IntEnum | | 14 | +from typing import TYPE_CHECKING | | 15 | + | | 16 | +if TYPE_CHECKING: | | 17 | + from collections.abc import Iterable, Mapping | | 18 | + | | 19 | +RegexPattern = str | re.Pattern[str] | 4 | 20 |
| 5 | 21 |
| 6 | 22 | class VersionIncrement(IntEnum): | @@ -13,6 +29,12 @@ class VersionIncrement(IntEnum): | 13 | 29 | - PATCH: backwards-compatible bug fixes | 14 | 30 | - MINOR: backwards-compatible features | 15 | 31 | - MAJOR: incompatible API changes | | 32 | +
| | 33 | + Examples: | | 34 | + ```python | | 35 | + max([VersionIncrement.PATCH, VersionIncrement.MINOR]) | | 36 | + # VersionIncrement.MINOR | | 37 | + ``` | 16 | 38 | """ | 17 | 39 |
| 18 | 40 | NONE = 0 | @@ -21,13 +43,109 @@ class VersionIncrement(IntEnum): | 21 | 43 | MAJOR = 3 | 22 | 44 |
| 23 | 45 | def __str__(self) -> str: | | 46 | + """Return the legacy string representation used by existing callers.""" | 24 | 47 | return self.name | 25 | 48 |
| 26 | 49 | @classmethod | 27 | 50 | def from_value(cls, value: object) -> VersionIncrement: | | 51 | + """Convert external increment input into a known increment. | | 52 | +
| | 53 | + This lenient path is used for inputs such as CLI arguments where an | | 54 | + unknown string should behave like "no increment" instead of raising. | | 55 | + Matched ``bump_map`` values are validated separately so custom rules keep | | 56 | + their historical fail-fast behavior. | | 57 | + """ | 28 | 58 | if not isinstance(value, str): | 29 | 59 | return VersionIncrement.NONE | 30 | 60 | try: | 31 | 61 | return cls[value] | 32 | 62 | except KeyError: | 33 | 63 | return VersionIncrement.NONE | | 64 | + | | 65 | + @classmethod | | 66 | + def _compile_pattern(cls, regex: RegexPattern) -> re.Pattern[str]: | | 67 | + """Normalize a bump-pattern input into a compiled regular expression.""" | | 68 | + if isinstance(regex, re.Pattern): | | 69 | + return regex | | 70 | + return re.compile(regex) | | 71 | + | | 72 | + @classmethod | | 73 | + def _from_bump_map_value(cls, value: object) -> VersionIncrement: | | 74 | + """Resolve a configured bump-map value or reject invalid rule output. | | 75 | +
| | 76 | + This preserves the historical behavior where a matched custom bump-map | | 77 | + entry with an unsupported increment fails immediately instead of being | | 78 | + treated as a no-op. | | 79 | + """ | | 80 | + if value is None: | | 81 | + return cls.NONE | | 82 | + if value == "PATCH": | | 83 | + return cls.PATCH | | 84 | + if value == "MINOR": | | 85 | + return cls.MINOR | | 86 | + if value == "MAJOR": | | 87 | + return cls.MAJOR | | 88 | + raise ValueError(f"Invalid bump increment: {value!r}") | | 89 | + | | 90 | + @classmethod | | 91 | + def from_match( | | 92 | + cls, matched_text: str, increments_map: Mapping[str, object] | | 93 | + ) -> VersionIncrement: | | 94 | + """Resolve one extracted bump token against a configured bump map. | | 95 | +
| | 96 | + The first regular-expression key that matches wins, preserving the | | 97 | + current user-visible behavior of ``bump_map`` ordering. | | 98 | + """ | | 99 | + for match_pattern, increment in increments_map.items(): | | 100 | + if re.match(match_pattern, matched_text): | | 101 | + return cls._from_bump_map_value(increment) | | 102 | + return cls.NONE | | 103 | + | | 104 | + @classmethod | | 105 | + def from_message( | | 106 | + cls, message: str, regex: RegexPattern, increments_map: Mapping[str, object] | | 107 | + ) -> VersionIncrement: | | 108 | + """Compute the highest increment contributed by one commit message. | | 109 | +
| | 110 | + Commitizen historically evaluates each line of the full commit message | | 111 | + against ``bump_pattern`` and then keeps the highest resulting bump. | | 112 | + This method preserves that behavior while returning a comparable enum. | | 113 | + """ | | 114 | + select_pattern = cls._compile_pattern(regex) | | 115 | + increment = cls.NONE | | 116 | + | | 117 | + for line in message.split("\n"): | | 118 | + result = select_pattern.search(line) | | 119 | + if not result: | | 120 | + continue | | 121 | + | | 122 | + increment = max( | | 123 | + increment, | | 124 | + cls.from_match(result.group(1), increments_map), | | 125 | + ) | | 126 | + if increment == cls.MAJOR: | | 127 | + return increment | | 128 | + | | 129 | + return increment | | 130 | + | | 131 | + @classmethod | | 132 | + def get_highest_by_messages( | | 133 | + cls, | | 134 | + messages: Iterable[str], | | 135 | + regex: RegexPattern, | | 136 | + increments_map: Mapping[str, object], | | 137 | + ) -> VersionIncrement: | | 138 | + """Compute the highest increment across many commit messages. | | 139 | +
| | 140 | + An empty iterable yields ``NONE`` so callers can preserve the existing | | 141 | + "no increment" behavior explicitly instead of relying on exceptions from | | 142 | + ``max()``. | | 143 | + """ | | 144 | + select_pattern = cls._compile_pattern(regex) | | 145 | + return max( | | 146 | + ( | | 147 | + cls.from_message(message, select_pattern, increments_map) | | 148 | + for message in messages | | 149 | + ), | | 150 | + default=cls.NONE, | | 151 | + ) |
Collapse file Copy file name to clipboardExpand all lines: docs/customization/config_file.md +1-1Lines changed: 1 addition & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -160,7 +160,7 @@ Example: | 160 | 160 | | `schema_pattern` | `str` | `""` | (OPTIONAL) The regular expression used to do commit message validation. Used by `cz check`. | | 161 | 161 | | `info_path` | `str` | `""` | (OPTIONAL) The path to the file that contains explanation of the commit rules. Used by `cz info`. If not provided `cz info`, will load `info` instead. | | 162 | 162 | | `info` | `str` | `""` | (OPTIONAL) Explanation of the commit rules. Used by `cz info`. | | 163 | | -| `bump_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | | | 163 | +| `bump_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`). In JSON and YAML configs, `null` keeps a matched rule from bumping the version. | | 164 | 164 | | `bump_pattern` | `str` | `None` | (OPTIONAL) Regex to extract information from commit (subject and body) | | 165 | 165 | | `change_type_order` | `str` | `None` | (OPTIONAL) List of strings used to order the Changelog. All other types will be sorted alphabetically. Default is `["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"]` | | 166 | 166 | | `commit_parser` | `str` | `None` | (OPTIONAL) Regex to extract information used in creating changelog. [See more][changelog-spec] | | |
Collapse file Copy file name to clipboardExpand all lines: docs/customization/python_class.md +1-1Lines changed: 1 addition & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -99,7 +99,7 @@ You need to define 2 parameters inside your custom `BaseCommitizen`. | 99 | 99 | | Parameter | Type | Default | Description | | 100 | 100 | | -------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- | | 101 | 101 | | `bump_pattern` | `str` | `None` | Regex to extract information from commit (subject and body) | | 102 | | -| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | | | 102 | +| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`). Use `None` when a matched rule should not bump the version. | | 103 | 103 |
| 104 | 104 | Let's see an example. | 105 | 105 |
| |
|