From d5d3b5fa74ce152a9ceee4c9c13465ce9172a290 Mon Sep 17 00:00:00 2001 From: Yazdan Ranjbar Date: Tue, 9 Dec 2025 17:41:48 -0500 Subject: [PATCH] feat(version-schemes): add monotonic version scheme implementation --- commitizen/tags.py | 5 +- commitizen/version_schemes.py | 20 +++++ pyproject.toml | 1 + ...shows_description_when_use_help_option.txt | 8 +- ...shows_description_when_use_help_option.txt | 4 +- tests/test_bump_normalize_tag.py | 6 ++ ...est_version_scheme_monotonic_versioning.py | 76 +++++++++++++++++++ tests/test_version_schemes.py | 13 +++- 8 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 tests/test_version_scheme_monotonic_versioning.py diff --git a/commitizen/tags.py b/commitizen/tags.py index e3f54370f1..ee0c0f108e 100644 --- a/commitizen/tags.py +++ b/commitizen/tags.py @@ -228,7 +228,10 @@ def normalize_tag( version = self.scheme(version) if isinstance(version, str) else version tag_format = tag_format or self.tag_format - major, minor, patch = version.release + release = list(version.release) + while len(release) < 3: + release.append(0) + major, minor, patch = release[:3] prerelease = version.prerelease or "" t = Template(tag_format) diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index 0696d85aa5..3d54e84ccf 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -298,6 +298,26 @@ def _get_increment_base( return f"{self.major}.{self.minor}.{self.micro}" +class MonotonicVersion(BaseVersion): + """ + Monotonic versioning scheme + + Any increment bump simply increases the single numeric component. + """ + + parser: ClassVar[re.Pattern] = re.compile( + r"v?(?P([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.]+)?(\w+)?)" + ) + + def increment_base(self, increment: Increment | None = None) -> str: + return f"{self.major + 1}" + + def _get_increment_base( + self, increment: Increment | None, exact_increment: bool + ) -> str: + return self.increment_base(increment) + + class Pep440(BaseVersion): """ PEP 440 Version Scheme diff --git a/pyproject.toml b/pyproject.toml index 01b16fec3c..5272ca7575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ uv = "commitizen.providers:UvProvider" pep440 = "commitizen.version_schemes:Pep440" semver = "commitizen.version_schemes:SemVer" semver2 = "commitizen.version_schemes:SemVer2" +monotonic = "commitizen.version_schemes:MonotonicVersion" [dependency-groups] dev = ["ipython>=8.0", "tox>4", "poethepoet>=0.34.0"] diff --git a/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt b/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt index 4cf8e6c91b..454025c923 100644 --- a/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt @@ -8,8 +8,8 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog] [--changelog-to-stdout] [--git-output-to-stderr] [--retry] [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] - [--version-scheme {pep440,semver,semver2}] - [--version-type {pep440,semver,semver2}] + [--version-scheme {monotonic,pep440,semver,semver2}] + [--version-type {monotonic,pep440,semver,semver2}] [--build-metadata BUILD_METADATA] [--get-next] [--allow-no-commit] [MANUAL_VERSION] @@ -71,9 +71,9 @@ options: file name of changelog (default: 'CHANGELOG.md') --prerelease-offset PRERELEASE_OFFSET start pre-releases with this offset - --version-scheme {pep440,semver,semver2} + --version-scheme {monotonic,pep440,semver,semver2} choose version scheme - --version-type {pep440,semver,semver2} + --version-type {monotonic,pep440,semver,semver2} Deprecated, use --version-scheme instead --build-metadata BUILD_METADATA Add additional build-metadata to the version-number diff --git a/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt b/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt index 91b7f389b5..6bebc9bb10 100644 --- a/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt @@ -1,7 +1,7 @@ usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--unreleased-version UNRELEASED_VERSION] [--incremental] [--start-rev START_REV] [--merge-prerelease] - [--version-scheme {pep440,semver,semver2}] + [--version-scheme {monotonic,pep440,semver,semver2}] [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] [--extra EXTRA] [--tag-format TAG_FORMAT] [rev_range] @@ -28,7 +28,7 @@ options: --merge-prerelease collect all changes from prereleases into next non- prerelease. If not set, it will include prereleases in the changelog - --version-scheme {pep440,semver,semver2} + --version-scheme {monotonic,pep440,semver,semver2} choose version scheme --export-template EXPORT_TEMPLATE Export the changelog template into this file instead diff --git a/tests/test_bump_normalize_tag.py b/tests/test_bump_normalize_tag.py index 895acbd71a..e1d427acbe 100644 --- a/tests/test_bump_normalize_tag.py +++ b/tests/test_bump_normalize_tag.py @@ -1,6 +1,7 @@ import pytest from commitizen.tags import TagRules +from commitizen.version_schemes import MonotonicVersion conversion = [ (("1.2.3", "v$version"), "v1.2.3"), @@ -21,3 +22,8 @@ def test_create_tag(test_input, expected): rules = TagRules() new_tag = rules.normalize_tag(version, format) assert new_tag == expected + + +def test_create_tag_monotonic_scheme(): + rules = TagRules(MonotonicVersion) + assert rules.normalize_tag("4", "release-$version") == "release-4" diff --git a/tests/test_version_scheme_monotonic_versioning.py b/tests/test_version_scheme_monotonic_versioning.py new file mode 100644 index 0000000000..1c8532d8cb --- /dev/null +++ b/tests/test_version_scheme_monotonic_versioning.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import pytest + +from commitizen.version_schemes import MonotonicVersion, VersionProtocol +from tests.utils import VersionSchemeTestArgs + + +@pytest.mark.parametrize( + "version_args, expected_version", + [ + ( + VersionSchemeTestArgs( + current_version="1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2", + ), + ( + VersionSchemeTestArgs( + current_version="2", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3", + ), + ( + VersionSchemeTestArgs( + current_version="3", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4", + ), + ( + VersionSchemeTestArgs( + current_version="10", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "11", + ), + ], +) +def test_bump_monotonic_version( + version_args: VersionSchemeTestArgs, expected_version: str +): + assert ( + str( + MonotonicVersion(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, + ) + ) + == expected_version + ) + + +def test_monotonic_scheme_property(): + version = MonotonicVersion("1") + assert version.scheme is MonotonicVersion + + +def test_monotonic_implements_version_protocol(): + assert isinstance(MonotonicVersion("1"), VersionProtocol) diff --git a/tests/test_version_schemes.py b/tests/test_version_schemes.py index 8e2dae9027..37d6ca0770 100644 --- a/tests/test_version_schemes.py +++ b/tests/test_version_schemes.py @@ -12,7 +12,12 @@ from commitizen.config.base_config import BaseConfig from commitizen.exceptions import VersionSchemeUnknown -from commitizen.version_schemes import Pep440, SemVer, get_version_scheme +from commitizen.version_schemes import ( + MonotonicVersion, + Pep440, + SemVer, + get_version_scheme, +) def test_default_version_scheme_is_pep440(config: BaseConfig): @@ -52,6 +57,12 @@ def test_version_scheme_from_config_priority(config: BaseConfig): assert scheme is Pep440 +def test_version_scheme_monotonic(config: BaseConfig): + config.settings["version_scheme"] = "monotonic" + scheme = get_version_scheme(config.settings) + assert scheme is MonotonicVersion + + def test_warn_if_version_protocol_not_implemented( config: BaseConfig, mocker: MockerFixture ):