python-semantic-release 10.4.1__py3-none-any.whl → 10.5.1__py3-none-any.whl
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.
- {python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/METADATA +11 -9
- {python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/RECORD +15 -15
- semantic_release/changelog/context.py +1 -1
- semantic_release/cli/commands/version.py +76 -10
- semantic_release/cli/config.py +4 -1
- semantic_release/cli/masking_filter.py +1 -3
- semantic_release/errors.py +16 -0
- semantic_release/gitproject.py +187 -20
- semantic_release/version/declarations/pattern.py +2 -2
- semantic_release/version/translator.py +23 -5
- semantic_release/version/version.py +9 -0
- {python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/WHEEL +0 -0
- {python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/entry_points.txt +0 -0
- {python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/licenses/LICENSE +0 -0
- {python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/top_level.txt +0 -0
{python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/METADATA
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-semantic-release
|
|
3
|
-
Version: 10.
|
|
3
|
+
Version: 10.5.1
|
|
4
4
|
Summary: Automatic Semantic Versioning for Python projects
|
|
5
|
-
Author-email: Rolf Erik Lekang <me@rolflekang.com>
|
|
5
|
+
Author-email: Rolf Erik Lekang <me@rolflekang.com>, codejedi365 <codejedi365@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: changelog, https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md
|
|
8
8
|
Project-URL: documentation, https://python-semantic-release.readthedocs.io
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Requires-Python: ~=3.8
|
|
21
22
|
Description-Content-Type: text/x-rst
|
|
22
23
|
License-File: LICENSE
|
|
@@ -26,7 +27,7 @@ Requires-Dist: gitpython~=3.0
|
|
|
26
27
|
Requires-Dist: requests~=2.25
|
|
27
28
|
Requires-Dist: jinja2~=3.1
|
|
28
29
|
Requires-Dist: python-gitlab<7.0.0,>=4.0.0
|
|
29
|
-
Requires-Dist: tomlkit~=0.
|
|
30
|
+
Requires-Dist: tomlkit~=0.13.0
|
|
30
31
|
Requires-Dist: dotty-dict~=1.3
|
|
31
32
|
Requires-Dist: importlib-resources~=6.0
|
|
32
33
|
Requires-Dist: pydantic~=2.0
|
|
@@ -35,11 +36,12 @@ Requires-Dist: shellingham~=1.5
|
|
|
35
36
|
Requires-Dist: Deprecated~=1.2
|
|
36
37
|
Provides-Extra: build
|
|
37
38
|
Requires-Dist: build~=1.2; extra == "build"
|
|
39
|
+
Requires-Dist: tomlkit~=0.13.0; extra == "build"
|
|
38
40
|
Provides-Extra: docs
|
|
39
|
-
Requires-Dist: Sphinx~=
|
|
40
|
-
Requires-Dist: sphinxcontrib-apidoc==0.
|
|
41
|
+
Requires-Dist: Sphinx~=7.4; extra == "docs"
|
|
42
|
+
Requires-Dist: sphinxcontrib-apidoc==0.6.0; extra == "docs"
|
|
41
43
|
Requires-Dist: sphinx-autobuild==2024.2.4; extra == "docs"
|
|
42
|
-
Requires-Dist: furo~=
|
|
44
|
+
Requires-Dist: furo~=2025.9; extra == "docs"
|
|
43
45
|
Provides-Extra: test
|
|
44
46
|
Requires-Dist: coverage[toml]~=7.0; extra == "test"
|
|
45
47
|
Requires-Dist: filelock~=3.15; extra == "test"
|
|
@@ -48,9 +50,9 @@ Requires-Dist: freezegun~=1.5; extra == "test"
|
|
|
48
50
|
Requires-Dist: pyyaml~=6.0; extra == "test"
|
|
49
51
|
Requires-Dist: pytest~=8.3; extra == "test"
|
|
50
52
|
Requires-Dist: pytest-clarity~=1.0; extra == "test"
|
|
51
|
-
Requires-Dist: pytest-cov<
|
|
53
|
+
Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == "test"
|
|
52
54
|
Requires-Dist: pytest-env~=1.0; extra == "test"
|
|
53
|
-
Requires-Dist: pytest-lazy-fixtures~=1.
|
|
55
|
+
Requires-Dist: pytest-lazy-fixtures~=1.4; extra == "test"
|
|
54
56
|
Requires-Dist: pytest-mock~=3.0; extra == "test"
|
|
55
57
|
Requires-Dist: pytest-order~=1.3; extra == "test"
|
|
56
58
|
Requires-Dist: pytest-pretty~=1.2; extra == "test"
|
|
@@ -58,7 +60,7 @@ Requires-Dist: pytest-xdist~=3.0; extra == "test"
|
|
|
58
60
|
Requires-Dist: responses~=0.25.0; extra == "test"
|
|
59
61
|
Requires-Dist: requests-mock~=1.10; extra == "test"
|
|
60
62
|
Provides-Extra: dev
|
|
61
|
-
Requires-Dist: pre-commit~=3
|
|
63
|
+
Requires-Dist: pre-commit~=4.3; extra == "dev"
|
|
62
64
|
Requires-Dist: tox~=4.11; extra == "dev"
|
|
63
65
|
Requires-Dist: ruff==0.6.1; extra == "dev"
|
|
64
66
|
Provides-Extra: mypy
|
{python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/RECORD
RENAMED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
python_semantic_release-10.
|
|
1
|
+
python_semantic_release-10.5.1.dist-info/licenses/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
|
|
2
2
|
semantic_release/__init__.py,sha256=tRJWhrn_dUt0QycXD2DoJSfEP5uwmxngH7jvbG2i-hA,1317
|
|
3
3
|
semantic_release/__main__.py,sha256=pksxr6g1vkKq98Q1lShsxG8tk55IMiSMHzAHKyFU5x0,1704
|
|
4
4
|
semantic_release/const.py,sha256=wInJR7vcOgT1ysm5VuJQ6lD_ZGYnCwRVKz7Uz3htQc4,861
|
|
5
5
|
semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
|
|
6
|
-
semantic_release/errors.py,sha256=
|
|
7
|
-
semantic_release/gitproject.py,sha256=
|
|
6
|
+
semantic_release/errors.py,sha256=FyocaqHbRhux-iNmCf9nI7awyUaGKjG9_5C_QDvhEas,3399
|
|
7
|
+
semantic_release/gitproject.py,sha256=qF4GVZh-aaS4bVV3OmFQLmXMAxHNFyZCwUaWPtUmZ1k,16546
|
|
8
8
|
semantic_release/globals.py,sha256=IBhBbhZr2jx8dmpySnnu9m9jOGYu9Yu-vqHvAGQxgnw,464
|
|
9
9
|
semantic_release/helpers.py,sha256=2uQXOiuiemiviVD52SH2FF6cn14p_gqvREeHvP7dexw,10012
|
|
10
10
|
semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
|
|
12
|
-
semantic_release/changelog/context.py,sha256=
|
|
12
|
+
semantic_release/changelog/context.py,sha256=234LLcB_uuGaQ5N_zTbcQYF27ZwYIBmicIWkhFs7-aQ,5993
|
|
13
13
|
semantic_release/changelog/release_history.py,sha256=kX6D9VReq85wnHz0D6JpmJJCfWA6axV6RToSM0O5laU,10602
|
|
14
14
|
semantic_release/changelog/template.py,sha256=eqOjtVfBbksgTK4CAZOajfkaAcKueJ-FhFv99U3-J5E,5695
|
|
15
15
|
semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
semantic_release/cli/changelog_writer.py,sha256=2jP5b0LK61Y2tb22GQSFFmwHwXd2WjbopgsMAmsQsfY,9284
|
|
17
17
|
semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
|
|
18
|
-
semantic_release/cli/config.py,sha256=
|
|
18
|
+
semantic_release/cli/config.py,sha256=zNXlJHZBnFk2GEemgCg2Yw42EWihtFMot1twI7bEN-g,33668
|
|
19
19
|
semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
|
|
20
20
|
semantic_release/cli/github_actions_output.py,sha256=yC0nsMvEFGACjDwB8DdmGKwNGI8aIIhDxRHrmcS7tzA,5410
|
|
21
|
-
semantic_release/cli/masking_filter.py,sha256=
|
|
21
|
+
semantic_release/cli/masking_filter.py,sha256=7t7XFL7Iy4QTvaYevZ-jnTkJBz15GoBw1vjW3hihe38,3159
|
|
22
22
|
semantic_release/cli/util.py,sha256=4rf4xDHb7l-XpdcFNtslFz1xHslnBu6cTgWHXVnQXQs,3746
|
|
23
23
|
semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
semantic_release/cli/commands/changelog.py,sha256=wJfd4VVfrGnu2jnIpG25cdVcbXIX-ElKl3b2jdtPPa0,5391
|
|
25
25
|
semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
|
|
26
26
|
semantic_release/cli/commands/main.py,sha256=u1zhkkvKCZ2TtUqjzvdFTe5UZsvfws_pjqqo6CY0bBo,4351
|
|
27
27
|
semantic_release/cli/commands/publish.py,sha256=CE_LJTxFnc337MfpsfdJopi7QCwwE13GqGNQ-dNgWis,2871
|
|
28
|
-
semantic_release/cli/commands/version.py,sha256=
|
|
28
|
+
semantic_release/cli/commands/version.py,sha256=BvSEDFX5p8Mzl8gSBV0O0WjQn3TpD7zeS7gBLnyIjWQ,29424
|
|
29
29
|
semantic_release/commit_parser/__init__.py,sha256=uCC2YI-EDPUAa6TwWYJA8TS7baWE2bXrLEGcrhJKofI,1323
|
|
30
30
|
semantic_release/commit_parser/_base.py,sha256=DLsHnbXG-39JkUbcnsBCSV2GmV35w1rasyoMhK8G0UE,3058
|
|
31
31
|
semantic_release/commit_parser/angular.py,sha256=MY_fo9F4EZ-ac8wYzBR0uD94O5Li2D-8zEMR01wss4c,18534
|
|
@@ -70,15 +70,15 @@ semantic_release/hvcs/util.py,sha256=PUNV4yUlpzDtNCFmh2joaPdU4JyfUBnVp0zaQsT9EDQ
|
|
|
70
70
|
semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
|
|
71
71
|
semantic_release/version/algorithm.py,sha256=IxgYNF78W7qdMzdV4WsoljwfJgB-sn2XdfkevD0aZlo,16254
|
|
72
72
|
semantic_release/version/declaration.py,sha256=eot_lUyFaEhzK4bPncfv9tahf51LdxZP6EaS54h3aAs,3635
|
|
73
|
-
semantic_release/version/translator.py,sha256=
|
|
74
|
-
semantic_release/version/version.py,sha256=
|
|
73
|
+
semantic_release/version/translator.py,sha256=iIfu3WreB9qqPHgqJLILbBluVQQNcpP0DEsnn_WzAaM,3689
|
|
74
|
+
semantic_release/version/version.py,sha256=Y3Qqqv7CypirikT7jNqqFMNAvR2UjV-VQx-c2G0mZc0,14519
|
|
75
75
|
semantic_release/version/declarations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
76
|
semantic_release/version/declarations/enum.py,sha256=3n5Py9DoFkmItIdsmtQrJgmAhepTv_1EnogzSdXu1Wg,244
|
|
77
77
|
semantic_release/version/declarations/i_version_replacer.py,sha256=oP6BxJuxwI44roI6448tomShv1sMoy9ry8TlhhIQtfc,2416
|
|
78
|
-
semantic_release/version/declarations/pattern.py,sha256=
|
|
78
|
+
semantic_release/version/declarations/pattern.py,sha256=sKk0uQpJjWVZc8RJUjxQoEPUvFLxXNGGBow5h1IqCTM,8378
|
|
79
79
|
semantic_release/version/declarations/toml.py,sha256=2K4DtX5Qq1iHT8cG8mISPTMmp50w6Av0KmLAKZPYqq8,4931
|
|
80
|
-
python_semantic_release-10.
|
|
81
|
-
python_semantic_release-10.
|
|
82
|
-
python_semantic_release-10.
|
|
83
|
-
python_semantic_release-10.
|
|
84
|
-
python_semantic_release-10.
|
|
80
|
+
python_semantic_release-10.5.1.dist-info/METADATA,sha256=BMnYUZ_wcxmoCISQx404tvKP6cj-yrPjvXJjcN7RJ3s,4064
|
|
81
|
+
python_semantic_release-10.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
82
|
+
python_semantic_release-10.5.1.dist-info/entry_points.txt,sha256=kzkCyDJsMOwgpFwEWKE9wxN1tXaUP6g6GIO4xtc0QuE,162
|
|
83
|
+
python_semantic_release-10.5.1.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
|
|
84
|
+
python_semantic_release-10.5.1.dist-info/RECORD,,
|
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
import click
|
|
11
11
|
import shellingham # type: ignore[import]
|
|
12
12
|
from click_option_group import MutuallyExclusiveOptionGroup, optgroup
|
|
13
|
-
from git import Repo
|
|
13
|
+
from git import GitCommandError, Repo
|
|
14
14
|
from requests import HTTPError
|
|
15
15
|
|
|
16
16
|
from semantic_release.changelog.release_history import ReleaseHistory
|
|
@@ -27,9 +27,14 @@ from semantic_release.const import DEFAULT_SHELL, DEFAULT_VERSION
|
|
|
27
27
|
from semantic_release.enums import LevelBump
|
|
28
28
|
from semantic_release.errors import (
|
|
29
29
|
BuildDistributionsError,
|
|
30
|
+
DetachedHeadGitError,
|
|
30
31
|
GitCommitEmptyIndexError,
|
|
32
|
+
GitFetchError,
|
|
31
33
|
InternalError,
|
|
34
|
+
LocalGitError,
|
|
32
35
|
UnexpectedResponse,
|
|
36
|
+
UnknownUpstreamBranchError,
|
|
37
|
+
UpstreamBranchChangedError,
|
|
33
38
|
)
|
|
34
39
|
from semantic_release.gitproject import GitProject
|
|
35
40
|
from semantic_release.globals import logger
|
|
@@ -75,10 +80,13 @@ def is_forced_prerelease(
|
|
|
75
80
|
)
|
|
76
81
|
|
|
77
82
|
|
|
78
|
-
def last_released(
|
|
83
|
+
def last_released(
|
|
84
|
+
repo_dir: Path, tag_format: str, add_partial_tags: bool = False
|
|
85
|
+
) -> tuple[Tag, Version] | None:
|
|
79
86
|
with Repo(str(repo_dir)) as git_repo:
|
|
80
87
|
ts_and_vs = tags_and_versions(
|
|
81
|
-
git_repo.tags,
|
|
88
|
+
git_repo.tags,
|
|
89
|
+
VersionTranslator(tag_format=tag_format, add_partial_tags=add_partial_tags),
|
|
82
90
|
)
|
|
83
91
|
|
|
84
92
|
return ts_and_vs[0] if ts_and_vs else None
|
|
@@ -449,7 +457,11 @@ def version( # noqa: C901
|
|
|
449
457
|
if print_last_released or print_last_released_tag:
|
|
450
458
|
# TODO: get tag format a better way
|
|
451
459
|
if not (
|
|
452
|
-
last_release := last_released(
|
|
460
|
+
last_release := last_released(
|
|
461
|
+
config.repo_dir,
|
|
462
|
+
tag_format=config.tag_format,
|
|
463
|
+
add_partial_tags=config.add_partial_tags,
|
|
464
|
+
)
|
|
453
465
|
):
|
|
454
466
|
logger.warning("No release tags found.")
|
|
455
467
|
return
|
|
@@ -470,6 +482,7 @@ def version( # noqa: C901
|
|
|
470
482
|
major_on_zero = runtime.major_on_zero
|
|
471
483
|
no_verify = runtime.no_git_verify
|
|
472
484
|
opts = runtime.global_cli_options
|
|
485
|
+
add_partial_tags = config.add_partial_tags
|
|
473
486
|
gha_output = VersionGitHubActionsOutput(
|
|
474
487
|
gh_client=hvcs_client if isinstance(hvcs_client, Github) else None,
|
|
475
488
|
mode=(
|
|
@@ -491,6 +504,17 @@ def version( # noqa: C901
|
|
|
491
504
|
logger.info("Forcing use of %s as the prerelease token", prerelease_token)
|
|
492
505
|
translator.prerelease_token = prerelease_token
|
|
493
506
|
|
|
507
|
+
# Check if the repository is shallow and unshallow it if necessary
|
|
508
|
+
# This ensures we have the full history for commit analysis
|
|
509
|
+
project = GitProject(
|
|
510
|
+
directory=runtime.repo_dir,
|
|
511
|
+
commit_author=runtime.commit_author,
|
|
512
|
+
credential_masker=runtime.masker,
|
|
513
|
+
)
|
|
514
|
+
if project.is_shallow_clone():
|
|
515
|
+
logger.info("Repository is a shallow clone, converting to full clone...")
|
|
516
|
+
project.git_unshallow(noop=opts.noop)
|
|
517
|
+
|
|
494
518
|
# Only push if we're committing changes
|
|
495
519
|
if push_changes and not commit_changes and not create_tag:
|
|
496
520
|
logger.info("changes will not be pushed because --no-commit disables pushing")
|
|
@@ -683,12 +707,6 @@ def version( # noqa: C901
|
|
|
683
707
|
license_name="" if not isinstance(license_cfg, str) else license_cfg,
|
|
684
708
|
)
|
|
685
709
|
|
|
686
|
-
project = GitProject(
|
|
687
|
-
directory=runtime.repo_dir,
|
|
688
|
-
commit_author=runtime.commit_author,
|
|
689
|
-
credential_masker=runtime.masker,
|
|
690
|
-
)
|
|
691
|
-
|
|
692
710
|
# Preparing for committing changes; we always stage files even if we're not committing them in order to support a two-stage commit
|
|
693
711
|
project.git_add(paths=all_paths_to_add, noop=opts.noop)
|
|
694
712
|
if commit_changes:
|
|
@@ -727,6 +745,33 @@ def version( # noqa: C901
|
|
|
727
745
|
)
|
|
728
746
|
|
|
729
747
|
if commit_changes:
|
|
748
|
+
# Verify that the upstream branch has not changed before pushing
|
|
749
|
+
# This prevents conflicts if another commit was pushed while we were preparing the release
|
|
750
|
+
# We check HEAD~1 because we just made a release commit
|
|
751
|
+
try:
|
|
752
|
+
project.verify_upstream_unchanged(
|
|
753
|
+
local_ref="HEAD~1",
|
|
754
|
+
upstream_ref=config.remote.name,
|
|
755
|
+
noop=opts.noop,
|
|
756
|
+
)
|
|
757
|
+
except UpstreamBranchChangedError as exc:
|
|
758
|
+
click.echo(str(exc), err=True)
|
|
759
|
+
click.echo(
|
|
760
|
+
"Upstream branch has changed. Please pull the latest changes and try again.",
|
|
761
|
+
err=True,
|
|
762
|
+
)
|
|
763
|
+
ctx.exit(1)
|
|
764
|
+
except (
|
|
765
|
+
DetachedHeadGitError,
|
|
766
|
+
GitCommandError,
|
|
767
|
+
UnknownUpstreamBranchError,
|
|
768
|
+
GitFetchError,
|
|
769
|
+
LocalGitError,
|
|
770
|
+
) as exc:
|
|
771
|
+
click.echo(str(exc), err=True)
|
|
772
|
+
click.echo("Unable to verify upstream due to error!", err=True)
|
|
773
|
+
ctx.exit(1)
|
|
774
|
+
|
|
730
775
|
# TODO: integrate into push branch
|
|
731
776
|
with Repo(str(runtime.repo_dir)) as git_repo:
|
|
732
777
|
active_branch = git_repo.active_branch.name
|
|
@@ -744,6 +789,27 @@ def version( # noqa: C901
|
|
|
744
789
|
tag=new_version.as_tag(),
|
|
745
790
|
noop=opts.noop,
|
|
746
791
|
)
|
|
792
|
+
# Create or update partial tags for releases
|
|
793
|
+
if add_partial_tags and not prerelease:
|
|
794
|
+
partial_tags = [new_version.as_major_tag(), new_version.as_minor_tag()]
|
|
795
|
+
# If build metadata is set, also retag the version without the metadata
|
|
796
|
+
if build_metadata:
|
|
797
|
+
partial_tags.append(new_version.as_patch_tag())
|
|
798
|
+
|
|
799
|
+
for partial_tag in partial_tags:
|
|
800
|
+
project.git_tag(
|
|
801
|
+
tag_name=partial_tag,
|
|
802
|
+
message=f"{partial_tag} => {new_version.as_tag()}",
|
|
803
|
+
isotimestamp=commit_date.isoformat(),
|
|
804
|
+
noop=opts.noop,
|
|
805
|
+
force=True,
|
|
806
|
+
)
|
|
807
|
+
project.git_push_tag(
|
|
808
|
+
remote_url=remote_url,
|
|
809
|
+
tag=partial_tag,
|
|
810
|
+
noop=opts.noop,
|
|
811
|
+
force=True,
|
|
812
|
+
)
|
|
747
813
|
|
|
748
814
|
# Update GitHub Actions output value now that release has occurred
|
|
749
815
|
gha_output.released = True
|
semantic_release/cli/config.py
CHANGED
|
@@ -366,6 +366,7 @@ class RawConfig(BaseModel):
|
|
|
366
366
|
remote: RemoteConfig = RemoteConfig()
|
|
367
367
|
no_git_verify: bool = False
|
|
368
368
|
tag_format: str = "v{version}"
|
|
369
|
+
add_partial_tags: bool = False
|
|
369
370
|
publish: PublishConfig = PublishConfig()
|
|
370
371
|
version_toml: Optional[Tuple[str, ...]] = None
|
|
371
372
|
version_variables: Optional[Tuple[str, ...]] = None
|
|
@@ -827,7 +828,9 @@ class RuntimeContext:
|
|
|
827
828
|
|
|
828
829
|
# version_translator
|
|
829
830
|
version_translator = VersionTranslator(
|
|
830
|
-
tag_format=raw.tag_format,
|
|
831
|
+
tag_format=raw.tag_format,
|
|
832
|
+
prerelease_token=branch_config.prerelease_token,
|
|
833
|
+
add_partial_tags=raw.add_partial_tags,
|
|
831
834
|
)
|
|
832
835
|
|
|
833
836
|
build_cmd_env = {}
|
|
@@ -62,9 +62,7 @@ class MaskingFilter(LoggingFilter):
|
|
|
62
62
|
|
|
63
63
|
def mask(self, msg: str) -> str:
|
|
64
64
|
if not isinstance(msg, str):
|
|
65
|
-
logger.debug(
|
|
66
|
-
"cannot mask object of type %s", type(msg)
|
|
67
|
-
)
|
|
65
|
+
logger.debug("cannot mask object of type %s", type(msg))
|
|
68
66
|
return msg
|
|
69
67
|
for mask, values in self._redact_patterns.items():
|
|
70
68
|
repl_string = (
|
semantic_release/errors.py
CHANGED
|
@@ -106,3 +106,19 @@ class GitTagError(SemanticReleaseBaseError):
|
|
|
106
106
|
|
|
107
107
|
class GitPushError(SemanticReleaseBaseError):
|
|
108
108
|
"""Raised when there is a failure to push to the git remote."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class GitFetchError(SemanticReleaseBaseError):
|
|
112
|
+
"""Raised when there is a failure to fetch from the git remote."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class LocalGitError(SemanticReleaseBaseError):
|
|
116
|
+
"""Raised when there is a failure with local git operations."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class UnknownUpstreamBranchError(SemanticReleaseBaseError):
|
|
120
|
+
"""Raised when the upstream branch cannot be determined."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class UpstreamBranchChangedError(SemanticReleaseBaseError):
|
|
124
|
+
"""Raised when the upstream branch has changed before pushing."""
|
semantic_release/gitproject.py
CHANGED
|
@@ -12,11 +12,16 @@ from git import GitCommandError, Repo
|
|
|
12
12
|
from semantic_release.cli.masking_filter import MaskingFilter
|
|
13
13
|
from semantic_release.cli.util import indented, noop_report
|
|
14
14
|
from semantic_release.errors import (
|
|
15
|
+
DetachedHeadGitError,
|
|
15
16
|
GitAddError,
|
|
16
17
|
GitCommitEmptyIndexError,
|
|
17
18
|
GitCommitError,
|
|
19
|
+
GitFetchError,
|
|
18
20
|
GitPushError,
|
|
19
21
|
GitTagError,
|
|
22
|
+
LocalGitError,
|
|
23
|
+
UnknownUpstreamBranchError,
|
|
24
|
+
UpstreamBranchChangedError,
|
|
20
25
|
)
|
|
21
26
|
from semantic_release.globals import logger
|
|
22
27
|
|
|
@@ -85,6 +90,42 @@ class GitProject:
|
|
|
85
90
|
with Repo(str(self.project_root)) as repo:
|
|
86
91
|
return repo.is_dirty()
|
|
87
92
|
|
|
93
|
+
def is_shallow_clone(self) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Check if the repository is a shallow clone.
|
|
96
|
+
|
|
97
|
+
:return: True if the repository is a shallow clone, False otherwise
|
|
98
|
+
"""
|
|
99
|
+
with Repo(str(self.project_root)) as repo:
|
|
100
|
+
shallow_file = Path(repo.git_dir, "shallow")
|
|
101
|
+
return shallow_file.exists()
|
|
102
|
+
|
|
103
|
+
def git_unshallow(self, noop: bool = False) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Convert a shallow clone to a full clone by fetching the full history.
|
|
106
|
+
|
|
107
|
+
:param noop: Whether or not to actually run the unshallow command
|
|
108
|
+
"""
|
|
109
|
+
if noop:
|
|
110
|
+
noop_report("would have run:\n" " git fetch --unshallow")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
with Repo(str(self.project_root)) as repo:
|
|
114
|
+
try:
|
|
115
|
+
self.logger.info("Converting shallow clone to full clone...")
|
|
116
|
+
repo.git.fetch("--unshallow")
|
|
117
|
+
self.logger.info("Repository unshallowed successfully")
|
|
118
|
+
except GitCommandError as err:
|
|
119
|
+
# If the repository is already a full clone, git fetch --unshallow will fail
|
|
120
|
+
# with "fatal: --unshallow on a complete repository does not make sense"
|
|
121
|
+
# We can safely ignore this error by checking the stderr message
|
|
122
|
+
stderr = str(err.stderr) if err.stderr else ""
|
|
123
|
+
if "does not make sense" in stderr or "complete repository" in stderr:
|
|
124
|
+
self.logger.debug("Repository is already a full clone")
|
|
125
|
+
else:
|
|
126
|
+
self.logger.exception(str(err))
|
|
127
|
+
raise
|
|
128
|
+
|
|
88
129
|
def git_add(
|
|
89
130
|
self,
|
|
90
131
|
paths: Sequence[Path | str],
|
|
@@ -197,7 +238,12 @@ class GitProject:
|
|
|
197
238
|
raise GitCommitError("Failed to commit changes") from err
|
|
198
239
|
|
|
199
240
|
def git_tag(
|
|
200
|
-
self,
|
|
241
|
+
self,
|
|
242
|
+
tag_name: str,
|
|
243
|
+
message: str,
|
|
244
|
+
isotimestamp: str,
|
|
245
|
+
force: bool = False,
|
|
246
|
+
noop: bool = False,
|
|
201
247
|
) -> None:
|
|
202
248
|
try:
|
|
203
249
|
datetime.fromisoformat(isotimestamp)
|
|
@@ -207,21 +253,25 @@ class GitProject:
|
|
|
207
253
|
if noop:
|
|
208
254
|
command = str.join(
|
|
209
255
|
" ",
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
256
|
+
filter(
|
|
257
|
+
None,
|
|
258
|
+
[
|
|
259
|
+
f"GIT_COMMITTER_DATE={isotimestamp}",
|
|
260
|
+
*(
|
|
261
|
+
[
|
|
262
|
+
f"GIT_AUTHOR_NAME={self._commit_author.name}",
|
|
263
|
+
f"GIT_AUTHOR_EMAIL={self._commit_author.email}",
|
|
264
|
+
f"GIT_COMMITTER_NAME={self._commit_author.name}",
|
|
265
|
+
f"GIT_COMMITTER_EMAIL={self._commit_author.email}",
|
|
266
|
+
]
|
|
267
|
+
if self._commit_author
|
|
268
|
+
else [""]
|
|
269
|
+
),
|
|
270
|
+
f"git tag -a {tag_name} -m '{message}'",
|
|
271
|
+
"--force" if force else "",
|
|
272
|
+
],
|
|
273
|
+
),
|
|
274
|
+
).strip()
|
|
225
275
|
|
|
226
276
|
noop_report(
|
|
227
277
|
indented(
|
|
@@ -238,7 +288,7 @@ class GitProject:
|
|
|
238
288
|
{"GIT_COMMITTER_DATE": isotimestamp},
|
|
239
289
|
):
|
|
240
290
|
try:
|
|
241
|
-
repo.git.tag(
|
|
291
|
+
repo.git.tag(tag_name, a=True, m=message, force=force)
|
|
242
292
|
except GitCommandError as err:
|
|
243
293
|
self.logger.exception(str(err))
|
|
244
294
|
raise GitTagError(f"Failed to create tag ({tag_name})") from err
|
|
@@ -264,13 +314,15 @@ class GitProject:
|
|
|
264
314
|
f"Failed to push branch ({branch}) to remote"
|
|
265
315
|
) from err
|
|
266
316
|
|
|
267
|
-
def git_push_tag(
|
|
317
|
+
def git_push_tag(
|
|
318
|
+
self, remote_url: str, tag: str, noop: bool = False, force: bool = False
|
|
319
|
+
) -> None:
|
|
268
320
|
if noop:
|
|
269
321
|
noop_report(
|
|
270
322
|
indented(
|
|
271
323
|
f"""\
|
|
272
324
|
would have run:
|
|
273
|
-
git push {self._cred_masker.mask(remote_url)} tag {tag}
|
|
325
|
+
git push {self._cred_masker.mask(remote_url)} tag {tag} {"--force" if force else ""}
|
|
274
326
|
""" # noqa: E501
|
|
275
327
|
)
|
|
276
328
|
)
|
|
@@ -278,7 +330,122 @@ class GitProject:
|
|
|
278
330
|
|
|
279
331
|
with Repo(str(self.project_root)) as repo:
|
|
280
332
|
try:
|
|
281
|
-
repo.git.push(remote_url, "tag", tag)
|
|
333
|
+
repo.git.push(remote_url, "tag", tag, force=force)
|
|
282
334
|
except GitCommandError as err:
|
|
283
335
|
self.logger.exception(str(err))
|
|
284
336
|
raise GitPushError(f"Failed to push tag ({tag}) to remote") from err
|
|
337
|
+
|
|
338
|
+
def verify_upstream_unchanged( # noqa: C901
|
|
339
|
+
self, local_ref: str = "HEAD", upstream_ref: str = "origin", noop: bool = False
|
|
340
|
+
) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Verify that the upstream branch has not changed since the given local reference.
|
|
343
|
+
|
|
344
|
+
:param local_ref: The local reference to compare against upstream (default: HEAD)
|
|
345
|
+
:param upstream_ref: The name of the upstream remote or specific remote branch (default: origin)
|
|
346
|
+
:param noop: Whether to skip the actual verification (for dry-run mode)
|
|
347
|
+
|
|
348
|
+
:raises UpstreamBranchChangedError: If the upstream branch has changed
|
|
349
|
+
"""
|
|
350
|
+
if not local_ref.strip():
|
|
351
|
+
raise ValueError("Local reference cannot be empty")
|
|
352
|
+
if not upstream_ref.strip():
|
|
353
|
+
raise ValueError("Upstream reference cannot be empty")
|
|
354
|
+
|
|
355
|
+
if noop:
|
|
356
|
+
noop_report(
|
|
357
|
+
indented(
|
|
358
|
+
"""\
|
|
359
|
+
would have verified that upstream branch has not changed
|
|
360
|
+
"""
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
with Repo(str(self.project_root)) as repo:
|
|
366
|
+
# Get the current active branch
|
|
367
|
+
try:
|
|
368
|
+
active_branch = repo.active_branch
|
|
369
|
+
except TypeError:
|
|
370
|
+
# When in detached HEAD state, active_branch raises TypeError
|
|
371
|
+
err_msg = (
|
|
372
|
+
"Repository is in detached HEAD state, cannot verify upstream state"
|
|
373
|
+
)
|
|
374
|
+
raise DetachedHeadGitError(err_msg) from None
|
|
375
|
+
|
|
376
|
+
# Get the tracking branch (upstream branch)
|
|
377
|
+
if (tracking_branch := active_branch.tracking_branch()) is not None:
|
|
378
|
+
upstream_full_ref_name = tracking_branch.name
|
|
379
|
+
self.logger.info("Upstream branch name: %s", upstream_full_ref_name)
|
|
380
|
+
else:
|
|
381
|
+
# If no tracking branch is set, derive it
|
|
382
|
+
upstream_name = (
|
|
383
|
+
upstream_ref.strip()
|
|
384
|
+
if upstream_ref.find("/") == -1
|
|
385
|
+
else upstream_ref.strip().split("/", maxsplit=1)[0]
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if not repo.remotes or upstream_name not in repo.remotes:
|
|
389
|
+
err_msg = "No remote found; cannot verify upstream state!"
|
|
390
|
+
raise UnknownUpstreamBranchError(err_msg)
|
|
391
|
+
|
|
392
|
+
upstream_full_ref_name = (
|
|
393
|
+
f"{upstream_name}/{active_branch.name}"
|
|
394
|
+
if upstream_ref.find("/") == -1
|
|
395
|
+
else upstream_ref.strip()
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if upstream_full_ref_name not in repo.refs:
|
|
399
|
+
err_msg = f"No upstream branch found for '{active_branch.name}'; cannot verify upstream state!"
|
|
400
|
+
raise UnknownUpstreamBranchError(err_msg)
|
|
401
|
+
|
|
402
|
+
# Extract the remote name from the tracking branch
|
|
403
|
+
# tracking_branch.name is in the format "remote/branch"
|
|
404
|
+
remote_name, remote_branch_name = upstream_full_ref_name.split(
|
|
405
|
+
"/", maxsplit=1
|
|
406
|
+
)
|
|
407
|
+
remote_ref_obj = repo.remotes[remote_name]
|
|
408
|
+
|
|
409
|
+
# Fetch the latest changes from the remote
|
|
410
|
+
self.logger.info("Fetching latest changes from remote '%s'", remote_name)
|
|
411
|
+
try:
|
|
412
|
+
remote_ref_obj.fetch()
|
|
413
|
+
except GitCommandError as err:
|
|
414
|
+
self.logger.exception(str(err))
|
|
415
|
+
err_msg = f"Failed to fetch from remote '{remote_name}'"
|
|
416
|
+
raise GitFetchError(err_msg) from err
|
|
417
|
+
|
|
418
|
+
# Get the SHA of the upstream branch
|
|
419
|
+
try:
|
|
420
|
+
upstream_commit_ref = remote_ref_obj.refs[remote_branch_name].commit
|
|
421
|
+
upstream_sha = upstream_commit_ref.hexsha
|
|
422
|
+
except AttributeError as err:
|
|
423
|
+
self.logger.exception(str(err))
|
|
424
|
+
err_msg = f"Unable to determine upstream branch SHA for '{upstream_full_ref_name}'"
|
|
425
|
+
raise GitFetchError(err_msg) from err
|
|
426
|
+
|
|
427
|
+
# Get the SHA of the specified ref (default: HEAD)
|
|
428
|
+
try:
|
|
429
|
+
local_commit = repo.commit(repo.git.rev_parse(local_ref))
|
|
430
|
+
except GitCommandError as err:
|
|
431
|
+
self.logger.exception(str(err))
|
|
432
|
+
err_msg = f"Unable to determine the SHA for local ref '{local_ref}'"
|
|
433
|
+
raise LocalGitError(err_msg) from err
|
|
434
|
+
|
|
435
|
+
# Compare the two SHAs
|
|
436
|
+
if local_commit.hexsha != upstream_sha and not any(
|
|
437
|
+
commit.hexsha == upstream_sha for commit in local_commit.iter_parents()
|
|
438
|
+
):
|
|
439
|
+
err_msg = str.join(
|
|
440
|
+
"\n",
|
|
441
|
+
(
|
|
442
|
+
f"[LOCAL SHA] {local_commit.hexsha} != {upstream_sha} [UPSTREAM SHA].",
|
|
443
|
+
f"Upstream branch '{upstream_full_ref_name}' has changed!",
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
raise UpstreamBranchChangedError(err_msg)
|
|
447
|
+
|
|
448
|
+
self.logger.info(
|
|
449
|
+
"Verified upstream branch '%s' has not changed",
|
|
450
|
+
upstream_full_ref_name,
|
|
451
|
+
)
|
|
@@ -228,8 +228,8 @@ class PatternVersionDeclaration(IVersionReplacer):
|
|
|
228
228
|
# Negative lookbehind to ensure we don't match part of a variable name
|
|
229
229
|
f"""(?x)(?P<quote1>['"])?(?<![\\w.-]){regex_escape(variable)}(?P=quote1)?""",
|
|
230
230
|
# Supports walrus, equals sign, double-equals, colon, or @ as assignment operator
|
|
231
|
-
# ignoring whitespace separation
|
|
232
|
-
r"\s*(:=|==|[:=@])\s*",
|
|
231
|
+
# ignoring whitespace separation. Also allows a space as the separator for c-macro style definitions.
|
|
232
|
+
r"\s*(:=|==|[:=@ ])\s*",
|
|
233
233
|
# Supports optional matching quotations around a version pattern (tag or raw format)
|
|
234
234
|
f"""(?P<quote2>['"])?{value_replace_pattern_str}(?P=quote2)?""",
|
|
235
235
|
],
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from re import VERBOSE, compile as regexp, escape as regex_escape
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from semantic_release.const import SEMVER_REGEX
|
|
6
7
|
from semantic_release.globals import logger
|
|
7
8
|
from semantic_release.helpers import check_tag_format
|
|
8
9
|
from semantic_release.version.version import Version
|
|
9
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from re import Pattern
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class VersionTranslator:
|
|
12
16
|
"""
|
|
@@ -17,7 +21,7 @@ class VersionTranslator:
|
|
|
17
21
|
_VERSION_REGEX = SEMVER_REGEX
|
|
18
22
|
|
|
19
23
|
@classmethod
|
|
20
|
-
def _invert_tag_format_to_re(cls, tag_format: str) ->
|
|
24
|
+
def _invert_tag_format_to_re(cls, tag_format: str) -> Pattern[str]:
|
|
21
25
|
r"""
|
|
22
26
|
Unpick the "tag_format" format string and create a regex which can be used to
|
|
23
27
|
convert a tag to a version string.
|
|
@@ -31,9 +35,11 @@ class VersionTranslator:
|
|
|
31
35
|
>>> assert m is not None
|
|
32
36
|
>>> assert m.expand(r"\g<version>") == version
|
|
33
37
|
"""
|
|
34
|
-
pat =
|
|
35
|
-
tag_format.replace(
|
|
36
|
-
|
|
38
|
+
pat = regexp(
|
|
39
|
+
regex_escape(tag_format).replace(
|
|
40
|
+
regex_escape(r"{version}"), r"(?P<version>.+)"
|
|
41
|
+
),
|
|
42
|
+
flags=VERBOSE,
|
|
37
43
|
)
|
|
38
44
|
logger.debug("inverted tag_format %r to %r", tag_format, pat.pattern)
|
|
39
45
|
return pat
|
|
@@ -42,11 +48,19 @@ class VersionTranslator:
|
|
|
42
48
|
self,
|
|
43
49
|
tag_format: str = "v{version}",
|
|
44
50
|
prerelease_token: str = "rc", # noqa: S107
|
|
51
|
+
add_partial_tags: bool = False,
|
|
45
52
|
) -> None:
|
|
46
53
|
check_tag_format(tag_format)
|
|
47
54
|
self.tag_format = tag_format
|
|
48
55
|
self.prerelease_token = prerelease_token
|
|
56
|
+
self.add_partial_tags = add_partial_tags
|
|
49
57
|
self.from_tag_re = self._invert_tag_format_to_re(self.tag_format)
|
|
58
|
+
self.partial_tag_re = regexp(
|
|
59
|
+
regex_escape(tag_format).replace(
|
|
60
|
+
regex_escape(r"{version}"), r"[0-9]+(\.(0|[1-9][0-9]*))?$"
|
|
61
|
+
),
|
|
62
|
+
flags=VERBOSE,
|
|
63
|
+
)
|
|
50
64
|
|
|
51
65
|
def from_string(self, version_str: str) -> Version:
|
|
52
66
|
"""
|
|
@@ -69,6 +83,10 @@ class VersionTranslator:
|
|
|
69
83
|
tag_match = self.from_tag_re.match(tag)
|
|
70
84
|
if not tag_match:
|
|
71
85
|
return None
|
|
86
|
+
if self.add_partial_tags:
|
|
87
|
+
partial_tag_match = self.partial_tag_re.match(tag)
|
|
88
|
+
if partial_tag_match:
|
|
89
|
+
return None
|
|
72
90
|
raw_version_str = tag_match.group("version")
|
|
73
91
|
return self.from_string(raw_version_str)
|
|
74
92
|
|
|
@@ -203,6 +203,15 @@ class Version:
|
|
|
203
203
|
def as_tag(self) -> str:
|
|
204
204
|
return self.tag_format.format(version=str(self))
|
|
205
205
|
|
|
206
|
+
def as_major_tag(self) -> str:
|
|
207
|
+
return self.tag_format.format(version=f"{self.major}")
|
|
208
|
+
|
|
209
|
+
def as_minor_tag(self) -> str:
|
|
210
|
+
return self.tag_format.format(version=f"{self.major}.{self.minor}")
|
|
211
|
+
|
|
212
|
+
def as_patch_tag(self) -> str:
|
|
213
|
+
return self.tag_format.format(version=f"{self.major}.{self.minor}.{self.patch}")
|
|
214
|
+
|
|
206
215
|
def as_semver_tag(self) -> str:
|
|
207
216
|
return f"v{self!s}"
|
|
208
217
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_semantic_release-10.4.1.dist-info → python_semantic_release-10.5.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|