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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions commitizen/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

if TYPE_CHECKING:
import sys
from collections.abc import Iterable, Sequence
from collections.abc import Iterable, Iterator, Sequence

# Self is Python 3.11+ but backported in typing-extensions
if sys.version_info < (3, 11):
Expand Down Expand Up @@ -232,22 +232,26 @@ def find_tag_for(
# If the requested version is incomplete (e.g., "1.2"), try to find the latest
# matching tag that shares the provided prefix.
if len(release) < 3:
matching_versions: list[tuple[VersionProtocol, GitTag]] = []
for tag in tags:
try:
tag_version = self.extract_version(tag)
except InvalidVersion:
continue
if tag_version.release[: len(release)] != release:
continue
matching_versions.append((tag_version, tag))
matching_versions = [
(tag_version, tag)
for tag_version, tag in self._parse_tag_versions(tags)
if tag_version.release[: len(release)] == release
]

if matching_versions:
_, latest_tag = max(matching_versions, key=lambda vt: vt[0])
return latest_tag

possible_tags = set(self.normalize_tag(version, f) for f in self.tag_formats)
candidates = [t for t in tags if t.name in possible_tags]
if not candidates:
# Tag formats with regex-only parts (e.g. `\+.*`) cannot be rendered
# by `normalize_tag`, so fall back to comparing extracted versions.
candidates = [
tag
for tag_version, tag in self._parse_tag_versions(tags)
if tag_version == version
]
if len(candidates) > 1:
warnings.warn(
UserWarning(
Expand Down Expand Up @@ -279,3 +283,19 @@ def _extract_version(self, match: re.Match[str]) -> str:
if devrelease := groups.get("devrelease"):
parts.append(devrelease)
return "".join(parts)

def _parse_tag_versions(
self, tags: Iterable[GitTag]
) -> Iterator[tuple[VersionProtocol, GitTag]]:
"""
Yield each tag with its extracted version, skipping invalid tags.

Used when searching tags by version: a tag that matches no tag format,
or whose version is rejected by the version scheme, is unrelated to the
search and must not abort it.
"""
for tag in tags:
try:
yield self.extract_version(tag), tag
except InvalidVersion:
continue
37 changes: 37 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,43 @@ def test_bump_detect_legacy_tags_from_scm(
assert git.tag_exist("v0.4.3") is True


def test_bump_detect_legacy_tag_with_build_metadata_from_scm(
tmp_commitizen_project: Path, util: UtilFixture, capsys: pytest.CaptureFixture
):
"""Regression test for #2015."""
tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml"
tmp_commitizen_cfg_file.write_text(
"\n".join(
[
"[tool.commitizen]",
'version_provider = "scm"',
'version_scheme = "pep440"',
'tag_format = "$version"',
"legacy_tag_formats = [",
" '$major.$minor.$patch$prerelease\\+.*'",
"]",
]
),
)
util.create_file_and_commit("feat: initial")
util.create_tag("1.0.1rc0+gha")
util.create_file_and_commit("fix: bar")

with pytest.raises(DryRunExit):
util.run_cli(
"bump",
"--yes",
"--prerelease",
"rc",
"--build-metadata",
"gha",
"--get-next",
)

out, _ = capsys.readouterr()
assert out.strip() == "1.0.1rc1+gha"


def test_bump_warn_but_dont_fail_on_invalid_tags(
tmp_commitizen_project: Path,
util: UtilFixture,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ def test_find_tag_for_partial_version_ignores_invalid_tags():
assert found.name == "1.2.1"


def test_find_tag_for_full_version_matching_regex_only_legacy_tag_format():
"""Regression test for #2015: a legacy tag format containing regex-only
parts (here ``\\+.*`` for build metadata) cannot be rendered back into a tag
name, so the tag must be found by comparing its extracted version.
"""
tags = [
_git_tag("not-a-version"),
_git_tag("1.2.3foo"),
_git_tag("1.0.0"),
_git_tag("1.0.1rc0+gha"),
]

rules = TagRules(legacy_tag_formats=[r"$major.$minor.$patch$prerelease\+.*"])

found = rules.find_tag_for(tags, "1.0.1rc0")

assert found is not None
assert found.name == "1.0.1rc0+gha"
assert rules.find_tag_for(tags, "1.0.2") is None


def test_is_version_tag_accepts_semver2_prerelease_in_custom_tag_format():
"""Regression test for #1614: a SemVer2-style prerelease segment such as
``rc.0`` (with a literal dot) must be recognised when it appears at the
Expand Down
Loading