Skip to content

Commit 9915cb6

Browse files
authored
feat: Add tag_filter as a way to pre-filter tags (#225)
Fixes #219
1 parent 080b4bc commit 9915cb6

File tree

7 files changed

+402
-81
lines changed

7 files changed

+402
-81
lines changed

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@ jobs:
3636
run: |
3737
python -m pip install -U pip pytest setuptools editables
3838
pip install .
39+
- name: Install Mercurial
40+
shell: bash
41+
run: |
42+
case "$RUNNER_OS" in
43+
"Linux")
44+
sudo apt install mercurial
45+
;;
46+
"Windows")
47+
choco install hg
48+
;;
49+
"macOS")
50+
brew install mercurial
51+
;;
52+
"*")
53+
echo "$RUNNER_OS not supported"
54+
exit 1
55+
;;
56+
esac
57+
58+
echo "[ui]" >> ~/.hgrc
59+
echo "username = \"John Doe <ci@test.org>\"" >> ~/.hgrc
3960
- name: Run Tests
4061
run: |
4162
git config --global user.name "John Doe"

docs/metadata.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,25 @@ Alternatively, you can specify a default version in the configuration:
5353
fallback_version = "0.0.0"
5454
```
5555

56-
You can specify another regex pattern to match the SCM tag, in which a `version` group is required:
56+
To control which scm tags are used to generate the version, you can use two
57+
fields: `tag_filter` and `tag_regex`.
5758

5859
```toml
5960
[tool.pdm.version]
6061
source = "scm"
61-
tag_regex = '^(?:\D*)?(?P<version>([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$'
62+
tag_filter = "test/*"
63+
tag_regex = '^test/(?:\D*)?(?P<version>([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$'
6264
```
6365

66+
`tag_filter` filters the set of tags which are considered as candidates to
67+
capture your project's version. For `git` repositories, this field is a glob
68+
matched against the tag. For `hg` repositories, it is a regular expression used
69+
with the `latesttag` function.
70+
71+
`tag_regex` configures how you extract a version from a tag. It is applied after
72+
`tag_filter` extracts candidate tags to extract the version from that tag. It is
73+
a python style regular expression.
74+
6475
+++ 2.2.0
6576

6677
To customize the format of the version string, specify the `version_format` option with a format function:
@@ -117,10 +128,10 @@ write_template = "__version__ = '{}'"
117128
```
118129

119130
!!! note
120-
The path in `write_to` is relative to the root of the wheel file, hence the `package-dir` part should be stripped.
131+
The path in `write_to` is relative to the root of the wheel file, hence the `package-dir` part should be stripped.
121132

122133
!!! note
123-
`pdm-backend` will rewrite the whole file each time, so you can't have additional contents in that file.
134+
`pdm-backend` will rewrite the whole file each time, so you can't have additional contents in that file.
124135

125136
## Variables expansion
126137

@@ -159,7 +170,7 @@ dependencies = [
159170
```
160171

161172
!!! note
162-
The triple slashes `///` is required for the compatibility of Windows and POSIX systems.
173+
The triple slashes `///` is required for the compatibility of Windows and POSIX systems.
163174

164175
!!! note
165-
The relative paths will be expanded into the absolute paths on the local machine. So it makes no sense to include them in a distribution, since others who install the package will not have the same paths.
176+
The relative paths will be expanded into the absolute paths on the local machine. So it makes no sense to include them in a distribution, since others who install the package will not have the same paths.

pdm.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/pdm/backend/hooks/version/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def resolve_version_from_scm(
7575
write_to: str | None = None,
7676
write_template: str = "{}\n",
7777
tag_regex: str | None = None,
78+
tag_filter: str | None = None,
7879
version_format: str | None = None,
7980
fallback_version: str | None = None,
8081
) -> str:
@@ -88,7 +89,10 @@ def resolve_version_from_scm(
8889
else:
8990
version_formatter = None
9091
version = get_version_from_scm(
91-
context.root, tag_regex=tag_regex, version_formatter=version_formatter
92+
context.root,
93+
tag_regex=tag_regex,
94+
version_formatter=version_formatter,
95+
tag_filter=tag_filter,
9296
)
9397
if version is None:
9498
if fallback_version is not None:

src/pdm/backend/hooks/version/scm.py

Lines changed: 72 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from dataclasses import dataclass
1515
from datetime import datetime, timezone
1616
from pathlib import Path
17-
from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple
17+
from typing import TYPE_CHECKING, Callable, NamedTuple
1818

1919
from pdm.backend._vendor.packaging.version import Version
2020

@@ -29,6 +29,7 @@
2929
@dataclass(frozen=True)
3030
class Config:
3131
tag_regex: re.Pattern
32+
tag_filter: str | None
3233

3334

3435
def _subprocess_call(
@@ -164,28 +165,27 @@ def tag_to_version(config: Config, tag: str) -> Version:
164165
return Version(version)
165166

166167

167-
def tags_to_versions(config: Config, tags: Iterable[str]) -> list[Version]:
168-
"""
169-
take tags that might be prefixed with a keyword and return only the version part
170-
:param tags: an iterable of tags
171-
:param config: optional configuration object
172-
"""
173-
return [tag_to_version(config, tag) for tag in tags if tag]
174-
175-
176168
def git_parse_version(root: StrPath, config: Config) -> SCMVersion | None:
177-
GIT = shutil.which("git")
178-
if not GIT:
169+
git = shutil.which("git")
170+
if not git:
179171
return None
180172

181-
ret, repo, _ = _subprocess_call([GIT, "rev-parse", "--show-toplevel"], root)
173+
ret, repo, _ = _subprocess_call([git, "rev-parse", "--show-toplevel"], root)
182174
if ret or not repo:
183175
return None
184176

185177
if os.path.isfile(os.path.join(repo, ".git/shallow")):
186178
warnings.warn(f"{repo!r} is shallow and may cause errors")
187-
describe_cmd = [GIT, "describe", "--dirty", "--tags", "--long", "--match", "*.*"]
188-
ret, output, err = _subprocess_call(describe_cmd, repo)
179+
describe_cmd = [
180+
git,
181+
"describe",
182+
"--dirty",
183+
"--tags",
184+
"--long",
185+
"--match",
186+
config.tag_filter or "*.*",
187+
]
188+
ret, output, _ = _subprocess_call(describe_cmd, repo)
189189
branch = _git_get_branch(repo)
190190

191191
if ret:
@@ -201,54 +201,44 @@ def git_parse_version(root: StrPath, config: Config) -> SCMVersion | None:
201201
return meta(config, tag, number or None, dirty, node, branch)
202202

203203

204-
def get_latest_normalizable_tag(root: StrPath) -> str:
205-
# Gets all tags containing a '.' from oldest to newest
206-
cmd = [
207-
"hg",
208-
"log",
209-
"-r",
210-
"ancestors(.) and tag('re:\\.')",
211-
"--template",
212-
"{tags}\n",
213-
]
214-
_, output, _ = _subprocess_call(cmd, root)
215-
outlines = output.split()
216-
if not outlines:
217-
return "null"
218-
tag = outlines[-1].split()[-1]
219-
return tag
204+
def get_distance_revset(tag: str | None) -> str:
205+
return (
206+
"(branch(.)" # look for revisions in this branch only
207+
" and {rev}::." # after the last tag
208+
# ignore commits that only modify .hgtags and nothing else:
209+
" and (merge() or file('re:^(?!\\.hgtags).*$'))"
210+
" and not {rev})" # ignore the tagged commit itself
211+
).format(rev=f"tag({tag!r})" if tag is not None else "null")
220212

221213

222-
def hg_get_graph_distance(root: StrPath, rev1: str, rev2: str = ".") -> int:
223-
cmd = ["hg", "log", "-q", "-r", f"{rev1}::{rev2}"]
214+
def hg_get_graph_distance(root: StrPath, tag: str | None) -> int:
215+
cmd = ["hg", "log", "-q", "-r", get_distance_revset(tag)]
224216
_, out, _ = _subprocess_call(cmd, root)
225-
return len(out.strip().splitlines()) - 1
217+
return len(out.strip().splitlines())
226218

227219

228220
def _hg_tagdist_normalize_tagcommit(
229-
config: Config, root: StrPath, tag: str, dist: int, node: str, branch: str
221+
config: Config,
222+
root: StrPath,
223+
tag: str,
224+
dist: int,
225+
node: str,
226+
branch: str,
227+
dirty: bool,
230228
) -> SCMVersion:
231-
dirty = node.endswith("+")
232-
node = "h" + node.strip("+")
233-
234229
# Detect changes since the specified tag
235-
revset = (
236-
"(branch(.)" # look for revisions in this branch only
237-
" and tag({tag!r})::." # after the last tag
238-
# ignore commits that only modify .hgtags and nothing else:
239-
" and (merge() or file('re:^(?!\\.hgtags).*$'))"
240-
" and not tag({tag!r}))" # ignore the tagged commit itself
241-
).format(tag=tag)
242230
if tag != "0.0":
243231
_, commits, _ = _subprocess_call(
244-
["hg", "log", "-r", revset, "--template", "{node|short}"],
232+
["hg", "log", "-r", get_distance_revset(tag), "--template", "{node|short}"],
245233
root,
246234
)
247235
else:
248236
commits = "True"
249237

250238
if commits or dirty:
251-
return meta(config, tag, distance=dist, node=node, dirty=dirty, branch=branch)
239+
return meta(
240+
config, tag, distance=dist or None, node=node, dirty=dirty, branch=branch
241+
)
252242
else:
253243
return meta(config, tag)
254244

@@ -280,32 +270,40 @@ def _bump_regex(version: str) -> str:
280270

281271

282272
def hg_parse_version(root: StrPath, config: Config) -> SCMVersion | None:
283-
if not shutil.which("hg"):
273+
hg = shutil.which("hg")
274+
if not hg:
284275
return None
285-
_, output, _ = _subprocess_call("hg id -i -b -t", root)
286-
identity_data = output.split()
287-
if not identity_data:
288-
return None
289-
node = identity_data.pop(0)
290-
branch = identity_data.pop(0)
291-
if "tip" in identity_data:
292-
# tip is not a real tag
293-
identity_data.remove("tip")
294-
tags = tags_to_versions(config, identity_data)
295-
dirty = node[-1] == "+"
296-
if tags:
297-
return meta(config, tags[0], dirty=dirty, branch=branch)
298-
299-
if node.strip("+") == "0" * 12:
300-
return meta(config, "0.0", dirty=dirty, branch=branch)
301276

277+
tag_filter = config.tag_filter or "\\."
278+
_, output, _ = _subprocess_call(
279+
[
280+
hg,
281+
"log",
282+
"-r",
283+
".",
284+
"--template",
285+
f"{{latesttag(r're:{tag_filter}')}}-{{node|short}}-{{branch}}",
286+
],
287+
root,
288+
)
289+
tag: str | None
290+
tag, node, branch = output.rsplit("-", 2)
291+
# If no tag exists passes the tag filter.
292+
if tag == "null":
293+
tag = None
294+
295+
_, id_output, _ = _subprocess_call(
296+
[hg, "id", "-i"],
297+
root,
298+
)
299+
dirty = id_output.endswith("+")
302300
try:
303-
tag = get_latest_normalizable_tag(root)
304301
dist = hg_get_graph_distance(root, tag)
305-
if tag == "null":
302+
if tag is None:
306303
tag = "0.0"
307-
dist = int(dist) + 1
308-
return _hg_tagdist_normalize_tagcommit(config, root, tag, dist, node, branch)
304+
return _hg_tagdist_normalize_tagcommit(
305+
config, root, tag, dist, node, branch, dirty=dirty
306+
)
309307
except ValueError:
310308
return None # unpacking failed, old hg
311309

@@ -332,11 +330,15 @@ def get_version_from_scm(
332330
root: str | Path,
333331
*,
334332
tag_regex: str | None = None,
333+
tag_filter: str | None = None,
335334
version_formatter: Callable[[SCMVersion], str] | None = None,
336335
) -> str | None:
337-
config = Config(tag_regex=re.compile(tag_regex) if tag_regex else DEFAULT_TAG_REGEX)
336+
config = Config(
337+
tag_regex=re.compile(tag_regex) if tag_regex else DEFAULT_TAG_REGEX,
338+
tag_filter=tag_filter,
339+
)
338340
for func in (git_parse_version, hg_parse_version):
339-
version = func(root, config) # type: ignore
341+
version = func(root, config)
340342
if version:
341343
if version_formatter is None:
342344
version_formatter = format_version

src/pdm/backend/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def _build_filter(patterns: Iterable[str]) -> Callable[[str], bool]:
107107

108108

109109
@contextmanager
110-
def cd(path: str) -> Generator[None, None, None]:
110+
def cd(path: str | Path) -> Generator[None, None, None]:
111111
_old_cwd = os.getcwd()
112112
os.chdir(path)
113113
try:

0 commit comments

Comments
 (0)