From 5306cbf2436284e61b4e092c4e4e9e82c1317fcc Mon Sep 17 00:00:00 2001 From: "Axel H." Date: Sun, 17 Nov 2024 19:49:55 +0100 Subject: [PATCH 01/20] refactor(get_tag_regexes): dedup tag regexes definition --- commitizen/defaults.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/commitizen/defaults.py b/commitizen/defaults.py index d776e38d7..45b6500e0 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -138,17 +138,15 @@ class Settings(TypedDict, total=False): def get_tag_regexes( version_regex: str, ) -> dict[str, str]: + regexs = { + "version": version_regex, + "major": r"(?P\d+)", + "minor": r"(?P\d+)", + "patch": r"(?P\d+)", + "prerelease": r"(?P\w+\d+)?", + "devrelease": r"(?P\.dev\d+)?", + } return { - "$version": version_regex, - "$major": r"(?P\d+)", - "$minor": r"(?P\d+)", - "$patch": r"(?P\d+)", - "$prerelease": r"(?P\w+\d+)?", - "$devrelease": r"(?P\.dev\d+)?", - "${version}": version_regex, - "${major}": r"(?P\d+)", - "${minor}": r"(?P\d+)", - "${patch}": r"(?P\d+)", - "${prerelease}": r"(?P\w+\d+)?", - "${devrelease}": r"(?P\.dev\d+)?", + **{f"${k}": v for k, v in regexs.items()}, + **{f"${{{k}}}": v for k, v in regexs.items()}, } From 74554c2b4244341cda0f0db177fb78fa99d86646 Mon Sep 17 00:00:00 2001 From: "Axel H." Date: Mon, 18 Nov 2024 03:54:21 +0100 Subject: [PATCH 02/20] feat(tags): adds `legacy_tag_formats` and `ignored_tag_formats` settings --- commitizen/bump.py | 30 +- commitizen/changelog.py | 84 ++---- commitizen/changelog_formats/asciidoc.py | 23 +- commitizen/changelog_formats/base.py | 28 +- commitizen/changelog_formats/markdown.py | 25 +- .../changelog_formats/restructuredtext.py | 31 +-- commitizen/changelog_formats/textile.py | 28 +- commitizen/commands/bump.py | 38 +-- commitizen/commands/changelog.py | 39 ++- commitizen/commands/init.py | 2 +- commitizen/defaults.py | 6 +- commitizen/providers/scm_provider.py | 66 +---- commitizen/tags.py | 257 ++++++++++++++++++ commitizen/version_schemes.py | 12 +- docs/config.md | 20 ++ docs/faq.md | 33 +++ docs/tutorials/monorepo_guidance.md | 2 + docs/tutorials/tag_format.md | 101 +++++++ mkdocs.yml | 1 + tests/commands/test_bump_command.py | 29 ++ tests/commands/test_changelog_command.py | 242 ++++++++++++++--- ...from_rev_version_range_with_legacy_tags.md | 17 ++ ...changelog_incremental_change_tag_format.md | 17 ++ tests/providers/test_scm_provider.py | 23 ++ tests/test_bump_normalize_tag.py | 5 +- tests/test_changelog.py | 115 +++++++- tests/test_changelog_format_asciidoc.py | 19 +- tests/test_changelog_format_markdown.py | 19 +- .../test_changelog_format_restructuredtext.py | 5 + tests/test_changelog_format_textile.py | 19 +- tests/test_conf.py | 4 + tests/test_version_schemes.py | 14 +- 32 files changed, 982 insertions(+), 372 deletions(-) create mode 100644 commitizen/tags.py create mode 100644 docs/tutorials/tag_format.md create mode 100644 tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md create mode 100644 tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md diff --git a/commitizen/bump.py b/commitizen/bump.py index 908899eb0..adfab64cb 100644 --- a/commitizen/bump.py +++ b/commitizen/bump.py @@ -11,7 +11,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] @@ -142,34 +142,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, diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 7f300354b..95bf21d6f 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -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 @@ -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], @@ -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: @@ -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 = { @@ -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. @@ -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 diff --git a/commitizen/changelog_formats/asciidoc.py b/commitizen/changelog_formats/asciidoc.py index 6007a56d1..ed3e8607b 100644 --- a/commitizen/changelog_formats/asciidoc.py +++ b/commitizen/changelog_formats/asciidoc.py @@ -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=+) (?P.*)$") - 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) diff --git a/commitizen/changelog_formats/base.py b/commitizen/changelog_formats/base.py index 53527a060..f69cf8f00 100644 --- a/commitizen/changelog_formats/base.py +++ b/commitizen/changelog_formats/base.py @@ -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 @@ -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): @@ -63,9 +58,10 @@ 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: @@ -73,7 +69,7 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata: 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 """ diff --git a/commitizen/changelog_formats/markdown.py b/commitizen/changelog_formats/markdown.py index 29c1cce54..e3d30fe17 100644 --- a/commitizen/changelog_formats/markdown.py +++ b/commitizen/changelog_formats/markdown.py @@ -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" @@ -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) diff --git a/commitizen/changelog_formats/restructuredtext.py b/commitizen/changelog_formats/restructuredtext.py index 8bcf9a4a4..b7e4e105a 100644 --- a/commitizen/changelog_formats/restructuredtext.py +++ b/commitizen/changelog_formats/restructuredtext.py @@ -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 @@ -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 diff --git a/commitizen/changelog_formats/textile.py b/commitizen/changelog_formats/textile.py index 8750f0056..6693e5e00 100644 --- a/commitizen/changelog_formats/textile.py +++ b/commitizen/changelog_formats/textile.py @@ -1,39 +1,23 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING from .base import BaseFormat +if TYPE_CHECKING: + from commitizen.tags import VersionTag + class Textile(BaseFormat): extension = "textile" RE_TITLE = re.compile(r"^h(?P<level>\d)\. (?P<title>.*)$") - def parse_version_from_title(self, line: str) -> str | None: + def parse_version_from_title(self, line: str) -> VersionTag | None: if not self.RE_TITLE.match(line): return None - m = re.search(self.version_parser, line) - if not m: - return None - if "version" in m.groupdict(): - return m.group("version") - matches = m.groupdict() - if not all( - [ - version_segment in matches - for version_segment in ("major", "minor", "patch") - ] - ): - return None - - 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']}" - return partial_version + return self.tag_rules.search_version(line) def parse_title_level(self, line: str) -> int | None: m = self.RE_TITLE.match(line) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 8e9f0f181..60853094f 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -2,6 +2,7 @@ import warnings from logging import getLogger +from typing import cast import questionary @@ -9,6 +10,7 @@ from commitizen.changelog_formats import get_changelog_format from commitizen.commands.changelog import Changelog from commitizen.config import BaseConfig +from commitizen.defaults import Settings from commitizen.exceptions import ( BumpCommitFailedError, BumpTagFailedError, @@ -24,6 +26,7 @@ NoVersionSpecifiedError, ) from commitizen.providers import get_provider +from commitizen.tags import TagRules from commitizen.version_schemes import ( Increment, InvalidVersion, @@ -84,7 +87,7 @@ def __init__(self, config: BaseConfig, arguments: dict): ) ) self.scheme = get_version_scheme( - self.config, arguments["version_scheme"] or deprecated_version_type + self.config.settings, arguments["version_scheme"] or deprecated_version_type ) self.file_name = arguments["file_name"] or self.config.settings.get( "changelog_file" @@ -98,18 +101,20 @@ def __init__(self, config: BaseConfig, arguments: dict): ) self.extras = arguments["extras"] - def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool: + def is_initial_tag( + self, current_tag: git.GitTag | None, is_yes: bool = False + ) -> bool: """Check if reading the whole git tree up to HEAD is needed.""" is_initial = False - if not git.tag_exist(current_tag_version): + if not current_tag: if is_yes: is_initial = True else: - out.info(f"Tag {current_tag_version} could not be found. ") + out.info("No tag matching configuration could not be found.") out.info( "Possible causes:\n" "- version in configuration is not the current version\n" - "- tag_format is missing, check them using 'git tag --list'\n" + "- tag_format or legacy_tag_formats is missing, check them using 'git tag --list'\n" ) is_initial = questionary.confirm("Is this the first tag created?").ask() return is_initial @@ -143,7 +148,6 @@ def __call__(self) -> None: # noqa: C901 except TypeError: raise NoVersionSpecifiedError() - tag_format: str = self.bump_settings["tag_format"] bump_commit_message: str = self.bump_settings["bump_message"] version_files: list[str] = self.bump_settings["version_files"] major_version_zero: bool = self.bump_settings["major_version_zero"] @@ -221,13 +225,13 @@ def __call__(self) -> None: # noqa: C901 or self.changelog_config ) - current_tag_version: str = bump.normalize_tag( - current_version, - tag_format=tag_format, - scheme=self.scheme, + rules = TagRules.from_settings(cast(Settings, self.bump_settings)) + current_tag = rules.find_tag_for(git.get_tags(), current_version) + current_tag_version = getattr( + current_tag, "name", rules.normalize_tag(current_version) ) - is_initial = self.is_initial_tag(current_tag_version, is_yes) + is_initial = self.is_initial_tag(current_tag, is_yes) if manual_version: try: @@ -239,10 +243,10 @@ def __call__(self) -> None: # noqa: C901 ) from exc else: if increment is None: - if is_initial: - commits = git.get_commits() + if current_tag: + commits = git.get_commits(current_tag.name) else: - commits = git.get_commits(current_tag_version) + commits = git.get_commits() # No commits, there is no need to create an empty tag. # Unless we previously had a prerelease. @@ -280,11 +284,7 @@ def __call__(self) -> None: # noqa: C901 exact_increment=increment_mode == "exact", ) - new_tag_version = bump.normalize_tag( - new_version, - tag_format=tag_format, - scheme=self.scheme, - ) + new_tag_version = rules.normalize_tag(new_version) message = bump.create_commit_message( current_version, new_version, bump_commit_message ) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 25e644aae..80a72651e 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Callable, cast -from commitizen import bump, changelog, defaults, factory, git, out +from commitizen import changelog, defaults, factory, git, out from commitizen.changelog_formats import get_changelog_format from commitizen.config import BaseConfig from commitizen.cz.base import ChangelogReleaseHook, MessageBuilderHook @@ -20,6 +20,7 @@ NotAllowed, ) from commitizen.git import GitTag, smart_open +from commitizen.tags import TagRules from commitizen.version_schemes import get_version_scheme @@ -53,7 +54,9 @@ def __init__(self, config: BaseConfig, args): ) self.dry_run = args["dry_run"] - self.scheme = get_version_scheme(self.config, args.get("version_scheme")) + self.scheme = get_version_scheme( + self.config.settings, args.get("version_scheme") + ) current_version = ( args.get("current_version", config.settings.get("version")) or "" @@ -73,9 +76,14 @@ def __init__(self, config: BaseConfig, args): self.tag_format: str = ( args.get("tag_format") or self.config.settings["tag_format"] ) - self.merge_prerelease = args.get( - "merge_prerelease" - ) or self.config.settings.get("changelog_merge_prerelease") + self.tag_rules = TagRules( + scheme=self.scheme, + tag_format=self.tag_format, + legacy_tag_formats=self.config.settings["legacy_tag_formats"], + ignored_tag_formats=self.config.settings["ignored_tag_formats"], + merge_prereleases=args.get("merge_prerelease") + or self.config.settings["changelog_merge_prerelease"], + ) self.template = ( args.get("template") @@ -152,7 +160,6 @@ def __call__(self): changelog_release_hook: ChangelogReleaseHook | None = ( self.cz.changelog_release_hook ) - merge_prerelease = self.merge_prerelease if self.export_template_to: return self.export_template() @@ -168,28 +175,19 @@ def __call__(self): # Don't continue if no `file_name` specified. assert self.file_name - tags = ( - changelog.get_version_tags(self.scheme, git.get_tags(), self.tag_format) - or [] - ) + tags = self.tag_rules.get_version_tags(git.get_tags(), warn=True) end_rev = "" if self.incremental: changelog_meta = self.changelog_format.get_metadata(self.file_name) if changelog_meta.latest_version: - latest_tag_version: str = bump.normalize_tag( - changelog_meta.latest_version, - tag_format=self.tag_format, - scheme=self.scheme, - ) start_rev = self._find_incremental_rev( - strip_local_version(latest_tag_version), tags + strip_local_version(changelog_meta.latest_version_tag), tags ) if self.rev_range: start_rev, end_rev = changelog.get_oldest_and_newest_rev( tags, - version=self.rev_range, - tag_format=self.tag_format, - scheme=self.scheme, + self.rev_range, + self.tag_rules, ) commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order") if not commits and ( @@ -205,8 +203,7 @@ def __call__(self): change_type_map=change_type_map, changelog_message_builder_hook=changelog_message_builder_hook, changelog_release_hook=changelog_release_hook, - merge_prerelease=merge_prerelease, - scheme=self.scheme, + rules=self.tag_rules, ) if self.change_type_order: tree = changelog.order_changelog_tree(tree, self.change_type_order) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index ffc5e3eb3..e39dfbe29 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -98,7 +98,7 @@ def __call__(self): version_provider = self._ask_version_provider() # select tag = self._ask_tag() # confirm & select version_scheme = self._ask_version_scheme() # select - version = get_version_scheme(self.config, version_scheme)(tag) + version = get_version_scheme(self.config.settings, version_scheme)(tag) tag_format = self._ask_tag_format(tag) # confirm & text update_changelog_on_bump = self._ask_update_changelog_on_bump() # confirm major_version_zero = self._ask_major_version_zero(version) # confirm diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 45b6500e0..0b78e1b0b 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -2,7 +2,7 @@ import pathlib from collections import OrderedDict -from collections.abc import Iterable, MutableMapping +from collections.abc import Iterable, MutableMapping, Sequence from typing import Any, TypedDict # Type @@ -35,6 +35,8 @@ class Settings(TypedDict, total=False): version_scheme: str | None version_type: str | None tag_format: str + legacy_tag_formats: Sequence[str] + ignored_tag_formats: Sequence[str] bump_message: str | None retry_after_failure: bool allow_abort: bool @@ -77,6 +79,8 @@ class Settings(TypedDict, total=False): "version_provider": "commitizen", "version_scheme": None, "tag_format": "$version", # example v$version + "legacy_tag_formats": [], + "ignored_tag_formats": [], "bump_message": None, # bumped v$current_version to $new_version "retry_after_failure": False, "allow_abort": False, diff --git a/commitizen/providers/scm_provider.py b/commitizen/providers/scm_provider.py index 33e470cfc..cb575148c 100644 --- a/commitizen/providers/scm_provider.py +++ b/commitizen/providers/scm_provider.py @@ -1,17 +1,8 @@ from __future__ import annotations -import re -from typing import Callable - -from commitizen.defaults import get_tag_regexes from commitizen.git import get_tags from commitizen.providers.base_provider import VersionProvider -from commitizen.version_schemes import ( - InvalidVersion, - Version, - VersionProtocol, - get_version_scheme, -) +from commitizen.tags import TagRules class ScmProvider(VersionProvider): @@ -23,57 +14,14 @@ class ScmProvider(VersionProvider): It is meant for `setuptools-scm` or any package manager `*-scm` provider. """ - TAG_FORMAT_REGEXS = get_tag_regexes(r"(?P<version>.+)") - - def _tag_format_matcher(self) -> Callable[[str], VersionProtocol | None]: - version_scheme = get_version_scheme(self.config) - pattern = self.config.settings["tag_format"] - if pattern == "$version": - pattern = version_scheme.parser.pattern - for var, tag_pattern in self.TAG_FORMAT_REGEXS.items(): - pattern = pattern.replace(var, tag_pattern) - - regex = re.compile(f"^{pattern}$", re.VERBOSE) - - def matcher(tag: str) -> Version | None: - match = regex.match(tag) - if not match: - return None - groups = match.groupdict() - if "version" in groups: - ver = groups["version"] - elif "major" in groups: - ver = "".join( - ( - groups["major"], - f".{groups['minor']}" if groups.get("minor") else "", - f".{groups['patch']}" if groups.get("patch") else "", - groups["prerelease"] if groups.get("prerelease") else "", - groups["devrelease"] if groups.get("devrelease") else "", - ) - ) - elif pattern == version_scheme.parser.pattern: - ver = tag - else: - return None - - try: - return version_scheme(ver) - except InvalidVersion: - return None - - return matcher - def get_version(self) -> str: - matcher = self._tag_format_matcher() - matches = sorted( - version - for t in get_tags(reachable_only=True) - if (version := matcher(t.name)) - ) - if not matches: + rules = TagRules.from_settings(self.config.settings) + tags = get_tags(reachable_only=True) + version_tags = rules.get_version_tags(tags) + versions = sorted(rules.extract_version(t) for t in version_tags) + if not versions: return "0.0.0" - return str(matches[-1]) + return str(versions[-1]) def set_version(self, version: str): # Not necessary diff --git a/commitizen/tags.py b/commitizen/tags.py new file mode 100644 index 000000000..962e428ef --- /dev/null +++ b/commitizen/tags.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import re +import warnings +from collections.abc import Sequence +from dataclasses import dataclass, field +from functools import cached_property +from string import Template +from typing import TYPE_CHECKING, NamedTuple + +from typing_extensions import Self + +from commitizen import out +from commitizen.defaults import DEFAULT_SETTINGS, Settings, get_tag_regexes +from commitizen.git import GitTag +from commitizen.version_schemes import ( + DEFAULT_SCHEME, + InvalidVersion, + Version, + VersionScheme, + get_version_scheme, +) + +if TYPE_CHECKING: + from commitizen.version_schemes import VersionScheme + + +class VersionTag(NamedTuple): + """Represent a version and its matching tag form.""" + + version: str + tag: str + + +@dataclass +class TagRules: + """ + Encapsulate tag-related rules. + + It allows to filter or match tags according to rules provided in settings: + - `tag_format`: the current format of the tags generated on `bump` + - `legacy_tag_formats`: previous known formats of the tag + - `ignored_tag_formats`: known formats that should be ignored + - `merge_prereleases`: if `True`, prereleases will be merged with their release counterpart + - `version_scheme`: the version scheme to use, which will be used to parse and format versions + + This class is meant to abstract and centralize all the logic related to tags. + To ensure consistency, it is recommended to use this class to handle tags. + + Example: + + ```py + settings = DEFAULT_SETTINGS.clone() + settings.update({ + "tag_format": "v{version}" + "legacy_tag_formats": ["version{version}", "ver{version}"], + "ignored_tag_formats": ["ignored{version}"], + }) + + rules = TagRules.from_settings(settings) + + assert rules.is_version_tag("v1.0.0") + assert rules.is_version_tag("version1.0.0") + assert rules.is_version_tag("ver1.0.0") + assert not rules.is_version_tag("ignored1.0.0", warn=True) # Does not warn + 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") + try: + assert rules.extract_version("not-a-v1.0.0") + except InvalidVersion: + print "Does not match a tag format" + ``` + """ + + scheme: VersionScheme = DEFAULT_SCHEME + tag_format: str = DEFAULT_SETTINGS["tag_format"] + legacy_tag_formats: Sequence[str] = field(default_factory=list) + ignored_tag_formats: Sequence[str] = field(default_factory=list) + merge_prereleases: bool = False + + @cached_property + def version_regexes(self) -> Sequence[re.Pattern]: + """Regexes for all legit tag formats, current and legacy""" + tag_formats = [self.tag_format, *self.legacy_tag_formats] + regexes = (self._format_regex(p) for p in tag_formats) + return [re.compile(r) for r in regexes] + + @cached_property + def ignored_regexes(self) -> Sequence[re.Pattern]: + """Regexes for known but ignored tag formats""" + regexes = (self._format_regex(p, star=True) for p in self.ignored_tag_formats) + return [re.compile(r) for r in regexes] + + def _format_regex(self, tag_pattern: str, star: bool = False) -> str: + """ + Format a tag pattern into a regex pattern. + + If star is `True`, the `*` character will be considered as a wildcard. + """ + tag_regexes = get_tag_regexes(self.scheme.parser.pattern) + format_regex = tag_pattern.replace("*", "(?:.*?)") if star else tag_pattern + for pattern, regex in tag_regexes.items(): + format_regex = format_regex.replace(pattern, regex) + return format_regex + + def is_version_tag(self, tag: str | GitTag, warn: bool = False) -> bool: + """ + True if a given tag is a legit version tag. + + if `warn` is `True`, it will print a warning message if the tag is not a version tag. + """ + tag = tag.name if isinstance(tag, GitTag) else tag + is_legit = any(regex.match(tag) for regex in self.version_regexes) + if warn and not is_legit and not self.is_ignored_tag(tag): + out.warn(f"InvalidVersion {tag} doesn't match any configured tag format") + return is_legit + + def is_ignored_tag(self, tag: str | GitTag) -> bool: + """True if a given tag can be ignored""" + tag = tag.name if isinstance(tag, GitTag) else tag + return any(regex.match(tag) for regex in self.ignored_regexes) + + def get_version_tags( + self, tags: Sequence[GitTag], warn: bool = False + ) -> Sequence[GitTag]: + """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: + """ + Extract a version from the tag as defined in tag formats. + + Raises `InvalidVersion` if the tag does not match any format. + """ + candidates = ( + m for regex in self.version_regexes if (m := regex.fullmatch(tag.name)) + ) + if not (m := next(candidates, None)): + raise InvalidVersion() + if "version" in m.groupdict(): + return self.scheme(m.group("version")) + + parts = m.groupdict() + version = parts["major"] + + if minor := parts.get("minor"): + version = f"{version}.{minor}" + if patch := parts.get("patch"): + version = f"{version}.{patch}" + + if parts.get("prerelease"): + version = f"{version}-{parts['prerelease']}" + if parts.get("devrelease"): + version = f"{version}{parts['devrelease']}" + return self.scheme(version) + + def include_in_changelog(self, tag: GitTag) -> bool: + """Check if a tag should be included in the changelog""" + try: + version = self.extract_version(tag) + except InvalidVersion: + return False + + if self.merge_prereleases and version.is_prerelease: + return False + + return True + + def search_version(self, text: str, last: bool = False) -> VersionTag | None: + """ + Search the first or last version tag occurrence in text. + + It searches for complete versions only (aka `major`, `minor` and `patch`) + """ + candidates = ( + m for regex in self.version_regexes if len(m := list(regex.finditer(text))) + ) + if not (matches := next(candidates, [])): + return None + + match = matches[-1 if last else 0] + + if "version" in match.groupdict(): + return VersionTag(match.group("version"), match.group(0)) + + parts = match.groupdict() + try: + version = f"{parts['major']}.{parts['minor']}.{parts['patch']}" + except KeyError: + return None + + if parts.get("prerelease"): + version = f"{version}-{parts['prerelease']}" + if parts.get("devrelease"): + version = f"{version}{parts['devrelease']}" + return VersionTag(version, match.group(0)) + + def normalize_tag( + self, version: Version | str, tag_format: str | 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 | + """ + version = self.scheme(version) if isinstance(version, str) else version + tag_format = tag_format or self.tag_format + + 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 find_tag_for( + self, tags: Sequence[GitTag], version: Version | str + ) -> GitTag | None: + """Find the first matching tag for a given version.""" + version = self.scheme(version) if isinstance(version, str) else version + possible_tags = [ + self.normalize_tag(version, f) + for f in (self.tag_format, *self.legacy_tag_formats) + ] + candidates = [t for t in tags if any(t.name == p for p in possible_tags)] + if len(candidates) > 1: + warnings.warn( + UserWarning( + f"Multiple tags found for version {version}: {', '.join(t.name for t in candidates)}" + ) + ) + return next(iter(candidates), None) + + @classmethod + def from_settings(cls, settings: Settings) -> Self: + """Extract tag rules from settings""" + return cls( + scheme=get_version_scheme(settings), + tag_format=settings["tag_format"], + legacy_tag_formats=settings["legacy_tag_formats"], + ignored_tag_formats=settings["ignored_tag_formats"], + merge_prereleases=settings["changelog_merge_prerelease"], + ) diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index 554864e3b..2486be58c 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -22,8 +22,7 @@ from packaging.version import InvalidVersion # noqa: F401: Rexpose the common exception from packaging.version import Version as _BaseVersion -from commitizen.config.base_config import BaseConfig -from commitizen.defaults import MAJOR, MINOR, PATCH +from commitizen.defaults import MAJOR, MINOR, PATCH, Settings from commitizen.exceptions import VersionSchemeUnknown if TYPE_CHECKING: @@ -42,7 +41,7 @@ Increment: TypeAlias = Literal["MAJOR", "MINOR", "PATCH"] Prerelease: TypeAlias = Literal["alpha", "beta", "rc"] -DEFAULT_VERSION_PARSER = r"v?(?P<version>([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?(\w+)?)" +DEFAULT_VERSION_PARSER = r"v?(?P<version>([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.]+)?(\w+)?)" @runtime_checkable @@ -408,14 +407,15 @@ def __str__(self) -> str: """All known registered version schemes""" -def get_version_scheme(config: BaseConfig, name: str | None = None) -> VersionScheme: +def get_version_scheme(settings: Settings, name: str | None = None) -> VersionScheme: """ Get the version scheme as defined in the configuration or from an overridden `name` :raises VersionSchemeUnknown: if the version scheme is not found. """ - deprecated_setting: str | None = config.settings.get("version_type") + # TODO: Remove the deprecated `version_type` handling + deprecated_setting: str | None = settings.get("version_type") if deprecated_setting: warnings.warn( DeprecationWarning( @@ -423,7 +423,7 @@ def get_version_scheme(config: BaseConfig, name: str | None = None) -> VersionSc "Please use `version_scheme` instead" ) ) - name = name or config.settings.get("version_scheme") or deprecated_setting + name = name or settings.get("version_scheme") or deprecated_setting if not name: return DEFAULT_SCHEME diff --git a/docs/config.md b/docs/config.md index 210b5d7ff..5ec189487 100644 --- a/docs/config.md +++ b/docs/config.md @@ -51,6 +51,26 @@ Default: `$version` Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [Read more][tag_format] +### `legacy_tag_formats` + +Type: `list` + +Default: `[ ]` + +Legacy git tag formats, useful for old projects that changed tag format. +Tags matching those formats will be recognized as version tags and be included in the changelog. +Each entry use the the syntax as [`tag_format`](#tag_format). [Read more][tag_format] + +### `ignored_tag_formats` + +Type: `list` + +Default: `[ ]` + +Tags matching those formats will be totally ignored and won't raise a warning. +Each entry use the the syntax as [`tag_format`](#tag_format) with the addition of `*` +that will match everything (non-greedy). [Read more][tag_format] + ### `update_changelog_on_bump` Type: `bool` diff --git a/docs/faq.md b/docs/faq.md index 4bcb2bc7c..29d9f4051 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -107,3 +107,36 @@ If you would like to learn more about both schemes, there are plenty of good res [#173]: https://github.com/commitizen-tools/commitizen/issues/173 [#385]: https://github.com/commitizen-tools/commitizen/pull/385 + +## How to change the tag format ? + +You can use the [`legacy_tag_formats`](config.md#legacy_tag_formats) to list old tag formats. +New bumped tags will be in the new format but old ones will still work for: +- changelog generation (full, incremental and version range) +- bump new version computation (automatically guessed or increment given) + + +So given if you change from `myproject-$version` to `${version}` and then `v${version}`, +your commitizen configuration will look like this: + +```toml +tag_format = "v${version}" +legacy_tag_formats = [ + "${version}", + "myproject-$version", +] +``` + +## How to avoid warnings for expected non-version tags + +You can explicitly ignore them with [`ignored_tag_formats`](config.md#ignored_tag_formats). + +```toml +tag_format = "v${version}" +ignored_tag_formats = [ + "stable", + "component-*", + "env/*", + "v${major}.${minor}", +] +``` diff --git a/docs/tutorials/monorepo_guidance.md b/docs/tutorials/monorepo_guidance.md index ba6d70fd8..817f92321 100644 --- a/docs/tutorials/monorepo_guidance.md +++ b/docs/tutorials/monorepo_guidance.md @@ -27,6 +27,7 @@ Sample `.cz.toml` for each component: name = "cz_customize" version = "0.0.0" tag_format = "${version}-library-b" # the component name can be a prefix or suffix with or without a separator +ignored_tag_formats = ["${version}-library-*"] # Avoid noise from other tags update_changelog_on_bump = true ``` @@ -36,6 +37,7 @@ update_changelog_on_bump = true name = "cz_customize" version = "0.0.0" tag_format = "${version}-library-z" +ignored_tag_formats = ["${version}-library-*"] # Avoid noise from other tags update_changelog_on_bump = true ``` diff --git a/docs/tutorials/tag_format.md b/docs/tutorials/tag_format.md new file mode 100644 index 000000000..59c42bea1 --- /dev/null +++ b/docs/tutorials/tag_format.md @@ -0,0 +1,101 @@ +# Managing tag formats + +## Tag format and version scheme + +For most projects, the tag format is simply the version number which is set like this: + +```yaml +[tool.commitizen] +tag_format: $version +version_scheme: pep440 +``` + +As this is the default value so you don't have to specify it. + +This setting means that: + +- The tag generated on bump will have this format: `1.0.0` : + - the version is generated following PEP440 scheme + - the tag is exactly the generated version +- All tags having this format will be recognized as version tag when: + - searching the last while bumping a release + - searching previous versions while: + - generating incremental changelog + - generating a changelog for a version range +- The changelog versions (section titles) will have this format +- The `scm` version provider will identify the current version using this tag format + +The version format will change depending on your configured version scheme. +For most, it will only impact pre-releases and [developmental releases](dev_releases.md) formats (i.e. `1.0.0-rc.1` vs. `1.0.0.rc1`) + +But you may need a different tagging convention, let's say using `semver` and prefixed with a `v`. +In this case you will define your settings like this: + +```yaml +[tool.commitizen] +tag_format: v${version} +version_scheme: semver +``` + +As a result, the tag generated on bump will have this format: `v1.0.0` and the version will be generated following `semver` scheme. + +!!! note + Both `$version` and `${version}` syntaxes are strictly equivalent. You can use the one you prefer. + +See [the `version_scheme` section in `bump` command documentation](../commands/bump.md#version_scheme) for more details on version schemes and how to define your own. +See [`tag_format`](../config.md#tag_format) and [`version_scheme`](../config.md#version_scheme) settings in [Configuration reference](../config.md) for more details on these settings. + +## Changing convention + +Now, let's say you need to change the tag format for some reason (company convention, [migration to a monorepo](monorepo_guidance.md)...). +You will obviously want to keep all those features working as expected. + +Commitizen can deal with it as long as you provide the legacy tag format in the configuration. + +Using the previous example, let say you want to move from `v${version}` to `component-${version}`. +Then `component-${version}` will be the new tag format and `v${version}` the legacy one. + +```yaml +[tool.commitizen] +tag_format: component-${version} +legacy_tag_formats: + - v${version} +``` + +This way, you won't loose your version history, you'll still be able to generate you changelog properly +and on the next version bump, your last version in the form `v${version}` will be properly recognizef if you use the `scm` version provider. +Your new tag will be in the form `component-${version}`. + +## Known tags to ignore + +Now let's say you have some known tags you want to ignore, either because they are not versions, either because they are not versions of the component you are dealing with. +As a consequence, you don't want them to trigger a warning because Commitizen detected an unknown tag format: + +Then you can tell Commitizen about it using the [`ignored_tag_formats`](../config.md#ignored_tag_formats) setting: + +```yaml +[tool.commitizen] +ignored_tag_formats: + - prod + - other-component-${version} + - prefix-* +``` + +This will ignore: + +- The `prod` tag +- Any version tag prefixed with `other-component-` +- Any tag prefixed with `prefix-` + + +!!! tip + Note the `*` in the `prefix-*` pattern. This is a wildcard and only exists for `ignored_tag_formats`. + It will match any string from any length. This allows to exclude by prefix, whether it is followed by a version or not. + +!!! tip + If you don't want to be warned when Commitizen detect an unknown tag, you can by setting: + ``` + [tool.commitizen] + ignored_tag_formats = ["*"] + ``` + But be aware that you will not be warned if you have a typo in your tag formats. diff --git a/mkdocs.yml b/mkdocs.yml index f6a7eaa42..6a642161d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - Customization: "customization.md" - Tutorials: - Writing commits: "tutorials/writing_commits.md" + - Managing tags formats: "tutorials/tag_format.md" - Auto check commits: "tutorials/auto_check.md" - Auto prepare commit message: "tutorials/auto_prepare_commit_message.md" - GitLab CI: "tutorials/gitlab_ci.md" diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 52e2defde..b5ff7e6ed 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -7,6 +7,7 @@ from textwrap import dedent from unittest.mock import MagicMock, call +import py import pytest from pytest_mock import MockFixture @@ -1627,3 +1628,31 @@ def test_bump_allow_no_commit_with_manual_version( cli.main() out, _ = capsys.readouterr() assert "bump: version 1.0.0 → 2.0.0" in out + + +def test_bump_detect_legacy_tags_from_scm( + tmp_commitizen_project: py.path.local, mocker: MockFixture +): + project_root = Path(tmp_commitizen_project) + tmp_commitizen_cfg_file = project_root / "pyproject.toml" + tmp_commitizen_cfg_file.write_text( + "\n".join( + [ + "[tool.commitizen]", + 'version_provider = "scm"', + 'tag_format = "v$version"', + "legacy_tag_formats = [", + ' "legacy-${version}"', + "]", + ] + ), + ) + create_file_and_commit("feat: new file") + create_tag("legacy-0.4.2") + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--increment", "patch", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("v0.4.3") diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index a6ff7db2d..f794b8d9f 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -8,6 +8,7 @@ from dateutil import relativedelta from jinja2 import FileSystemLoader from pytest_mock import MockFixture +from pytest_regressions.file_regression import FileRegressionFixture from commitizen import __file__ as commitizen_init from commitizen import cli, git @@ -954,8 +955,17 @@ def test_changelog_from_rev_latest_version_from_arg( @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.freeze_time("2022-02-13") -def test_changelog_from_rev_single_version_not_found( - mocker: MockFixture, config_path, changelog_path +@pytest.mark.parametrize( + "rev_range,tag", + ( + pytest.param("0.8.0", "0.2.0", id="single-not-found"), + pytest.param("0.1.0..0.3.0", "0.3.0", id="lower-bound-not-found"), + pytest.param("0.1.0..0.3.0", "0.1.0", id="upper-bound-not-found"), + pytest.param("0.3.0..0.4.0", "0.2.0", id="none-found"), + ), +) +def test_changelog_from_rev_range_not_found( + mocker: MockFixture, config_path, rev_range: str, tag: str ): """Provides an invalid revision ID to changelog command""" with open(config_path, "a", encoding="utf-8") as f: @@ -963,26 +973,46 @@ def test_changelog_from_rev_single_version_not_found( # create commit and tag create_file_and_commit("feat: new file") - testargs = ["cz", "bump", "--yes"] + create_tag(tag) + create_file_and_commit("feat: new file") + create_tag("1.0.0") + + testargs = ["cz", "changelog", rev_range] # it shouldn't exist mocker.patch.object(sys, "argv", testargs) - cli.main() + with pytest.raises(NoCommitsFoundError) as excinfo: + cli.main() - wait_for_tag() + assert "Could not find a valid revision" in str(excinfo) - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() - wait_for_tag() +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_multiple_matching_tags( + mocker: MockFixture, config_path, changelog_path +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "new-$version"\nlegacy_tag_formats = ["legacy-$version"]') + + create_file_and_commit("feat: new file") + create_tag("legacy-1.0.0") + create_file_and_commit("feat: new file") + create_tag("legacy-2.0.0") + create_tag("new-2.0.0") - testargs = ["cz", "changelog", "0.8.0"] # it shouldn't exist + testargs = ["cz", "changelog", "1.0.0..2.0.0"] # it shouldn't exist mocker.patch.object(sys, "argv", testargs) - with pytest.raises(NoCommitsFoundError) as excinfo: + with pytest.warns() as warnings: cli.main() - assert "Could not find a valid revision" in str(excinfo) + assert len(warnings) == 1 + warning = warnings[0] + assert "Multiple tags found for version 2.0.0" in str(warning.message) + + with open(changelog_path) as f: + out = f.read() + + # Ensure only one tag is rendered + assert out.count("2.0.0") == 1 @pytest.mark.usefixtures("tmp_commitizen_project") @@ -1016,34 +1046,6 @@ def test_changelog_from_rev_range_default_tag_format( assert "new file" not in out -@pytest.mark.usefixtures("tmp_commitizen_project") -@pytest.mark.freeze_time("2022-02-13") -def test_changelog_from_rev_range_version_not_found(mocker: MockFixture, config_path): - """Provides an invalid end revision ID to changelog command""" - with open(config_path, "a", encoding="utf-8") as f: - f.write('tag_format = "$version"\n') - - # create commit and tag - create_file_and_commit("feat: new file") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() - - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") - - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() - - testargs = ["cz", "changelog", "0.5.0..0.8.0"] # it shouldn't exist - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(NoCommitsFoundError) as excinfo: - cli.main() - - assert "Could not find a valid revision" in str(excinfo) - - @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.freeze_time("2022-02-13") def test_changelog_from_rev_version_range_including_first_tag( @@ -1116,6 +1118,41 @@ def test_changelog_from_rev_version_range_from_arg( file_regression.check(out, extension=".md") +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_range_with_legacy_tags( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + changelog = Path(changelog_path) + Path(config_path).write_text( + "\n".join( + [ + "[tool.commitizen]", + 'version_provider = "scm"', + 'tag_format = "v$version"', + "legacy_tag_formats = [", + ' "legacy-${version}",', + ' "old-${version}",', + "]", + ] + ), + ) + + create_file_and_commit("feat: new file") + create_tag("old-0.2.0") + create_file_and_commit("feat: new file") + create_tag("legacy-0.3.0") + create_file_and_commit("feat: new file") + create_tag("legacy-0.4.0") + + testargs = ["cz", "changelog", "0.2.0..0.4.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + file_regression.check(changelog.read_text(), extension=".md") + + @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.freeze_time("2022-02-13") def test_changelog_from_rev_version_with_big_range_from_arg( @@ -1639,6 +1676,127 @@ def test_changelog_only_tag_matching_tag_format_included_suffix_sep( assert "## 0.2.0 (2021-06-11)" not in out +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_legacy_tags( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.writelines( + [ + 'tag_format = "v${version}"\n', + "legacy_tag_formats = [\n", + ' "older-${version}",\n', + ' "oldest-${version}",\n', + "]\n", + ] + ) + create_file_and_commit("feat: new file") + git.tag("oldest-0.1.0") + create_file_and_commit("feat: new file") + git.tag("older-0.2.0") + create_file_and_commit("feat: another new file") + git.tag("v0.3.0") + create_file_and_commit("feat: another new file") + git.tag("not-0.3.1") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out = open(changelog_path).read() + assert "## v0.3.0" in out + assert "## older-0.2.0" in out + assert "## oldest-0.1.0" in out + assert "## v0.3.0" in out + assert "## not-0.3.1" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2024-11-18") +def test_changelog_incremental_change_tag_format( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, +): + mocker.patch("commitizen.git.GitTag.date", "2024-11-18") + config = Path(config_path) + base_config = config.read_text() + config.write_text( + "\n".join( + ( + base_config, + 'tag_format = "older-${version}"', + ) + ) + ) + create_file_and_commit("feat: new file") + git.tag("older-0.1.0") + create_file_and_commit("feat: new file") + git.tag("older-0.2.0") + mocker.patch.object(sys, "argv", ["cz", "changelog"]) + cli.main() + + config.write_text( + "\n".join( + ( + base_config, + 'tag_format = "v${version}"', + 'legacy_tag_formats = ["older-${version}"]', + ) + ) + ) + create_file_and_commit("feat: another new file") + git.tag("v0.3.0") + mocker.patch.object(sys, "argv", ["cz", "changelog", "--incremental"]) + cli.main() + out = open(changelog_path).read() + assert "## v0.3.0" in out + assert "## older-0.2.0" in out + assert "## older-0.1.0" in out + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_ignored_tags( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, + capsys: pytest.CaptureFixture, +): + with open(config_path, "a", encoding="utf-8") as f: + f.writelines( + [ + 'tag_format = "v${version}"\n', + "ignored_tag_formats = [\n", + ' "ignored",\n', + ' "ignore-${version}",\n', + "]\n", + ] + ) + create_file_and_commit("feat: new file") + git.tag("ignore-0.1.0") + create_file_and_commit("feat: new file") + git.tag("ignored") + create_file_and_commit("feat: another new file") + git.tag("v0.3.0") + create_file_and_commit("feat: another new file") + git.tag("not-ignored") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out = open(changelog_path).read() + _, err = capsys.readouterr() + assert "## ignore-0.1.0" not in out + assert "InvalidVersion ignore-0.1.0" not in err + assert "## ignored" not in out + assert "InvalidVersion ignored" not in err + assert "## not-ignored" not in out + assert "InvalidVersion not-ignored" in err + assert "## v0.3.0" in out + assert "InvalidVersion v0.3.0" not in err + + def test_changelog_template_extra_quotes( mocker: MockFixture, tmp_commitizen_project: Path, diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md new file mode 100644 index 000000000..5d37333aa --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md @@ -0,0 +1,17 @@ +## legacy-0.4.0 (2022-02-13) + +### Feat + +- new file + +## legacy-0.3.0 (2022-02-13) + +### Feat + +- new file + +## old-0.2.0 (2022-02-13) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md b/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md new file mode 100644 index 000000000..2f0cc2909 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md @@ -0,0 +1,17 @@ +## v0.3.0 (2024-11-18) + +### Feat + +- another new file + +## older-0.2.0 (2024-11-18) + +### Feat + +- new file + +## older-0.1.0 (2024-11-18) + +### Feat + +- new file diff --git a/tests/providers/test_scm_provider.py b/tests/providers/test_scm_provider.py index 01e7ab994..9d955b232 100644 --- a/tests/providers/test_scm_provider.py +++ b/tests/providers/test_scm_provider.py @@ -113,3 +113,26 @@ def test_scm_provider_default_with_commits_and_tags(config: BaseConfig): merge_branch("master") assert provider.get_version() == "1.1.0rc0" + + +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider_detect_legacy_tags(config: BaseConfig): + config.settings["version_provider"] = "scm" + config.settings["tag_format"] = "v${version}" + config.settings["legacy_tag_formats"] = [ + "legacy-${version}", + "old-${version}", + ] + provider = get_provider(config) + + create_file_and_commit("test: fake commit") + create_tag("old-0.4.1") + assert provider.get_version() == "0.4.1" + + create_file_and_commit("test: fake commit") + create_tag("legacy-0.4.2") + assert provider.get_version() == "0.4.2" + + create_file_and_commit("test: fake commit") + create_tag("v0.5.0") + assert provider.get_version() == "0.5.0" diff --git a/tests/test_bump_normalize_tag.py b/tests/test_bump_normalize_tag.py index c1eb696af..895acbd71 100644 --- a/tests/test_bump_normalize_tag.py +++ b/tests/test_bump_normalize_tag.py @@ -1,6 +1,6 @@ import pytest -from commitizen import bump +from commitizen.tags import TagRules conversion = [ (("1.2.3", "v$version"), "v1.2.3"), @@ -18,5 +18,6 @@ @pytest.mark.parametrize("test_input,expected", conversion) def test_create_tag(test_input, expected): version, format = test_input - new_tag = bump.normalize_tag(version, format) + rules = TagRules() + new_tag = rules.normalize_tag(version, format) assert new_tag == expected diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 76ee80600..accbf5d33 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,4 +1,5 @@ import re +from dataclasses import dataclass from pathlib import Path from typing import Optional @@ -11,6 +12,7 @@ ConventionalCommitsCz, ) from commitizen.exceptions import InvalidConfigurationError +from commitizen.version_schemes import Pep440 COMMITS_DATA = [ { @@ -522,12 +524,14 @@ def test_get_commit_tag_is_None(gitcommits, tags): @pytest.mark.parametrize("test_input", TAGS) def test_valid_tag_included_in_changelog(test_input): tag = git.GitTag(*test_input) - assert changelog.tag_included_in_changelog(tag, [], False) + rules = changelog.TagRules() + assert rules.include_in_changelog(tag) def test_invalid_tag_included_in_changelog(): tag = git.GitTag("not_a_version", "rev", "date") - assert not changelog.tag_included_in_changelog(tag, [], False) + rules = changelog.TagRules() + assert not rules.include_in_changelog(tag) COMMITS_TREE = ( @@ -1080,8 +1084,11 @@ def test_invalid_tag_included_in_changelog(): def test_generate_tree_from_commits(gitcommits, tags, merge_prereleases): parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.bump_pattern + rules = changelog.TagRules( + merge_prereleases=merge_prereleases, + ) tree = changelog.generate_tree_from_commits( - gitcommits, tags, parser, changelog_pattern, merge_prerelease=merge_prereleases + gitcommits, tags, parser, changelog_pattern, rules=rules ) expected = ( COMMITS_TREE_AFTER_MERGED_PRERELEASES if merge_prereleases else COMMITS_TREE @@ -1451,3 +1458,105 @@ def test_get_smart_tag_range_returns_an_extra_for_a_single_tag(tags): start = tags[0] # len here is 1, but we expect one more tag as designed res = changelog.get_smart_tag_range(tags, start.name) assert 2 == len(res) + + +@dataclass +class TagDef: + name: str + is_version: bool + is_legacy: bool + is_ignored: bool + + +TAGS_PARAMS = ( + pytest.param(TagDef("1.2.3", True, False, False), id="version"), + # We test with `v-` prefix as `v` prefix is a special case kept for backward compatibility + pytest.param(TagDef("v-1.2.3", False, True, False), id="v-prefix"), + pytest.param(TagDef("project-1.2.3", False, True, False), id="project-prefix"), + pytest.param(TagDef("ignored", False, False, True), id="ignored"), + pytest.param(TagDef("unknown", False, False, False), id="unknown"), +) + + +@pytest.mark.parametrize("tag", TAGS_PARAMS) +def test_tag_rules_tag_format_only(tag: TagDef): + rules = changelog.TagRules(Pep440, "$version") + assert rules.is_version_tag(tag.name) is tag.is_version + + +@pytest.mark.parametrize("tag", TAGS_PARAMS) +def test_tag_rules_with_legacy_tags(tag: TagDef): + rules = changelog.TagRules( + scheme=Pep440, + tag_format="$version", + legacy_tag_formats=["v-$version", "project-${version}"], + ) + assert rules.is_version_tag(tag.name) is tag.is_version or tag.is_legacy + + +@pytest.mark.parametrize("tag", TAGS_PARAMS) +def test_tag_rules_with_ignored_tags(tag: TagDef): + rules = changelog.TagRules( + scheme=Pep440, tag_format="$version", ignored_tag_formats=["ignored"] + ) + assert rules.is_ignored_tag(tag.name) is tag.is_ignored + + +def test_tags_rules_get_version_tags(capsys: pytest.CaptureFixture): + tags = [ + git.GitTag("v1.1.0", "17efb44d2cd16f6621413691a543e467c7d2dda6", "2019-04-14"), + git.GitTag("v1.0.0", "aa44a92d68014d0da98965c0c2cb8c07957d4362", "2019-03-01"), + git.GitTag("1.0.0b2", "aab33d13110f26604fb786878856ec0b9e5fc32b", "2019-01-18"), + git.GitTag( + "project-not-a-version", + "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "2019-01-17", + ), + git.GitTag( + "not-a-version", "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "2018-12-17" + ), + git.GitTag( + "star-something", "c52eca6f74f844ab3ffbde61d98fe96071e132b2", "2018-11-12" + ), + git.GitTag("known", "b3f89892222340150e32631ae6b7aab65230036f", "2018-09-22"), + git.GitTag( + "ignored-0.9.3", "684e0259cc95c7c5e94854608cd3dcebbd53219e", "2018-09-22" + ), + git.GitTag( + "project-0.9.3", "dacc86159b260ee98eb5f57941c99ba731a01399", "2018-07-28" + ), + git.GitTag( + "anything-0.9", "5141f54503d2e1cf39bd666c0ca5ab5eb78772ab", "2018-01-10" + ), + git.GitTag( + "project-0.9.2", "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", "2017-11-11" + ), + git.GitTag( + "ignored-0.9.1", "46e9032e18a819e466618c7a014bcb0e9981af9e", "2017-11-11" + ), + ] + + rules = changelog.TagRules( + scheme=Pep440, + tag_format="v$version", + legacy_tag_formats=["$version", "project-${version}"], + ignored_tag_formats=[ + "known", + "ignored-${version}", + "star-*", + "*-${major}.${minor}", + ], + ) + + version_tags = rules.get_version_tags(tags, warn=True) + assert {t.name for t in version_tags} == { + "v1.1.0", + "v1.0.0", + "1.0.0b2", + "project-0.9.3", + "project-0.9.2", + } + + captured = capsys.readouterr() + assert captured.err.count("InvalidVersion") == 2 + assert captured.err.count("not-a-version") == 2 diff --git a/tests/test_changelog_format_asciidoc.py b/tests/test_changelog_format_asciidoc.py index 0c5930df4..59ca56191 100644 --- a/tests/test_changelog_format_asciidoc.py +++ b/tests/test_changelog_format_asciidoc.py @@ -55,6 +55,7 @@ """ EXPECTED_C = Metadata( latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_end=3, unreleased_start=1, @@ -105,20 +106,21 @@ def format(config: BaseConfig) -> AsciiDoc: @pytest.fixture def format_with_tags(config: BaseConfig, request) -> AsciiDoc: config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] return AsciiDoc(config) VERSIONS_EXAMPLES = [ - ("== [1.0.0] - 2017-06-20", "1.0.0"), + ("== [1.0.0] - 2017-06-20", ("1.0.0", "1.0.0")), ( "= https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3[10.0.0-next.3] (2020-04-22)", - "10.0.0-next.3", + ("10.0.0-next.3", "10.0.0-next.3"), ), - ("=== 0.19.1 (Jan 7, 2020)", "0.19.1"), - ("== 1.0.0", "1.0.0"), - ("== v1.0.0", "1.0.0"), - ("== v1.0.0 - (2012-24-32)", "1.0.0"), - ("= version 2020.03.24", "2020.03.24"), + ("=== 0.19.1 (Jan 7, 2020)", ("0.19.1", "0.19.1")), + ("== 1.0.0", ("1.0.0", "1.0.0")), + ("== v1.0.0", ("1.0.0", "v1.0.0")), + ("== v1.0.0 - (2012-24-32)", ("1.0.0", "v1.0.0")), + ("= version 2020.03.24", ("2020.03.24", "2020.03.24")), ("== [Unreleased]", None), ("All notable changes to this project will be documented in this file.", None), ("= Changelog", None), @@ -128,7 +130,7 @@ def format_with_tags(config: BaseConfig, request) -> AsciiDoc: @pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) def test_changelog_detect_version( - line_from_changelog: str, output_version: str, format: AsciiDoc + line_from_changelog: str, output_version: tuple[str, str] | None, format: AsciiDoc ): version = format.parse_version_from_title(line_from_changelog) assert version == output_version @@ -186,6 +188,7 @@ def test_get_matadata( "1-0-0-a1.dev1-example", "1.0.0-a1.dev1", ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), ), indirect=["format_with_tags"], ) diff --git a/tests/test_changelog_format_markdown.py b/tests/test_changelog_format_markdown.py index 52612b8e2..e1f0d6731 100644 --- a/tests/test_changelog_format_markdown.py +++ b/tests/test_changelog_format_markdown.py @@ -55,6 +55,7 @@ """ EXPECTED_C = Metadata( latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_end=3, unreleased_start=1, @@ -105,20 +106,21 @@ def format(config: BaseConfig) -> Markdown: @pytest.fixture def format_with_tags(config: BaseConfig, request) -> Markdown: config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] return Markdown(config) VERSIONS_EXAMPLES = [ - ("## [1.0.0] - 2017-06-20", "1.0.0"), + ("## [1.0.0] - 2017-06-20", ("1.0.0", "1.0.0")), ( "# [10.0.0-next.3](https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3) (2020-04-22)", - "10.0.0-next.3", + ("10.0.0-next.3", "10.0.0-next.3"), ), - ("### 0.19.1 (Jan 7, 2020)", "0.19.1"), - ("## 1.0.0", "1.0.0"), - ("## v1.0.0", "1.0.0"), - ("## v1.0.0 - (2012-24-32)", "1.0.0"), - ("# version 2020.03.24", "2020.03.24"), + ("### 0.19.1 (Jan 7, 2020)", ("0.19.1", "0.19.1")), + ("## 1.0.0", ("1.0.0", "1.0.0")), + ("## v1.0.0", ("1.0.0", "v1.0.0")), + ("## v1.0.0 - (2012-24-32)", ("1.0.0", "v1.0.0")), + ("# version 2020.03.24", ("2020.03.24", "2020.03.24")), ("## [Unreleased]", None), ("All notable changes to this project will be documented in this file.", None), ("# Changelog", None), @@ -128,7 +130,7 @@ def format_with_tags(config: BaseConfig, request) -> Markdown: @pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) def test_changelog_detect_version( - line_from_changelog: str, output_version: str, format: Markdown + line_from_changelog: str, output_version: tuple[str, str] | None, format: Markdown ): version = format.parse_version_from_title(line_from_changelog) assert version == output_version @@ -191,6 +193,7 @@ def test_get_matadata( "1-0-0-a1.dev1-example", "1.0.0-a1.dev1", ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), ), indirect=["format_with_tags"], ) diff --git a/tests/test_changelog_format_restructuredtext.py b/tests/test_changelog_format_restructuredtext.py index 11356ae28..74b6b736f 100644 --- a/tests/test_changelog_format_restructuredtext.py +++ b/tests/test_changelog_format_restructuredtext.py @@ -22,6 +22,7 @@ def case( content: str, latest_version: str | None = None, latest_version_position: int | None = None, + latest_version_tag: str | None = None, unreleased_start: int | None = None, unreleased_end: int | None = None, ): @@ -30,6 +31,7 @@ def case( dedent(content).strip(), Metadata( latest_version=latest_version, + latest_version_tag=latest_version_tag, latest_version_position=latest_version_position, unreleased_start=unreleased_start, unreleased_end=unreleased_end, @@ -93,6 +95,7 @@ def case( ====== """, latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_start=0, unreleased_end=3, @@ -303,6 +306,7 @@ def format(config: BaseConfig) -> RestructuredText: @pytest.fixture def format_with_tags(config: BaseConfig, request) -> RestructuredText: config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] return RestructuredText(config) @@ -357,6 +361,7 @@ def test_is_overlined_title(format: RestructuredText, text: str, expected: bool) "1-0-0-a1.dev1-example", "1.0.0-a1.dev1", ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), ), indirect=["format_with_tags"], ) diff --git a/tests/test_changelog_format_textile.py b/tests/test_changelog_format_textile.py index 3fac5c175..eb03484ad 100644 --- a/tests/test_changelog_format_textile.py +++ b/tests/test_changelog_format_textile.py @@ -55,6 +55,7 @@ """ EXPECTED_C = Metadata( latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_end=3, unreleased_start=1, @@ -98,20 +99,21 @@ def format(config: BaseConfig) -> Textile: @pytest.fixture def format_with_tags(config: BaseConfig, request) -> Textile: config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] return Textile(config) VERSIONS_EXAMPLES = [ - ("h2. [1.0.0] - 2017-06-20", "1.0.0"), + ("h2. [1.0.0] - 2017-06-20", ("1.0.0", "1.0.0")), ( 'h1. "10.0.0-next.3":https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3 (2020-04-22)', - "10.0.0-next.3", + ("10.0.0-next.3", "10.0.0-next.3"), ), - ("h3. 0.19.1 (Jan 7, 2020)", "0.19.1"), - ("h2. 1.0.0", "1.0.0"), - ("h2. v1.0.0", "1.0.0"), - ("h2. v1.0.0 - (2012-24-32)", "1.0.0"), - ("h1. version 2020.03.24", "2020.03.24"), + ("h3. 0.19.1 (Jan 7, 2020)", ("0.19.1", "0.19.1")), + ("h2. 1.0.0", ("1.0.0", "1.0.0")), + ("h2. v1.0.0", ("1.0.0", "v1.0.0")), + ("h2. v1.0.0 - (2012-24-32)", ("1.0.0", "v1.0.0")), + ("h1. version 2020.03.24", ("2020.03.24", "2020.03.24")), ("h2. [Unreleased]", None), ("All notable changes to this project will be documented in this file.", None), ("h1. Changelog", None), @@ -121,7 +123,7 @@ def format_with_tags(config: BaseConfig, request) -> Textile: @pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) def test_changelog_detect_version( - line_from_changelog: str, output_version: str, format: Textile + line_from_changelog: str, output_version: tuple[str, str] | None, format: Textile ): version = format.parse_version_from_title(line_from_changelog) assert version == output_version @@ -179,6 +181,7 @@ def test_get_matadata( "1-0-0-a1.dev1-example", "1.0.0-a1.dev1", ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), ), indirect=["format_with_tags"], ) diff --git a/tests/test_conf.py b/tests/test_conf.py index 3e0a44c7d..80d58983e 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -72,6 +72,8 @@ "version_provider": "commitizen", "version_scheme": None, "tag_format": "$version", + "legacy_tag_formats": [], + "ignored_tag_formats": [], "bump_message": None, "retry_after_failure": False, "allow_abort": False, @@ -101,6 +103,8 @@ "version_provider": "commitizen", "version_scheme": None, "tag_format": "$version", + "legacy_tag_formats": [], + "ignored_tag_formats": [], "bump_message": None, "retry_after_failure": False, "allow_abort": False, diff --git a/tests/test_version_schemes.py b/tests/test_version_schemes.py index 686c0bfde..8e2dae902 100644 --- a/tests/test_version_schemes.py +++ b/tests/test_version_schemes.py @@ -16,31 +16,31 @@ def test_default_version_scheme_is_pep440(config: BaseConfig): - scheme = get_version_scheme(config) + scheme = get_version_scheme(config.settings) assert scheme is Pep440 def test_version_scheme_from_config(config: BaseConfig): config.settings["version_scheme"] = "semver" - scheme = get_version_scheme(config) + scheme = get_version_scheme(config.settings) assert scheme is SemVer def test_version_scheme_from_name(config: BaseConfig): config.settings["version_scheme"] = "pep440" - scheme = get_version_scheme(config, "semver") + scheme = get_version_scheme(config.settings, "semver") assert scheme is SemVer def test_raise_for_unknown_version_scheme(config: BaseConfig): with pytest.raises(VersionSchemeUnknown): - get_version_scheme(config, "unknown") + get_version_scheme(config.settings, "unknown") def test_version_scheme_from_deprecated_config(config: BaseConfig): config.settings["version_type"] = "semver" with pytest.warns(DeprecationWarning): - scheme = get_version_scheme(config) + scheme = get_version_scheme(config.settings) assert scheme is SemVer @@ -48,7 +48,7 @@ def test_version_scheme_from_config_priority(config: BaseConfig): config.settings["version_scheme"] = "pep440" config.settings["version_type"] = "semver" with pytest.warns(DeprecationWarning): - scheme = get_version_scheme(config) + scheme = get_version_scheme(config.settings) assert scheme is Pep440 @@ -63,4 +63,4 @@ class NotVersionProtocol: mocker.patch.object(metadata, "entry_points", return_value=(ep,)) with pytest.warns(match="VersionProtocol"): - get_version_scheme(config, "any") + get_version_scheme(config.settings, "any") From 9aae58e56cf24b6f5e43250d818edd564de072dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 15:26:50 +0000 Subject: [PATCH 03/20] =?UTF-8?q?bump:=20version=204.3.0=20=E2=86=92=204.4?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 10 ++++++++++ commitizen/__version__.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd16cc9cc..c8861315d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - tomli - repo: https://github.com/commitizen-tools/commitizen - rev: v4.3.0 # automatically updated by Commitizen + rev: v4.4.0 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7409d22..30d2b7659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v4.4.0 (2025-03-02) + +### Feat + +- **tags**: adds `legacy_tag_formats` and `ignored_tag_formats` settings + +### Refactor + +- **get_tag_regexes**: dedup tag regexes definition + ## v4.3.0 (2025-02-28) ### Feat diff --git a/commitizen/__version__.py b/commitizen/__version__.py index 111dc9172..ecdb1cef9 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "4.3.0" +__version__ = "4.4.0" diff --git a/pyproject.toml b/pyproject.toml index 0c03a6d50..570eabd3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "commitizen" -version = "4.3.0" +version = "4.4.0" description = "Python commitizen client tool" authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] maintainers = [ @@ -87,7 +87,7 @@ build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "4.3.0" +version = "4.4.0" tag_format = "v$version" version_files = [ "pyproject.toml:version", From bc8479e7aa1a5b9d2f491b79e3a4d4015519903c Mon Sep 17 00:00:00 2001 From: Wei Lee <weilee.rx@gmail.com> Date: Sun, 2 Mar 2025 23:27:56 +0800 Subject: [PATCH 04/20] docs(config): add uv provider description (#1362) --- docs/config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.md b/docs/config.md index 5ec189487..3d5324958 100644 --- a/docs/config.md +++ b/docs/config.md @@ -352,6 +352,7 @@ Commitizen provides some version providers for some well known formats: | `scm` | Fetch the version from git and does not need to set it back | | `pep621` | Get and set version from `pyproject.toml` `project.version` field | | `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field | +| `uv` | Get and set version from `pyproject.toml` `project.version` field and `uv.lock` `pacakge.version` field whose `package.name` field is the same as `pyproject.toml` `project.name` field | | `cargo` | Get and set version from `Cargo.toml` `project.version` field | | `npm` | Get and set version from `package.json` `version` field, `package-lock.json` `version,packages.''.version` fields if the file exists, and `npm-shrinkwrap.json` `version,packages.''.version` fields if the file exists | | `composer` | Get and set version from `composer.json` `project.version` field | From 79dd19dc132de2fae6d3f07e4f17383bac6b99e8 Mon Sep 17 00:00:00 2001 From: Jakob Keller <57402305+jakob-keller@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:28:31 +0100 Subject: [PATCH 05/20] fix(tags): fixes ImportError on Python >=3.11 (#1363) (#1364) --- commitizen/tags.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/commitizen/tags.py b/commitizen/tags.py index 962e428ef..915dd3120 100644 --- a/commitizen/tags.py +++ b/commitizen/tags.py @@ -8,8 +8,6 @@ from string import Template from typing import TYPE_CHECKING, NamedTuple -from typing_extensions import Self - from commitizen import out from commitizen.defaults import DEFAULT_SETTINGS, Settings, get_tag_regexes from commitizen.git import GitTag @@ -22,8 +20,16 @@ ) if TYPE_CHECKING: + import sys + 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 + else: + from typing import Self + class VersionTag(NamedTuple): """Represent a version and its matching tag form.""" From b494c556437473519f8ab69020c7256ba84714c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:28:55 +0000 Subject: [PATCH 06/20] =?UTF-8?q?bump:=20version=204.4.0=20=E2=86=92=204.4?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 6 ++++++ commitizen/__version__.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8861315d..86997bc3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - tomli - repo: https://github.com/commitizen-tools/commitizen - rev: v4.4.0 # automatically updated by Commitizen + rev: v4.4.1 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d2b7659..8fc88ad69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v4.4.1 (2025-03-02) + +### Fix + +- **tags**: fixes ImportError on Python >=3.11 (#1363) (#1364) + ## v4.4.0 (2025-03-02) ### Feat diff --git a/commitizen/__version__.py b/commitizen/__version__.py index ecdb1cef9..9905939ff 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "4.4.0" +__version__ = "4.4.1" diff --git a/pyproject.toml b/pyproject.toml index 570eabd3a..314ddd925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "commitizen" -version = "4.4.0" +version = "4.4.1" description = "Python commitizen client tool" authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] maintainers = [ @@ -87,7 +87,7 @@ build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "4.4.0" +version = "4.4.1" tag_format = "v$version" version_files = [ "pyproject.toml:version", From a202e661bacf9643373255965f34bbdb382cb299 Mon Sep 17 00:00:00 2001 From: "Axel H." <noirbizarre@gmail.com> Date: Sun, 2 Mar 2025 17:35:30 +0100 Subject: [PATCH 07/20] ci(pre-commit): update `pre-commit` hooks to latest version and fix detected errors --- .pre-commit-config.yaml | 6 +++--- commitizen/tags.py | 20 +++++++++++--------- docs/config.md | 19 +++++++++---------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86997bc3a..bb1beaefc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-vcs-permalinks - id: end-of-file-fixer @@ -30,13 +30,13 @@ repos: - id: detect-private-key - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 + rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: [ black~=23.11 ] - repo: https://github.com/codespell-project/codespell - rev: v2.2.4 + rev: v2.4.1 hooks: - id: codespell name: Run codespell to check for common misspellings in files diff --git a/commitizen/tags.py b/commitizen/tags.py index 915dd3120..aa11cb7a9 100644 --- a/commitizen/tags.py +++ b/commitizen/tags.py @@ -55,28 +55,30 @@ class TagRules: Example: - ```py + ```python settings = DEFAULT_SETTINGS.clone() - settings.update({ - "tag_format": "v{version}" - "legacy_tag_formats": ["version{version}", "ver{version}"], - "ignored_tag_formats": ["ignored{version}"], - }) + settings.update( + { + "tag_format": "v{version}", + "legacy_tag_formats": ["version{version}", "ver{version}"], + "ignored_tag_formats": ["ignored{version}"], + } + ) rules = TagRules.from_settings(settings) assert rules.is_version_tag("v1.0.0") assert rules.is_version_tag("version1.0.0") assert rules.is_version_tag("ver1.0.0") - assert not rules.is_version_tag("ignored1.0.0", warn=True) # Does not warn - assert not rules.is_version_tag("warn1.0.0", warn=True) # Does warn + assert not rules.is_version_tag("ignored1.0.0", warn=True) # Does not warn + 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") try: assert rules.extract_version("not-a-v1.0.0") except InvalidVersion: - print "Does not match a tag format" + print("Does not match a tag format") ``` """ diff --git a/docs/config.md b/docs/config.md index 3d5324958..d1ae90b29 100644 --- a/docs/config.md +++ b/docs/config.md @@ -352,7 +352,7 @@ Commitizen provides some version providers for some well known formats: | `scm` | Fetch the version from git and does not need to set it back | | `pep621` | Get and set version from `pyproject.toml` `project.version` field | | `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field | -| `uv` | Get and set version from `pyproject.toml` `project.version` field and `uv.lock` `pacakge.version` field whose `package.name` field is the same as `pyproject.toml` `project.name` field | +| `uv` | Get and set version from `pyproject.toml` `project.version` field and `uv.lock` `package.version` field whose `package.name` field is the same as `pyproject.toml` `project.name` field | | `cargo` | Get and set version from `Cargo.toml` `project.version` field | | `npm` | Get and set version from `package.json` `version` field, `package-lock.json` `version,packages.''.version` fields if the file exists, and `npm-shrinkwrap.json` `version,packages.''.version` fields if the file exists | | `composer` | Get and set version from `composer.json` `project.version` field | @@ -386,22 +386,21 @@ class MyProvider(VersionProvider): def set_version(self, version: str): self.file.write_text(version) - ``` ```python title="setup.py" from setuptools import setup setup( - name='my-commitizen-provider', - version='0.1.0', - py_modules=['my_provider'], - install_requires=['commitizen'], - entry_points = { - 'commitizen.provider': [ - 'my-provider = my_provider:MyProvider', + name="my-commitizen-provider", + version="0.1.0", + py_modules=["my_provider"], + install_requires=["commitizen"], + entry_points={ + "commitizen.provider": [ + "my-provider = my_provider:MyProvider", ] - } + }, ) ``` From 58eecdccbaf02879dc5672f431485b582c66ffad Mon Sep 17 00:00:00 2001 From: Capi Etheriel <barraponto@gmail.com> Date: Sun, 16 Mar 2025 19:18:22 -0300 Subject: [PATCH 08/20] update precommit example --- docs/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 378b81919..3c6257c36 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -94,7 +94,7 @@ repos: hooks: - id: commitizen - id: commitizen-branch - stages: [push] + stages: [pre-push] ``` After the configuration is added, you'll need to run: From 13bc11a9c560bad69c24a08b153b7ad0ca769fff Mon Sep 17 00:00:00 2001 From: Wei Lee <weilee.rx@gmail.com> Date: Thu, 20 Mar 2025 22:11:40 +0800 Subject: [PATCH 09/20] build(pyproject.toml): fix license metadata --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 314ddd925..cedccabb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ maintainers = [ { name = "Wei Lee", email = "weilee.rx@gmail.com" }, { name = "Axel H.", email = "noirbizarre@gmail.com" }, ] -license = { text = "LICENSE" } +license = { file = "LICENSE" } readme = "docs/README.md" requires-python = ">=3.9,<4.0" dependencies = [ @@ -42,6 +42,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", + "License :: OSI Approved :: MIT License", ] [project.urls] From 17021fa05ef37e754fd37053c822071c3368b3ca Mon Sep 17 00:00:00 2001 From: Wei Lee <weilee.rx@gmail.com> Date: Sun, 30 Mar 2025 17:50:23 +0800 Subject: [PATCH 10/20] fix(commands/init): add missing uv provider to "cz init" --- commitizen/commands/init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index e39dfbe29..14ec8067e 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -228,6 +228,7 @@ def _ask_version_provider(self) -> str: "npm": "npm: Get and set version from package.json:project.version field", "pep621": "pep621: Get and set version from pyproject.toml:project.version field", "poetry": "poetry: Get and set version from pyproject.toml:tool.poetry.version field", + "uv": "uv: Get and Get and set version from pyproject.toml and uv.lock", "scm": "scm: Fetch the version from git and does not need to set it back", } From 904173e3193714d2db6a67615bd8ee1132065526 Mon Sep 17 00:00:00 2001 From: Wei Lee <weilee.rx@gmail.com> Date: Fri, 4 Apr 2025 23:48:55 +0800 Subject: [PATCH 11/20] feat(init): set uv to default value if both pyproject.toml and uv.lock present --- commitizen/commands/init.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index 14ec8067e..df872ec7e 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -24,6 +24,10 @@ class ProjectInfo: def has_pyproject(self) -> bool: return os.path.isfile("pyproject.toml") + @property + def has_uv_lock(self) -> bool: + return os.path.isfile("uv.lock") + @property def has_setup(self) -> bool: return os.path.isfile("setup.py") @@ -32,6 +36,10 @@ def has_setup(self) -> bool: def has_pre_commit_config(self) -> bool: return os.path.isfile(".pre-commit-config.yaml") + @property + def is_python_uv(self) -> bool: + return self.has_pyproject and self.has_uv_lock + @property def is_python_poetry(self) -> bool: if not self.has_pyproject: @@ -236,6 +244,8 @@ def _ask_version_provider(self) -> str: if self.project_info.is_python: if self.project_info.is_python_poetry: default_val = "poetry" + elif self.project_info.is_python_uv: + default_val = "uv" else: default_val = "pep621" elif self.project_info.is_rust_cargo: From e0b1c7743d5ea0bcba82e4ff515ce2caf5e87865 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 22:07:31 +0000 Subject: [PATCH 12/20] =?UTF-8?q?bump:=20version=204.4.1=20=E2=86=92=204.5?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 10 ++++++++++ commitizen/__version__.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb1beaefc..6c1cbd767 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - tomli - repo: https://github.com/commitizen-tools/commitizen - rev: v4.4.1 # automatically updated by Commitizen + rev: v4.5.0 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc88ad69..6250af20d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v4.5.0 (2025-04-04) + +### Feat + +- **init**: set uv to default value if both pyproject.toml and uv.lock present + +### Fix + +- **commands/init**: add missing uv provider to "cz init" + ## v4.4.1 (2025-03-02) ### Fix diff --git a/commitizen/__version__.py b/commitizen/__version__.py index 9905939ff..9faa2c2dd 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "4.4.1" +__version__ = "4.5.0" diff --git a/pyproject.toml b/pyproject.toml index cedccabb0..009bd13ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "commitizen" -version = "4.4.1" +version = "4.5.0" description = "Python commitizen client tool" authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] maintainers = [ @@ -88,7 +88,7 @@ build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "4.4.1" +version = "4.5.0" tag_format = "v$version" version_files = [ "pyproject.toml:version", From d666aa0bae8aec53b23b0495f8b007eeac4fdba8 Mon Sep 17 00:00:00 2001 From: Carlos Sanchez <carlos@apache.org> Date: Fri, 21 Mar 2025 17:32:39 +0100 Subject: [PATCH 13/20] fix: print which tag is invalid and the regex used for validation --- commitizen/tags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commitizen/tags.py b/commitizen/tags.py index aa11cb7a9..5724bb257 100644 --- a/commitizen/tags.py +++ b/commitizen/tags.py @@ -146,7 +146,9 @@ def extract_version(self, tag: GitTag) -> Version: m for regex in self.version_regexes if (m := regex.fullmatch(tag.name)) ) if not (m := next(candidates, None)): - raise InvalidVersion() + raise InvalidVersion( + f"Invalid version tag: '{tag.name}' does not match any configured tag format" + ) if "version" in m.groupdict(): return self.scheme(m.group("version")) From 1451bc53f274b3794776e345212b518736272eca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:24:52 +0000 Subject: [PATCH 14/20] =?UTF-8?q?bump:=20version=204.5.0=20=E2=86=92=204.5?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 6 ++++++ commitizen/__version__.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c1cbd767..19b4394cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - tomli - repo: https://github.com/commitizen-tools/commitizen - rev: v4.5.0 # automatically updated by Commitizen + rev: v4.5.1 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 6250af20d..39eee25df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v4.5.1 (2025-04-09) + +### Fix + +- print which tag is invalid + ## v4.5.0 (2025-04-04) ### Feat diff --git a/commitizen/__version__.py b/commitizen/__version__.py index 9faa2c2dd..f9f7166e6 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "4.5.0" +__version__ = "4.5.1" diff --git a/pyproject.toml b/pyproject.toml index 009bd13ca..c9ea806cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "commitizen" -version = "4.5.0" +version = "4.5.1" description = "Python commitizen client tool" authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] maintainers = [ @@ -88,7 +88,7 @@ build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "4.5.0" +version = "4.5.1" tag_format = "v$version" version_files = [ "pyproject.toml:version", From d7b1a89061f44bda94ded6f0d54f193373d5b6d6 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Ruiz <alex@flawedcode.org> Date: Thu, 20 Mar 2025 14:25:47 +0100 Subject: [PATCH 15/20] feat(git): add parents' digests in commit information --- commitizen/git.py | 16 +++++++++++--- tests/test_git.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/commitizen/git.py b/commitizen/git.py index 7de8e1f1c..19ca46b6c 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -44,13 +44,20 @@ def __eq__(self, other) -> bool: class GitCommit(GitObject): def __init__( - self, rev, title, body: str = "", author: str = "", author_email: str = "" + self, + rev, + title, + body: str = "", + author: str = "", + author_email: str = "", + parents: list[str] | None = None, ): self.rev = rev.strip() self.title = title.strip() self.body = body.strip() self.author = author.strip() self.author_email = author_email.strip() + self.parents = parents or [] @property def message(self): @@ -137,7 +144,9 @@ def get_commits( for rev_and_commit in git_log_entries: if not rev_and_commit: continue - rev, title, author, author_email, *body_list = rev_and_commit.split("\n") + rev, parents, title, author, author_email, *body_list = rev_and_commit.split( + "\n" + ) if rev_and_commit: git_commit = GitCommit( rev=rev.strip(), @@ -145,6 +154,7 @@ def get_commits( body="\n".join(body_list).strip(), author=author, author_email=author_email, + parents=[p for p in parents.strip().split(" ") if p], ) git_commits.append(git_commit) return git_commits @@ -286,7 +296,7 @@ def smart_open(*args, **kargs): def _get_log_as_str_list(start: str | None, end: str, args: str) -> list[str]: """Get string representation of each log entry""" delimiter = "----------commit-delimiter----------" - log_format: str = "%H%n%s%n%an%n%ae%n%b" + log_format: str = "%H%n%P%n%s%n%an%n%ae%n%b" git_log_cmd = ( f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args}" ) diff --git a/tests/test_git.py b/tests/test_git.py index f929ba6a4..8b2fc2b86 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -132,11 +132,13 @@ def test_get_commits_author_and_email(): def test_get_commits_without_email(mocker: MockFixture): raw_commit = ( "a515bb8f71c403f6f7d1c17b9d8ebf2ce3959395\n" + "95bbfc703eb99cb49ba0d6ffd8469911303dbe63 12d3b4bdaa996ea7067a07660bb5df4772297bdd\n" "\n" "user name\n" "\n" "----------commit-delimiter----------\n" "12d3b4bdaa996ea7067a07660bb5df4772297bdd\n" + "de33bc5070de19600f2f00262b3c15efea762408\n" "feat(users): add username\n" "user name\n" "\n" @@ -159,16 +161,19 @@ def test_get_commits_without_email(mocker: MockFixture): def test_get_commits_without_breakline_in_each_commit(mocker: MockFixture): raw_commit = ( "ae9ba6fc5526cf478f52ef901418d85505109744\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" "bump: version 2.13.0 → 2.14.0\n" "GitHub Action\n" "action@github.com\n" "----------commit-delimiter----------\n" "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "b4dc83284dc8c9729032a774a037df1d1f2397d5 20a54bf1b82cd7b573351db4d1e8814dd0be205d\n" "Merge pull request #332 from cliles/feature/271-redux\n" "User\n" "user@email.com\n" "Feature/271 redux----------commit-delimiter----------\n" "20a54bf1b82cd7b573351db4d1e8814dd0be205d\n" + "658f38c3fe832cdab63ed4fb1f7b3a0969a583be\n" "feat(#271): enable creation of annotated tags when bumping\n" "User 2\n" "user@email.edu\n" @@ -193,6 +198,55 @@ def test_get_commits_without_breakline_in_each_commit(mocker: MockFixture): ) +def test_get_commits_with_and_without_parents(mocker: MockFixture): + raw_commit = ( + "4206e661bacf9643373255965f34bbdb382cb2b9\n" + "ae9ba6fc5526cf478f52ef901418d85505109744 bf8479e7aa1a5b9d2f491b79e3a4d4015519903e\n" + "Merge pull request from someone\n" + "Maintainer\n" + "maintainer@email.com\n" + "This is a much needed feature----------commit-delimiter----------\n" + "ae9ba6fc5526cf478f52ef901418d85505109744\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "Release 0.1.0\n" + "GitHub Action\n" + "action@github.com\n" + "----------commit-delimiter----------\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "\n" + "Initial commit\n" + "User\n" + "user@email.com\n" + "----------commit-delimiter----------\n" + ) + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=raw_commit)) + + commits = git.get_commits() + + assert commits[0].author == "Maintainer" + assert commits[1].author == "GitHub Action" + assert commits[2].author == "User" + + assert commits[0].author_email == "maintainer@email.com" + assert commits[1].author_email == "action@github.com" + assert commits[2].author_email == "user@email.com" + + assert commits[0].title == "Merge pull request from someone" + assert commits[1].title == "Release 0.1.0" + assert commits[2].title == "Initial commit" + + assert commits[0].body == "This is a much needed feature" + assert commits[1].body == "" + assert commits[2].body == "" + + assert commits[0].parents == [ + "ae9ba6fc5526cf478f52ef901418d85505109744", + "bf8479e7aa1a5b9d2f491b79e3a4d4015519903e", + ] + assert commits[1].parents == ["ff2f56ca844de72a9d59590831087bf5a97bac84"] + assert commits[2].parents == [] + + def test_get_commits_with_signature(): config_file = ".git/config" config_backup = ".git/config.bak" From 57ea95f652439e78804e16dca822f36f9f5debcc Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Ruiz <alex@flawedcode.org> Date: Thu, 20 Mar 2025 14:25:54 +0100 Subject: [PATCH 16/20] feat(changelog): expose commit parents' digests when processing commits COMMIT_DATA in test_changelog.py had to be type annotated to avoid an issue with mypy linting when creating a GitCommit where the tool would expect the wrong type for the arguments. --- commitizen/changelog.py | 1 + tests/test_changelog.py | 75 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 95bf21d6f..704efe607 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -172,6 +172,7 @@ def process_commit_message( ): message: dict = { "sha1": commit.rev, + "parents": commit.parents, "author": commit.author, "author_email": commit.author_email, **parsed.groupdict(), diff --git a/tests/test_changelog.py b/tests/test_changelog.py index accbf5d33..df42b8226 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,7 +1,7 @@ import re from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import Any, Optional import pytest from jinja2 import FileSystemLoader @@ -14,9 +14,10 @@ from commitizen.exceptions import InvalidConfigurationError from commitizen.version_schemes import Pep440 -COMMITS_DATA = [ +COMMITS_DATA: list[dict[str, Any]] = [ { "rev": "141ee441c9c9da0809c554103a558eb17c30ed17", + "parents": ["6c4948501031b7d6405b54b21d3d635827f9421b"], "title": "bump: version 1.1.1 → 1.2.0", "body": "", "author": "Commitizen", @@ -24,6 +25,7 @@ }, { "rev": "6c4948501031b7d6405b54b21d3d635827f9421b", + "parents": ["ddd220ad515502200fe2dde443614c1075d26238"], "title": "docs: how to create custom bumps", "body": "", "author": "Commitizen", @@ -31,6 +33,7 @@ }, { "rev": "ddd220ad515502200fe2dde443614c1075d26238", + "parents": ["ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc"], "title": "feat: custom cz plugins now support bumping version", "body": "", "author": "Commitizen", @@ -38,6 +41,7 @@ }, { "rev": "ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc", + "parents": ["56c8a8da84e42b526bcbe130bd194306f7c7e813"], "title": "docs: added bump gif", "body": "", "author": "Commitizen", @@ -45,6 +49,7 @@ }, { "rev": "56c8a8da84e42b526bcbe130bd194306f7c7e813", + "parents": ["74c6134b1b2e6bb8b07ed53410faabe99b204f36"], "title": "bump: version 1.1.0 → 1.1.1", "body": "", "author": "Commitizen", @@ -52,6 +57,7 @@ }, { "rev": "74c6134b1b2e6bb8b07ed53410faabe99b204f36", + "parents": ["cbc7b5f22c4e74deff4bc92d14e19bd93524711e"], "title": "refactor: changed stdout statements", "body": "", "author": "Commitizen", @@ -59,6 +65,7 @@ }, { "rev": "cbc7b5f22c4e74deff4bc92d14e19bd93524711e", + "parents": ["1ba46f2a63cb9d6e7472eaece21528c8cd28b118"], "title": "fix(bump): commit message now fits better with semver", "body": "", "author": "Commitizen", @@ -66,6 +73,7 @@ }, { "rev": "1ba46f2a63cb9d6e7472eaece21528c8cd28b118", + "parents": ["c35dbffd1bb98bb0b3d1593797e79d1c3366af8f"], "title": "fix: conventional commit 'breaking change' in body instead of title", "body": "closes #16", "author": "Commitizen", @@ -73,6 +81,7 @@ }, { "rev": "c35dbffd1bb98bb0b3d1593797e79d1c3366af8f", + "parents": ["25313397a4ac3dc5b5c986017bee2a614399509d"], "title": "refactor(schema): command logic removed from commitizen base", "body": "", "author": "Commitizen", @@ -80,6 +89,7 @@ }, { "rev": "25313397a4ac3dc5b5c986017bee2a614399509d", + "parents": ["d2f13ac41b4e48995b3b619d931c82451886e6ff"], "title": "refactor(info): command logic removed from commitizen base", "body": "", "author": "Commitizen", @@ -87,6 +97,7 @@ }, { "rev": "d2f13ac41b4e48995b3b619d931c82451886e6ff", + "parents": ["d839e317e5b26671b010584ad8cc6bf362400fa1"], "title": "refactor(example): command logic removed from commitizen base", "body": "", "author": "Commitizen", @@ -94,6 +105,7 @@ }, { "rev": "d839e317e5b26671b010584ad8cc6bf362400fa1", + "parents": ["12d0e65beda969f7983c444ceedc2a01584f4e08"], "title": "refactor(commit): moved most of the commit logic to the commit command", "body": "", "author": "Commitizen", @@ -101,6 +113,7 @@ }, { "rev": "12d0e65beda969f7983c444ceedc2a01584f4e08", + "parents": ["fb4c85abe51c228e50773e424cbd885a8b6c610d"], "title": "docs(README): updated documentation url)", "body": "", "author": "Commitizen", @@ -108,6 +121,7 @@ }, { "rev": "fb4c85abe51c228e50773e424cbd885a8b6c610d", + "parents": ["17efb44d2cd16f6621413691a543e467c7d2dda6"], "title": "docs: mkdocs documentation", "body": "", "author": "Commitizen", @@ -115,6 +129,7 @@ }, { "rev": "17efb44d2cd16f6621413691a543e467c7d2dda6", + "parents": ["6012d9eecfce8163d75c8fff179788e9ad5347da"], "title": "Bump version 1.0.0 → 1.1.0", "body": "", "author": "Commitizen", @@ -122,6 +137,7 @@ }, { "rev": "6012d9eecfce8163d75c8fff179788e9ad5347da", + "parents": ["0c7fb0ca0168864dfc55d83c210da57771a18319"], "title": "test: fixed issues with conf", "body": "", "author": "Commitizen", @@ -129,6 +145,7 @@ }, { "rev": "0c7fb0ca0168864dfc55d83c210da57771a18319", + "parents": ["cb1dd2019d522644da5bdc2594dd6dee17122d7f"], "title": "docs(README): some new information about bump", "body": "", "author": "Commitizen", @@ -136,6 +153,7 @@ }, { "rev": "cb1dd2019d522644da5bdc2594dd6dee17122d7f", + "parents": ["9c7450f85df6bf6be508e79abf00855a30c3c73c"], "title": "feat: new working bump command", "body": "", "author": "Commitizen", @@ -143,6 +161,7 @@ }, { "rev": "9c7450f85df6bf6be508e79abf00855a30c3c73c", + "parents": ["9f3af3772baab167e3fd8775d37f041440184251"], "title": "feat: create version tag", "body": "", "author": "Commitizen", @@ -150,6 +169,7 @@ }, { "rev": "9f3af3772baab167e3fd8775d37f041440184251", + "parents": ["b0d6a3defbfde14e676e7eb34946409297d0221b"], "title": "docs: added new changelog", "body": "", "author": "Commitizen", @@ -157,6 +177,7 @@ }, { "rev": "b0d6a3defbfde14e676e7eb34946409297d0221b", + "parents": ["d630d07d912e420f0880551f3ac94e933f9d3beb"], "title": "feat: update given files with new version", "body": "", "author": "Commitizen", @@ -164,6 +185,7 @@ }, { "rev": "d630d07d912e420f0880551f3ac94e933f9d3beb", + "parents": ["1792b8980c58787906dbe6836f93f31971b1ec2d"], "title": "fix: removed all from commit", "body": "", "author": "Commitizen", @@ -171,6 +193,7 @@ }, { "rev": "1792b8980c58787906dbe6836f93f31971b1ec2d", + "parents": ["52def1ea3555185ba4b936b463311949907e31ec"], "title": "feat(config): new set key, used to set version to cfg", "body": "", "author": "Commitizen", @@ -178,6 +201,7 @@ }, { "rev": "52def1ea3555185ba4b936b463311949907e31ec", + "parents": ["3127e05077288a5e2b62893345590bf1096141b7"], "title": "feat: support for pyproject.toml", "body": "", "author": "Commitizen", @@ -185,6 +209,7 @@ }, { "rev": "3127e05077288a5e2b62893345590bf1096141b7", + "parents": ["fd480ed90a80a6ffa540549408403d5b60d0e90c"], "title": "feat: first semantic version bump implementation", "body": "", "author": "Commitizen", @@ -192,6 +217,7 @@ }, { "rev": "fd480ed90a80a6ffa540549408403d5b60d0e90c", + "parents": ["e4840a059731c0bf488381ffc77e989e85dd81ad"], "title": "fix: fix config file not working", "body": "", "author": "Commitizen", @@ -199,6 +225,7 @@ }, { "rev": "e4840a059731c0bf488381ffc77e989e85dd81ad", + "parents": ["aa44a92d68014d0da98965c0c2cb8c07957d4362"], "title": "refactor: added commands folder, better integration with decli", "body": "", "author": "Commitizen", @@ -206,6 +233,7 @@ }, { "rev": "aa44a92d68014d0da98965c0c2cb8c07957d4362", + "parents": ["58bb709765380dbd46b74ce6e8978515764eb955"], "title": "Bump version: 1.0.0b2 → 1.0.0", "body": "", "author": "Commitizen", @@ -213,6 +241,7 @@ }, { "rev": "58bb709765380dbd46b74ce6e8978515764eb955", + "parents": ["97afb0bb48e72b6feca793091a8a23c706693257"], "title": "docs(README): new badges", "body": "", "author": "Commitizen", @@ -220,6 +249,10 @@ }, { "rev": "97afb0bb48e72b6feca793091a8a23c706693257", + "parents": [ + "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "e004a90b81ea5b374f118759bce5951202d03d69", + ], "title": "Merge pull request #10 from Woile/feat/decli", "body": "Feat/decli", "author": "Commitizen", @@ -227,6 +260,7 @@ }, { "rev": "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "parents": ["f5781d1a2954d71c14ade2a6a1a95b91310b2577"], "title": "style: black to files", "body": "", "author": "Commitizen", @@ -234,6 +268,7 @@ }, { "rev": "f5781d1a2954d71c14ade2a6a1a95b91310b2577", + "parents": ["80105fb3c6d45369bc0cbf787bd329fba603864c"], "title": "ci: added travis", "body": "", "author": "Commitizen", @@ -241,6 +276,7 @@ }, { "rev": "80105fb3c6d45369bc0cbf787bd329fba603864c", + "parents": ["a96008496ffefb6b1dd9b251cb479eac6a0487f7"], "title": "refactor: removed delegator, added decli and many tests", "body": "BREAKING CHANGE: API is stable", "author": "Commitizen", @@ -248,6 +284,7 @@ }, { "rev": "a96008496ffefb6b1dd9b251cb479eac6a0487f7", + "parents": ["aab33d13110f26604fb786878856ec0b9e5fc32b"], "title": "docs: updated test command", "body": "", "author": "Commitizen", @@ -255,6 +292,7 @@ }, { "rev": "aab33d13110f26604fb786878856ec0b9e5fc32b", + "parents": ["b73791563d2f218806786090fb49ef70faa51a3a"], "title": "Bump version: 1.0.0b1 → 1.0.0b2", "body": "", "author": "Commitizen", @@ -262,6 +300,7 @@ }, { "rev": "b73791563d2f218806786090fb49ef70faa51a3a", + "parents": ["7aa06a454fb717408b3657faa590731fb4ab3719"], "title": "docs(README): updated to reflect current state", "body": "", "author": "Commitizen", @@ -269,6 +308,10 @@ }, { "rev": "7aa06a454fb717408b3657faa590731fb4ab3719", + "parents": [ + "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "9589a65880016996cff156b920472b9d28d771ca", + ], "title": "Merge pull request #9 from Woile/dev", "body": "feat: py3 only, tests and conventional commits 1.0", "author": "Commitizen", @@ -276,6 +319,7 @@ }, { "rev": "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "parents": ["ed830019581c83ba633bfd734720e6758eca6061"], "title": "Bump version: 0.9.11 → 1.0.0b1", "body": "", "author": "Commitizen", @@ -283,6 +327,7 @@ }, { "rev": "ed830019581c83ba633bfd734720e6758eca6061", + "parents": ["c52eca6f74f844ab3ffbde61d98ef96071e132b7"], "title": "feat: py3 only, tests and conventional commits 1.0", "body": "more tests\npyproject instead of Pipfile\nquestionary instead of whaaaaat (promptkit 2.0.0 support)", "author": "Commitizen", @@ -290,6 +335,7 @@ }, { "rev": "c52eca6f74f844ab3ffbde61d98ef96071e132b7", + "parents": ["0326652b2657083929507ee66d4d1a0899e861ba"], "title": "Bump version: 0.9.10 → 0.9.11", "body": "", "author": "Commitizen", @@ -297,6 +343,7 @@ }, { "rev": "0326652b2657083929507ee66d4d1a0899e861ba", + "parents": ["b3f89892222340150e32631ae6b7aab65230036f"], "title": "fix(config): load config reads in order without failing if there is no commitizen section", "body": "Closes #8", "author": "Commitizen", @@ -304,6 +351,7 @@ }, { "rev": "b3f89892222340150e32631ae6b7aab65230036f", + "parents": ["5e837bf8ef0735193597372cd2d85e31a8f715b9"], "title": "Bump version: 0.9.9 → 0.9.10", "body": "", "author": "Commitizen", @@ -311,6 +359,7 @@ }, { "rev": "5e837bf8ef0735193597372cd2d85e31a8f715b9", + "parents": ["684e0259cc95c7c5e94854608cd3dcebbd53219e"], "title": "fix: parse scope (this is my punishment for not having tests)", "body": "", "author": "Commitizen", @@ -318,6 +367,7 @@ }, { "rev": "684e0259cc95c7c5e94854608cd3dcebbd53219e", + "parents": ["ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15"], "title": "Bump version: 0.9.8 → 0.9.9", "body": "", "author": "Commitizen", @@ -325,6 +375,7 @@ }, { "rev": "ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15", + "parents": ["64168f18d4628718c49689ee16430549e96c5d4b"], "title": "fix: parse scope empty", "body": "", "author": "Commitizen", @@ -332,6 +383,7 @@ }, { "rev": "64168f18d4628718c49689ee16430549e96c5d4b", + "parents": ["9d4def716ef235a1fa5ae61614366423fbc8256f"], "title": "Bump version: 0.9.7 → 0.9.8", "body": "", "author": "Commitizen", @@ -339,6 +391,7 @@ }, { "rev": "9d4def716ef235a1fa5ae61614366423fbc8256f", + "parents": ["33b0bf1a0a4dc60aac45ed47476d2e5473add09e"], "title": "fix(scope): parse correctly again", "body": "", "author": "Commitizen", @@ -346,6 +399,7 @@ }, { "rev": "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", + "parents": ["696885e891ec35775daeb5fec3ba2ab92c2629e1"], "title": "Bump version: 0.9.6 → 0.9.7", "body": "", "author": "Commitizen", @@ -353,6 +407,7 @@ }, { "rev": "696885e891ec35775daeb5fec3ba2ab92c2629e1", + "parents": ["bef4a86761a3bda309c962bae5d22ce9b57119e4"], "title": "fix(scope): parse correctly", "body": "", "author": "Commitizen", @@ -360,6 +415,7 @@ }, { "rev": "bef4a86761a3bda309c962bae5d22ce9b57119e4", + "parents": ["72472efb80f08ee3fd844660afa012c8cb256e4b"], "title": "Bump version: 0.9.5 → 0.9.6", "body": "", "author": "Commitizen", @@ -367,6 +423,7 @@ }, { "rev": "72472efb80f08ee3fd844660afa012c8cb256e4b", + "parents": ["b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b"], "title": "refactor(conventionalCommit): moved filters to questions instead of message", "body": "", "author": "Commitizen", @@ -374,6 +431,7 @@ }, { "rev": "b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b", + "parents": ["3e31714dc737029d96898f412e4ecd2be1bcd0ce"], "title": "fix(manifest): included missing files", "body": "", "author": "Commitizen", @@ -381,6 +439,7 @@ }, { "rev": "3e31714dc737029d96898f412e4ecd2be1bcd0ce", + "parents": ["9df721e06595fdd216884c36a28770438b4f4a39"], "title": "Bump version: 0.9.4 → 0.9.5", "body": "", "author": "Commitizen", @@ -388,6 +447,7 @@ }, { "rev": "9df721e06595fdd216884c36a28770438b4f4a39", + "parents": ["0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f"], "title": "fix(config): home path for python versions between 3.0 and 3.5", "body": "", "author": "Commitizen", @@ -395,6 +455,7 @@ }, { "rev": "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", + "parents": ["973c6b3e100f6f69a3fe48bd8ee55c135b96c318"], "title": "Bump version: 0.9.3 → 0.9.4", "body": "", "author": "Commitizen", @@ -402,6 +463,7 @@ }, { "rev": "973c6b3e100f6f69a3fe48bd8ee55c135b96c318", + "parents": ["dacc86159b260ee98eb5f57941c99ba731a01399"], "title": "feat(cli): added version", "body": "", "author": "Commitizen", @@ -409,6 +471,7 @@ }, { "rev": "dacc86159b260ee98eb5f57941c99ba731a01399", + "parents": ["4368f3c3cbfd4a1ced339212230d854bc5bab496"], "title": "Bump version: 0.9.2 → 0.9.3", "body": "", "author": "Commitizen", @@ -416,6 +479,7 @@ }, { "rev": "4368f3c3cbfd4a1ced339212230d854bc5bab496", + "parents": ["da94133288727d35dae9b91866a25045038f2d38"], "title": "feat(committer): conventional commit is a bit more intelligent now", "body": "", "author": "Commitizen", @@ -423,6 +487,7 @@ }, { "rev": "da94133288727d35dae9b91866a25045038f2d38", + "parents": ["1541f54503d2e1cf39bd777c0ca5ab5eb78772ba"], "title": "docs(README): motivation", "body": "", "author": "Commitizen", @@ -430,6 +495,7 @@ }, { "rev": "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", + "parents": ["ddc855a637b7879108308b8dbd85a0fd27c7e0e7"], "title": "Bump version: 0.9.1 → 0.9.2", "body": "", "author": "Commitizen", @@ -437,6 +503,7 @@ }, { "rev": "ddc855a637b7879108308b8dbd85a0fd27c7e0e7", + "parents": ["46e9032e18a819e466618c7a014bcb0e9981af9e"], "title": "refactor: renamed conventional_changelog to conventional_commits, not backward compatible", "body": "", "author": "Commitizen", @@ -444,6 +511,7 @@ }, { "rev": "46e9032e18a819e466618c7a014bcb0e9981af9e", + "parents": ["0fef73cd7dc77a25b82e197e7c1d3144a58c1350"], "title": "Bump version: 0.9.0 → 0.9.1", "body": "", "author": "Commitizen", @@ -451,6 +519,7 @@ }, { "rev": "0fef73cd7dc77a25b82e197e7c1d3144a58c1350", + "parents": [], "title": "fix(setup.py): future is now required for every python version", "body": "", "author": "Commitizen", @@ -489,6 +558,7 @@ def gitcommits() -> list: commit["body"], commit["author"], commit["author_email"], + commit["parents"], ) for commit in COMMITS_DATA ] @@ -1108,6 +1178,7 @@ def test_generate_tree_from_commits(gitcommits, tags, merge_prereleases): assert change["author"] == "Commitizen" assert change["author_email"] in "author@cz.dev" assert "sha1" in change + assert "parents" in change def test_generate_tree_from_commits_with_no_commits(tags): From 3f767e154f1459207523b688b8794f0343191f69 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Ruiz <alex@flawedcode.org> Date: Fri, 21 Mar 2025 12:53:45 +0100 Subject: [PATCH 17/20] docs(customization): add "parents" to the list of fields in Changes --- docs/customization.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/customization.md b/docs/customization.md index 132f4f049..50113301d 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -507,12 +507,16 @@ Each `Change` has the following fields: | scope | `str | None` | An optional scope | | message | `str` | The commit message body | | sha1 | `str` | The commit `sha1` | +| parents | `list[str]` | The parent commit(s) `sha1`(s) | | author | `str` | The commit author name | | author_email | `str` | The commit author email | !!! Note The field values depend on the customization class and/or the settings you provide +The `parents` field can be used to identify merge commits and generate a changelog based on those. Another use case +is listing commits that belong to the same pull request. + When using another template (either provided by a plugin or by yourself), you can also pass extra template variables by: From 35f5c23b6dad6bd6be24f783857751bca71ae36d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 13 Apr 2025 07:26:06 +0000 Subject: [PATCH 18/20] =?UTF-8?q?bump:=20version=204.5.1=20=E2=86=92=204.6?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 7 +++++++ commitizen/__version__.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19b4394cf..1373277b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - tomli - repo: https://github.com/commitizen-tools/commitizen - rev: v4.5.1 # automatically updated by Commitizen + rev: v4.6.0 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 39eee25df..1d2ab1e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v4.6.0 (2025-04-13) + +### Feat + +- **changelog**: expose commit parents' digests when processing commits +- **git**: add parents' digests in commit information + ## v4.5.1 (2025-04-09) ### Fix diff --git a/commitizen/__version__.py b/commitizen/__version__.py index f9f7166e6..db01fb213 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "4.5.1" +__version__ = "4.6.0" diff --git a/pyproject.toml b/pyproject.toml index c9ea806cf..6475ec01a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "commitizen" -version = "4.5.1" +version = "4.6.0" description = "Python commitizen client tool" authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] maintainers = [ @@ -88,7 +88,7 @@ build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "4.5.1" +version = "4.6.0" tag_format = "v$version" version_files = [ "pyproject.toml:version", From 58ca48db42fe994bda1617ea998b9089a2222b03 Mon Sep 17 00:00:00 2001 From: Jakob Keller <57402305+jakob-keller@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:43:12 +0200 Subject: [PATCH 19/20] ci(github-actions): bump deprecated ubuntu-20.04 to ubuntu-22.04 Signed-off-by: Jakob Keller <57402305+jakob-keller@users.noreply.github.com> --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f2363745c..b50b02a68 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - platform: [ubuntu-20.04, macos-latest, windows-latest] + platform: [ubuntu-22.04, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 From af285656734fd74c13a8d373a2a0ff303e0971f1 Mon Sep 17 00:00:00 2001 From: Jakob Keller <57402305+jakob-keller@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:30:57 +0200 Subject: [PATCH 20/20] build(deps): bump argcomplete from 3.5.3 to 3.6.2 Signed-off-by: Jakob Keller <57402305+jakob-keller@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 073e1bc24..cccd7a532 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,14 +2,14 @@ [[package]] name = "argcomplete" -version = "3.5.3" +version = "3.6.2" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, - {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, + {file = "argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591"}, + {file = "argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"}, ] [package.extras] @@ -1965,4 +1965,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "b0f8544806163bc0dddc039eb313f9d82119b845b3a19dedc381e9c88e8f4466" +content-hash = "e15b424a0569f939e297c8abfcf09753f1fbcc5b4ad891163cc0982accd3b372" diff --git a/pyproject.toml b/pyproject.toml index 6475ec01a..5be231aaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "tomlkit (>=0.5.3,<1.0.0)", "jinja2>=2.10.3", "pyyaml>=3.08", - "argcomplete >=1.12.1,<3.6", + "argcomplete >=1.12.1,<3.7", "typing-extensions (>=4.0.1,<5.0.0) ; python_version < '3.11'", "charset-normalizer (>=2.1.0,<4)", # Use the Python 3.11 and 3.12 compatible API: https://github.com/python/importlib_metadata#compatibility