Skip to content
Open
21 changes: 20 additions & 1 deletion commitizen/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def normalize_tag(
version = self.scheme(version) if isinstance(version, str) else version
tag_format = tag_format or self.tag_format

major, minor, patch = version.release
major, minor, patch = (list(version.release) + [0, 0, 0])[:3]
prerelease = version.prerelease or ""

t = Template(tag_format)
Expand All @@ -245,6 +245,25 @@ def find_tag_for(
) -> GitTag | None:
"""Find the first matching tag for a given version."""
version = self.scheme(version) if isinstance(version, str) else version
release = version.release

# 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[Version, 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))

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 len(candidates) > 1:
Expand Down
33 changes: 33 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,8 @@ def test_bump_invalid_manual_version_raises_exception(mocker, manual_version):
"0.1.1",
"0.2.0",
"1.0.0",
"1.2",
"1",
],
)
def test_bump_manual_version(mocker, manual_version):
Expand Down Expand Up @@ -966,6 +968,37 @@ def test_bump_manual_version_disallows_major_version_zero(mocker):
assert expected_error_message in str(excinfo.value)


@pytest.mark.parametrize(
"initial_version, expected_version_after_bump",
[
("1", "1.1.0"),
("1.2", "1.3.0"),
],
)
def test_bump_version_with_less_components_in_config(
tmp_commitizen_project_initial,
mocker: MockFixture,
initial_version,
expected_version_after_bump,
):
tmp_commitizen_project = tmp_commitizen_project_initial(version=initial_version)

testargs = ["cz", "bump", "--yes"]
mocker.patch.object(sys, "argv", testargs)

cli.main()

tag_exists = git.tag_exist(expected_version_after_bump)
assert tag_exists is True

for version_file in [
tmp_commitizen_project.join("__version__.py"),
tmp_commitizen_project.join("pyproject.toml"),
]:
with open(version_file) as f:
assert expected_version_after_bump in f.read()


@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file"))
def test_bump_with_pre_bump_hooks(
commit_msg, mocker: MockFixture, tmp_commitizen_project
Expand Down
101 changes: 101 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from commitizen.git import GitTag
from commitizen.tags import TagRules


def _git_tag(name: str) -> GitTag:
return GitTag(name, "rev", "2024-01-01")


def test_find_tag_for_partial_version_returns_latest_match():
tags = [
_git_tag("1.2.0"),
_git_tag("1.2.2"),
_git_tag("1.2.1"),
_git_tag("1.3.0"),
]

rules = TagRules()

found = rules.find_tag_for(tags, "1.2")

assert found is not None
assert found.name == "1.2.2"


def test_find_tag_for_full_version_remains_exact():
tags = [
_git_tag("1.2.0"),
_git_tag("1.2.2"),
_git_tag("1.2.1"),
]

rules = TagRules()

found = rules.find_tag_for(tags, "1.2.1")

assert found is not None
assert found.name == "1.2.1"


def test_find_tag_for_partial_version_with_prereleases_prefers_latest_version():
tags = [
_git_tag("1.2.0b1"),
_git_tag("1.2.0"),
_git_tag("1.2.1b1"),
]

rules = TagRules()

found = rules.find_tag_for(tags, "1.2")

assert found is not None
# 1.2.1b1 > 1.2.0 so it should be selected
assert found.name == "1.2.1b1"


def test_find_tag_for_partial_version_respects_tag_format():
tags = [
_git_tag("v1.2.0"),
_git_tag("v1.2.1"),
_git_tag("v1.3.0"),
]

rules = TagRules(tag_format="v$version")

found = rules.find_tag_for(tags, "1.2")

assert found is not None
assert found.name == "v1.2.1"

found = rules.find_tag_for(tags, "1")

assert found is not None
assert found.name == "v1.3.0"


def test_find_tag_for_partial_version_returns_none_when_no_match():
tags = [
_git_tag("2.0.0"),
_git_tag("2.1.0"),
]

rules = TagRules()

found = rules.find_tag_for(tags, "1.2")

assert found is None


def test_find_tag_for_partial_version_ignores_invalid_tags():
tags = [
_git_tag("not-a-version"),
_git_tag("1.2.0"),
_git_tag("1.2.1"),
]

rules = TagRules()

found = rules.find_tag_for(tags, "1.2")

assert found is not None
assert found.name == "1.2.1"