Skip to content

Commit

Permalink
feat(tags): adds legacy_tag_formats and ignored_tag_formats settings
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre committed Feb 7, 2025
1 parent b63eb81 commit fa48ec9
Show file tree
Hide file tree
Showing 32 changed files with 810 additions and 374 deletions.
30 changes: 1 addition & 29 deletions commitizen/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message, encoding
from commitizen.exceptions import CurrentVersionNotFoundError
from commitizen.git import GitCommit, smart_open
from commitizen.version_schemes import DEFAULT_SCHEME, Increment, Version, VersionScheme
from commitizen.version_schemes import Increment, Version

VERSION_TYPES = [None, PATCH, MINOR, MAJOR]

Expand Down Expand Up @@ -131,34 +131,6 @@ def _version_to_regex(version: str) -> str:
return version.replace(".", r"\.").replace("+", r"\+")


def normalize_tag(
version: Version | str,
tag_format: str,
scheme: VersionScheme | None = None,
) -> str:
"""The tag and the software version might be different.
That's why this function exists.
Example:
| tag | version (PEP 0440) |
| --- | ------- |
| v0.9.0 | 0.9.0 |
| ver1.0.0 | 1.0.0 |
| ver1.0.0.a0 | 1.0.0a0 |
"""
scheme = scheme or DEFAULT_SCHEME
version = scheme(version) if isinstance(version, str) else version

major, minor, patch = version.release
prerelease = version.prerelease or ""

t = Template(tag_format)
return t.safe_substitute(
version=version, major=major, minor=minor, patch=patch, prerelease=prerelease
)


def create_commit_message(
current_version: Version | str,
new_version: Version | str,
Expand Down
84 changes: 26 additions & 58 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,13 @@
Template,
)

from commitizen import out
from commitizen.bump import normalize_tag
from commitizen.cz.base import ChangelogReleaseHook
from commitizen.defaults import get_tag_regexes
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
from commitizen.git import GitCommit, GitTag
from commitizen.version_schemes import (
DEFAULT_SCHEME,
BaseVersion,
InvalidVersion,
)
from commitizen.tags import TagRules

if TYPE_CHECKING:
from commitizen.cz.base import MessageBuilderHook
from commitizen.version_schemes import VersionScheme


@dataclass
Expand All @@ -69,50 +61,19 @@ class Metadata:
unreleased_end: int | None = None
latest_version: str | None = None
latest_version_position: int | None = None
latest_version_tag: str | None = None

def __post_init__(self):
if self.latest_version and not self.latest_version_tag:
# Test syntactic sugar
# latest version tag is optional if same as latest version
self.latest_version_tag = self.latest_version


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 tag_included_in_changelog(
tag: GitTag,
used_tags: list,
merge_prerelease: bool,
scheme: VersionScheme = DEFAULT_SCHEME,
) -> bool:
if tag in used_tags:
return False

try:
version = scheme(tag.name)
except InvalidVersion:
return False

if merge_prerelease and version.is_prerelease:
return False

return True


def get_version_tags(
scheme: type[BaseVersion], tags: list[GitTag], tag_format: str
) -> list[GitTag]:
valid_tags: list[GitTag] = []
TAG_FORMAT_REGEXS = get_tag_regexes(scheme.parser.pattern)
tag_format_regex = tag_format
for pattern, regex in TAG_FORMAT_REGEXS.items():
tag_format_regex = tag_format_regex.replace(pattern, regex)
for tag in tags:
if re.match(tag_format_regex, tag.name):
valid_tags.append(tag)
else:
out.warn(
f"InvalidVersion {tag.name} doesn't match configured tag format {tag_format}"
)
return valid_tags


def generate_tree_from_commits(
commits: list[GitCommit],
tags: list[GitTag],
Expand All @@ -122,13 +83,13 @@ def generate_tree_from_commits(
change_type_map: dict[str, str] | None = None,
changelog_message_builder_hook: MessageBuilderHook | None = None,
changelog_release_hook: ChangelogReleaseHook | None = None,
merge_prerelease: bool = False,
scheme: VersionScheme = DEFAULT_SCHEME,
rules: TagRules | None = None,
) -> Iterable[dict]:
pat = re.compile(changelog_pattern)
map_pat = re.compile(commit_parser, re.MULTILINE)
body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL)
current_tag: GitTag | None = None
rules = rules or TagRules()

# Check if the latest commit is not tagged
if commits:
Expand All @@ -148,8 +109,10 @@ def generate_tree_from_commits(
for commit in 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, scheme=scheme
if (
commit_tag
and commit_tag not in used_tags
and rules.include_in_changelog(commit_tag)
):
used_tags.append(commit_tag)
release = {
Expand Down Expand Up @@ -343,8 +306,7 @@ def get_smart_tag_range(
def get_oldest_and_newest_rev(
tags: list[GitTag],
version: str,
tag_format: str,
scheme: VersionScheme | None = None,
rules: TagRules,
) -> tuple[str | None, str | None]:
"""Find the tags for the given version.
Expand All @@ -358,22 +320,28 @@ def get_oldest_and_newest_rev(
oldest, newest = version.split("..")
except ValueError:
newest = version
newest_tag = normalize_tag(newest, tag_format=tag_format, scheme=scheme)
if not (newest_tag := rules.find_tag_for(tags, newest)):
raise NoCommitsFoundError("Could not find a valid revision range.")

oldest_tag = None
oldest_tag_name = None
if oldest:
oldest_tag = normalize_tag(oldest, tag_format=tag_format, scheme=scheme)
if not (oldest_tag := rules.find_tag_for(tags, oldest)):
raise NoCommitsFoundError("Could not find a valid revision range.")
oldest_tag_name = oldest_tag.name

tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag)
tags_range = get_smart_tag_range(
tags, newest=newest_tag.name, oldest=oldest_tag_name
)
if not tags_range:
raise NoCommitsFoundError("Could not find a valid revision range.")

oldest_rev: str | None = tags_range[-1].name
newest_rev = newest_tag
newest_rev = newest_tag.name

# check if it's the first tag created
# and it's also being requested as part of the range
if oldest_rev == tags[-1].name and oldest_rev == oldest_tag:
if oldest_rev == tags[-1].name and oldest_rev == oldest_tag_name:
return None, newest_rev

# when they are the same, and it's also the
Expand Down
23 changes: 6 additions & 17 deletions commitizen/changelog_formats/asciidoc.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING

from .base import BaseFormat

if TYPE_CHECKING:
from commitizen.tags import VersionTag


class AsciiDoc(BaseFormat):
extension = "adoc"

RE_TITLE = re.compile(r"^(?P<level>=+) (?P<title>.*)$")

def parse_version_from_title(self, line: str) -> str | None:
def parse_version_from_title(self, line: str) -> VersionTag | None:
m = self.RE_TITLE.match(line)
if not m:
return None
# Capture last match as AsciiDoc use postfixed URL labels
matches = list(re.finditer(self.version_parser, m.group("title")))
if not matches:
return None
if "version" in matches[-1].groupdict():
return matches[-1].group("version")
partial_matches = matches[-1].groupdict()
try:
partial_version = f"{partial_matches['major']}.{partial_matches['minor']}.{partial_matches['patch']}"
except KeyError:
return None

if partial_matches.get("prerelease"):
partial_version = f"{partial_version}-{partial_matches['prerelease']}"
if partial_matches.get("devrelease"):
partial_version = f"{partial_version}{partial_matches['devrelease']}"
return partial_version
return self.tag_rules.search_version(m.group("title"), last=True)

def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
Expand Down
28 changes: 12 additions & 16 deletions commitizen/changelog_formats/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from __future__ import annotations

import os
import re
from abc import ABCMeta
from re import Pattern
from typing import IO, Any, ClassVar

from commitizen.changelog import Metadata
from commitizen.config.base_config import BaseConfig
from commitizen.defaults import get_tag_regexes
from commitizen.tags import TagRules, VersionTag
from commitizen.version_schemes import get_version_scheme

from . import ChangelogFormat
Expand All @@ -28,15 +26,12 @@ def __init__(self, config: BaseConfig):
self.config = config
self.encoding = self.config.settings["encoding"]
self.tag_format = self.config.settings["tag_format"]

@property
def version_parser(self) -> Pattern:
tag_regex: str = self.tag_format
version_regex = get_version_scheme(self.config).parser.pattern
TAG_FORMAT_REGEXS = get_tag_regexes(version_regex)
for pattern, regex in TAG_FORMAT_REGEXS.items():
tag_regex = tag_regex.replace(pattern, regex)
return re.compile(tag_regex)
self.tag_rules = TagRules(
scheme=get_version_scheme(self.config.settings),
tag_format=self.tag_format,
legacy_tag_formats=self.config.settings["legacy_tag_formats"],
ignored_tag_formats=self.config.settings["ignored_tag_formats"],
)

def get_metadata(self, filepath: str) -> Metadata:
if not os.path.isfile(filepath):
Expand All @@ -63,17 +58,18 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
meta.unreleased_end = index

# Try to find the latest release done
version = self.parse_version_from_title(line)
if version:
meta.latest_version = version
parsed = self.parse_version_from_title(line)
if parsed:
meta.latest_version = parsed.version
meta.latest_version_tag = parsed.tag
meta.latest_version_position = index
break # there's no need for more info
if meta.unreleased_start is not None and meta.unreleased_end is None:
meta.unreleased_end = index

return meta

def parse_version_from_title(self, line: str) -> str | None:
def parse_version_from_title(self, line: str) -> VersionTag | None:
"""
Extract the version from a title line if any
"""
Expand Down
25 changes: 6 additions & 19 deletions commitizen/changelog_formats/markdown.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING

from .base import BaseFormat

if TYPE_CHECKING:
from commitizen.tags import VersionTag


class Markdown(BaseFormat):
extension = "md"
Expand All @@ -12,28 +16,11 @@ class Markdown(BaseFormat):

RE_TITLE = re.compile(r"^(?P<level>#+) (?P<title>.*)$")

def parse_version_from_title(self, line: str) -> str | None:
def parse_version_from_title(self, line: str) -> VersionTag | None:
m = self.RE_TITLE.match(line)
if not m:
return None
m = re.search(self.version_parser, m.group("title"))
if not m:
return None
if "version" in m.groupdict():
return m.group("version")
matches = m.groupdict()
try:
partial_version = (
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
)
except KeyError:
return None

if matches.get("prerelease"):
partial_version = f"{partial_version}-{matches['prerelease']}"
if matches.get("devrelease"):
partial_version = f"{partial_version}{matches['devrelease']}"
return partial_version
return self.tag_rules.search_version(m.group("title"))

def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
Expand Down
31 changes: 5 additions & 26 deletions commitizen/changelog_formats/restructuredtext.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import re
import sys
from itertools import zip_longest
from typing import IO, TYPE_CHECKING, Any, Union
Expand Down Expand Up @@ -64,31 +63,11 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
elif unreleased_title_kind and unreleased_title_kind == kind:
meta.unreleased_end = index
# Try to find the latest release done
m = re.search(self.version_parser, title)
if m:
matches = m.groupdict()
if "version" in matches:
version = m.group("version")
meta.latest_version = version
meta.latest_version_position = index
break # there's no need for more info
try:
partial_version = (
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
)
if matches.get("prerelease"):
partial_version = (
f"{partial_version}-{matches['prerelease']}"
)
if matches.get("devrelease"):
partial_version = (
f"{partial_version}{matches['devrelease']}"
)
meta.latest_version = partial_version
meta.latest_version_position = index
break
except KeyError:
pass
if version := self.tag_rules.search_version(title):
meta.latest_version = version[0]
meta.latest_version_tag = version[1]
meta.latest_version_position = index
break
if meta.unreleased_start is not None and meta.unreleased_end is None:
meta.unreleased_end = (
meta.latest_version_position if meta.latest_version else index + 1
Expand Down
Loading

0 comments on commit fa48ec9

Please sign in to comment.