Skip to content

Commit 99a0895

Browse files
committed
feat(providers): add scm provider reading version from the last tag matching tag_format
1 parent 5a83c5d commit 99a0895

File tree

4 files changed

+89
-2
lines changed

4 files changed

+89
-2
lines changed

Diff for: commitizen/providers.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
from __future__ import annotations
22

33
import json
4+
import re
45
from abc import ABC, abstractmethod
56
from pathlib import Path
6-
from typing import Any, ClassVar, cast
7+
from typing import Any, ClassVar, Pattern, cast
78

89
import importlib_metadata as metadata
910
import tomlkit
11+
from packaging.version import VERSION_PATTERN, Version
1012

1113
from commitizen.config.base_config import BaseConfig
1214
from commitizen.exceptions import VersionProviderUnknown
15+
from commitizen.git import get_tags
1316

1417
ENTRYPOINT = "commitizen.provider"
1518
DEFAULT = "commitizen"
@@ -157,6 +160,41 @@ class ComposerProvider(JsonProvider):
157160
indent = 4
158161

159162

163+
class ScmProvider(VersionProvider):
164+
"""
165+
A provider dedicated to be coupled with `setuptools-scm`
166+
or any package manager `-scm` provider.
167+
168+
Version is fetched from git history (using `git describe`)
169+
and does not need to be set back.
170+
"""
171+
172+
TAG_FORMAT_REGEXS = {
173+
"$version": r".+",
174+
"$major": r"\d+",
175+
"$minor": r"\d+",
176+
"$patch": r"\d+",
177+
"$prerelease": r"\w+",
178+
"$devrelease": r"(\.?dev\d+)?",
179+
}
180+
181+
def _tag_format_matcher(self) -> Pattern[str]:
182+
tag_format = self.config.settings.get("tag_format") or VERSION_PATTERN
183+
for var, pattern in self.TAG_FORMAT_REGEXS.items():
184+
tag_format = tag_format.replace(var, pattern)
185+
return re.compile(f"^{tag_format}$", re.VERBOSE | re.IGNORECASE)
186+
187+
def get_version(self) -> str:
188+
matcher = self._tag_format_matcher()
189+
return next(
190+
(str(Version(t.name)) for t in get_tags() if matcher.match(t.name)), "0.0.0"
191+
)
192+
193+
def set_version(self, version: str):
194+
# Not necessary
195+
pass
196+
197+
160198
def get_provider(config: BaseConfig) -> VersionProvider:
161199
"""
162200
Get the version provider as defined in the configuration

Diff for: docs/config.md

+4
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,16 @@ Commitizen provides some version providers for some well known formats:
123123
| name | description |
124124
| ---- | ----------- |
125125
| `commitizen` | Default version provider: Fetch and set version in commitizen config. |
126+
| `scm` | Fetch the version from git and does not need to set it back |
126127
| `pep621` | Get and set version from `pyproject.toml` `project.version` field |
127128
| `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field |
128129
| `cargo` | Get and set version from `Cargo.toml` `project.version` field |
129130
| `npm` | Get and set version from `package.json` `project.version` field |
130131
| `composer` | Get and set version from `composer.json` `project.version` field |
131132

133+
!!! note
134+
The `scm` provider is meant to be used with `setuptools-scm` or any packager `*-scm` plugin.
135+
132136
### Custom version provider
133137

134138
You can add you own version provider by extending `VersionProvider` and exposing it on the `commitizen.provider` entrypoint.

Diff for: pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ composer = "commitizen.providers:ComposerProvider"
111111
npm = "commitizen.providers:NpmProvider"
112112
pep621 = "commitizen.providers:Pep621Provider"
113113
poetry = "commitizen.providers:PoetryProvider"
114+
scm = "commitizen.providers:ScmProvider"
114115

115116
[tool.isort]
116117
profile = "black"

Diff for: tests/test_version_providers.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
from pathlib import Path
55
from textwrap import dedent
6-
from typing import TYPE_CHECKING, Iterator
6+
from typing import TYPE_CHECKING, Iterator, Optional
77

88
import pytest
99

@@ -16,8 +16,10 @@
1616
NpmProvider,
1717
Pep621Provider,
1818
PoetryProvider,
19+
ScmProvider,
1920
get_provider,
2021
)
22+
from tests.utils import create_file_and_commit, create_tag
2123

2224
if TYPE_CHECKING:
2325
from pytest_mock import MockerFixture
@@ -185,3 +187,45 @@ def test_composer_provider(config: BaseConfig, chdir: Path):
185187
}
186188
"""
187189
)
190+
191+
192+
TEST_VERSION = "0.1.0"
193+
194+
195+
@pytest.mark.parametrize(
196+
"tag_format,tag",
197+
(
198+
("v$version", f"v{TEST_VERSION}"),
199+
("v$minor.$major.$patch", f"v{TEST_VERSION}"),
200+
(None, TEST_VERSION),
201+
),
202+
)
203+
@pytest.mark.usefixtures("tmp_git_project")
204+
def test_scm_provider(config: BaseConfig, tag_format: Optional[str], tag: str):
205+
create_file_and_commit("test: fake commit")
206+
create_tag(tag)
207+
create_file_and_commit("test: fake commit")
208+
create_tag("should-not-match")
209+
210+
config.settings["version_provider"] = "scm"
211+
config.settings["tag_format"] = tag_format
212+
213+
provider = get_provider(config)
214+
assert isinstance(provider, ScmProvider)
215+
assert provider.get_version() == TEST_VERSION
216+
217+
# SHould not fail on set_version()
218+
provider.set_version("43.1")
219+
220+
221+
@pytest.mark.usefixtures("tmp_git_project")
222+
def test_scm_provider_default_triple_zero_without_matching_tag(config: BaseConfig):
223+
create_file_and_commit("test: fake commit")
224+
create_tag("should-not-match")
225+
create_file_and_commit("test: fake commit")
226+
227+
config.settings["version_provider"] = "scm"
228+
229+
provider = get_provider(config)
230+
assert isinstance(provider, ScmProvider)
231+
assert provider.get_version() == "0.0.0"

0 commit comments

Comments
 (0)