Skip to content

Commit 2676152

Browse files
committed
fix(tags): find current tag matched by a regex-only legacy tag format
Closes #2015
1 parent 100bfe3 commit 2676152

3 files changed

Lines changed: 88 additions & 10 deletions

File tree

‎commitizen/tags.py‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
if TYPE_CHECKING:
2323
import sys
24-
from collections.abc import Iterable, Sequence
24+
from collections.abc import Iterable, Iterator, Sequence
2525

2626
# Self is Python 3.11+ but backported in typing-extensions
2727
if sys.version_info < (3, 11):
@@ -232,22 +232,26 @@ def find_tag_for(
232232
# If the requested version is incomplete (e.g., "1.2"), try to find the latest
233233
# matching tag that shares the provided prefix.
234234
if len(release) < 3:
235-
matching_versions: list[tuple[VersionProtocol, GitTag]] = []
236-
for tag in tags:
237-
try:
238-
tag_version = self.extract_version(tag)
239-
except InvalidVersion:
240-
continue
241-
if tag_version.release[: len(release)] != release:
242-
continue
243-
matching_versions.append((tag_version, tag))
235+
matching_versions = [
236+
(tag_version, tag)
237+
for tag_version, tag in self._parse_tag_versions(tags)
238+
if tag_version.release[: len(release)] == release
239+
]
244240

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

249245
possible_tags = set(self.normalize_tag(version, f) for f in self.tag_formats)
250246
candidates = [t for t in tags if t.name in possible_tags]
247+
if not candidates:
248+
# Tag formats with regex-only parts (e.g. `\+.*`) cannot be rendered
249+
# by `normalize_tag`, so fall back to comparing extracted versions.
250+
candidates = [
251+
tag
252+
for tag_version, tag in self._parse_tag_versions(tags)
253+
if tag_version == version
254+
]
251255
if len(candidates) > 1:
252256
warnings.warn(
253257
UserWarning(
@@ -279,3 +283,19 @@ def _extract_version(self, match: re.Match[str]) -> str:
279283
if devrelease := groups.get("devrelease"):
280284
parts.append(devrelease)
281285
return "".join(parts)
286+
287+
def _parse_tag_versions(
288+
self, tags: Iterable[GitTag]
289+
) -> Iterator[tuple[VersionProtocol, GitTag]]:
290+
"""
291+
Yield each tag with its extracted version, skipping invalid tags.
292+
293+
Used when searching tags by version: a tag that matches no tag format,
294+
or whose version is rejected by the version scheme, is unrelated to the
295+
search and must not abort it.
296+
"""
297+
for tag in tags:
298+
try:
299+
yield self.extract_version(tag), tag
300+
except InvalidVersion:
301+
continue

‎tests/commands/test_bump_command.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,43 @@ def test_bump_detect_legacy_tags_from_scm(
13451345
assert git.tag_exist("v0.4.3") is True
13461346

13471347

1348+
def test_bump_detect_legacy_tag_with_build_metadata_from_scm(
1349+
tmp_commitizen_project: Path, util: UtilFixture, capsys: pytest.CaptureFixture
1350+
):
1351+
"""Regression test for #2015."""
1352+
tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml"
1353+
tmp_commitizen_cfg_file.write_text(
1354+
"\n".join(
1355+
[
1356+
"[tool.commitizen]",
1357+
'version_provider = "scm"',
1358+
'version_scheme = "pep440"',
1359+
'tag_format = "$version"',
1360+
"legacy_tag_formats = [",
1361+
" '$major.$minor.$patch$prerelease\\+.*'",
1362+
"]",
1363+
]
1364+
),
1365+
)
1366+
util.create_file_and_commit("feat: initial")
1367+
util.create_tag("1.0.1rc0+gha")
1368+
util.create_file_and_commit("fix: bar")
1369+
1370+
with pytest.raises(DryRunExit):
1371+
util.run_cli(
1372+
"bump",
1373+
"--yes",
1374+
"--prerelease",
1375+
"rc",
1376+
"--build-metadata",
1377+
"gha",
1378+
"--get-next",
1379+
)
1380+
1381+
out, _ = capsys.readouterr()
1382+
assert out.strip() == "1.0.1rc1+gha"
1383+
1384+
13481385
def test_bump_warn_but_dont_fail_on_invalid_tags(
13491386
tmp_commitizen_project: Path,
13501387
util: UtilFixture,

‎tests/test_tags.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,27 @@ def test_find_tag_for_partial_version_ignores_invalid_tags():
101101
assert found.name == "1.2.1"
102102

103103

104+
def test_find_tag_for_full_version_matching_regex_only_legacy_tag_format():
105+
"""Regression test for #2015: a legacy tag format containing regex-only
106+
parts (here ``\\+.*`` for build metadata) cannot be rendered back into a tag
107+
name, so the tag must be found by comparing its extracted version.
108+
"""
109+
tags = [
110+
_git_tag("not-a-version"),
111+
_git_tag("1.2.3foo"),
112+
_git_tag("1.0.0"),
113+
_git_tag("1.0.1rc0+gha"),
114+
]
115+
116+
rules = TagRules(legacy_tag_formats=[r"$major.$minor.$patch$prerelease\+.*"])
117+
118+
found = rules.find_tag_for(tags, "1.0.1rc0")
119+
120+
assert found is not None
121+
assert found.name == "1.0.1rc0+gha"
122+
assert rules.find_tag_for(tags, "1.0.2") is None
123+
124+
104125
def test_is_version_tag_accepts_semver2_prerelease_in_custom_tag_format():
105126
"""Regression test for #1614: a SemVer2-style prerelease segment such as
106127
``rc.0`` (with a literal dot) must be recognised when it appears at the

0 commit comments

Comments
 (0)