Skip to content

Feature: version schemes endpoint #733

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 2 commits into from
Jun 20, 2023
Merged
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
140 changes: 8 additions & 132 deletions commitizen/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@

import os
import re
import sys
import typing
from collections import OrderedDict
from itertools import zip_longest
from string import Template

from packaging.version import Version

from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message
from commitizen.defaults import 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
from commitizen.version_schemes import DEFAULT_SCHEME, VersionScheme, Version


def find_increment(
Expand Down Expand Up @@ -54,115 +44,6 @@ def find_increment(
return increment


def prerelease_generator(
current_version: str, prerelease: str | None = None, offset: int = 0
) -> str:
"""Generate prerelease

X.YaN # Alpha release
X.YbN # Beta release
X.YrcN # Release Candidate
X.Y # Final

This function might return something like 'alpha1'
but it will be handled by Version.
"""
if not prerelease:
return ""

version = Version(current_version)
# version.pre is needed for mypy check
if version.is_prerelease and version.pre and prerelease.startswith(version.pre[0]):
prev_prerelease: int = version.pre[1]
new_prerelease_number = prev_prerelease + 1
else:
new_prerelease_number = offset
pre_version = f"{prerelease}{new_prerelease_number}"
return pre_version


def devrelease_generator(devrelease: int = None) -> str:
"""Generate devrelease

The devrelease version should be passed directly and is not
inferred based on the previous version.
"""
if devrelease is None:
return ""

return f"dev{devrelease}"


def semver_generator(current_version: str, increment: str = None) -> str:
version = Version(current_version)
prev_release = list(version.release)
increments = [MAJOR, MINOR, PATCH]
increments_version = dict(zip_longest(increments, prev_release, fillvalue=0))

# This flag means that current version
# must remove its prerelease tag,
# so it doesn't matter the increment.
# Example: 1.0.0a0 with PATCH/MINOR -> 1.0.0
if not version.is_prerelease:
if increment == MAJOR:
increments_version[MAJOR] += 1
increments_version[MINOR] = 0
increments_version[PATCH] = 0
elif increment == MINOR:
increments_version[MINOR] += 1
increments_version[PATCH] = 0
elif increment == PATCH:
increments_version[PATCH] += 1

return str(
f"{increments_version['MAJOR']}."
f"{increments_version['MINOR']}."
f"{increments_version['PATCH']}"
)


def generate_version(
current_version: str,
increment: str,
prerelease: str | None = None,
prerelease_offset: int = 0,
devrelease: int | None = None,
is_local_version: bool = False,
version_type_cls: type[VersionProtocol] | None = None,
) -> VersionProtocol:
"""Based on the given increment a proper semver will be generated.

For now the rules and versioning scheme is based on
python's PEP 0440.
More info: https://www.python.org/dev/peps/pep-0440/

Example:
PATCH 1.0.0 -> 1.0.1
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_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_type_cls(f"{version.public}+{semver}{pre_version}{dev_version}")
else:
dev_version = devrelease_generator(devrelease=devrelease)
pre_version = prerelease_generator(
current_version, prerelease=prerelease, offset=prerelease_offset
)
semver = semver_generator(current_version, increment=increment)

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


def update_version_in_files(
current_version: str, new_version: str, files: list[str], *, check_consistency=False
) -> None:
Expand Down Expand Up @@ -219,9 +100,9 @@ def _version_to_regex(version: str) -> str:


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

Expand All @@ -234,19 +115,14 @@ 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_type_cls(version)
scheme = scheme or DEFAULT_SCHEME
version = scheme(version) if isinstance(version, str) else version

if not tag_format:
return str(version)

major, minor, patch = version.release
prerelease = ""
# version.pre is needed for mypy check
if version.is_prerelease and version.pre:
prerelease = f"{version.pre[0]}{version.pre[1]}"
prerelease = version.prerelease or ""

t = Template(tag_format)
return t.safe_substitute(
Expand All @@ -257,7 +133,7 @@ def normalize_tag(
def create_commit_message(
current_version: Version | str,
new_version: Version | str,
message_template: str = None,
message_template: str | None = None,
) -> str:
if message_template is None:
message_template = bump_message
Expand Down
59 changes: 23 additions & 36 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,53 +24,41 @@
- [x] hook after changelog is generated (api calls)
- [x] add support for change_type maps
"""

from __future__ import annotations

import os
import re
import sys
import typing
from collections import OrderedDict, defaultdict
from datetime import date
from typing import Callable, Iterable
from typing import TYPE_CHECKING, Callable, Iterable, cast

from jinja2 import Environment, PackageLoader
from packaging.version import InvalidVersion, Version

from commitizen import defaults
from commitizen.bump import normalize_tag
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
from commitizen.git import GitCommit, GitTag
from commitizen.version_schemes import DEFAULT_SCHEME, Pep440, InvalidVersion

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


def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None:
return next((tag for tag in tags if tag.rev == commit.rev), None)


def get_version(tag: GitTag) -> Version | None:
version = None
try:
version = Version(tag.name)
except InvalidVersion:
pass
return version


def tag_included_in_changelog(
tag: GitTag, used_tags: list, merge_prerelease: bool
tag: GitTag,
used_tags: list,
merge_prerelease: bool,
scheme: VersionScheme = DEFAULT_SCHEME,
) -> bool:
if tag in used_tags:
return False

version = get_version(tag)
if version is None:
try:
version = scheme(tag.name)
except InvalidVersion:
return False

if merge_prerelease and version.is_prerelease:
Expand All @@ -88,6 +76,7 @@ def generate_tree_from_commits(
change_type_map: dict[str, str] | None = None,
changelog_message_builder_hook: Callable | None = None,
merge_prerelease: bool = False,
scheme: VersionScheme = DEFAULT_SCHEME,
) -> Iterable[dict]:
pat = re.compile(changelog_pattern)
map_pat = re.compile(commit_parser, re.MULTILINE)
Expand All @@ -113,7 +102,7 @@ def generate_tree_from_commits(
commit_tag = get_commit_tag(commit, tags)

if commit_tag is not None and tag_included_in_changelog(
commit_tag, used_tags, merge_prerelease
commit_tag, used_tags, merge_prerelease, scheme=scheme
):
used_tags.append(commit_tag)
yield {
Expand Down Expand Up @@ -186,13 +175,15 @@ def render_changelog(tree: Iterable) -> str:
return changelog


def parse_version_from_markdown(value: str) -> str | None:
def parse_version_from_markdown(
value: str, scheme: VersionScheme = Pep440
) -> str | None:
if not value.startswith("#"):
return None
m = re.search(defaults.version_parser, value)
m = scheme.parser.search(value)
if not m:
return None
return m.groupdict().get("version")
return cast(str, m.group("version"))


def parse_title_type_of_line(value: str) -> str | None:
Expand All @@ -203,7 +194,7 @@ def parse_title_type_of_line(value: str) -> str | None:
return m.groupdict().get("title")


def get_metadata(filepath: str) -> dict:
def get_metadata(filepath: str, scheme: VersionScheme = Pep440) -> dict:
unreleased_start: int | None = None
unreleased_end: int | None = None
unreleased_title: str | None = None
Expand Down Expand Up @@ -236,7 +227,7 @@ def get_metadata(filepath: str) -> dict:
unreleased_end = index

# Try to find the latest release done
version = parse_version_from_markdown(line)
version = parse_version_from_markdown(line, scheme)
if version:
latest_version = version
latest_version_position = index
Expand Down Expand Up @@ -328,7 +319,7 @@ def get_oldest_and_newest_rev(
tags: list[GitTag],
version: str,
tag_format: str,
version_type_cls: type[VersionProtocol] | None = None,
scheme: VersionScheme | None = None,
) -> tuple[str | None, str | None]:
"""Find the tags for the given version.

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

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

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

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

from commitizen import commands, config, out, version_types
from commitizen import commands, config, out, version_schemes
from commitizen.exceptions import (
CommitizenException,
ExitCode,
Expand Down Expand Up @@ -204,19 +204,25 @@
"default": None,
"help": "start pre-releases with this offset",
},
{
"name": ["--version-scheme"],
"help": "choose version scheme",
"default": None,
"choices": version_schemes.KNOWN_SCHEMES,
},
{
"name": ["--version-type"],
"help": "Deprecated, use --version-scheme",
"default": None,
"choices": version_schemes.KNOWN_SCHEMES,
},
{
"name": "manual_version",
"type": str,
"nargs": "?",
"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 Expand Up @@ -275,6 +281,12 @@
"If not set, it will include prereleases in the changelog"
),
},
{
"name": ["--version-scheme"],
"help": "choose version scheme",
"default": None,
"choices": version_schemes.KNOWN_SCHEMES,
},
],
},
{
Expand Down Expand Up @@ -354,7 +366,7 @@


def commitizen_excepthook(
type, value, traceback, debug=False, no_raise: list[int] = None
type, value, traceback, debug=False, no_raise: list[int] | None = None
):
traceback = traceback if isinstance(traceback, TracebackType) else None
if not no_raise:
Expand Down
Loading