Skip to content

Commit

Permalink
feat(providers): add scm provider reading version from the last tag m…
Browse files Browse the repository at this point in the history
…atching tag_format

Fixes #641
  • Loading branch information
noirbizarre committed Jan 1, 2023
1 parent 5a83c5d commit 16c8449
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 2 deletions.
59 changes: 58 additions & 1 deletion commitizen/providers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from __future__ import annotations

import json
import re
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, ClassVar, cast
from typing import Any, Callable, ClassVar, Optional, cast

import importlib_metadata as metadata
import tomlkit
from packaging.version import VERSION_PATTERN, Version

from commitizen.config.base_config import BaseConfig
from commitizen.exceptions import VersionProviderUnknown
from commitizen.git import get_tags

ENTRYPOINT = "commitizen.provider"
DEFAULT = "commitizen"
Expand Down Expand Up @@ -157,6 +160,60 @@ class ComposerProvider(JsonProvider):
indent = 4


class ScmProvider(VersionProvider):
"""
A provider dedicated to be coupled with `setuptools-scm`
or any package manager `-scm` provider.
Version is fetched from git history (using `git describe`)
and does not need to be set back.
"""

TAG_FORMAT_REGEXS = {
"$version": r"(?P<version>.+)",
"$major": r"(?P<major>\d+)",
"$minor": r"(?P<minor>\d+)",
"$patch": r"(?P<patch>\d+)",
"$prerelease": r"(?P<prerelease>\w+\d+)?",
"$devrelease": r"(?P<devrelease>\.?dev\d+)?",
}

def _tag_format_matcher(self) -> Callable[[str], Optional[str]]:
tag_format = self.config.settings.get("tag_format") or VERSION_PATTERN
for var, pattern in self.TAG_FORMAT_REGEXS.items():
tag_format = tag_format.replace(var, pattern)

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

def matcher(tag: str) -> Optional[str]:
m = regex.match(tag)
if not m:
return None
elif "version" in m.groupdict():
return m.group("version")
elif "devrelease" in m.groupdict():
return "{major}.{minor}.{patch}{devrelease}".format(**m.groupdict())
elif "prerelease" in m.groupdict():
return "{major}.{minor}.{patch}{prerelease}".format(**m.groupdict())
elif "major" in m.groupdict():
return "{major}.{minor}.{patch}".format(**m.groupdict())
elif tag_format == VERSION_PATTERN:
return str(Version(tag))
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"
)

def set_version(self, version: str):
# Not necessary
pass


def get_provider(config: BaseConfig) -> VersionProvider:
"""
Get the version provider as defined in the configuration
Expand Down
4 changes: 4 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,16 @@ Commitizen provides some version providers for some well known formats:
| name | description |
| ---- | ----------- |
| `commitizen` | Default version provider: Fetch and set version in commitizen config. |
| `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 |
| `cargo` | Get and set version from `Cargo.toml` `project.version` field |
| `npm` | Get and set version from `package.json` `project.version` field |
| `composer` | Get and set version from `composer.json` `project.version` field |

!!! note
The `scm` provider is meant to be used with `setuptools-scm` or any packager `*-scm` plugin.

### Custom version provider

You can add you own version provider by extending `VersionProvider` and exposing it on the `commitizen.provider` entrypoint.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ composer = "commitizen.providers:ComposerProvider"
npm = "commitizen.providers:NpmProvider"
pep621 = "commitizen.providers:Pep621Provider"
poetry = "commitizen.providers:PoetryProvider"
scm = "commitizen.providers:ScmProvider"

[tool.isort]
profile = "black"
Expand Down
60 changes: 59 additions & 1 deletion tests/test_version_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING, Iterator
from typing import TYPE_CHECKING, Iterator, Optional

import pytest

Expand All @@ -16,8 +16,10 @@
NpmProvider,
Pep621Provider,
PoetryProvider,
ScmProvider,
get_provider,
)
from tests.utils import create_file_and_commit, create_tag

if TYPE_CHECKING:
from pytest_mock import MockerFixture
Expand Down Expand Up @@ -185,3 +187,59 @@ def test_composer_provider(config: BaseConfig, chdir: Path):
}
"""
)


@pytest.mark.parametrize(
"tag_format,tag,version",
(
(None, "0.1.0", "0.1.0"),
(None, "v0.1.0", "0.1.0"),
("v$version", "v0.1.0", "0.1.0"),
("version-$version", "version-0.1.0", "0.1.0"),
("v$major.$minor.$patch", "v0.1.0", "0.1.0"),
("v$minor.$major.$patch", "v1.0.0", "0.1.0"),
("version-$major.$minor.$patch", "version-0.1.0", "0.1.0"),
("v$minor.$major.$patch$prerelease", "v1.0.0rc1", "0.1.0rc1"),
("v$minor.$major.$patch$prerelease$devrelease", "v1.0.0.dev0", "0.1.0.dev0"),
),
)
@pytest.mark.usefixtures("tmp_git_project")
def test_scm_provider(
config: BaseConfig, tag_format: Optional[str], tag: str, version: str
):
create_file_and_commit("test: fake commit")
create_tag(tag)
create_file_and_commit("test: fake commit")
create_tag("should-not-match")

config.settings["version_provider"] = "scm"
config.settings["tag_format"] = tag_format

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

# Should not fail on set_version()
provider.set_version("43.1")


@pytest.mark.usefixtures("tmp_git_project")
def test_scm_provider_default_without_matching_tag(config: BaseConfig):
create_file_and_commit("test: fake commit")
create_tag("should-not-match")
create_file_and_commit("test: fake commit")

config.settings["version_provider"] = "scm"

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_without_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"

0 comments on commit 16c8449

Please sign in to comment.