Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions commitizen/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
if TYPE_CHECKING:
from collections.abc import Generator, Iterable

from commitizen.version_schemes import Increment, Version
from commitizen.version_schemes import Increment, VersionProtocol

VERSION_TYPES = [None, PATCH, MINOR, MAJOR]

Expand Down Expand Up @@ -131,8 +131,8 @@ def _resolve_files_and_regexes(


def create_commit_message(
current_version: Version | str,
new_version: Version | str,
current_version: VersionProtocol | str,
new_version: VersionProtocol | str,
message_template: str | None = None,
) -> str:
if message_template is None:
Expand Down
38 changes: 36 additions & 2 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
InvalidCommandArgumentError,
NoCommandFoundError,
)
from commitizen.version_increment import VersionIncrement

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -542,13 +543,19 @@ def __call__(
},
{
"name": ["--major"],
"help": "Output just the major version. Must be used with --project or --verbose.",
"help": (
"Output just the major version. Must be used with MANUAL_VERSION, "
"--project, or --verbose."
),
"action": "store_true",
"exclusive_group": "group2",
},
{
"name": ["--minor"],
"help": "Output just the minor version. Must be used with --project or --verbose.",
"help": (
"Output just the minor version. Must be used with MANUAL_VERSION, "
"--project, or --verbose."
),
"action": "store_true",
"exclusive_group": "group2",
},
Expand All @@ -558,6 +565,33 @@ def __call__(
"action": "store_true",
"exclusive_group": "group2",
},
{
"name": ["--patch"],
"help": (
"Output the patch version only. Must be used with MANUAL_VERSION, "
"--project, or --verbose."
),
"action": "store_true",
"exclusive_group": "group2",
},
{
"name": ["--next"],
"help": "Output the next version.",
"type": str,
"nargs": "?",
"default": None,
"const": "USE_GIT_COMMITS",
"choices": ["USE_GIT_COMMITS"]
+ [str(increment) for increment in VersionIncrement],
"exclusive_group": "group2",
},
{
"name": "manual_version",
"type": str,
"nargs": "?",
"help": "Use the version provided instead of the version from the project. Can be used to test the selected version scheme.",
"metavar": "MANUAL_VERSION",
},
],
},
],
Expand Down
10 changes: 7 additions & 3 deletions commitizen/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
NoAnswersError,
)
from commitizen.git import get_latest_tag_name, get_tag_names, smart_open
from commitizen.version_schemes import KNOWN_SCHEMES, Version, get_version_scheme
from commitizen.version_schemes import (
KNOWN_SCHEMES,
VersionProtocol,
get_version_scheme,
)

if TYPE_CHECKING:
from commitizen.config import (
Expand Down Expand Up @@ -265,7 +269,7 @@ def _ask_version_scheme(self) -> str:
).unsafe_ask()
return scheme

def _ask_major_version_zero(self, version: Version) -> bool:
def _ask_major_version_zero(self, version: VersionProtocol) -> bool:
"""Ask for setting: major_version_zero"""
if version.major > 0:
return False
Expand Down Expand Up @@ -323,7 +327,7 @@ def _write_config_to_file(
cz_name: str,
version_provider: str,
version_scheme: str,
version: Version,
version: VersionProtocol,
tag_format: str,
update_changelog_on_bump: bool,
major_version_zero: bool,
Expand Down
94 changes: 73 additions & 21 deletions commitizen/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,32 @@
import sys
from typing import TypedDict

from packaging.version import InvalidVersion

from commitizen import out
from commitizen.__version__ import __version__
from commitizen.config import BaseConfig
from commitizen.exceptions import NoVersionSpecifiedError, VersionSchemeUnknown
from commitizen.providers import get_provider
from commitizen.tags import TagRules
from commitizen.version_schemes import get_version_scheme
from commitizen.version_increment import VersionIncrement
from commitizen.version_schemes import Increment, get_version_scheme


class VersionArgs(TypedDict, total=False):
manual_version: str | None
next: str | None

# Exclusive groups 1
commitizen: bool
report: bool
project: bool
verbose: bool

# Exclusive groups 2
major: bool
minor: bool
patch: bool
tag: bool


Expand All @@ -43,40 +53,82 @@ def __call__(self) -> None:
if self.arguments.get("verbose"):
out.write(f"Installed Commitizen Version: {__version__}")

if not self.arguments.get("commitizen") and (
self.arguments.get("project") or self.arguments.get("verbose")
if self.arguments.get("commitizen"):
out.write(__version__)
return

if (
self.arguments.get("project")
or self.arguments.get("verbose")
or self.arguments.get("next")
or self.arguments.get("manual_version")
):
version_str = self.arguments.get("manual_version")
if version_str is None:
try:
version_str = get_provider(self.config).get_version()
except NoVersionSpecifiedError:
out.error("No project information in this project.")
return
try:
version = get_provider(self.config).get_version()
except NoVersionSpecifiedError:
out.error("No project information in this project.")
return
try:
version_scheme = get_version_scheme(self.config.settings)(version)
scheme_factory = get_version_scheme(self.config.settings)
except VersionSchemeUnknown:
out.error("Unknown version scheme.")
return

try:
version = scheme_factory(version_str)
except InvalidVersion:
out.error(f"Invalid version: '{version_str}'")
return

if next_increment_str := self.arguments.get("next"):
if next_increment_str == "USE_GIT_COMMITS":
out.error("--next USE_GIT_COMMITS is not implemented yet.")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what we have to refactor later, right? move the logic out of bump so it can be reused

return

next_increment = VersionIncrement.from_value(next_increment_str)
increment: Increment | None
if next_increment == VersionIncrement.NONE:
increment = None
elif next_increment == VersionIncrement.PATCH:
increment = "PATCH"
elif next_increment == VersionIncrement.MINOR:
increment = "MINOR"
else:
increment = "MAJOR"
version = version.bump(increment=increment)

if self.arguments.get("major"):
version = f"{version_scheme.major}"
elif self.arguments.get("minor"):
version = f"{version_scheme.minor}"
elif self.arguments.get("tag"):
out.write(version.major)
return
if self.arguments.get("minor"):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize now that this creates problems with the (not)monotonic kind of versions (and possible non-semver). I'm not sure what to do about it.

I think for now it's fine that if you diverge too much from semver in your custom version scheme, then you won't get the full range of features.

out.write(version.minor)
return
if self.arguments.get("patch"):
out.write(version.micro)
return
Comment on lines +107 to +110
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new --patch output path isn’t covered by tests (there are tests for --major/--minor, but none for --patch). Adding a simple assertion for the patch component would protect this behavior from regressions.

Copilot uses AI. Check for mistakes.

display_version: str
if self.arguments.get("tag"):
tag_rules = TagRules.from_settings(self.config.settings)
version = tag_rules.normalize_tag(version_scheme)
display_version = tag_rules.normalize_tag(version)
else:
display_version = str(version)

out.write(
f"Project Version: {version}"
f"Project Version: {display_version}"
if self.arguments.get("verbose")
else version
else display_version
)
return

if self.arguments.get("major") or self.arguments.get("minor"):
out.error(
"Major or minor version can only be used with --project or --verbose."
)
return
for argument in ("major", "minor", "patch"):
if self.arguments.get(argument):
out.error(
f"{argument} can only be used with MANUAL_VERSION, --project or --verbose."
)
return

if self.arguments.get("tag"):
out.error("Tag can only be used with --project or --verbose.")
Expand Down
22 changes: 11 additions & 11 deletions commitizen/out.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,35 @@
sys.stdout.reconfigure(encoding="utf-8")


def write(value: str, *args: object) -> None:
def write(value: object, *args: object) -> None:
"""Intended to be used when value is multiline."""
print(value, *args)


def line(value: str, *args: object, **kwargs: Any) -> None:
def line(value: object, *args: object, **kwargs: Any) -> None:
"""Wrapper in case I want to do something different later."""
print(value, *args, **kwargs)


def error(value: str) -> None:
message = colored(value, "red")
def error(value: object) -> None:
message = colored(str(value), "red")
line(message, file=sys.stderr)


def success(value: str) -> None:
message = colored(value, "green")
def success(value: object) -> None:
message = colored(str(value), "green")
line(message)


def info(value: str) -> None:
message = colored(value, "blue")
def info(value: object) -> None:
message = colored(str(value), "blue")
line(message)


def diagnostic(value: str) -> None:
def diagnostic(value: object) -> None:
line(value, file=sys.stderr)


def warn(value: str) -> None:
message = colored(value, "magenta")
def warn(value: object) -> None:
message = colored(str(value), "magenta")
line(message, file=sys.stderr)
14 changes: 6 additions & 8 deletions commitizen/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from commitizen.version_schemes import (
DEFAULT_SCHEME,
InvalidVersion,
Version,
VersionProtocol,
VersionScheme,
get_version_scheme,
)
Expand All @@ -23,8 +23,6 @@
import sys
from collections.abc import Iterable, Sequence

from commitizen.version_schemes import VersionScheme

# Self is Python 3.11+ but backported in typing-extensions
if sys.version_info < (3, 11):
from typing_extensions import Self
Expand Down Expand Up @@ -75,7 +73,7 @@ class TagRules:
assert not rules.is_version_tag("warn1.0.0", warn=True) # Does warn

assert rules.search_version("# My v1.0.0 version").version == "1.0.0"
assert rules.extract_version("v1.0.0") == Version("1.0.0")
assert rules.extract_version("v1.0.0") == rules.scheme("1.0.0")
try:
assert rules.extract_version("not-a-v1.0.0")
except InvalidVersion:
Expand Down Expand Up @@ -145,7 +143,7 @@ def get_version_tags(
"""Filter in version tags and warn on unexpected tags"""
return [tag for tag in tags if self.is_version_tag(tag, warn)]

def extract_version(self, tag: GitTag) -> Version:
def extract_version(self, tag: GitTag) -> VersionProtocol:
"""
Extract a version from the tag as defined in tag formats.

Expand Down Expand Up @@ -195,7 +193,7 @@ def search_version(self, text: str, last: bool = False) -> VersionTag | None:
return VersionTag(version, match.group(0))

def normalize_tag(
self, version: Version | str, tag_format: str | None = None
self, version: VersionProtocol | str, tag_format: str | None = None
) -> str:
"""
The tag and the software version might be different.
Expand Down Expand Up @@ -225,7 +223,7 @@ def normalize_tag(
)

def find_tag_for(
self, tags: Iterable[GitTag], version: Version | str
self, tags: Iterable[GitTag], version: VersionProtocol | str
) -> GitTag | None:
"""Find the first matching tag for a given version."""
version = self.scheme(version) if isinstance(version, str) else version
Expand All @@ -234,7 +232,7 @@ def find_tag_for(
# If the requested version is incomplete (e.g., "1.2"), try to find the latest
# matching tag that shares the provided prefix.
if len(release) < 3:
matching_versions: list[tuple[Version, GitTag]] = []
matching_versions: list[tuple[VersionProtocol, GitTag]] = []
for tag in tags:
try:
tag_version = self.extract_version(tag)
Expand Down
33 changes: 33 additions & 0 deletions commitizen/version_increment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from enum import IntEnum


class VersionIncrement(IntEnum):
"""Semantic versioning bump increments.

IntEnum keeps a total order compatible with NONE < PATCH < MINOR < MAJOR
for comparisons across the codebase.

- NONE: no bump (docs-only / style commits, etc.)
- PATCH: backwards-compatible bug fixes
- MINOR: backwards-compatible features
- MAJOR: incompatible API changes
"""

NONE = 0
PATCH = 1
MINOR = 2
MAJOR = 3

def __str__(self) -> str:
return self.name

@classmethod
def from_value(cls, value: object) -> VersionIncrement:
if not isinstance(value, str):
return VersionIncrement.NONE
try:
return cls[value]
except KeyError:
return VersionIncrement.NONE
Loading
Loading