Skip to content

Commit

Permalink
Add tag_filter as a way to pre-filter tags
Browse files Browse the repository at this point in the history
Fixes #219
  • Loading branch information
mezuzza committed Apr 29, 2024
1 parent 080b4bc commit 4a24fe1
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 81 deletions.
23 changes: 17 additions & 6 deletions docs/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,25 @@ Alternatively, you can specify a default version in the configuration:
fallback_version = "0.0.0"
```

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

```toml
[tool.pdm.version]
source = "scm"
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]*))?$)$'
tag_filter = "test/*"
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]*))?$)$'
```

`tag_filter` filters the set of tags which are considered as candidates to
capture your project's version. For `git` repositories, this field is a glob
matched against the tag. For `hg` repositories, it is a regular expression used
with the `latesttag` function.

`tag_regex` configures how you extract a version from a tag. It is applied after
`tag_filter` extracts candidate tags to extract the version from that tag. It is
a python style regular expression.

+++ 2.2.0

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

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

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

## Variables expansion

Expand Down Expand Up @@ -159,7 +170,7 @@ dependencies = [
```

!!! note
The triple slashes `///` is required for the compatibility of Windows and POSIX systems.
The triple slashes `///` is required for the compatibility of Windows and POSIX systems.

!!! note
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.
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.
6 changes: 3 additions & 3 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/pdm/backend/hooks/version/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def resolve_version_from_scm(
write_to: str | None = None,
write_template: str = "{}\n",
tag_regex: str | None = None,
tag_filter: str | None = None,
version_format: str | None = None,
fallback_version: str | None = None,
) -> str:
Expand All @@ -88,7 +89,10 @@ def resolve_version_from_scm(
else:
version_formatter = None
version = get_version_from_scm(
context.root, tag_regex=tag_regex, version_formatter=version_formatter
context.root,
tag_regex=tag_regex,
version_formatter=version_formatter,
tag_filter=tag_filter,
)
if version is None:
if fallback_version is not None:
Expand Down
142 changes: 72 additions & 70 deletions src/pdm/backend/hooks/version/scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple
from typing import TYPE_CHECKING, Callable, NamedTuple

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

Expand All @@ -29,6 +29,7 @@
@dataclass(frozen=True)
class Config:
tag_regex: re.Pattern
tag_filter: str | None


def _subprocess_call(
Expand Down Expand Up @@ -164,28 +165,27 @@ def tag_to_version(config: Config, tag: str) -> Version:
return Version(version)


def tags_to_versions(config: Config, tags: Iterable[str]) -> list[Version]:
"""
take tags that might be prefixed with a keyword and return only the version part
:param tags: an iterable of tags
:param config: optional configuration object
"""
return [tag_to_version(config, tag) for tag in tags if tag]


def git_parse_version(root: StrPath, config: Config) -> SCMVersion | None:
GIT = shutil.which("git")
if not GIT:
git = shutil.which("git")
if not git:
return None

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

if os.path.isfile(os.path.join(repo, ".git/shallow")):
warnings.warn(f"{repo!r} is shallow and may cause errors")
describe_cmd = [GIT, "describe", "--dirty", "--tags", "--long", "--match", "*.*"]
ret, output, err = _subprocess_call(describe_cmd, repo)
describe_cmd = [
git,
"describe",
"--dirty",
"--tags",
"--long",
"--match",
config.tag_filter or "*.*",
]
ret, output, _ = _subprocess_call(describe_cmd, repo)
branch = _git_get_branch(repo)

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


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


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


def _hg_tagdist_normalize_tagcommit(
config: Config, root: StrPath, tag: str, dist: int, node: str, branch: str
config: Config,
root: StrPath,
tag: str,
dist: int,
node: str,
branch: str,
dirty: bool,
) -> SCMVersion:
dirty = node.endswith("+")
node = "h" + node.strip("+")

# Detect changes since the specified tag
revset = (
"(branch(.)" # look for revisions in this branch only
" and tag({tag!r})::." # after the last tag
# ignore commits that only modify .hgtags and nothing else:
" and (merge() or file('re:^(?!\\.hgtags).*$'))"
" and not tag({tag!r}))" # ignore the tagged commit itself
).format(tag=tag)
if tag != "0.0":
_, commits, _ = _subprocess_call(
["hg", "log", "-r", revset, "--template", "{node|short}"],
["hg", "log", "-r", get_distance_revset(tag), "--template", "{node|short}"],
root,
)
else:
commits = "True"

if commits or dirty:
return meta(config, tag, distance=dist, node=node, dirty=dirty, branch=branch)
return meta(
config, tag, distance=dist or None, node=node, dirty=dirty, branch=branch
)
else:
return meta(config, tag)

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


def hg_parse_version(root: StrPath, config: Config) -> SCMVersion | None:
if not shutil.which("hg"):
hg = shutil.which("hg")
if not hg:
return None
_, output, _ = _subprocess_call("hg id -i -b -t", root)
identity_data = output.split()
if not identity_data:
return None
node = identity_data.pop(0)
branch = identity_data.pop(0)
if "tip" in identity_data:
# tip is not a real tag
identity_data.remove("tip")
tags = tags_to_versions(config, identity_data)
dirty = node[-1] == "+"
if tags:
return meta(config, tags[0], dirty=dirty, branch=branch)

if node.strip("+") == "0" * 12:
return meta(config, "0.0", dirty=dirty, branch=branch)

tag_filter = config.tag_filter or "\\."
_, output, _ = _subprocess_call(
[
hg,
"log",
"-r",
".",
"--template",
f"{{latesttag(r're:{tag_filter}')}}-{{node|short}}-{{branch}}",
],
root,
)
tag: str | None
tag, node, branch = output.rsplit("-", 2)
# If no tag exists passes the tag filter.
if tag == "null":
tag = None

_, id_output, _ = _subprocess_call(
[hg, "id", "-i"],
root,
)
dirty = id_output.endswith("+")
try:
tag = get_latest_normalizable_tag(root)
dist = hg_get_graph_distance(root, tag)
if tag == "null":
if tag is None:
tag = "0.0"
dist = int(dist) + 1
return _hg_tagdist_normalize_tagcommit(config, root, tag, dist, node, branch)
return _hg_tagdist_normalize_tagcommit(
config, root, tag, dist, node, branch, dirty=dirty
)
except ValueError:
return None # unpacking failed, old hg

Expand All @@ -332,11 +330,15 @@ def get_version_from_scm(
root: str | Path,
*,
tag_regex: str | None = None,
tag_filter: str | None = None,
version_formatter: Callable[[SCMVersion], str] | None = None,
) -> str | None:
config = Config(tag_regex=re.compile(tag_regex) if tag_regex else DEFAULT_TAG_REGEX)
config = Config(
tag_regex=re.compile(tag_regex) if tag_regex else DEFAULT_TAG_REGEX,
tag_filter=tag_filter,
)
for func in (git_parse_version, hg_parse_version):
version = func(root, config) # type: ignore
version = func(root, config)
if version:
if version_formatter is None:
version_formatter = format_version
Expand Down
2 changes: 1 addition & 1 deletion src/pdm/backend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _build_filter(patterns: Iterable[str]) -> Callable[[str], bool]:


@contextmanager
def cd(path: str) -> Generator[None, None, None]:
def cd(path: str | Path) -> Generator[None, None, None]:
_old_cwd = os.getcwd()
os.chdir(path)
try:
Expand Down
Loading

0 comments on commit 4a24fe1

Please sign in to comment.