vcs-versioning 2.2.3__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/CHANGELOG.md +47 -0
- {vcs_versioning-2.2.3/src/vcs_versioning.egg-info → vcs_versioning-2.3.0}/PKG-INFO +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/pyproject.toml +3 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_git.py +103 -4
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_hg.py +151 -33
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_hg_git.py +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_jj.py +91 -82
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_scm_workdir.py +64 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/__init__.py +0 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_config.py +3 -14
- vcs_versioning-2.3.0/src/vcs_versioning/_dynamic_metadata.py +49 -0
- vcs_versioning-2.3.0/src/vcs_versioning/_file_finders/_git.py +208 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/_hg.py +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/_jj.py +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_get_version_impl.py +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_legacy_parse.py +2 -2
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_paths.py +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_protocols.py +14 -6
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_run_cmd.py +3 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_test_utils.py +1 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_standard.py +2 -3
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/overrides.py +10 -4
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/test_api.py +14 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0/src/vcs_versioning.egg-info}/PKG-INFO +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/SOURCES.txt +4 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/entry_points.txt +3 -0
- vcs_versioning-2.3.0/testing_vcs/test_dynamic_metadata.py +49 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_git.py +91 -23
- vcs_versioning-2.3.0/testing_vcs/test_hg_tag_strict.py +206 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_jj.py +63 -4
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_mercurial.py +1 -1
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_tag_config.py +11 -2
- vcs_versioning-2.3.0/testing_vcs/test_tag_strict_diagnostics.py +293 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_version.py +1 -1
- vcs_versioning-2.2.3/src/vcs_versioning/_file_finders/_git.py +0 -122
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/LICENSE.txt +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/MANIFEST.in +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/README.md +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/setup.cfg +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/setup.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/__init__.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/__main__.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/__init__.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/_args.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_compat.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_discover.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_dump_version.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_entrypoints.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_environment.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_exceptions.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_fallback_workdir.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_fallbacks.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/__init__.py +3 -3
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_integration.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_integrator_helpers.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_log.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_modify_version.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_node_utils.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_overrides.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_project_overrides.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_pyproject_reading.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_requirement_cls.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_scm_metadata.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_scm_version.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_toml.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_types.py +3 -3
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_cls.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_fields.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_inference.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_common.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_worktree_discovery.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/py.typed +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/requires.txt +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/top_level.txt +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/__init__.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/conftest.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_better_root_errors.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_chain_api.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_compat.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_config.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_expect_parse.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_file_finders.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_hg_git.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_integrator_helpers.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_internal_log_level.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_legacy_parse.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_overrides_api.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_overrides_env_reader.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_project_overrides.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_project_path.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_regressions.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_scm_metadata.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_version_scheme_towncrier.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_version_schemes.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_workdir_api.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_workdir_discovery.py +0 -0
- {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/uv.lock +0 -0
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- towncrier release notes start -->
|
|
4
4
|
|
|
5
|
+
## 2.3.0 (2026-08-13)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add a ``vcs_versioning.dynamic_metadata`` provider for the [dynamic-metadata](https://github.com/scikit-build/dynamic-metadata) system. ([#1465](https://github.com/pypa/setuptools-scm/issues/1465))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Make the `tag.strict` and `scm.git.describe_command` diagnostics actionable and non-conflicting.
|
|
15
|
+
|
|
16
|
+
The `tag.strict` future-default notice is now reported by the git backend rather than at configuration time, and only when the future default would actually select a different tag for the repository -- the message names both the current and the future version string. Projects the change cannot affect are silent, and setting an explicit `describe_command` no longer triggers it at all, so the two warnings can no longer contradict each other.
|
|
17
|
+
|
|
18
|
+
The `describe_command` notice is likewise limited to the case where it and an explicit `tag.strict` really disagree, and no longer claims that `tag.prefix` has no effect -- prefix stripping applies regardless of how the tag was selected.
|
|
19
|
+
|
|
20
|
+
Both are logged at warning level instead of raised as warnings, so `SETUPTOOLS_SCM_DEBUG=ERROR` silences them. ([#1429](https://github.com/pypa/setuptools-scm/issues/1429))
|
|
21
|
+
- Honour `export-ignore` on directories and submodules again in the git file finder.
|
|
22
|
+
|
|
23
|
+
The switch from `git archive` to `git ls-files --recurse-submodules` lost two parts of the archive semantics: `--recurse-submodules` listed every submodule regardless of `export-ignore`, and the `:(exclude,attr:export-ignore)` pathspec only matches files, so an `export-ignore` on a directory no longer excluded the files below it. Projects that kept vendored submodules in an `export-ignore`d directory suddenly shipped them in their sdists.
|
|
24
|
+
|
|
25
|
+
The finder now lists a repository without recursion, checks `export-ignore` for directories via `git check-attr` (which is what `git archive` effectively does when it skips a tree), and only then descends into the submodules that survived. Submodule contents are still listed - with their own `.gitattributes` applied - so `export-ignore` in the parent repository now controls exactly which submodules get packaged. Submodules that are not checked out are skipped instead of failing the listing. ([#1469](https://github.com/pypa/setuptools-scm/issues/1469))
|
|
26
|
+
- Honour `tag.strict` on Mercurial changesets that carry tags of their own, and report the coming strict default for Mercurial repositories.
|
|
27
|
+
|
|
28
|
+
`tag.strict` was only applied when looking for the latest tag, so a checked-out changeset tagged `event-2024` still produced version `2024` even with `tag.strict = true`, while git rejected the same tag. Strict matching now applies to the tags on the changeset too: a changeset carrying only event-style tags is treated as untagged and versioning continues from the last real version tag, matching `git describe --match`. When several tags sit on one changeset, the version-shaped one is now selected instead of whichever Mercurial happened to list first.
|
|
29
|
+
|
|
30
|
+
The `tag.strict` divergence diagnostic added in #1429 now covers Mercurial as well, naming the current and future version whenever the coming default would change them. Both backends share the message, and the git-only helpers moved to `_backends/_scm_workdir.py`.
|
|
31
|
+
|
|
32
|
+
Note that the Mercurial backend required a dot in version tags before setuptools-scm 9, so for Mercurial projects the coming strict default restores the historical behavior. ([#1495](https://github.com/pypa/setuptools-scm/issues/1495))
|
|
33
|
+
|
|
34
|
+
## 2.2.4 (2026-08-07)
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Speed up jujutsu version inference in large repositories.
|
|
39
|
+
|
|
40
|
+
The jj backend filtered whole-ancestry revsets with `empty()`, which makes
|
|
41
|
+
jj diff every commit in the history and took minutes on large repos. The
|
|
42
|
+
scan for the newest real commit is now bounded to the most recent
|
|
43
|
+
generations, distances are counted without the filter, and the working copy
|
|
44
|
+
is snapshotted only once per run (later queries pass
|
|
45
|
+
`--ignore-working-copy`).
|
|
46
|
+
|
|
47
|
+
As a side effect distances now match `git describe --long`: merge commits
|
|
48
|
+
count even though jj considers them empty. ([#1477](https://github.com/pypa/setuptools-scm/issues/1477))
|
|
49
|
+
- Improve reproducibility of scm_file_list.json ([#1488](https://github.com/pypa/setuptools-scm/issues/1488))
|
|
50
|
+
- Route the hg-git tag mismatch warning through the module logger instead of the root logger. ([#1489](https://github.com/pypa/setuptools-scm/issues/1489))
|
|
51
|
+
|
|
5
52
|
## 2.2.3 (2026-07-28)
|
|
6
53
|
|
|
7
54
|
### Fixed
|
|
@@ -91,6 +91,9 @@ node-and-timestamp = "vcs_versioning._version_schemes:get_local_node_and_timesta
|
|
|
91
91
|
"semver-pep440-release-branch" = "vcs_versioning._version_schemes:release_branch_semver_version"
|
|
92
92
|
"towncrier-fragments" = "vcs_versioning._version_schemes._towncrier:version_from_fragments"
|
|
93
93
|
|
|
94
|
+
[project.entry-points."dynamic_metadata.provider"]
|
|
95
|
+
"vcs_versioning" = "vcs_versioning._dynamic_metadata"
|
|
96
|
+
|
|
94
97
|
[tool.setuptools.packages.find]
|
|
95
98
|
where = ["src"]
|
|
96
99
|
namespaces = false
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
+
import fnmatch
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
@@ -23,10 +24,17 @@ from .._run_cmd import CompletedProcess as _CompletedProcess
|
|
|
23
24
|
from .._run_cmd import require_command as _require_command
|
|
24
25
|
from .._run_cmd import run as _run
|
|
25
26
|
from .._scm_version import ScmVersion, meta, tag_to_version
|
|
26
|
-
from ._scm_workdir import
|
|
27
|
+
from ._scm_workdir import (
|
|
28
|
+
STRICT_DIAGNOSTIC,
|
|
29
|
+
Workdir,
|
|
30
|
+
config_location,
|
|
31
|
+
get_latest_file_mtime,
|
|
32
|
+
report_once,
|
|
33
|
+
version_outcome,
|
|
34
|
+
)
|
|
27
35
|
|
|
28
36
|
if TYPE_CHECKING:
|
|
29
|
-
from .._protocols import DescribeCapable
|
|
37
|
+
from .._protocols import DescribeCapable, GitQueryable
|
|
30
38
|
from . import _hg_git as hg_git
|
|
31
39
|
log = logging.getLogger(__name__)
|
|
32
40
|
|
|
@@ -74,12 +82,14 @@ def run_git(
|
|
|
74
82
|
*,
|
|
75
83
|
check: bool = False,
|
|
76
84
|
timeout: int | None = None,
|
|
85
|
+
input: str | None = None,
|
|
77
86
|
) -> _CompletedProcess:
|
|
78
87
|
return _run(
|
|
79
88
|
["git", "--git-dir", repo / ".git", *args],
|
|
80
89
|
cwd=repo,
|
|
81
90
|
check=check,
|
|
82
91
|
timeout=timeout,
|
|
92
|
+
input=input,
|
|
83
93
|
)
|
|
84
94
|
|
|
85
95
|
|
|
@@ -232,7 +242,10 @@ class GitWorkdir(Workdir):
|
|
|
232
242
|
def default_describe(self) -> _CompletedProcess:
|
|
233
243
|
match_glob = self.config.tag.describe_match_glob()
|
|
234
244
|
cmd = make_describe_command(match_glob)
|
|
235
|
-
|
|
245
|
+
res = self.run_git(cmd[1:])
|
|
246
|
+
if self.config.tag.strict is None:
|
|
247
|
+
_warn_if_strict_would_differ(self, self.config, res)
|
|
248
|
+
return res
|
|
236
249
|
|
|
237
250
|
def get_scm_version(self) -> ScmVersion | None:
|
|
238
251
|
"""Obtain version metadata from this git work directory."""
|
|
@@ -255,7 +268,7 @@ class GitWorkdir(Workdir):
|
|
|
255
268
|
git_files, git_dirs = _git_ls_files_and_dirs(
|
|
256
269
|
str(self.path), timeout=self._subprocess_timeout
|
|
257
270
|
)
|
|
258
|
-
return scm_find_files(base, git_files, git_dirs)
|
|
271
|
+
return sorted(scm_find_files(base, git_files, git_dirs))
|
|
259
272
|
|
|
260
273
|
def is_file_tracked(self, path: Path) -> bool:
|
|
261
274
|
res = self.run_git(
|
|
@@ -264,6 +277,90 @@ class GitWorkdir(Workdir):
|
|
|
264
277
|
return res.returncode == 0
|
|
265
278
|
|
|
266
279
|
|
|
280
|
+
def _describe_tag(output: str) -> str | None:
|
|
281
|
+
"""Extract just the tag name from a ``git describe --long`` output."""
|
|
282
|
+
if not output.strip():
|
|
283
|
+
return None
|
|
284
|
+
return _git_parse_describe(output.strip())[0]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _strict_match_glob(prefix: str) -> str:
|
|
288
|
+
"""The ``--match`` glob ``tag.strict = true`` would produce."""
|
|
289
|
+
return f"{prefix}*[0-9]*.*[0-9]*"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _describe_outcome(output: str, config: Configuration) -> str:
|
|
293
|
+
"""Render what a ``git describe`` *output* would yield as a version."""
|
|
294
|
+
if not output.strip():
|
|
295
|
+
return version_outcome(config, None)
|
|
296
|
+
|
|
297
|
+
tag, distance, node, dirty = _git_parse_describe(output.strip())
|
|
298
|
+
return version_outcome(config, tag, distance=distance, dirty=dirty, node=node)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _warn_if_strict_would_differ(
|
|
302
|
+
wd: GitQueryable, config: Configuration, permissive: _CompletedProcess
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Report the ``tag.strict`` future default only when it changes the answer.
|
|
305
|
+
|
|
306
|
+
``tag.strict`` is unset, so the permissive glob was used. Stay silent
|
|
307
|
+
unless the strict glob would pick a different tag, so that projects the
|
|
308
|
+
future default cannot affect are never nagged.
|
|
309
|
+
"""
|
|
310
|
+
permissive_tag = _describe_tag(permissive.stdout)
|
|
311
|
+
strict_glob = _strict_match_glob(config.tag.prefix)
|
|
312
|
+
|
|
313
|
+
if permissive_tag is None:
|
|
314
|
+
# nothing matched the wider glob, so nothing matches the narrower one
|
|
315
|
+
return
|
|
316
|
+
if fnmatch.fnmatchcase(permissive_tag, strict_glob):
|
|
317
|
+
# the strict glob matches a subset of the permissive one, so a
|
|
318
|
+
# permissive answer that is itself strict-matching is also the
|
|
319
|
+
# closest strict-matching tag -- no subprocess needed
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
strict = wd.run_git(make_describe_command(strict_glob)[1:])
|
|
323
|
+
report_once(
|
|
324
|
+
f"strict-divergence:{wd.path}:{permissive_tag}:{_describe_tag(strict.stdout)}",
|
|
325
|
+
STRICT_DIAGNOSTIC,
|
|
326
|
+
_describe_outcome(permissive.stdout, config),
|
|
327
|
+
_describe_outcome(strict.stdout, config),
|
|
328
|
+
config_location(config),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _warn_if_describe_command_overrides_strict(
|
|
333
|
+
wd: GitQueryable, config: Configuration, describe_res: _CompletedProcess
|
|
334
|
+
) -> None:
|
|
335
|
+
"""Report that ``describe_command`` beat an explicit ``tag.strict``.
|
|
336
|
+
|
|
337
|
+
Only fires when the two actually disagree about which tag to use --
|
|
338
|
+
setting both is harmless as long as they pick the same tag. ``tag.prefix``
|
|
339
|
+
is deliberately not mentioned: it still strips the prefix before version
|
|
340
|
+
parsing, so combining it with ``describe_command`` is legitimate.
|
|
341
|
+
"""
|
|
342
|
+
strict_glob = _strict_match_glob(config.tag.prefix)
|
|
343
|
+
strict = wd.run_git(make_describe_command(strict_glob)[1:])
|
|
344
|
+
strict_tag = _describe_tag(strict.stdout)
|
|
345
|
+
describe_tag = _describe_tag(describe_res.stdout)
|
|
346
|
+
if strict_tag == describe_tag:
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
report_once(
|
|
350
|
+
f"describe-overrides-strict:{wd.path}:{describe_tag}:{strict_tag}",
|
|
351
|
+
"scm.git.describe_command takes precedence over tag.strict, and they"
|
|
352
|
+
" disagree for this repository:\n"
|
|
353
|
+
" describe_command gives: %s\n"
|
|
354
|
+
" tag.strict = %s would give: %s\n"
|
|
355
|
+
"Drop tag.strict, or drop describe_command and let tag.prefix/tag.strict"
|
|
356
|
+
" build the match pattern, in %s.",
|
|
357
|
+
_describe_outcome(describe_res.stdout, config),
|
|
358
|
+
str(config.tag.strict).lower(),
|
|
359
|
+
_describe_outcome(strict.stdout, config),
|
|
360
|
+
config_location(config),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
267
364
|
def warn_on_shallow(wd: GitWorkdir) -> None:
|
|
268
365
|
"""experimental, may change at any time"""
|
|
269
366
|
if wd.is_shallow() and not wd.head_is_exact_tag():
|
|
@@ -410,6 +507,8 @@ def version_from_describe(
|
|
|
410
507
|
describe_res = wd.run_git(cmd_args[1:])
|
|
411
508
|
else:
|
|
412
509
|
describe_res = _run(cmd_args, wd.path, timeout=wd._subprocess_timeout)
|
|
510
|
+
if config.tag.strict is not None:
|
|
511
|
+
_warn_if_describe_command_overrides_strict(wd, config, describe_res)
|
|
413
512
|
else:
|
|
414
513
|
describe_res = wd.default_describe()
|
|
415
514
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import dataclasses
|
|
3
4
|
import datetime
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
@@ -15,12 +16,92 @@ from .._run_cmd import require_command as _require_command
|
|
|
15
16
|
from .._run_cmd import run as _run
|
|
16
17
|
from .._scm_version import ScmVersion, meta, tag_to_version
|
|
17
18
|
from .._version_cls import Version
|
|
18
|
-
from
|
|
19
|
+
from .._version_schemes import format_version
|
|
20
|
+
from ._scm_workdir import (
|
|
21
|
+
STRICT_DIAGNOSTIC,
|
|
22
|
+
Workdir,
|
|
23
|
+
config_location,
|
|
24
|
+
get_latest_file_mtime,
|
|
25
|
+
report_once,
|
|
26
|
+
)
|
|
19
27
|
|
|
20
28
|
log = logging.getLogger(__name__)
|
|
21
29
|
|
|
22
30
|
_HG_PSEUDO_TAGS = frozenset({"tip", "qbase", "qtip", "qparent"})
|
|
23
31
|
|
|
32
|
+
_KEEP: Any = object()
|
|
33
|
+
"""Sentinel for "use the configured value" -- ``None`` is a real strictness."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def hg_tag_pattern(config: Configuration, strict: bool | None = _KEEP) -> str:
|
|
37
|
+
"""Build the Mercurial regex ``latesttag()`` is given from tag config.
|
|
38
|
+
|
|
39
|
+
*strict* overrides ``config.tag.strict``, which the diagnostics use to ask
|
|
40
|
+
what the other setting would have selected.
|
|
41
|
+
"""
|
|
42
|
+
if strict is _KEEP:
|
|
43
|
+
strict = config.tag.strict
|
|
44
|
+
prefix = re.escape(config.tag.prefix) if config.tag.prefix else ""
|
|
45
|
+
if strict:
|
|
46
|
+
# Require at least one dot in the version part
|
|
47
|
+
return rf"{prefix}\d+\.\d+"
|
|
48
|
+
else:
|
|
49
|
+
return rf"{prefix}\d+"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def matches_tag_pattern(
|
|
53
|
+
tag: str, config: Configuration, strict: bool | None = _KEEP
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""Whether *tag* satisfies the configured strictness.
|
|
56
|
+
|
|
57
|
+
The pattern is the same one ``latesttag()`` is given on the distance path,
|
|
58
|
+
so both paths agree about which tags count as version tags.
|
|
59
|
+
"""
|
|
60
|
+
return re.search(hg_tag_pattern(config, strict), tag) is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def select_tag(
|
|
64
|
+
tags: list[str], config: Configuration, strict: bool | None = _KEEP
|
|
65
|
+
) -> str | None:
|
|
66
|
+
"""Pick the version tag to use from the tags on the current changeset.
|
|
67
|
+
|
|
68
|
+
Pre-filters with tag_regex so non-version tags are silently skipped
|
|
69
|
+
without emitting warnings from tag_to_version().
|
|
70
|
+
Strips tag.prefix before matching when configured.
|
|
71
|
+
|
|
72
|
+
Under ``tag.strict = true`` a tag that is not version-shaped is
|
|
73
|
+
rejected outright rather than falling back to a looser match, so a
|
|
74
|
+
changeset carrying only event-style tags falls through to the distance
|
|
75
|
+
path -- the same thing ``git describe --match`` does (#1495).
|
|
76
|
+
"""
|
|
77
|
+
if strict is _KEEP:
|
|
78
|
+
strict = config.tag.strict
|
|
79
|
+
tag_prefix = config.tag.prefix
|
|
80
|
+
for tag_str in tags:
|
|
81
|
+
check_str = tag_str
|
|
82
|
+
if tag_prefix and tag_str.startswith(tag_prefix):
|
|
83
|
+
check_str = tag_str[len(tag_prefix) :]
|
|
84
|
+
if not config.tag.regex.match(check_str):
|
|
85
|
+
log.debug("skipping non-version tag %r", tag_str)
|
|
86
|
+
continue
|
|
87
|
+
# only narrow when strictness was asked for, so the permissive
|
|
88
|
+
# path keeps selecting exactly what it always has
|
|
89
|
+
if strict and not matches_tag_pattern(tag_str, config, strict):
|
|
90
|
+
log.debug("skipping tag %r: not version-shaped under tag.strict", tag_str)
|
|
91
|
+
continue
|
|
92
|
+
if tag_to_version(tag_str, config) is not None:
|
|
93
|
+
return tag_str
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rendered(version: ScmVersion | None, tag: str | None) -> str:
|
|
98
|
+
"""Render one side of the ``tag.strict`` comparison for the diagnostic."""
|
|
99
|
+
if version is None:
|
|
100
|
+
return "no matching tag, falling back to the fallback version"
|
|
101
|
+
if tag is None:
|
|
102
|
+
return format_version(version)
|
|
103
|
+
return f"{format_version(version)} (from tag {tag!r})"
|
|
104
|
+
|
|
24
105
|
|
|
25
106
|
def _get_hg_command() -> str:
|
|
26
107
|
"""Read the hg command from resolved runtime settings.
|
|
@@ -95,12 +176,71 @@ class HgWorkdir(Workdir):
|
|
|
95
176
|
tags = self._parse_tags(tags_str)
|
|
96
177
|
|
|
97
178
|
# Try to get version from current tags
|
|
179
|
+
result: ScmVersion | None
|
|
98
180
|
tag_version = self._get_version_from_tags(tags, config)
|
|
99
181
|
if tag_version:
|
|
100
|
-
|
|
182
|
+
result = meta(tag_version, dirty=dirty, branch=branch, config=config)
|
|
183
|
+
else:
|
|
184
|
+
# Fall back to distance-based versioning
|
|
185
|
+
result = self._get_distance_based_version(
|
|
186
|
+
config, dirty, branch, node, node_date
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if config.tag.strict is None:
|
|
190
|
+
self._report_strict_divergence(config, tags, result)
|
|
191
|
+
return result
|
|
101
192
|
|
|
102
|
-
|
|
103
|
-
|
|
193
|
+
def _report_strict_divergence(
|
|
194
|
+
self, config: Configuration, tags: list[str], result: ScmVersion | None
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Report the coming ``tag.strict`` default when it changes the version.
|
|
197
|
+
|
|
198
|
+
Mirrors the git backend: stay silent unless strict matching would
|
|
199
|
+
select a different tag, so repositories the change cannot affect are
|
|
200
|
+
never nagged (#1495).
|
|
201
|
+
"""
|
|
202
|
+
permissive_tag = select_tag(tags, config, strict=False)
|
|
203
|
+
strict_tag = select_tag(tags, config, strict=True)
|
|
204
|
+
|
|
205
|
+
if permissive_tag is not None and permissive_tag == strict_tag:
|
|
206
|
+
# the changeset is tagged with a version-shaped tag either way
|
|
207
|
+
return
|
|
208
|
+
if permissive_tag is None and strict_tag is None:
|
|
209
|
+
# neither takes the exact-tag path, so compare the latest tags
|
|
210
|
+
permissive_tag = self.get_latest_normalizable_tag(config, strict=False)
|
|
211
|
+
if permissive_tag is None:
|
|
212
|
+
return
|
|
213
|
+
if matches_tag_pattern(permissive_tag, config, strict=True):
|
|
214
|
+
# the permissive answer is itself version-shaped, and strict
|
|
215
|
+
# matches a subset, so both paths land on the same tag
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# whichever side found no tag on this changeset falls through to the
|
|
219
|
+
# distance path, so name the tag it would count from there
|
|
220
|
+
if permissive_tag is None:
|
|
221
|
+
permissive_tag = self.get_latest_normalizable_tag(config, strict=False)
|
|
222
|
+
if strict_tag is None:
|
|
223
|
+
strict_tag = self.get_latest_normalizable_tag(config, strict=True)
|
|
224
|
+
|
|
225
|
+
strict_config = dataclasses.replace(
|
|
226
|
+
config, tag=dataclasses.replace(config.tag, strict=True)
|
|
227
|
+
)
|
|
228
|
+
strict_result = self.get_meta(strict_config)
|
|
229
|
+
if (
|
|
230
|
+
result is not None
|
|
231
|
+
and strict_result is not None
|
|
232
|
+
and format_version(result) == format_version(strict_result)
|
|
233
|
+
):
|
|
234
|
+
# different tags, same resulting version -- nothing to act on
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
report_once(
|
|
238
|
+
f"strict-divergence:{self.path}:{permissive_tag}:{strict_tag}",
|
|
239
|
+
STRICT_DIAGNOSTIC,
|
|
240
|
+
_rendered(result, permissive_tag),
|
|
241
|
+
_rendered(strict_result, strict_tag),
|
|
242
|
+
config_location(config),
|
|
243
|
+
)
|
|
104
244
|
|
|
105
245
|
def _get_node_info(self) -> tuple[str, str, str] | None:
|
|
106
246
|
"""Get node, tags, and date information from mercurial log."""
|
|
@@ -163,24 +303,11 @@ class HgWorkdir(Workdir):
|
|
|
163
303
|
def _get_version_from_tags(
|
|
164
304
|
self, tags: list[str], config: Configuration
|
|
165
305
|
) -> Version | None:
|
|
166
|
-
"""Try to get a version from the current tags.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"""
|
|
172
|
-
tag_prefix = config.tag.prefix
|
|
173
|
-
for tag_str in tags:
|
|
174
|
-
check_str = tag_str
|
|
175
|
-
if tag_prefix and tag_str.startswith(tag_prefix):
|
|
176
|
-
check_str = tag_str[len(tag_prefix) :]
|
|
177
|
-
if not config.tag.regex.match(check_str):
|
|
178
|
-
log.debug("skipping non-version tag %r", tag_str)
|
|
179
|
-
continue
|
|
180
|
-
version = tag_to_version(tag_str, config)
|
|
181
|
-
if version is not None:
|
|
182
|
-
return version
|
|
183
|
-
return None
|
|
306
|
+
"""Try to get a version from the current tags."""
|
|
307
|
+
tag_str = select_tag(tags, config)
|
|
308
|
+
if tag_str is None:
|
|
309
|
+
return None
|
|
310
|
+
return tag_to_version(tag_str, config)
|
|
184
311
|
|
|
185
312
|
def _get_distance_based_version(
|
|
186
313
|
self,
|
|
@@ -234,20 +361,11 @@ class HgWorkdir(Workdir):
|
|
|
234
361
|
check=True,
|
|
235
362
|
).stdout
|
|
236
363
|
|
|
237
|
-
def _hg_tag_pattern(self, config: Configuration) -> str:
|
|
238
|
-
"""Build a Mercurial regex pattern from tag configuration."""
|
|
239
|
-
prefix = re.escape(config.tag.prefix) if config.tag.prefix else ""
|
|
240
|
-
if config.tag.strict:
|
|
241
|
-
# Require at least one dot in the version part
|
|
242
|
-
return rf"{prefix}\d+\.\d+"
|
|
243
|
-
else:
|
|
244
|
-
return rf"{prefix}\d+"
|
|
245
|
-
|
|
246
364
|
def get_latest_normalizable_tag(
|
|
247
|
-
self, config: Configuration | None = None
|
|
365
|
+
self, config: Configuration | None = None, strict: bool | None = _KEEP
|
|
248
366
|
) -> str | None:
|
|
249
367
|
if config is not None:
|
|
250
|
-
pattern =
|
|
368
|
+
pattern = hg_tag_pattern(config, strict)
|
|
251
369
|
else:
|
|
252
370
|
pattern = r"\."
|
|
253
371
|
result = self.hg_log(
|
|
@@ -191,7 +191,7 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
|
|
|
191
191
|
tag = hg_tag
|
|
192
192
|
break
|
|
193
193
|
else:
|
|
194
|
-
|
|
194
|
+
log.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
|
|
195
195
|
return _FAKE_GIT_DESCRIBE_ERROR
|
|
196
196
|
|
|
197
197
|
res = self.run_hg(["log", "-r", f"'{tag}'::.", "-T", "."])
|