python-semantic-release 9.15.0__py3-none-any.whl → 9.15.2__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-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/METADATA +1 -1
- {python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/RECORD +20 -20
- semantic_release/__init__.py +1 -1
- semantic_release/__main__.py +8 -0
- semantic_release/changelog/template.py +2 -2
- semantic_release/cli/commands/main.py +11 -2
- semantic_release/cli/commands/version.py +1 -0
- semantic_release/cli/config.py +8 -4
- semantic_release/commit_parser/angular.py +6 -2
- semantic_release/commit_parser/emoji.py +2 -2
- semantic_release/commit_parser/util.py +11 -1
- semantic_release/data/templates/angular/md/.components/changes.md.j2 +1 -1
- semantic_release/enums.py +30 -0
- semantic_release/gitproject.py +50 -20
- semantic_release/version/algorithm.py +233 -308
- {python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/AUTHORS.rst +0 -0
- {python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/LICENSE +0 -0
- {python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/WHEEL +0 -0
- {python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/entry_points.txt +0 -0
- {python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/top_level.txt +0 -0
{python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/RECORD
RENAMED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
semantic_release/__init__.py,sha256=
|
|
2
|
-
semantic_release/__main__.py,sha256=
|
|
1
|
+
semantic_release/__init__.py,sha256=PaDXUxHfhYwLilGM9c0vX8F6Jooc8hMhPm3CL8UIX0M,1229
|
|
2
|
+
semantic_release/__main__.py,sha256=KOIBOvLruqfi5ArXcWK3ucIZ7NB55kfCbycJaxx6aQg,1485
|
|
3
3
|
semantic_release/const.py,sha256=Z1o2QNh60wSLeF-_1TemMBjU3ZXbV0XghnUFsbTVfOs,831
|
|
4
|
-
semantic_release/enums.py,sha256=
|
|
4
|
+
semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
|
|
5
5
|
semantic_release/errors.py,sha256=PY9rmviSFBZkqawW6VXbUfmF9C_RNOIObcmeGxLefMo,2904
|
|
6
|
-
semantic_release/gitproject.py,sha256=
|
|
6
|
+
semantic_release/gitproject.py,sha256=G4XrucN-ZwT1Kj4RMrABcr1vWb0bjKgurEeJjcL-61c,9422
|
|
7
7
|
semantic_release/globals.py,sha256=imI9WKGa6MS2pTRAZiWZ2qIJup2eWnBz3OZmIj2YIHM,158
|
|
8
8
|
semantic_release/helpers.py,sha256=d1jOX0SNyqPc_3wr14xR25FfpqhMd4Ev7MNBOWlScc0,5581
|
|
9
9
|
semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
|
|
11
11
|
semantic_release/changelog/context.py,sha256=jyVluJq8Vu6TyyzQQrsBIQRKm7kEnh1GZt8ObwibR5k,5374
|
|
12
12
|
semantic_release/changelog/release_history.py,sha256=nbd-WYVfQSWN4SKPPWEGU6QgNZLBrNxKKxfhPGKqnKc,8499
|
|
13
|
-
semantic_release/changelog/template.py,sha256=
|
|
13
|
+
semantic_release/changelog/template.py,sha256=O4EKXVJtN1z6FowcRUiZdZmi9u_TsTiXcHmYJnGyt94,5721
|
|
14
14
|
semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
semantic_release/cli/changelog_writer.py,sha256=RupwYqApOeAMidIvjCttnyyGNRxtLftmxDBmEu5azX0,9049
|
|
16
16
|
semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
|
|
17
|
-
semantic_release/cli/config.py,sha256=
|
|
17
|
+
semantic_release/cli/config.py,sha256=kb8r20PJ-oEJIS69zOQotbHcDpppsRuqwuazA7gQl0Y,30976
|
|
18
18
|
semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
|
|
19
19
|
semantic_release/cli/github_actions_output.py,sha256=6oNwjnQBg9XF5QgGc4TgbwX_-W0aj65VwGSL4ALvqVg,2296
|
|
20
20
|
semantic_release/cli/masking_filter.py,sha256=DxqjiJyABlzwwwZ1r8JGQpb6QrF00StJFm0-2-s5Fv0,3071
|
|
@@ -22,23 +22,23 @@ semantic_release/cli/util.py,sha256=FyXaBkeL7nXKjy3X9rQLEwvn7p46xPekp2V8Z-5MVrk,
|
|
|
22
22
|
semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
semantic_release/cli/commands/changelog.py,sha256=w_sXuFJHIqcueBQdeNeWn6fqbLVPPl_c-dEhv3Pb_BA,3870
|
|
24
24
|
semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
|
|
25
|
-
semantic_release/cli/commands/main.py,sha256=
|
|
25
|
+
semantic_release/cli/commands/main.py,sha256=237rn_Od4LOWfjUjiUKI_jSV820MfcCtRpwPjxjLbyU,4312
|
|
26
26
|
semantic_release/cli/commands/publish.py,sha256=y_LalPti_kZeQJzl2CR2pTZUK8DCMvNSTe4NaMC5TJA,2875
|
|
27
|
-
semantic_release/cli/commands/version.py,sha256=
|
|
27
|
+
semantic_release/cli/commands/version.py,sha256=vC4myixs_w8UPj58ZqJmmochl8RWUBmCbseoOEnVmD8,24220
|
|
28
28
|
semantic_release/commit_parser/__init__.py,sha256=cv5HFBdw7OJd4Laj4Ex8ZZ5Tml8GwXgQcXW6Pasr2Ao,615
|
|
29
29
|
semantic_release/commit_parser/_base.py,sha256=LAscBtS3_28jebRCeR-eGo3UtAsuxCWBzgb7FF4n4Vo,3046
|
|
30
|
-
semantic_release/commit_parser/angular.py,sha256=
|
|
31
|
-
semantic_release/commit_parser/emoji.py,sha256=
|
|
30
|
+
semantic_release/commit_parser/angular.py,sha256=zDYYOK1itsYJ0Ar7-cf29MnfrEpbQTeQCAcExWPH1fM,10486
|
|
31
|
+
semantic_release/commit_parser/emoji.py,sha256=6HtvKvJZwPAvY2fNOXU_gpKf1rGrMiDd0rYamyrdQZ8,10002
|
|
32
32
|
semantic_release/commit_parser/scipy.py,sha256=Fm_6WUaliLmqD397uVXwpOSDZ7LpMFu59oz-inKeHko,4526
|
|
33
33
|
semantic_release/commit_parser/tag.py,sha256=4uwIKBqUM2SE6UTGIw-a7B6Jg1OONXmGwXsTyL3yZBA,3490
|
|
34
34
|
semantic_release/commit_parser/token.py,sha256=RXdoCmKMTKMmUJGEUvfCiAOCPRrV0WubXojwa6FbvFg,7363
|
|
35
|
-
semantic_release/commit_parser/util.py,sha256=
|
|
35
|
+
semantic_release/commit_parser/util.py,sha256=KmJ-M-CJbc7q37GG7o595SmeBwrRrrnjP4q1C05jGYI,2463
|
|
36
36
|
semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=BzpatS7WYBcpHii8qiDemyI1ygXM6Q1nbxdcdcps__U,2107
|
|
37
37
|
semantic_release/data/templates/angular/md/CHANGELOG.md.j2,sha256=FZmrQ-qOIoSoJmAa_NFaRelfmqUpypU2xlDeScdGOf4,729
|
|
38
38
|
semantic_release/data/templates/angular/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
|
|
39
39
|
semantic_release/data/templates/angular/md/.components/changelog_init.md.j2,sha256=MyEQdWUemGXKWDhvpTmubZdozd3iaLECZAI1VRlE7mg,862
|
|
40
40
|
semantic_release/data/templates/angular/md/.components/changelog_update.md.j2,sha256=uVF4wbbjTMvl6Kbsq9xy3YIrj-uhBnHylEfA7S76aHI,2606
|
|
41
|
-
semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=
|
|
41
|
+
semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=AnF6sEsdaOeCS6E0_Q1PS430jufC1wg4ZMIJdMuBuNI,4000
|
|
42
42
|
semantic_release/data/templates/angular/md/.components/first_release.md.j2,sha256=jMUZiLwhMqAOjdYOCphJxJr8C411cKRzhsabyebj_AY,162
|
|
43
43
|
semantic_release/data/templates/angular/md/.components/macros.md.j2,sha256=GEql_lFHWqtfZUMYo8WT3E_YOTf0gvGhjqRQw0c7mlk,5890
|
|
44
44
|
semantic_release/data/templates/angular/md/.components/unreleased_changes.md.j2,sha256=HRLj6cyRfPZXC0s-0Av6s0Gp3jKxWg9AIEtIXBVqJuY,177
|
|
@@ -62,14 +62,14 @@ semantic_release/hvcs/remote_hvcs_base.py,sha256=cV8qYHtP47bmfIZqV4K2EiMHskFEoIo
|
|
|
62
62
|
semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARlg56hgU,663
|
|
63
63
|
semantic_release/hvcs/util.py,sha256=guxisysY_IW5tv7aaV-iVPEVJzgbOs375kiRRpSquTI,2879
|
|
64
64
|
semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
|
|
65
|
-
semantic_release/version/algorithm.py,sha256=
|
|
65
|
+
semantic_release/version/algorithm.py,sha256=UiJch0cpMxbuL-K_yaah8VF3PVy8mdwj1K8hArcwvdo,15183
|
|
66
66
|
semantic_release/version/declaration.py,sha256=f6Ld7hIhrqvDrRBapJHr-KDimuyo-4IG8009Zu9BIgU,7357
|
|
67
67
|
semantic_release/version/translator.py,sha256=P1noIsVBn8u6zNOFjG0xKYOWapxqf_PHSMvMeLJ9kXg,3050
|
|
68
68
|
semantic_release/version/version.py,sha256=6PCtSbLP88U1daoxnCwHc--YguZo4waGNLqJ5JfeczE,14175
|
|
69
|
-
python_semantic_release-9.15.
|
|
70
|
-
python_semantic_release-9.15.
|
|
71
|
-
python_semantic_release-9.15.
|
|
72
|
-
python_semantic_release-9.15.
|
|
73
|
-
python_semantic_release-9.15.
|
|
74
|
-
python_semantic_release-9.15.
|
|
75
|
-
python_semantic_release-9.15.
|
|
69
|
+
python_semantic_release-9.15.2.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
|
|
70
|
+
python_semantic_release-9.15.2.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
|
|
71
|
+
python_semantic_release-9.15.2.dist-info/METADATA,sha256=yD3qvMg3m175Z40HU3Gj5P8Z7z9Z8q4jhcwX3KS3JNM,3812
|
|
72
|
+
python_semantic_release-9.15.2.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
73
|
+
python_semantic_release-9.15.2.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
|
|
74
|
+
python_semantic_release-9.15.2.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
|
|
75
|
+
python_semantic_release-9.15.2.dist-info/RECORD,,
|
semantic_release/__init__.py
CHANGED
semantic_release/__main__.py
CHANGED
|
@@ -34,7 +34,15 @@ def main() -> None:
|
|
|
34
34
|
),
|
|
35
35
|
file=sys.stderr,
|
|
36
36
|
)
|
|
37
|
+
|
|
37
38
|
print(f"::ERROR:: {err}", file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
if not globals.debug:
|
|
41
|
+
print(
|
|
42
|
+
"Run semantic-release in very verbose mode (-vv) to see the full traceback.",
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
|
|
38
46
|
sys.exit(1)
|
|
39
47
|
|
|
40
48
|
|
|
@@ -124,9 +124,9 @@ def recursive_render(
|
|
|
124
124
|
# is used for inserting into a current changelog. When using stream rendering
|
|
125
125
|
# of the same file, it always came back empty
|
|
126
126
|
log.debug("rendering %s to %s", src_file_path, output_file_path)
|
|
127
|
-
rendered_file = environment.get_template(src_file_path).render()
|
|
127
|
+
rendered_file = environment.get_template(src_file_path).render().rstrip()
|
|
128
128
|
with open(output_file_path, "w", encoding="utf-8") as output_file:
|
|
129
|
-
output_file.write(rendered_file)
|
|
129
|
+
output_file.write(f"{rendered_file}\n")
|
|
130
130
|
|
|
131
131
|
rendered_paths.append(output_file_path)
|
|
132
132
|
else:
|
|
@@ -15,6 +15,7 @@ from semantic_release.cli.cli_context import CliContextObj
|
|
|
15
15
|
from semantic_release.cli.config import GlobalCommandLineOptions
|
|
16
16
|
from semantic_release.cli.const import DEFAULT_CONFIG_FILE
|
|
17
17
|
from semantic_release.cli.util import rprint
|
|
18
|
+
from semantic_release.enums import SemanticReleaseLogLevels
|
|
18
19
|
|
|
19
20
|
# if TYPE_CHECKING:
|
|
20
21
|
# pass
|
|
@@ -108,7 +109,15 @@ def main(
|
|
|
108
109
|
"""
|
|
109
110
|
console = Console(stderr=True)
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
log_levels = [
|
|
113
|
+
SemanticReleaseLogLevels.WARNING,
|
|
114
|
+
SemanticReleaseLogLevels.INFO,
|
|
115
|
+
SemanticReleaseLogLevels.DEBUG,
|
|
116
|
+
SemanticReleaseLogLevels.SILLY,
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
log_level = log_levels[verbosity]
|
|
120
|
+
|
|
112
121
|
logging.basicConfig(
|
|
113
122
|
level=log_level,
|
|
114
123
|
format=FORMAT,
|
|
@@ -123,7 +132,7 @@ def main(
|
|
|
123
132
|
logger = logging.getLogger(__name__)
|
|
124
133
|
logger.debug("logging level set to: %s", logging.getLevelName(log_level))
|
|
125
134
|
|
|
126
|
-
if log_level
|
|
135
|
+
if log_level <= logging.DEBUG:
|
|
127
136
|
globals.debug = True
|
|
128
137
|
|
|
129
138
|
if noop:
|
semantic_release/cli/config.py
CHANGED
|
@@ -360,9 +360,12 @@ class RawConfig(BaseModel):
|
|
|
360
360
|
try:
|
|
361
361
|
# Check for repository & walk up parent directories
|
|
362
362
|
with Repo(str(dir_path), search_parent_directories=True) as git_repo:
|
|
363
|
-
found_path =
|
|
364
|
-
git_repo.working_tree_dir or git_repo.working_dir
|
|
365
|
-
|
|
363
|
+
found_path = (
|
|
364
|
+
Path(git_repo.working_tree_dir or git_repo.working_dir)
|
|
365
|
+
.expanduser()
|
|
366
|
+
.absolute()
|
|
367
|
+
)
|
|
368
|
+
|
|
366
369
|
except InvalidGitRepositoryError as err:
|
|
367
370
|
raise InvalidGitRepositoryError("No valid git repository found!") from err
|
|
368
371
|
|
|
@@ -370,7 +373,8 @@ class RawConfig(BaseModel):
|
|
|
370
373
|
logging.warning(
|
|
371
374
|
"Found .git/ in higher parent directory rather than provided in configuration."
|
|
372
375
|
)
|
|
373
|
-
|
|
376
|
+
|
|
377
|
+
return found_path.resolve()
|
|
374
378
|
|
|
375
379
|
@field_validator("commit_parser", mode="after")
|
|
376
380
|
@classmethod
|
|
@@ -21,7 +21,11 @@ from semantic_release.commit_parser.token import (
|
|
|
21
21
|
ParseError,
|
|
22
22
|
ParseResult,
|
|
23
23
|
)
|
|
24
|
-
from semantic_release.commit_parser.util import
|
|
24
|
+
from semantic_release.commit_parser.util import (
|
|
25
|
+
breaking_re,
|
|
26
|
+
parse_paragraphs,
|
|
27
|
+
sort_numerically,
|
|
28
|
+
)
|
|
25
29
|
from semantic_release.enums import LevelBump
|
|
26
30
|
from semantic_release.errors import InvalidParserOptions
|
|
27
31
|
|
|
@@ -195,7 +199,7 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
195
199
|
predicate.split(","),
|
|
196
200
|
)
|
|
197
201
|
)
|
|
198
|
-
accumulator["linked_issues"] =
|
|
202
|
+
accumulator["linked_issues"] = sort_numerically(
|
|
199
203
|
set(accumulator["linked_issues"]).union(new_issue_refs)
|
|
200
204
|
)
|
|
201
205
|
# TODO: breaking change v10, removes resolution footers from descriptions
|
|
@@ -18,7 +18,7 @@ from semantic_release.commit_parser.token import (
|
|
|
18
18
|
ParsedMessageResult,
|
|
19
19
|
ParseResult,
|
|
20
20
|
)
|
|
21
|
-
from semantic_release.commit_parser.util import parse_paragraphs
|
|
21
|
+
from semantic_release.commit_parser.util import parse_paragraphs, sort_numerically
|
|
22
22
|
from semantic_release.enums import LevelBump
|
|
23
23
|
from semantic_release.errors import InvalidParserOptions
|
|
24
24
|
|
|
@@ -186,7 +186,7 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
186
186
|
predicate.split(","),
|
|
187
187
|
)
|
|
188
188
|
)
|
|
189
|
-
accumulator["linked_issues"] =
|
|
189
|
+
accumulator["linked_issues"] = sort_numerically(
|
|
190
190
|
set(accumulator["linked_issues"]).union(new_issue_refs)
|
|
191
191
|
)
|
|
192
192
|
# TODO: breaking change v10, removes resolution footers from descriptions
|
|
@@ -6,28 +6,34 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING: # pragma: no cover
|
|
8
8
|
from re import Pattern
|
|
9
|
-
from typing import TypedDict
|
|
9
|
+
from typing import Sequence, TypedDict
|
|
10
10
|
|
|
11
11
|
class RegexReplaceDef(TypedDict):
|
|
12
12
|
pattern: Pattern
|
|
13
13
|
repl: str
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
number_pattern = regexp(r"(\d+)")
|
|
17
|
+
|
|
16
18
|
breaking_re = regexp(r"BREAKING[ -]CHANGE:\s?(.*)")
|
|
19
|
+
|
|
17
20
|
un_word_wrap: RegexReplaceDef = {
|
|
18
21
|
# Match a line ending where the next line is not indented, or a bullet
|
|
19
22
|
"pattern": regexp(r"((?<!-)\n(?![\s*-]))"),
|
|
20
23
|
"repl": r" ", # Replace with a space
|
|
21
24
|
}
|
|
25
|
+
|
|
22
26
|
un_word_wrap_hyphen: RegexReplaceDef = {
|
|
23
27
|
"pattern": regexp(r"((?<=\w)-\n(?=\w))"),
|
|
24
28
|
"repl": r"-", # Replace with single hyphen
|
|
25
29
|
}
|
|
30
|
+
|
|
26
31
|
trim_line_endings: RegexReplaceDef = {
|
|
27
32
|
# Match line endings with optional whitespace
|
|
28
33
|
"pattern": regexp(r"[\r\t\f\v ]*\r?\n"),
|
|
29
34
|
"repl": "\n", # remove the optional whitespace & remove windows newlines
|
|
30
35
|
}
|
|
36
|
+
|
|
31
37
|
spread_out_git_footers: RegexReplaceDef = {
|
|
32
38
|
# Match a git footer line, and add an extra newline after it
|
|
33
39
|
# only be flexible enough for a double space indent (otherwise its probably on purpose)
|
|
@@ -65,3 +71,7 @@ def parse_paragraphs(text: str) -> list[str]:
|
|
|
65
71
|
],
|
|
66
72
|
)
|
|
67
73
|
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sort_numerically(iterable: Sequence[str] | set[str]) -> list[str]:
|
|
77
|
+
return sorted(iterable, key=lambda x: int((number_pattern.search(x) or [-1])[0]))
|
|
@@ -69,7 +69,7 @@ EXAMPLE:
|
|
|
69
69
|
#}{% if breaking_commits | length > 0
|
|
70
70
|
%}{# PREPROCESS COMMITS
|
|
71
71
|
#}{% set brk_ns = namespace(commits=breaking_commits)
|
|
72
|
-
%}{{
|
|
72
|
+
%}{{ apply_alphabetical_ordering_by_brk_descriptions(brk_ns) | default("", true)
|
|
73
73
|
}}{#
|
|
74
74
|
#}{% set brking_descriptions = []
|
|
75
75
|
%}{#
|
semantic_release/enums.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from enum import IntEnum, unique
|
|
4
5
|
|
|
5
6
|
|
|
@@ -37,3 +38,32 @@ class LevelBump(IntEnum):
|
|
|
37
38
|
>>> LevelBump.from_string("minor") == LevelBump.MINOR
|
|
38
39
|
"""
|
|
39
40
|
return cls[val.upper().replace("-", "_")]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SemanticReleaseLogLevels(IntEnum):
|
|
44
|
+
"""IntEnum representing the log levels used by semantic-release."""
|
|
45
|
+
|
|
46
|
+
FATAL = logging.FATAL
|
|
47
|
+
CRITICAL = logging.CRITICAL
|
|
48
|
+
ERROR = logging.ERROR
|
|
49
|
+
WARNING = logging.WARNING
|
|
50
|
+
INFO = logging.INFO
|
|
51
|
+
DEBUG = logging.DEBUG
|
|
52
|
+
SILLY = 5
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Return the level name rather than 'SemanticReleaseLogLevels.<level>'
|
|
57
|
+
E.g.
|
|
58
|
+
>>> str(SemanticReleaseLogLevels.DEBUG)
|
|
59
|
+
'DEBUG'
|
|
60
|
+
>>> str(SemanticReleaseLogLevels.CRITICAL)
|
|
61
|
+
'CRITICAL'
|
|
62
|
+
"""
|
|
63
|
+
return self.name.upper()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
logging.addLevelName(
|
|
67
|
+
SemanticReleaseLogLevels.SILLY,
|
|
68
|
+
str(SemanticReleaseLogLevels.SILLY),
|
|
69
|
+
)
|
semantic_release/gitproject.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from contextlib import nullcontext
|
|
6
|
+
from datetime import datetime
|
|
6
7
|
from logging import getLogger
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
@@ -48,7 +49,9 @@ class GitProject:
|
|
|
48
49
|
return self._logger
|
|
49
50
|
|
|
50
51
|
def _get_custom_environment(
|
|
51
|
-
self,
|
|
52
|
+
self,
|
|
53
|
+
repo: Repo,
|
|
54
|
+
custom_vars: dict[str, str] | None = None,
|
|
52
55
|
) -> nullcontext[None] | _GeneratorContextManager[None]:
|
|
53
56
|
"""
|
|
54
57
|
git.custom_environment is a context manager but
|
|
@@ -56,15 +59,26 @@ class GitProject:
|
|
|
56
59
|
we need to throw it away and re-create it in
|
|
57
60
|
order to use it again
|
|
58
61
|
"""
|
|
62
|
+
author_vars = (
|
|
63
|
+
{
|
|
64
|
+
"GIT_AUTHOR_NAME": self._commit_author.name,
|
|
65
|
+
"GIT_AUTHOR_EMAIL": self._commit_author.email,
|
|
66
|
+
"GIT_COMMITTER_NAME": self._commit_author.name,
|
|
67
|
+
"GIT_COMMITTER_EMAIL": self._commit_author.email,
|
|
68
|
+
}
|
|
69
|
+
if self._commit_author
|
|
70
|
+
else {}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
custom_env_vars = {
|
|
74
|
+
**author_vars,
|
|
75
|
+
**(custom_vars or {}),
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
return (
|
|
60
79
|
nullcontext()
|
|
61
|
-
if not
|
|
62
|
-
else repo.git.custom_environment(
|
|
63
|
-
GIT_AUTHOR_NAME=self._commit_author.name,
|
|
64
|
-
GIT_AUTHOR_EMAIL=self._commit_author.email,
|
|
65
|
-
GIT_COMMITTER_NAME=self._commit_author.name,
|
|
66
|
-
GIT_COMMITTER_EMAIL=self._commit_author.email,
|
|
67
|
-
)
|
|
80
|
+
if not custom_env_vars
|
|
81
|
+
else repo.git.custom_environment(**custom_env_vars)
|
|
68
82
|
)
|
|
69
83
|
|
|
70
84
|
def is_dirty(self) -> bool:
|
|
@@ -182,19 +196,32 @@ class GitProject:
|
|
|
182
196
|
self.logger.exception(str(err))
|
|
183
197
|
raise GitCommitError("Failed to commit changes") from err
|
|
184
198
|
|
|
185
|
-
def git_tag(
|
|
199
|
+
def git_tag(
|
|
200
|
+
self, tag_name: str, message: str, isotimestamp: str, noop: bool = False
|
|
201
|
+
) -> None:
|
|
202
|
+
try:
|
|
203
|
+
datetime.fromisoformat(isotimestamp)
|
|
204
|
+
except ValueError as err:
|
|
205
|
+
raise ValueError("Invalid timestamp format") from err
|
|
206
|
+
|
|
186
207
|
if noop:
|
|
187
|
-
command = (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
command = str.join(
|
|
209
|
+
" ",
|
|
210
|
+
[
|
|
211
|
+
f"GIT_COMMITTER_DATE={isotimestamp}",
|
|
212
|
+
*(
|
|
213
|
+
[
|
|
214
|
+
f"GIT_AUTHOR_NAME={self._commit_author.name}",
|
|
215
|
+
f"GIT_AUTHOR_EMAIL={self._commit_author.email}",
|
|
216
|
+
f"GIT_COMMITTER_NAME={self._commit_author.name}",
|
|
217
|
+
f"GIT_COMMITTER_EMAIL={self._commit_author.email}",
|
|
218
|
+
]
|
|
219
|
+
if self._commit_author
|
|
220
|
+
else [""]
|
|
221
|
+
),
|
|
222
|
+
f"git tag -a {tag_name} -m '{message}'",
|
|
223
|
+
],
|
|
196
224
|
)
|
|
197
|
-
command += f"git tag -a {tag_name} -m '{message}'"
|
|
198
225
|
|
|
199
226
|
noop_report(
|
|
200
227
|
indented(
|
|
@@ -206,7 +233,10 @@ class GitProject:
|
|
|
206
233
|
)
|
|
207
234
|
return
|
|
208
235
|
|
|
209
|
-
with Repo(str(self.project_root)) as repo, self._get_custom_environment(
|
|
236
|
+
with Repo(str(self.project_root)) as repo, self._get_custom_environment(
|
|
237
|
+
repo,
|
|
238
|
+
{"GIT_COMMITTER_DATE": isotimestamp},
|
|
239
|
+
):
|
|
210
240
|
try:
|
|
211
241
|
repo.git.tag("-a", tag_name, m=message)
|
|
212
242
|
except GitCommandError as err:
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from queue import LifoQueue
|
|
5
6
|
from typing import TYPE_CHECKING, Iterable
|
|
6
7
|
|
|
7
8
|
from semantic_release.commit_parser import ParsedCommit
|
|
8
9
|
from semantic_release.const import DEFAULT_VERSION
|
|
9
|
-
from semantic_release.enums import LevelBump
|
|
10
|
-
from semantic_release.errors import
|
|
11
|
-
from semantic_release.version.version import Version
|
|
10
|
+
from semantic_release.enums import LevelBump, SemanticReleaseLogLevels
|
|
11
|
+
from semantic_release.errors import InternalError, InvalidVersion
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING: # pragma: no cover
|
|
14
|
-
from
|
|
14
|
+
from typing import Sequence
|
|
15
|
+
|
|
15
16
|
from git.objects.commit import Commit
|
|
16
|
-
from git.objects.tag import TagObject
|
|
17
|
-
from git.objects.tree import Tree
|
|
18
17
|
from git.refs.tag import Tag
|
|
19
18
|
from git.repo.base import Repo
|
|
20
19
|
|
|
@@ -24,8 +23,10 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
24
23
|
ParserOptions,
|
|
25
24
|
)
|
|
26
25
|
from semantic_release.version.translator import VersionTranslator
|
|
26
|
+
from semantic_release.version.version import Version
|
|
27
|
+
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def tags_and_versions(
|
|
@@ -44,99 +45,78 @@ def tags_and_versions(
|
|
|
44
45
|
try:
|
|
45
46
|
version = translator.from_tag(tag.name)
|
|
46
47
|
except (NotImplementedError, InvalidVersion) as e:
|
|
47
|
-
|
|
48
|
+
logger.warning(
|
|
48
49
|
"Couldn't parse tag %s as as Version: %s",
|
|
49
50
|
tag.name,
|
|
50
51
|
str(e),
|
|
51
|
-
exc_info=
|
|
52
|
+
exc_info=logger.isEnabledFor(logging.DEBUG),
|
|
52
53
|
)
|
|
53
54
|
continue
|
|
54
55
|
|
|
55
56
|
if version:
|
|
56
57
|
ts_and_vs.append((tag, version))
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
logger.info("found %s previous tags", len(ts_and_vs))
|
|
59
60
|
return sorted(ts_and_vs, reverse=True, key=lambda v: v[1])
|
|
60
61
|
|
|
61
62
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
) ->
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
to a released version, return None
|
|
71
|
-
"""
|
|
72
|
-
tag_sha_2_version_lookup = {
|
|
73
|
-
tag.commit.hexsha: version for tag, version in full_release_tags_and_versions
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
# Step 3. Latest full release version within the history of the current branch
|
|
77
|
-
# Breadth-first search the merge-base and its parent commits for one which matches
|
|
78
|
-
# the tag of the latest full release tag in history
|
|
79
|
-
def bfs(start_commit: Commit | TagObject | Blob | Tree) -> Version | None:
|
|
80
|
-
# Derived from Geeks for Geeks
|
|
81
|
-
# https://www.geeksforgeeks.org/python-program-for-breadth-first-search-or-bfs-for-a-graph/?ref=lbp
|
|
82
|
-
|
|
83
|
-
# Create a queue for BFS
|
|
84
|
-
q: Queue[Commit | TagObject | Blob | Tree] = Queue()
|
|
63
|
+
def _traverse_graph_for_commits(
|
|
64
|
+
head_commit: Commit,
|
|
65
|
+
latest_release_tag_str: str = "",
|
|
66
|
+
) -> Sequence[Commit]:
|
|
67
|
+
# Depth-first search
|
|
68
|
+
def dfs(start_commit: Commit, stop_nodes: set[Commit]) -> Sequence[Commit]:
|
|
69
|
+
# Create a stack for DFS
|
|
70
|
+
stack: LifoQueue[Commit] = LifoQueue()
|
|
85
71
|
|
|
86
72
|
# Create a set to store visited graph nodes (commit objects in this case)
|
|
87
|
-
visited: set[Commit
|
|
88
|
-
|
|
89
|
-
# Add the source node in the queue & mark as visited to start the search
|
|
90
|
-
q.put(start_commit)
|
|
91
|
-
visited.add(start_commit)
|
|
73
|
+
visited: set[Commit] = set()
|
|
92
74
|
|
|
93
|
-
# Initialize the result
|
|
94
|
-
|
|
75
|
+
# Initialize the result
|
|
76
|
+
commits: list[Commit] = []
|
|
95
77
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
node = q.get()
|
|
99
|
-
visited.add(node)
|
|
78
|
+
# Add the source node in the queue to start the search
|
|
79
|
+
stack.put(start_commit)
|
|
100
80
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
log.info(
|
|
106
|
-
"found latest version in branch history: %r (%s)",
|
|
107
|
-
str(version),
|
|
108
|
-
node.hexsha[:7],
|
|
109
|
-
)
|
|
110
|
-
result = version
|
|
111
|
-
break
|
|
81
|
+
# Traverse the git history capturing each commit found before it reaches a stop node
|
|
82
|
+
while not stack.empty():
|
|
83
|
+
if (node := stack.get()) in visited or node in stop_nodes:
|
|
84
|
+
continue
|
|
112
85
|
|
|
113
|
-
|
|
86
|
+
visited.add(node)
|
|
87
|
+
commits.append(node)
|
|
114
88
|
|
|
115
|
-
# Add all parent commits to the
|
|
89
|
+
# Add all parent commits to the stack from left to right so that the rightmost is popped first
|
|
90
|
+
# as the left side is generally the merged into branch
|
|
116
91
|
for parent in node.parents:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
92
|
+
logger.debug("queuing parent commit %s", parent.hexsha[:7])
|
|
93
|
+
stack.put(parent)
|
|
94
|
+
|
|
95
|
+
return commits
|
|
96
|
+
|
|
97
|
+
# Run a Depth First Search to find all the commits since the last release
|
|
98
|
+
commits_since_last_release = dfs(
|
|
99
|
+
start_commit=head_commit,
|
|
100
|
+
stop_nodes=set(
|
|
101
|
+
head_commit.repo.iter_commits(latest_release_tag_str)
|
|
102
|
+
if latest_release_tag_str
|
|
103
|
+
else []
|
|
104
|
+
),
|
|
105
|
+
)
|
|
125
106
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
107
|
+
log_msg = (
|
|
108
|
+
f"Found {len(commits_since_last_release)} commits since the last release!"
|
|
109
|
+
if len(commits_since_last_release) > 0
|
|
110
|
+
else "No commits found since the last release!"
|
|
111
|
+
)
|
|
112
|
+
logger.info(log_msg)
|
|
132
113
|
|
|
133
|
-
return
|
|
114
|
+
return commits_since_last_release
|
|
134
115
|
|
|
135
116
|
|
|
136
117
|
def _increment_version(
|
|
137
118
|
latest_version: Version,
|
|
138
119
|
latest_full_version: Version,
|
|
139
|
-
latest_full_version_in_history: Version,
|
|
140
120
|
level_bump: LevelBump,
|
|
141
121
|
prerelease: bool,
|
|
142
122
|
prerelease_token: str,
|
|
@@ -147,23 +127,25 @@ def _increment_version(
|
|
|
147
127
|
Using the given versions, along with a given `level_bump`, increment to
|
|
148
128
|
the next version according to whether or not this is a prerelease.
|
|
149
129
|
|
|
150
|
-
`latest_version`, `latest_full_version` and `latest_full_version_in_history`
|
|
151
|
-
can be the same, but aren't necessarily.
|
|
152
|
-
|
|
153
130
|
`latest_version` is the most recent version released from this branch's history.
|
|
154
|
-
`latest_full_version
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
is in this branch's history.
|
|
131
|
+
`latest_full_version`, the most recent full release (i.e. not a prerelease)
|
|
132
|
+
in this branch's history.
|
|
133
|
+
|
|
134
|
+
`latest_version` and `latest_full_version` can be the same, but aren't necessarily.
|
|
159
135
|
"""
|
|
160
136
|
local_vars = list(locals().items())
|
|
161
|
-
log
|
|
137
|
+
logger.log(
|
|
138
|
+
SemanticReleaseLogLevels.SILLY,
|
|
139
|
+
"_increment_version: %s",
|
|
140
|
+
str.join(", ", [f"{k} = {v}" for k, v in local_vars]),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Handle variations where the latest version is 0.x.x
|
|
162
144
|
if latest_version.major == 0:
|
|
163
145
|
if not allow_zero_version:
|
|
164
146
|
# Set up default version to be 1.0.0 if currently 0.x.x which means a commented
|
|
165
147
|
# breaking change is not required to bump to 1.0.0
|
|
166
|
-
|
|
148
|
+
logger.debug(
|
|
167
149
|
"Bumping major version as 0.x.x versions are disabled because of allow_zero_version=False"
|
|
168
150
|
)
|
|
169
151
|
level_bump = LevelBump.MAJOR
|
|
@@ -173,93 +155,99 @@ def _increment_version(
|
|
|
173
155
|
# breaking changes should increment the minor digit
|
|
174
156
|
# Correspondingly, we reduce the level that we increment the
|
|
175
157
|
# version by.
|
|
176
|
-
|
|
158
|
+
logger.debug(
|
|
177
159
|
"reducing version increment due to 0. version and major_on_zero=False"
|
|
178
160
|
)
|
|
179
161
|
|
|
180
162
|
level_bump = min(level_bump, LevelBump.MINOR)
|
|
181
163
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
log.debug(
|
|
189
|
-
"diff between the latest version %s and the latest full release version %s "
|
|
190
|
-
"is: %s",
|
|
191
|
-
latest_version,
|
|
192
|
-
latest_full_version_in_history,
|
|
193
|
-
diff_with_last_released_version,
|
|
194
|
-
)
|
|
195
|
-
# 6a i) if the level_bump > the level bump introduced by any prerelease tag
|
|
196
|
-
# before e.g. 1.2.4-rc.3 -> 1.3.0-rc.1
|
|
197
|
-
if level_bump > diff_with_last_released_version:
|
|
198
|
-
log.debug(
|
|
199
|
-
"this release has a greater bump than any change since the last full "
|
|
200
|
-
"release, %s",
|
|
201
|
-
latest_full_version_in_history,
|
|
202
|
-
)
|
|
203
|
-
return target_final_version.bump(level_bump).to_prerelease(
|
|
204
|
-
token=prerelease_token
|
|
205
|
-
)
|
|
164
|
+
logger.debug(
|
|
165
|
+
"prerelease=%s and the latest version %s %s prerelease",
|
|
166
|
+
prerelease,
|
|
167
|
+
latest_version,
|
|
168
|
+
"is a" if latest_version.is_prerelease else "is not a",
|
|
169
|
+
)
|
|
206
170
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
214
|
-
log.debug("this release will increment the prerelease revision")
|
|
215
|
-
return latest_version.to_prerelease(
|
|
216
|
-
token=prerelease_token,
|
|
217
|
-
revision=(
|
|
218
|
-
1
|
|
219
|
-
if latest_version.prerelease_token != prerelease_token
|
|
220
|
-
else (latest_version.prerelease_revision or 0) + 1
|
|
221
|
-
),
|
|
171
|
+
if level_bump == LevelBump.NO_RELEASE:
|
|
172
|
+
raise ValueError("level_bump must be at least PRERELEASE_REVISION")
|
|
173
|
+
|
|
174
|
+
if level_bump == LevelBump.PRERELEASE_REVISION and not latest_version.is_prerelease:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"Cannot increment a non-prerelease version with a prerelease level bump"
|
|
222
177
|
)
|
|
223
178
|
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
179
|
+
# assume we always want to increment the version that is the latest in the branch's history
|
|
180
|
+
base_version = latest_version
|
|
181
|
+
|
|
182
|
+
# if the current version is a prerelease & we want a new prerelease, then
|
|
183
|
+
# figure out if we need to bump the prerelease revision or start a new prerelease
|
|
227
184
|
if latest_version.is_prerelease:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
log.debug(
|
|
235
|
-
"diff between the latest version %s and the latest full release version %s "
|
|
236
|
-
"is: %s",
|
|
185
|
+
# find the change since the last full release because if the current version is a prerelease
|
|
186
|
+
# then we need to predict properly the next full version
|
|
187
|
+
diff_with_last_released_version = latest_version - latest_full_version
|
|
188
|
+
logger.debug(
|
|
189
|
+
"the diff b/w the latest version '%s' and the latest full release version '%s' is: %s",
|
|
237
190
|
latest_version,
|
|
238
|
-
|
|
191
|
+
latest_full_version,
|
|
239
192
|
diff_with_last_released_version,
|
|
240
193
|
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
194
|
+
|
|
195
|
+
# Since the difference is less than or equal to the level bump and we want a new prerelease,
|
|
196
|
+
# we can abort early and just increment the revision
|
|
197
|
+
if level_bump <= diff_with_last_released_version:
|
|
198
|
+
# 6a ii) if level_bump <= the level bump introduced by the previous tag (latest_version)
|
|
199
|
+
if prerelease:
|
|
200
|
+
logger.debug(
|
|
201
|
+
"there has already been at least a %s release since the last full release %s",
|
|
202
|
+
level_bump,
|
|
203
|
+
latest_full_version,
|
|
204
|
+
)
|
|
205
|
+
logger.debug("Incrementing the prerelease revision...")
|
|
206
|
+
new_revision = base_version.to_prerelease(
|
|
207
|
+
token=prerelease_token,
|
|
208
|
+
revision=(
|
|
209
|
+
1
|
|
210
|
+
if latest_version.prerelease_token != prerelease_token
|
|
211
|
+
else (latest_version.prerelease_revision or 0) + 1
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
logger.debug("Incremented %s to %s", base_version, new_revision)
|
|
215
|
+
return new_revision
|
|
216
|
+
|
|
217
|
+
# When we don't want a prerelease, but the previous version is a prerelease that
|
|
218
|
+
# had a greater bump than we currently are applying, choose the larger bump instead
|
|
219
|
+
# as it consumes this bump
|
|
220
|
+
logger.debug("Finalizing the prerelease version...")
|
|
221
|
+
return base_version.finalize_version()
|
|
222
|
+
|
|
223
|
+
# Fallthrough to handle all larger level bumps
|
|
224
|
+
logger.debug(
|
|
225
|
+
"this release has a greater bump than any change since the last full release, %s",
|
|
226
|
+
latest_full_version,
|
|
253
227
|
)
|
|
254
|
-
return latest_version.finalize_version()
|
|
255
228
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
229
|
+
# Fallthrough, if we don't want a prerelease, or if we do but the level bump is greater
|
|
230
|
+
#
|
|
231
|
+
# because the current version is a prerelease, we must start from the last full version
|
|
232
|
+
# Case 1: we identified that the level bump is greater than the change since
|
|
233
|
+
# the last full release, this will also reset the prerelease revision
|
|
234
|
+
# Case 2: we don't want a prerelease, so consider only the last full version in history
|
|
235
|
+
base_version = latest_full_version
|
|
236
|
+
|
|
237
|
+
# From the base version, we can now increment the version according to the level bump
|
|
238
|
+
# regardless of the prerelease status as bump() handles the reset and pass through
|
|
239
|
+
logger.debug("Bumping %s with a %s bump", base_version, level_bump)
|
|
240
|
+
target_next_version = base_version.bump(level_bump)
|
|
241
|
+
|
|
242
|
+
# Converting to/from a prerelease if necessary
|
|
243
|
+
target_next_version = (
|
|
244
|
+
target_next_version.to_prerelease(token=prerelease_token)
|
|
245
|
+
if prerelease
|
|
246
|
+
else target_next_version.finalize_version()
|
|
261
247
|
)
|
|
262
|
-
|
|
248
|
+
|
|
249
|
+
logger.debug("Incremented %s to %s", base_version, target_next_version)
|
|
250
|
+
return target_next_version
|
|
263
251
|
|
|
264
252
|
|
|
265
253
|
def next_version(
|
|
@@ -274,181 +262,118 @@ def next_version(
|
|
|
274
262
|
Evaluate the history within `repo`, and based on the tags and commits in the repo
|
|
275
263
|
history, identify the next semantic version that should be applied to a release
|
|
276
264
|
"""
|
|
277
|
-
# Step 1. All tags, sorted descending by semver ordering rules
|
|
278
|
-
all_git_tags_as_versions = tags_and_versions(repo.tags, translator)
|
|
279
|
-
all_full_release_tags_and_versions = list(
|
|
280
|
-
filter(lambda t_v: not t_v[1].is_prerelease, all_git_tags_as_versions)
|
|
281
|
-
)
|
|
282
|
-
log.info(
|
|
283
|
-
"Found %s full releases (excluding prereleases)",
|
|
284
|
-
len(all_full_release_tags_and_versions),
|
|
285
|
-
)
|
|
286
|
-
|
|
287
265
|
# Default initial version
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
266
|
+
# Since the translator is configured by the user, we can't guarantee that it will
|
|
267
|
+
# be able to parse the default version. So we first cast it to a tag using the default
|
|
268
|
+
# value and the users configured tag format, then parse it back to a version object
|
|
269
|
+
default_initial_version = translator.from_tag(
|
|
270
|
+
translator.str_to_tag(DEFAULT_VERSION)
|
|
291
271
|
)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
# guarantees that other branches exist
|
|
296
|
-
# Note the merge_base might be on our current branch, it's not
|
|
297
|
-
# necessarily the merge base of the current branch with `main`
|
|
298
|
-
other_ref = (
|
|
299
|
-
repo.active_branch
|
|
300
|
-
if latest_full_release_tag is None
|
|
301
|
-
else latest_full_release_tag.name
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
# Conditional log message to inform what was chosen as the comparison point
|
|
305
|
-
# to find the merge base of the current branch with the latest full release
|
|
306
|
-
log_msg = (
|
|
307
|
-
str.join(
|
|
308
|
-
", ",
|
|
309
|
-
[
|
|
310
|
-
"No full releases have been made yet",
|
|
311
|
-
f"the default version to use is {latest_full_release_version}",
|
|
312
|
-
],
|
|
313
|
-
)
|
|
314
|
-
if latest_full_release_tag is None
|
|
315
|
-
else str.join(
|
|
316
|
-
", ",
|
|
317
|
-
[
|
|
318
|
-
f"The last full release was {latest_full_release_version}",
|
|
319
|
-
f"tagged as {latest_full_release_tag!r}",
|
|
320
|
-
],
|
|
321
|
-
)
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
log.info(log_msg)
|
|
325
|
-
merge_bases = repo.merge_base(other_ref, repo.active_branch)
|
|
326
|
-
|
|
327
|
-
if len(merge_bases) < 1:
|
|
328
|
-
raise MissingMergeBaseError(
|
|
329
|
-
f"Unable to find merge-base between {other_ref} and {repo.active_branch.name}"
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
if len(merge_bases) > 1:
|
|
333
|
-
raise NotImplementedError(
|
|
334
|
-
str.join(
|
|
335
|
-
" ",
|
|
336
|
-
[
|
|
337
|
-
"This branch has more than one merge-base with the",
|
|
338
|
-
"latest version, which is not yet supported",
|
|
339
|
-
],
|
|
340
|
-
)
|
|
272
|
+
if default_initial_version is None:
|
|
273
|
+
raise InternalError(
|
|
274
|
+
"Translator was unable to parse the embedded default version"
|
|
341
275
|
)
|
|
342
276
|
|
|
343
|
-
|
|
277
|
+
# Step 1. All tags, sorted descending by semver ordering rules
|
|
278
|
+
all_git_tags_as_versions = tags_and_versions(repo.tags, translator)
|
|
344
279
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
f"The merge_base found by merge_base({str_tag_name}, {repo.active_branch}) "
|
|
351
|
-
"is None"
|
|
352
|
-
)
|
|
280
|
+
# Retrieve all commit hashes (regardless of merges) in the current branch's history from repo origin
|
|
281
|
+
commit_hash_set = {
|
|
282
|
+
commit.hexsha
|
|
283
|
+
for commit in _traverse_graph_for_commits(head_commit=repo.active_branch.commit)
|
|
284
|
+
}
|
|
353
285
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
286
|
+
# Filter all releases that are not found in the current branch's history
|
|
287
|
+
historic_versions: list[Version] = []
|
|
288
|
+
for tag, version in all_git_tags_as_versions:
|
|
289
|
+
# TODO: move this to tags_and_versions() function?
|
|
290
|
+
# Ignore the error that is raised when tag points to a Blob or Tree object rather
|
|
291
|
+
# than a commit object (tags that point to tags that then point to commits are resolved automatically)
|
|
292
|
+
with suppress(ValueError):
|
|
293
|
+
if tag.commit.hexsha in commit_hash_set:
|
|
294
|
+
historic_versions.append(version)
|
|
295
|
+
|
|
296
|
+
# Step 2. Get the latest final release version in the history of the current branch
|
|
297
|
+
# or fallback to the default 0.0.0 starting version value if none are found
|
|
298
|
+
latest_full_release_version = next(
|
|
299
|
+
filter(
|
|
300
|
+
lambda version: not version.is_prerelease,
|
|
301
|
+
historic_versions,
|
|
302
|
+
),
|
|
303
|
+
default_initial_version,
|
|
361
304
|
)
|
|
362
305
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if
|
|
366
|
-
else
|
|
306
|
+
logger.info(
|
|
307
|
+
f"The last full version in this branch's history was {latest_full_release_version}"
|
|
308
|
+
if latest_full_release_version != default_initial_version
|
|
309
|
+
else "No full releases found in this branch's history"
|
|
367
310
|
)
|
|
368
311
|
|
|
369
|
-
# Step
|
|
370
|
-
#
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
312
|
+
# Step 3. Determine the latest release version in the history of the current branch
|
|
313
|
+
# If we the desired result is a prerelease, we must determine if there was any previous
|
|
314
|
+
# prerelease in the history of the current branch beyond the latest_full_release_version.
|
|
315
|
+
# Important to note that, we only consider prereleases that are of the same prerelease token
|
|
316
|
+
# as the basis of incrementing the prerelease revision.
|
|
317
|
+
# If we are not looking for a prerelease, this is the same as the last full release.
|
|
318
|
+
latest_version = (
|
|
319
|
+
latest_full_release_version
|
|
320
|
+
if not prerelease
|
|
321
|
+
else next(
|
|
322
|
+
filter(
|
|
323
|
+
lambda version: all(
|
|
324
|
+
[
|
|
325
|
+
version.is_prerelease,
|
|
326
|
+
version.prerelease_token == translator.prerelease_token,
|
|
327
|
+
version >= latest_full_release_version,
|
|
328
|
+
]
|
|
329
|
+
),
|
|
330
|
+
historic_versions,
|
|
331
|
+
),
|
|
332
|
+
latest_full_release_version, # default
|
|
333
|
+
)
|
|
378
334
|
)
|
|
379
335
|
|
|
380
|
-
|
|
381
|
-
# If it's not a prerelease, we need to include commits back
|
|
382
|
-
# to the last full version in consideration for what kind of
|
|
383
|
-
# bump to produce. However if we're doing a prerelease, we can
|
|
384
|
-
# include prereleases here to potentially consider a smaller portion
|
|
385
|
-
# of history (from a prerelease since the last full release, onwards)
|
|
386
|
-
|
|
387
|
-
# Note that a side-effect of this is, if at some point the configuration
|
|
388
|
-
# for a particular branch pattern changes w.r.t. prerelease=True/False,
|
|
389
|
-
# the new kind of version will be produced from the commits already
|
|
390
|
-
# included in a prerelease since the last full release on the branch
|
|
391
|
-
tag_sha_2_version_lookup = {
|
|
392
|
-
tag.commit.hexsha: (tag, version)
|
|
393
|
-
for tag, version in all_git_tags_as_versions
|
|
394
|
-
if prerelease or not version.is_prerelease
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
# N.B. these should be sorted so long as we iterate the commits in reverse order
|
|
398
|
-
for commit in commits_since_last_full_release:
|
|
399
|
-
parse_result = commit_parser.parse(commit)
|
|
400
|
-
if isinstance(parse_result, ParsedCommit):
|
|
401
|
-
log.debug(
|
|
402
|
-
"adding %s to the levels identified in commits_since_last_full_release",
|
|
403
|
-
parse_result.bump,
|
|
404
|
-
)
|
|
405
|
-
parsed_levels.add(parse_result.bump)
|
|
336
|
+
logger.info("The latest release in this branch's history was %s", latest_version)
|
|
406
337
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
|
|
414
|
-
|
|
338
|
+
# Step 4. Walk the git tree to find all commits that have been made since the last release
|
|
339
|
+
commits_since_last_release = _traverse_graph_for_commits(
|
|
340
|
+
head_commit=repo.active_branch.commit,
|
|
341
|
+
latest_release_tag_str=(
|
|
342
|
+
# NOTE: the default_initial_version should not actually exist on the repository (ie v0.0.0)
|
|
343
|
+
# so we provide an empty tag string when there are no tags on the repository yet
|
|
344
|
+
latest_version.as_tag() if latest_version != default_initial_version else ""
|
|
345
|
+
),
|
|
346
|
+
)
|
|
415
347
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
commit.hexsha,
|
|
423
|
-
latest_version,
|
|
348
|
+
# Step 5. Parse the commits to determine the bump level that should be applied
|
|
349
|
+
parsed_levels: set[LevelBump] = {
|
|
350
|
+
parsed_result.bump # type: ignore[union-attr] # too complex for type checkers
|
|
351
|
+
for parsed_result in filter(
|
|
352
|
+
lambda parsed_result: isinstance(parsed_result, ParsedCommit),
|
|
353
|
+
map(commit_parser.parse, commits_since_last_release),
|
|
424
354
|
)
|
|
425
|
-
|
|
355
|
+
}
|
|
426
356
|
|
|
427
|
-
|
|
428
|
-
"parsed the following distinct levels from the commits since the last release: "
|
|
429
|
-
"%s",
|
|
357
|
+
logger.debug(
|
|
358
|
+
"parsed the following distinct levels from the commits since the last release: %s",
|
|
430
359
|
parsed_levels,
|
|
431
360
|
)
|
|
361
|
+
|
|
432
362
|
level_bump = max(parsed_levels, default=LevelBump.NO_RELEASE)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
363
|
+
logger.info("The type of the next release release is: %s", level_bump)
|
|
364
|
+
|
|
365
|
+
if all(
|
|
366
|
+
[
|
|
367
|
+
level_bump is LevelBump.NO_RELEASE,
|
|
368
|
+
latest_version.major != 0 or allow_zero_version,
|
|
369
|
+
]
|
|
370
|
+
):
|
|
371
|
+
logger.info("No release will be made")
|
|
372
|
+
return latest_version
|
|
438
373
|
|
|
439
374
|
return _increment_version(
|
|
440
375
|
latest_version=latest_version,
|
|
441
376
|
latest_full_version=latest_full_release_version,
|
|
442
|
-
latest_full_version_in_history=(
|
|
443
|
-
latest_full_version_in_history
|
|
444
|
-
or Version(
|
|
445
|
-
0,
|
|
446
|
-
0,
|
|
447
|
-
0,
|
|
448
|
-
prerelease_token=translator.prerelease_token,
|
|
449
|
-
tag_format=translator.tag_format,
|
|
450
|
-
)
|
|
451
|
-
),
|
|
452
377
|
level_bump=level_bump,
|
|
453
378
|
prerelease=prerelease,
|
|
454
379
|
prerelease_token=translator.prerelease_token,
|
{python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/AUTHORS.rst
RENAMED
|
File without changes
|
{python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_semantic_release-9.15.0.dist-info → python_semantic_release-9.15.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|