Skip to content

Commit

Permalink
fix(scm): only search tags that are reachable by the current commit
Browse files Browse the repository at this point in the history
The scm provider requires tags to be the source of truth for the current version, instead of files.  Therefore, the current version should be the highest tag version upstream from the current commit.
  • Loading branch information
chadrik authored and woile committed Feb 4, 2024
1 parent 893cf8c commit 1c022dd
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 17 deletions.
2 changes: 1 addition & 1 deletion commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def find_previous_final_version(
if not final_versions:
return None

final_versions = sorted(final_versions) # type: ignore [type-var]
final_versions = sorted(final_versions)
current_index = final_versions.index(current_version)
previous_index = current_index - 1
if previous_index < 0:
Expand Down
10 changes: 8 additions & 2 deletions commitizen/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,22 @@ def get_filenames_in_commit(git_reference: str = ""):
raise GitCommandError(c.err)


def get_tags(dateformat: str = "%Y-%m-%d") -> list[GitTag]:
def get_tags(
dateformat: str = "%Y-%m-%d", reachable_only: bool = False
) -> list[GitTag]:
inner_delimiter = "---inner_delimiter---"
formatter = (
f'"%(refname:lstrip=2){inner_delimiter}'
f"%(objectname){inner_delimiter}"
f"%(creatordate:format:{dateformat}){inner_delimiter}"
f'%(object)"'
)
c = cmd.run(f"git tag --format={formatter} --sort=-creatordate")
extra = "--merged" if reachable_only else ""
c = cmd.run(f"git tag --format={formatter} --sort=-creatordate {extra}")
if c.return_code != 0:
if reachable_only and c.err == "fatal: malformed object name HEAD\n":
# this can happen if there are no commits in the repo yet
return []
raise GitCommandError(c.err)

if c.err:
Expand Down
36 changes: 26 additions & 10 deletions commitizen/providers/scm_provider.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from __future__ import annotations

import re
from typing import Callable, cast
from typing import Callable


from commitizen.git import get_tags
from commitizen.version_schemes import get_version_scheme
from commitizen.version_schemes import (
get_version_scheme,
InvalidVersion,
Version,
VersionProtocol,
)

from commitizen.providers.base_provider import VersionProvider

Expand All @@ -28,7 +33,7 @@ class ScmProvider(VersionProvider):
"$devrelease": r"(?P<devrelease>\.dev\d+)?",
}

def _tag_format_matcher(self) -> Callable[[str], str | None]:
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":
Expand All @@ -38,15 +43,15 @@ def _tag_format_matcher(self) -> Callable[[str], str | None]:

regex = re.compile(f"^{pattern}$", re.VERBOSE)

def matcher(tag: str) -> str | None:
def matcher(tag: str) -> Version | None:
match = regex.match(tag)
if not match:
return None
groups = match.groupdict()
if "version" in groups:
return groups["version"]
ver = groups["version"]
elif "major" in groups:
return "".join(
ver = "".join(
(
groups["major"],
f".{groups['minor']}" if groups.get("minor") else "",
Expand All @@ -56,16 +61,27 @@ def matcher(tag: str) -> str | None:
)
)
elif pattern == version_scheme.parser.pattern:
return str(version_scheme(tag))
return None
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()
return next(
(cast(str, matcher(t.name)) for t in get_tags() if matcher(t.name)), "0.0.0"
matches = sorted(
version
for t in get_tags(reachable_only=True)
if (version := matcher(t.name))
)
if not matches:
return "0.0.0"
return str(matches[-1])

def set_version(self, version: str):
# Not necessary
Expand Down
20 changes: 19 additions & 1 deletion commitizen/version_schemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
import warnings
from itertools import zip_longest
from typing import TYPE_CHECKING, ClassVar, Protocol, Type, cast, runtime_checkable
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, Type, cast, runtime_checkable

import importlib_metadata as metadata
from packaging.version import InvalidVersion # noqa: F401: Rexpose the common exception
Expand Down Expand Up @@ -93,6 +93,24 @@ def micro(self) -> int:
"""The third item of :attr:`release` or ``0`` if unavailable."""
raise NotImplementedError("must be implemented")

def __lt__(self, other: Any) -> bool:
raise NotImplementedError("must be implemented")

def __le__(self, other: Any) -> bool:
raise NotImplementedError("must be implemented")

def __eq__(self, other: object) -> bool:
raise NotImplementedError("must be implemented")

def __ge__(self, other: Any) -> bool:
raise NotImplementedError("must be implemented")

def __gt__(self, other: Any) -> bool:
raise NotImplementedError("must be implemented")

def __ne__(self, other: object) -> bool:
raise NotImplementedError("must be implemented")

def bump(
self,
increment: str,
Expand Down
56 changes: 54 additions & 2 deletions tests/providers/test_scm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from commitizen.config.base_config import BaseConfig
from commitizen.providers import get_provider
from commitizen.providers.scm_provider import ScmProvider
from tests.utils import create_file_and_commit, create_tag
from tests.utils import (
create_branch,
create_file_and_commit,
create_tag,
merge_branch,
switch_branch,
)


@pytest.mark.parametrize(
Expand All @@ -22,7 +28,8 @@
# much more lenient but require a v prefix.
("v$version", "v0.1.0", "0.1.0"),
("v$version", "no-match-because-no-v-prefix", "0.0.0"),
("v$version", "v-match-TAG_FORMAT_REGEXS", "-match-TAG_FORMAT_REGEXS"),
# no match because not a valid version
("v$version", "v-match-TAG_FORMAT_REGEXS", "0.0.0"),
("version-$version", "version-0.1.0", "0.1.0"),
("version-$version", "version-0.1", "0.1"),
("version-$version", "version-0.1.0rc1", "0.1.0rc1"),
Expand Down Expand Up @@ -62,3 +69,48 @@ def test_scm_provider_default_without_commits_and_tags(config: BaseConfig):
provider = get_provider(config)
assert isinstance(provider, ScmProvider)
assert provider.get_version() == "0.0.0"


@pytest.mark.usefixtures("tmp_git_project")
def test_scm_provider_default_with_commits_and_tags(config: BaseConfig):
config.settings["version_provider"] = "scm"

provider = get_provider(config)
assert isinstance(provider, ScmProvider)
assert provider.get_version() == "0.0.0"

create_file_and_commit("Initial state")
create_tag("1.0.0")
# create develop
create_branch("develop")
switch_branch("develop")

# add a feature to develop
create_file_and_commit("develop: add beta feature1")
assert provider.get_version() == "1.0.0"
create_tag("1.1.0b0")

# create staging
create_branch("staging")
switch_branch("staging")
create_file_and_commit("staging: Starting release candidate")
assert provider.get_version() == "1.1.0b0"
create_tag("1.1.0rc0")

# add another feature to develop
switch_branch("develop")
create_file_and_commit("develop: add beta feature2")
assert provider.get_version() == "1.1.0b0"
create_tag("1.2.0b0")

# add a hotfix to master
switch_branch("master")
create_file_and_commit("master: add hotfix")
assert provider.get_version() == "1.0.0"
create_tag("1.0.1")

# merge the hotfix to staging
switch_branch("staging")
merge_branch("master")

assert provider.get_version() == "1.1.0rc0"
31 changes: 30 additions & 1 deletion tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
from commitizen import cmd, exceptions, git
from pytest_mock import MockFixture

from tests.utils import FakeCommand, create_file_and_commit, create_tag
from tests.utils import (
FakeCommand,
create_file_and_commit,
create_tag,
create_branch,
switch_branch,
)


def test_git_object_eq():
Expand Down Expand Up @@ -42,6 +48,29 @@ def test_get_tags(mocker: MockFixture):
assert git.get_tags() == []


def test_get_reachable_tags(tmp_commitizen_project):
with tmp_commitizen_project.as_cwd():
create_file_and_commit("Initial state")
create_tag("1.0.0")
# create develop
create_branch("develop")
switch_branch("develop")

# add a feature to develop
create_file_and_commit("develop")
create_tag("1.1.0b0")

# create staging
switch_branch("master")
create_file_and_commit("master")
create_tag("1.0.1")

tags = git.get_tags(reachable_only=True)
tag_names = set([t.name for t in tags])
# 1.1.0b0 is not present
assert tag_names == {"1.0.0", "1.0.1"}


def test_get_tag_names(mocker: MockFixture):
tag_str = "v1.0.0\n" "v0.5.0\n" "v0.0.1\n"
mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str))
Expand Down

0 comments on commit 1c022dd

Please sign in to comment.