Skip to content

Add version_type option to make version compatibly with semver #686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 19, 2023
29 changes: 22 additions & 7 deletions commitizen/bump.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import os
import re
import sys
import typing
from collections import OrderedDict
from itertools import zip_longest
from string import Template
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Tuple, Type, Union

from packaging.version import Version

from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message
from commitizen.exceptions import CurrentVersionNotFoundError
from commitizen.git import GitCommit, smart_open

if sys.version_info >= (3, 8):
from commitizen.version_types import VersionProtocol
else:
# workaround mypy issue for 3.7 python
VersionProtocol = typing.Any


def find_increment(
commits: List[GitCommit], regex: str, increments_map: Union[dict, OrderedDict]
Expand Down Expand Up @@ -120,7 +128,8 @@ def generate_version(
prerelease_offset: int = 0,
devrelease: Optional[int] = None,
is_local_version: bool = False,
) -> Version:
version_type_cls: Optional[Type[VersionProtocol]] = None,
) -> VersionProtocol:
"""Based on the given increment a proper semver will be generated.

For now the rules and versioning scheme is based on
Expand All @@ -132,15 +141,17 @@ def generate_version(
MINOR 1.0.0 -> 1.1.0
MAJOR 1.0.0 -> 2.0.0
"""
if version_type_cls is None:
version_type_cls = Version
if is_local_version:
version = Version(current_version)
version = version_type_cls(current_version)
dev_version = devrelease_generator(devrelease=devrelease)
pre_version = prerelease_generator(
str(version.local), prerelease=prerelease, offset=prerelease_offset
)
semver = semver_generator(str(version.local), increment=increment)

return Version(f"{version.public}+{semver}{pre_version}{dev_version}")
return version_type_cls(f"{version.public}+{semver}{pre_version}{dev_version}")
else:
dev_version = devrelease_generator(devrelease=devrelease)
pre_version = prerelease_generator(
Expand All @@ -149,7 +160,7 @@ def generate_version(
semver = semver_generator(current_version, increment=increment)

# TODO: post version
return Version(f"{semver}{pre_version}{dev_version}")
return version_type_cls(f"{semver}{pre_version}{dev_version}")


def update_version_in_files(
Expand Down Expand Up @@ -208,7 +219,9 @@ def _version_to_regex(version: str) -> str:


def normalize_tag(
version: Union[Version, str], tag_format: Optional[str] = None
version: Union[VersionProtocol, str],
tag_format: Optional[str] = None,
version_type_cls: Optional[Type[VersionProtocol]] = None,
) -> str:
"""The tag and the software version might be different.

Expand All @@ -221,8 +234,10 @@ def normalize_tag(
| ver1.0.0 | 1.0.0 |
| ver1.0.0.a0 | 1.0.0a0 |
"""
if version_type_cls is None:
version_type_cls = Version
if isinstance(version, str):
version = Version(version)
version = version_type_cls(version)

if not tag_format:
return str(version)
Expand Down
23 changes: 19 additions & 4 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@

import os
import re
import sys
import typing
from collections import OrderedDict, defaultdict
from datetime import date
from typing import Callable, Dict, Iterable, List, Optional, Tuple
from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type

from jinja2 import Environment, PackageLoader
from packaging.version import InvalidVersion, Version
Expand All @@ -39,6 +41,12 @@
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
from commitizen.git import GitCommit, GitTag

if sys.version_info >= (3, 8):
from commitizen.version_types import VersionProtocol
else:
# workaround mypy issue for 3.7 python
VersionProtocol = typing.Any


def get_commit_tag(commit: GitCommit, tags: List[GitTag]) -> Optional[GitTag]:
return next((tag for tag in tags if tag.rev == commit.rev), None)
Expand Down Expand Up @@ -313,7 +321,10 @@ def get_smart_tag_range(


def get_oldest_and_newest_rev(
tags: List[GitTag], version: str, tag_format: str
tags: List[GitTag],
version: str,
tag_format: str,
version_type_cls: Optional[Type[VersionProtocol]] = None,
) -> Tuple[Optional[str], Optional[str]]:
"""Find the tags for the given version.

Expand All @@ -328,11 +339,15 @@ def get_oldest_and_newest_rev(
except ValueError:
newest = version

newest_tag = normalize_tag(newest, tag_format=tag_format)
newest_tag = normalize_tag(
newest, tag_format=tag_format, version_type_cls=version_type_cls
)

oldest_tag = None
if oldest:
oldest_tag = normalize_tag(oldest, tag_format=tag_format)
oldest_tag = normalize_tag(
oldest, tag_format=tag_format, version_type_cls=version_type_cls
)

tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag)
if not tags_range:
Expand Down
8 changes: 7 additions & 1 deletion commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import argcomplete
from decli import cli

from commitizen import commands, config, out
from commitizen import commands, config, out, version_types
from commitizen.exceptions import (
CommitizenException,
ExitCode,
Expand Down Expand Up @@ -203,6 +203,12 @@
"help": "bump to the given version (e.g: 1.5.3)",
"metavar": "MANUAL_VERSION",
},
{
"name": ["--version-type"],
"help": "choose version type",
"default": None,
"choices": version_types.VERSION_TYPES,
},
],
},
{
Expand Down
17 changes: 14 additions & 3 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import questionary
from packaging.version import InvalidVersion, Version

from commitizen import bump, cmd, defaults, factory, git, hooks, out
from commitizen import bump, cmd, defaults, factory, git, hooks, out, version_types
from commitizen.commands.changelog import Changelog
from commitizen.config import BaseConfig
from commitizen.exceptions import (
Expand Down Expand Up @@ -62,6 +62,10 @@ def __init__(self, config: BaseConfig, arguments: dict):
self.retry = arguments["retry"]
self.pre_bump_hooks = self.config.settings["pre_bump_hooks"]
self.post_bump_hooks = self.config.settings["post_bump_hooks"]
version_type = arguments["version_type"] or self.config.settings.get(
"version_type"
)
self.version_type = version_type and version_types.VERSION_TYPES[version_type]

def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool:
"""Check if reading the whole git tree up to HEAD is needed."""
Expand Down Expand Up @@ -153,7 +157,9 @@ def __call__(self): # noqa: C901
self.cz.bump_map = defaults.bump_map_major_version_zero

current_tag_version: str = bump.normalize_tag(
current_version, tag_format=tag_format
current_version,
tag_format=tag_format,
version_type_cls=self.version_type,
)

is_initial = self.is_initial_tag(current_tag_version, is_yes)
Expand Down Expand Up @@ -209,9 +215,14 @@ def __call__(self): # noqa: C901
prerelease_offset=prerelease_offset,
devrelease=devrelease,
is_local_version=is_local_version,
version_type_cls=self.version_type,
)

new_tag_version = bump.normalize_tag(new_version, tag_format=tag_format)
new_tag_version = bump.normalize_tag(
new_version,
tag_format=tag_format,
version_type_cls=self.version_type,
)
message = bump.create_commit_message(
current_version, new_version, bump_commit_message
)
Expand Down
10 changes: 8 additions & 2 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from operator import itemgetter
from typing import Callable, Dict, List, Optional

from commitizen import bump, changelog, defaults, factory, git, out
from commitizen import bump, changelog, defaults, factory, git, out, version_types
from commitizen.config import BaseConfig
from commitizen.exceptions import (
DryRunExit,
Expand Down Expand Up @@ -53,6 +53,9 @@ def __init__(self, config: BaseConfig, args):
"merge_prerelease"
) or self.config.settings.get("changelog_merge_prerelease")

version_type = self.config.settings.get("version_type")
self.version_type = version_type and version_types.VERSION_TYPES[version_type]

def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
"""Try to find the 'start_rev'.

Expand Down Expand Up @@ -137,7 +140,9 @@ def __call__(self):
latest_version = changelog_meta.get("latest_version")
if latest_version:
latest_tag_version: str = bump.normalize_tag(
latest_version, tag_format=self.tag_format
latest_version,
tag_format=self.tag_format,
version_type_cls=self.version_type,
)
start_rev = self._find_incremental_rev(latest_tag_version, tags)

Expand All @@ -146,6 +151,7 @@ def __call__(self):
tags,
version=self.rev_range,
tag_format=self.tag_format,
version_type_cls=self.version_type,
)

commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order")
Expand Down
2 changes: 2 additions & 0 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Settings(TypedDict, total=False):
pre_bump_hooks: Optional[List[str]]
post_bump_hooks: Optional[List[str]]
prerelease_offset: int
version_type: Optional[str]


name: str = "cz_conventional_commits"
Expand Down Expand Up @@ -75,6 +76,7 @@ class Settings(TypedDict, total=False):
"pre_bump_hooks": [],
"post_bump_hooks": [],
"prerelease_offset": 0,
"version_type": None,
}

MAJOR = "MAJOR"
Expand Down
99 changes: 99 additions & 0 deletions commitizen/version_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import sys
from typing import Optional, Tuple, Union

if sys.version_info >= (3, 8):
from typing import Protocol as _Protocol
else:
_Protocol = object

from packaging.version import Version


class VersionProtocol(_Protocol):
def __init__(self, _version: Union[Version, str]):
raise NotImplementedError("must be implemented")

def __str__(self) -> str:
raise NotImplementedError("must be implemented")

@property
def release(self) -> Tuple[int, ...]:
raise NotImplementedError("must be implemented")

@property
def is_prerelease(self) -> bool:
raise NotImplementedError("must be implemented")

@property
def pre(self) -> Optional[Tuple[str, int]]:
raise NotImplementedError("must be implemented")

@property
def local(self) -> Optional[str]:
raise NotImplementedError("must be implemented")

@property
def public(self) -> str:
raise NotImplementedError("must be implemented")


class SemVerVersion(VersionProtocol):
def __init__(self, version: str):
self._version = Version(version)

@property
def release(self) -> Tuple[int, ...]:
return self._version.release

@property
def is_prerelease(self) -> bool:
return self._version.is_prerelease

@property
def pre(self) -> Optional[Tuple[str, int]]:
return self._version.pre

@property
def local(self) -> Optional[str]:
return self._version.local

@property
def public(self) -> str:
return self._version.public

def __str__(self) -> str:
parts = []

version = self._version

# Epoch
if version.epoch != 0:
parts.append(f"{version.epoch}!")

# Release segment
parts.append(".".join(str(x) for x in version.release))

# Pre-release
if version.pre:
pre = "".join(str(x) for x in version.pre)
parts.append(f"-{pre}")

# Post-release
if version.post is not None:
parts.append(f"-post{version.post}")

# Development release
if version.dev is not None:
parts.append(f"-dev{version.dev}")

# Local version segment
if version.local:
parts.append(f"+{version.local}")

return "".join(parts)


VERSION_TYPES = {
"pep440": Version,
"semver": SemVerVersion,
}
Loading