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.
Files changed (103) hide show
  1. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/CHANGELOG.md +47 -0
  2. {vcs_versioning-2.2.3/src/vcs_versioning.egg-info → vcs_versioning-2.3.0}/PKG-INFO +1 -1
  3. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/pyproject.toml +3 -0
  4. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_git.py +103 -4
  5. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_hg.py +151 -33
  6. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_hg_git.py +1 -1
  7. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_jj.py +91 -82
  8. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_scm_workdir.py +64 -1
  9. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/__init__.py +0 -1
  10. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_config.py +3 -14
  11. vcs_versioning-2.3.0/src/vcs_versioning/_dynamic_metadata.py +49 -0
  12. vcs_versioning-2.3.0/src/vcs_versioning/_file_finders/_git.py +208 -0
  13. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/_hg.py +1 -1
  14. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/_jj.py +1 -1
  15. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_get_version_impl.py +1 -1
  16. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_legacy_parse.py +2 -2
  17. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_paths.py +1 -1
  18. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_protocols.py +14 -6
  19. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_run_cmd.py +3 -0
  20. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_test_utils.py +1 -0
  21. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_standard.py +2 -3
  22. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/overrides.py +10 -4
  23. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/test_api.py +14 -0
  24. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0/src/vcs_versioning.egg-info}/PKG-INFO +1 -1
  25. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/SOURCES.txt +4 -0
  26. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/entry_points.txt +3 -0
  27. vcs_versioning-2.3.0/testing_vcs/test_dynamic_metadata.py +49 -0
  28. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_git.py +91 -23
  29. vcs_versioning-2.3.0/testing_vcs/test_hg_tag_strict.py +206 -0
  30. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_jj.py +63 -4
  31. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_mercurial.py +1 -1
  32. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_tag_config.py +11 -2
  33. vcs_versioning-2.3.0/testing_vcs/test_tag_strict_diagnostics.py +293 -0
  34. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_version.py +1 -1
  35. vcs_versioning-2.2.3/src/vcs_versioning/_file_finders/_git.py +0 -122
  36. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/LICENSE.txt +0 -0
  37. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/MANIFEST.in +0 -0
  38. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/README.md +0 -0
  39. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/setup.cfg +0 -0
  40. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/setup.py +0 -0
  41. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/__init__.py +0 -0
  42. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/__main__.py +0 -0
  43. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/__init__.py +0 -0
  44. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
  45. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/_args.py +0 -0
  46. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
  47. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
  48. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_compat.py +0 -0
  49. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_discover.py +0 -0
  50. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_dump_version.py +0 -0
  51. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_entrypoints.py +0 -0
  52. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_environment.py +0 -0
  53. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_exceptions.py +0 -0
  54. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_fallback_workdir.py +0 -0
  55. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_fallbacks.py +0 -0
  56. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/__init__.py +3 -3
  57. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_integration.py +0 -0
  58. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_integrator_helpers.py +0 -0
  59. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_log.py +0 -0
  60. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_modify_version.py +0 -0
  61. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_node_utils.py +0 -0
  62. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_overrides.py +0 -0
  63. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_project_overrides.py +0 -0
  64. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_pyproject_reading.py +0 -0
  65. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_requirement_cls.py +0 -0
  66. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_scm_metadata.py +0 -0
  67. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_scm_version.py +0 -0
  68. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_toml.py +0 -0
  69. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_types.py +3 -3
  70. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_cls.py +0 -0
  71. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_fields.py +0 -0
  72. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_inference.py +0 -0
  73. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
  74. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_common.py +0 -0
  75. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
  76. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/_worktree_discovery.py +0 -0
  77. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning/py.typed +0 -0
  78. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
  79. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/requires.txt +0 -0
  80. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/top_level.txt +0 -0
  81. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/__init__.py +0 -0
  82. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/conftest.py +0 -0
  83. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_better_root_errors.py +0 -0
  84. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_chain_api.py +0 -0
  85. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_compat.py +0 -0
  86. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_config.py +0 -0
  87. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_expect_parse.py +0 -0
  88. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_file_finders.py +0 -0
  89. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_hg_git.py +0 -0
  90. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_integrator_helpers.py +0 -0
  91. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_internal_log_level.py +0 -0
  92. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_legacy_parse.py +0 -0
  93. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_overrides_api.py +0 -0
  94. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_overrides_env_reader.py +0 -0
  95. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_project_overrides.py +0 -0
  96. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_project_path.py +0 -0
  97. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_regressions.py +0 -0
  98. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_scm_metadata.py +0 -0
  99. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_version_scheme_towncrier.py +0 -0
  100. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_version_schemes.py +0 -0
  101. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_workdir_api.py +0 -0
  102. {vcs_versioning-2.2.3 → vcs_versioning-2.3.0}/testing_vcs/test_workdir_discovery.py +0 -0
  103. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: the blessed package to manage your versions by vcs metadata
5
5
  Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
6
6
  License: MIT
@@ -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 Workdir, get_latest_file_mtime
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
- return self.run_git(cmd[1:])
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 ._scm_workdir import Workdir, get_latest_file_mtime
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
- return meta(tag_version, dirty=dirty, branch=branch, config=config)
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
- # Fall back to distance-based versioning
103
- return self._get_distance_based_version(config, dirty, branch, node, node_date)
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
- Pre-filters with tag_regex so non-version tags are silently skipped
169
- without emitting warnings from tag_to_version().
170
- Strips tag.prefix before matching when configured.
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 = self._hg_tag_pattern(config)
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
- logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
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", "."])