python-semantic-release 9.16.0__py3-none-any.whl → 9.17.0__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.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/METADATA +1 -1
- {python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/RECORD +24 -24
- semantic_release/__init__.py +1 -1
- semantic_release/changelog/context.py +3 -0
- semantic_release/changelog/release_history.py +66 -56
- semantic_release/changelog/template.py +0 -1
- semantic_release/cli/changelog_writer.py +6 -1
- semantic_release/cli/commands/version.py +1 -1
- semantic_release/cli/config.py +6 -1
- semantic_release/cli/masking_filter.py +1 -1
- semantic_release/commit_parser/_base.py +1 -1
- semantic_release/commit_parser/angular.py +214 -15
- semantic_release/commit_parser/emoji.py +211 -23
- semantic_release/commit_parser/scipy.py +7 -7
- semantic_release/commit_parser/tag.py +3 -1
- semantic_release/commit_parser/util.py +49 -5
- semantic_release/helpers.py +87 -4
- semantic_release/hvcs/_base.py +1 -1
- semantic_release/version/algorithm.py +18 -4
- {python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/AUTHORS.rst +0 -0
- {python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/LICENSE +0 -0
- {python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/WHEEL +0 -0
- {python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/entry_points.txt +0 -0
- {python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/top_level.txt +0 -0
{python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/RECORD
RENAMED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
semantic_release/__init__.py,sha256=
|
|
1
|
+
semantic_release/__init__.py,sha256=HGRoymlde9dCDLCL2q3_ispJjvMuGYmdsAyWsLPpHfM,1229
|
|
2
2
|
semantic_release/__main__.py,sha256=KOIBOvLruqfi5ArXcWK3ucIZ7NB55kfCbycJaxx6aQg,1485
|
|
3
3
|
semantic_release/const.py,sha256=Z1o2QNh60wSLeF-_1TemMBjU3ZXbV0XghnUFsbTVfOs,831
|
|
4
4
|
semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
|
|
5
5
|
semantic_release/errors.py,sha256=PY9rmviSFBZkqawW6VXbUfmF9C_RNOIObcmeGxLefMo,2904
|
|
6
6
|
semantic_release/gitproject.py,sha256=G4XrucN-ZwT1Kj4RMrABcr1vWb0bjKgurEeJjcL-61c,9422
|
|
7
7
|
semantic_release/globals.py,sha256=imI9WKGa6MS2pTRAZiWZ2qIJup2eWnBz3OZmIj2YIHM,158
|
|
8
|
-
semantic_release/helpers.py,sha256=
|
|
8
|
+
semantic_release/helpers.py,sha256=T7gEYx9npipoV1-lNc0n-_vwVbdVqeREpQ4mv66auTE,9529
|
|
9
9
|
semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
|
|
11
|
-
semantic_release/changelog/context.py,sha256=
|
|
12
|
-
semantic_release/changelog/release_history.py,sha256=
|
|
13
|
-
semantic_release/changelog/template.py,sha256=
|
|
11
|
+
semantic_release/changelog/context.py,sha256=TcBG-fVmYuxl7uD-AtsHVVE5Md5KwCRGJQxbE3dJdTc,5459
|
|
12
|
+
semantic_release/changelog/release_history.py,sha256=d_n35sJHFJHgni3-xKXfkCoMiSZLy3ChLU5e9Tn5g4c,9267
|
|
13
|
+
semantic_release/changelog/template.py,sha256=R3V5m-7kv9ES23e_g37fe17tk-ESgvwV0C9rp5uoeOE,5691
|
|
14
14
|
semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
semantic_release/cli/changelog_writer.py,sha256=
|
|
15
|
+
semantic_release/cli/changelog_writer.py,sha256=S91en98KrmXMA0AUwqsWa8zGY1i2-LRVFBcK0jWqfPw,9168
|
|
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=aYquSXNdjhF45BvqcdjaQSu1DcRJeugifOvpMyknwrU,32337
|
|
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
|
-
semantic_release/cli/masking_filter.py,sha256=
|
|
20
|
+
semantic_release/cli/masking_filter.py,sha256=ric34rnXfN5RiAVVaKnhiMJOxTnEl26kI06jQqZPZoQ,3072
|
|
21
21
|
semantic_release/cli/util.py,sha256=FyXaBkeL7nXKjy3X9rQLEwvn7p46xPekp2V8Z-5MVrk,3755
|
|
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
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=y8m8ZyJe9vGhNsl5El2a4QwNM-upTRHfJS2T9zJoFWc,24824
|
|
28
28
|
semantic_release/commit_parser/__init__.py,sha256=cv5HFBdw7OJd4Laj4Ex8ZZ5Tml8GwXgQcXW6Pasr2Ao,615
|
|
29
|
-
semantic_release/commit_parser/_base.py,sha256=
|
|
30
|
-
semantic_release/commit_parser/angular.py,sha256=
|
|
31
|
-
semantic_release/commit_parser/emoji.py,sha256=
|
|
32
|
-
semantic_release/commit_parser/scipy.py,sha256=
|
|
33
|
-
semantic_release/commit_parser/tag.py,sha256=
|
|
29
|
+
semantic_release/commit_parser/_base.py,sha256=oDifeTmFDpS238cp_DDrGzfidaKeAD5olCB5IM4Q6z8,3058
|
|
30
|
+
semantic_release/commit_parser/angular.py,sha256=tJqc9A-XJs85FH1EiDgm-k3_ryhPp8ln0EbrkRZkoWo,18367
|
|
31
|
+
semantic_release/commit_parser/emoji.py,sha256=zXlQUwTawMq9G7Uf5nriNQJv_tXFDev9UG3CHT3_Jvs,17283
|
|
32
|
+
semantic_release/commit_parser/scipy.py,sha256=0rYZglJ7uib-1Deu4J30wHh7AZS8KfO0eND82bPtDQ8,4526
|
|
33
|
+
semantic_release/commit_parser/tag.py,sha256=oGB3lgyp2Eu3Tg3jjxqNzN86N6bokSaFu6f4Ir6IS_k,3546
|
|
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=9diralZ8QbMOMpVEp9MtrMVdkGsD-fS1F4cmy6eyysU,3674
|
|
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
|
|
@@ -53,7 +53,7 @@ semantic_release/data/templates/angular/rst/.components/macros.rst.j2,sha256=luN
|
|
|
53
53
|
semantic_release/data/templates/angular/rst/.components/unreleased_changes.rst.j2,sha256=ARBhc1ZpKwehGKDvOMqukmN59mTJiHzHsS7rOfKYCt8,202
|
|
54
54
|
semantic_release/data/templates/angular/rst/.components/versioned_changes.rst.j2,sha256=NZfn1W14QochiAJ43oNKmcrCn_vgfbkKtvOTAw1jEc8,530
|
|
55
55
|
semantic_release/hvcs/__init__.py,sha256=JwoaLOF-12L-OBo_9-tOXXhdiHKeVungA9865to2oZk,494
|
|
56
|
-
semantic_release/hvcs/_base.py,sha256=
|
|
56
|
+
semantic_release/hvcs/_base.py,sha256=ycHg0WljEVTqjFXoGRrHMvbyqWqPs4HziB090IcPZSE,2664
|
|
57
57
|
semantic_release/hvcs/bitbucket.py,sha256=Hzk27OOcpedFKVX4z3cY8DIUNNxe6D57J0d3sWNPIOY,9687
|
|
58
58
|
semantic_release/hvcs/gitea.py,sha256=YTpAnJ4bBdMjIhgAvVMF8vPZ6O07IgWuXeqTbEU-GNE,12715
|
|
59
59
|
semantic_release/hvcs/github.py,sha256=PLqbXDpHvXqE-uY8dmv7MuyjjnkMv0Va0vXo3UQUIUA,19937
|
|
@@ -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=-ppIqvvcAvc4nMtsClNNEzpEHWzgv2oKFydMW6ium6M,15941
|
|
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.
|
|
70
|
-
python_semantic_release-9.
|
|
71
|
-
python_semantic_release-9.
|
|
72
|
-
python_semantic_release-9.
|
|
73
|
-
python_semantic_release-9.
|
|
74
|
-
python_semantic_release-9.
|
|
75
|
-
python_semantic_release-9.
|
|
69
|
+
python_semantic_release-9.17.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
|
|
70
|
+
python_semantic_release-9.17.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
|
|
71
|
+
python_semantic_release-9.17.0.dist-info/METADATA,sha256=1xRK6C1aw6HjxmCa963whjgtiQPjhbOcSW0vhaghQ_M,3812
|
|
72
|
+
python_semantic_release-9.17.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
73
|
+
python_semantic_release-9.17.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
|
|
74
|
+
python_semantic_release-9.17.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
|
|
75
|
+
python_semantic_release-9.17.0.dist-info/RECORD,,
|
semantic_release/__init__.py
CHANGED
|
@@ -8,6 +8,8 @@ from pathlib import Path
|
|
|
8
8
|
from re import compile as regexp
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Callable, Literal
|
|
10
10
|
|
|
11
|
+
from semantic_release.helpers import sort_numerically
|
|
12
|
+
|
|
11
13
|
if TYPE_CHECKING: # pragma: no cover
|
|
12
14
|
from jinja2 import Environment
|
|
13
15
|
|
|
@@ -87,6 +89,7 @@ def make_changelog_context(
|
|
|
87
89
|
read_file,
|
|
88
90
|
convert_md_to_rst,
|
|
89
91
|
autofit_text_width,
|
|
92
|
+
sort_numerically,
|
|
90
93
|
),
|
|
91
94
|
)
|
|
92
95
|
|
|
@@ -102,75 +102,85 @@ class ReleaseHistory:
|
|
|
102
102
|
|
|
103
103
|
released.setdefault(the_version, release)
|
|
104
104
|
|
|
105
|
-
# mypy will be happy if we make this an explicit string
|
|
106
|
-
commit_message = str(commit.message)
|
|
107
|
-
|
|
108
105
|
log.info(
|
|
109
106
|
"parsing commit [%s] %s",
|
|
110
107
|
commit.hexsha[:8],
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
parse_result = commit_parser.parse(commit)
|
|
114
|
-
commit_type = (
|
|
115
|
-
"unknown" if isinstance(parse_result, ParseError) else parse_result.type
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
has_exclusion_match = any(
|
|
119
|
-
pattern.match(commit_message) for pattern in exclude_commit_patterns
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
commit_level_bump = (
|
|
123
|
-
LevelBump.NO_RELEASE
|
|
124
|
-
if isinstance(parse_result, ParseError)
|
|
125
|
-
else parse_result.bump
|
|
108
|
+
str(commit.message).replace("\n", " ")[:54],
|
|
126
109
|
)
|
|
110
|
+
# returns a ParseResult or list of ParseResult objects,
|
|
111
|
+
# it is usually one, but we split a commit if a squashed merge is detected
|
|
112
|
+
parse_results = commit_parser.parse(commit)
|
|
113
|
+
if not isinstance(parse_results, list):
|
|
114
|
+
parse_results = [parse_results]
|
|
115
|
+
|
|
116
|
+
is_squash_commit = bool(len(parse_results) > 1)
|
|
117
|
+
|
|
118
|
+
# iterate through parsed commits to add to changelog definition
|
|
119
|
+
for parsed_result in parse_results:
|
|
120
|
+
commit_message = str(parsed_result.commit.message)
|
|
121
|
+
commit_type = (
|
|
122
|
+
"unknown"
|
|
123
|
+
if isinstance(parsed_result, ParseError)
|
|
124
|
+
else parsed_result.type
|
|
125
|
+
)
|
|
126
|
+
log.debug("commit has type '%s'", commit_type)
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# are included, then the changelog will be empty. Even if ther was other
|
|
131
|
-
# commits included, the true reason for a version bump would be missing.
|
|
132
|
-
if has_exclusion_match and commit_level_bump == LevelBump.NO_RELEASE:
|
|
133
|
-
log.info(
|
|
134
|
-
"Excluding commit [%s] %s",
|
|
135
|
-
commit.hexsha[:8],
|
|
136
|
-
commit_message.replace("\n", " ")[:50],
|
|
128
|
+
has_exclusion_match = any(
|
|
129
|
+
pattern.match(commit_message) for pattern in exclude_commit_patterns
|
|
137
130
|
)
|
|
138
|
-
continue
|
|
139
131
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
log.info(
|
|
145
|
-
str.join(
|
|
146
|
-
" ",
|
|
147
|
-
[
|
|
148
|
-
"Excluding commit %s (%s) because parser determined",
|
|
149
|
-
"it should not included in the changelog",
|
|
150
|
-
],
|
|
151
|
-
),
|
|
152
|
-
commit.hexsha[:8],
|
|
153
|
-
commit_message.replace("\n", " ")[:20],
|
|
132
|
+
commit_level_bump = (
|
|
133
|
+
LevelBump.NO_RELEASE
|
|
134
|
+
if isinstance(parsed_result, ParseError)
|
|
135
|
+
else parsed_result.bump
|
|
154
136
|
)
|
|
155
|
-
continue
|
|
156
137
|
|
|
157
|
-
|
|
138
|
+
# Skip excluded commits except for any commit causing a version bump
|
|
139
|
+
# Reasoning: if a commit causes a version bump, and no other commits
|
|
140
|
+
# are included, then the changelog will be empty. Even if ther was other
|
|
141
|
+
# commits included, the true reason for a version bump would be missing.
|
|
142
|
+
if has_exclusion_match and commit_level_bump == LevelBump.NO_RELEASE:
|
|
143
|
+
log.info(
|
|
144
|
+
"Excluding %s commit[%s] %s",
|
|
145
|
+
"piece of squashed" if is_squash_commit else "",
|
|
146
|
+
parsed_result.short_hash,
|
|
147
|
+
commit_message.split("\n", maxsplit=1)[0][:20],
|
|
148
|
+
)
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
isinstance(parsed_result, ParsedCommit)
|
|
153
|
+
and not parsed_result.include_in_changelog
|
|
154
|
+
):
|
|
155
|
+
log.info(
|
|
156
|
+
str.join(
|
|
157
|
+
" ",
|
|
158
|
+
[
|
|
159
|
+
"Excluding commit[%s] because parser determined",
|
|
160
|
+
"it should not included in the changelog",
|
|
161
|
+
],
|
|
162
|
+
),
|
|
163
|
+
parsed_result.short_hash,
|
|
164
|
+
)
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if the_version is None:
|
|
168
|
+
log.info(
|
|
169
|
+
"[Unreleased] adding commit[%s] to unreleased '%s'",
|
|
170
|
+
parsed_result.short_hash,
|
|
171
|
+
commit_type,
|
|
172
|
+
)
|
|
173
|
+
unreleased[commit_type].append(parsed_result)
|
|
174
|
+
continue
|
|
175
|
+
|
|
158
176
|
log.info(
|
|
159
|
-
"[
|
|
160
|
-
|
|
177
|
+
"[%s] adding commit[%s] to release '%s'",
|
|
178
|
+
the_version,
|
|
179
|
+
parsed_result.short_hash,
|
|
161
180
|
commit_type,
|
|
162
181
|
)
|
|
163
|
-
unreleased[commit_type].append(parse_result)
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
log.info(
|
|
167
|
-
"[%s] adding '%s' commit(%s) to release",
|
|
168
|
-
the_version,
|
|
169
|
-
commit_type,
|
|
170
|
-
commit.hexsha[:8],
|
|
171
|
-
)
|
|
172
182
|
|
|
173
|
-
|
|
183
|
+
released[the_version]["elements"][commit_type].append(parsed_result)
|
|
174
184
|
|
|
175
185
|
return cls(unreleased=unreleased, released=released)
|
|
176
186
|
|
|
@@ -24,6 +24,7 @@ from semantic_release.cli.const import (
|
|
|
24
24
|
)
|
|
25
25
|
from semantic_release.cli.util import noop_report
|
|
26
26
|
from semantic_release.errors import InternalError
|
|
27
|
+
from semantic_release.helpers import sort_numerically
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING: # pragma: no cover
|
|
29
30
|
from jinja2 import Environment
|
|
@@ -254,7 +255,11 @@ def generate_release_notes(
|
|
|
254
255
|
version=release["version"],
|
|
255
256
|
release=release,
|
|
256
257
|
mask_initial_release=mask_initial_release,
|
|
257
|
-
filters=(
|
|
258
|
+
filters=(
|
|
259
|
+
*hvcs_client.get_changelog_context_filters(),
|
|
260
|
+
autofit_text_width,
|
|
261
|
+
sort_numerically,
|
|
262
|
+
),
|
|
258
263
|
).bind_to_environment(
|
|
259
264
|
# Use a new, non-configurable environment for release notes -
|
|
260
265
|
# not user-configurable at the moment
|
semantic_release/cli/config.py
CHANGED
|
@@ -602,7 +602,12 @@ class RuntimeContext:
|
|
|
602
602
|
# Retrieve details from repository
|
|
603
603
|
with Repo(str(raw.repo_dir)) as git_repo:
|
|
604
604
|
try:
|
|
605
|
-
|
|
605
|
+
# Get the remote url by calling out to `git remote get-url`. This returns
|
|
606
|
+
# the expanded url, taking into account any insteadOf directives
|
|
607
|
+
# in the git configuration.
|
|
608
|
+
remote_url = raw.remote.url or git_repo.git.remote(
|
|
609
|
+
"get-url", raw.remote.name
|
|
610
|
+
)
|
|
606
611
|
active_branch = git_repo.active_branch.name
|
|
607
612
|
except ValueError as err:
|
|
608
613
|
raise MissingGitRemote(
|
|
@@ -27,7 +27,7 @@ class MaskingFilter(logging.Filter):
|
|
|
27
27
|
|
|
28
28
|
def add_mask_for(self, data: str, name: str = "redacted") -> MaskingFilter:
|
|
29
29
|
if data and data not in self._UNWANTED:
|
|
30
|
-
log.debug("Adding redact pattern %r to
|
|
30
|
+
log.debug("Adding redact pattern '%r' to redact_patterns", name)
|
|
31
31
|
self._redact_patterns[name].add(data)
|
|
32
32
|
return self
|
|
33
33
|
|
|
@@ -10,8 +10,10 @@ import re
|
|
|
10
10
|
from functools import reduce
|
|
11
11
|
from itertools import zip_longest
|
|
12
12
|
from re import compile as regexp
|
|
13
|
+
from textwrap import dedent
|
|
13
14
|
from typing import TYPE_CHECKING, Tuple
|
|
14
15
|
|
|
16
|
+
from git.objects.commit import Commit
|
|
15
17
|
from pydantic.dataclasses import dataclass
|
|
16
18
|
|
|
17
19
|
from semantic_release.commit_parser._base import CommitParser, ParserOptions
|
|
@@ -23,11 +25,13 @@ from semantic_release.commit_parser.token import (
|
|
|
23
25
|
)
|
|
24
26
|
from semantic_release.commit_parser.util import (
|
|
25
27
|
breaking_re,
|
|
28
|
+
deep_copy_commit,
|
|
29
|
+
force_str,
|
|
26
30
|
parse_paragraphs,
|
|
27
|
-
sort_numerically,
|
|
28
31
|
)
|
|
29
32
|
from semantic_release.enums import LevelBump
|
|
30
33
|
from semantic_release.errors import InvalidParserOptions
|
|
34
|
+
from semantic_release.helpers import sort_numerically, text_reducer
|
|
31
35
|
|
|
32
36
|
if TYPE_CHECKING: # pragma: no cover
|
|
33
37
|
from git.objects.commit import Commit
|
|
@@ -93,6 +97,10 @@ class AngularParserOptions(ParserOptions):
|
|
|
93
97
|
default_bump_level: LevelBump = LevelBump.NO_RELEASE
|
|
94
98
|
"""The minimum bump level to apply to valid commit message."""
|
|
95
99
|
|
|
100
|
+
# TODO: breaking change v10, change default to True
|
|
101
|
+
parse_squash_commits: bool = False
|
|
102
|
+
"""Toggle flag for whether or not to parse squash commits"""
|
|
103
|
+
|
|
96
104
|
@property
|
|
97
105
|
def tag_to_level(self) -> dict[str, LevelBump]:
|
|
98
106
|
"""A mapping of commit tags to the level bump they should result in."""
|
|
@@ -142,14 +150,23 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
142
150
|
)
|
|
143
151
|
) from err
|
|
144
152
|
|
|
145
|
-
self.
|
|
153
|
+
self.commit_prefix = regexp(
|
|
146
154
|
str.join(
|
|
147
155
|
"",
|
|
148
156
|
[
|
|
149
|
-
|
|
157
|
+
f"^{commit_type_pattern.pattern}",
|
|
150
158
|
r"(?:\((?P<scope>[^\n]+)\))?",
|
|
151
159
|
# TODO: remove ! support as it is not part of the angular commit spec (its part of conventional commits spec)
|
|
152
160
|
r"(?P<break>!)?:\s+",
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.re_parser = regexp(
|
|
166
|
+
str.join(
|
|
167
|
+
"",
|
|
168
|
+
[
|
|
169
|
+
self.commit_prefix.pattern,
|
|
153
170
|
r"(?P<subject>[^\n]+)",
|
|
154
171
|
r"(?:\n\n(?P<text>.+))?", # commit body
|
|
155
172
|
],
|
|
@@ -171,6 +188,42 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
171
188
|
),
|
|
172
189
|
flags=re.MULTILINE | re.IGNORECASE,
|
|
173
190
|
)
|
|
191
|
+
self.filters = {
|
|
192
|
+
"typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"),
|
|
193
|
+
"git-header-commit": (
|
|
194
|
+
regexp(r"^[\t ]*commit [0-9a-f]+$\n?", flags=re.MULTILINE),
|
|
195
|
+
"",
|
|
196
|
+
),
|
|
197
|
+
"git-header-author": (
|
|
198
|
+
regexp(r"^[\t ]*Author: .+$\n?", flags=re.MULTILINE),
|
|
199
|
+
"",
|
|
200
|
+
),
|
|
201
|
+
"git-header-date": (
|
|
202
|
+
regexp(r"^[\t ]*Date: .+$\n?", flags=re.MULTILINE),
|
|
203
|
+
"",
|
|
204
|
+
),
|
|
205
|
+
"git-squash-heading": (
|
|
206
|
+
regexp(
|
|
207
|
+
r"^[\t ]*Squashed commit of the following:.*$\n?",
|
|
208
|
+
flags=re.MULTILINE,
|
|
209
|
+
),
|
|
210
|
+
"",
|
|
211
|
+
),
|
|
212
|
+
"git-squash-commit-prefix": (
|
|
213
|
+
regexp(
|
|
214
|
+
str.join(
|
|
215
|
+
"",
|
|
216
|
+
[
|
|
217
|
+
r"^(?:[\t ]*[*-][\t ]+|[\t ]+)?", # bullet points or indentation
|
|
218
|
+
commit_type_pattern.pattern + r"\b", # prior to commit type
|
|
219
|
+
],
|
|
220
|
+
),
|
|
221
|
+
flags=re.MULTILINE,
|
|
222
|
+
),
|
|
223
|
+
# move commit type to the start of the line
|
|
224
|
+
r"\1",
|
|
225
|
+
),
|
|
226
|
+
}
|
|
174
227
|
|
|
175
228
|
@staticmethod
|
|
176
229
|
def get_default_options() -> AngularParserOptions:
|
|
@@ -216,7 +269,7 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
216
269
|
return None
|
|
217
270
|
|
|
218
271
|
parsed_break = parsed.group("break")
|
|
219
|
-
parsed_scope = parsed.group("scope")
|
|
272
|
+
parsed_scope = parsed.group("scope") or ""
|
|
220
273
|
parsed_subject = parsed.group("subject")
|
|
221
274
|
parsed_text = parsed.group("text")
|
|
222
275
|
parsed_type = parsed.group("type")
|
|
@@ -262,24 +315,170 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
262
315
|
linked_merge_request=linked_merge_request,
|
|
263
316
|
)
|
|
264
317
|
|
|
318
|
+
def parse_commit(self, commit: Commit) -> ParseResult:
|
|
319
|
+
if not (parsed_msg_result := self.parse_message(force_str(commit.message))):
|
|
320
|
+
return _logged_parse_error(
|
|
321
|
+
commit,
|
|
322
|
+
f"Unable to parse commit message: {commit.message!r}",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return ParsedCommit.from_parsed_message_result(commit, parsed_msg_result)
|
|
326
|
+
|
|
265
327
|
# Maybe this can be cached as an optimization, similar to how
|
|
266
328
|
# mypy/pytest use their own caching directories, for very large commit
|
|
267
329
|
# histories?
|
|
268
330
|
# The problem is the cache likely won't be present in CI environments
|
|
269
|
-
def parse(self, commit: Commit) -> ParseResult:
|
|
331
|
+
def parse(self, commit: Commit) -> ParseResult | list[ParseResult]:
|
|
270
332
|
"""
|
|
271
|
-
|
|
272
|
-
|
|
333
|
+
Parse a commit message
|
|
334
|
+
|
|
335
|
+
If the commit message is a squashed merge commit, it will be split into
|
|
336
|
+
multiple commits, each of which will be parsed separately. Single commits
|
|
337
|
+
will be returned as a list of a single ParseResult.
|
|
273
338
|
"""
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
339
|
+
separate_commits: list[Commit] = (
|
|
340
|
+
self.unsquash_commit(commit)
|
|
341
|
+
if self.options.parse_squash_commits
|
|
342
|
+
else [commit]
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Parse each commit individually if there were more than one
|
|
346
|
+
parsed_commits: list[ParseResult] = list(
|
|
347
|
+
map(self.parse_commit, separate_commits)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def add_linked_merge_request(
|
|
351
|
+
parsed_result: ParseResult, mr_number: str
|
|
352
|
+
) -> ParseResult:
|
|
353
|
+
return (
|
|
354
|
+
parsed_result
|
|
355
|
+
if not isinstance(parsed_result, ParsedCommit)
|
|
356
|
+
else ParsedCommit(
|
|
357
|
+
**{
|
|
358
|
+
**parsed_result._asdict(),
|
|
359
|
+
"linked_merge_request": mr_number,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# TODO: improve this for other VCS systems other than GitHub & BitBucket
|
|
365
|
+
# Github works as the first commit in a squash merge commit has the PR number
|
|
366
|
+
# appended to the first line of the commit message
|
|
367
|
+
lead_commit = next(iter(parsed_commits))
|
|
368
|
+
|
|
369
|
+
if isinstance(lead_commit, ParsedCommit) and lead_commit.linked_merge_request:
|
|
370
|
+
# If the first commit has linked merge requests, assume all commits
|
|
371
|
+
# are part of the same PR and add the linked merge requests to all
|
|
372
|
+
# parsed commits
|
|
373
|
+
parsed_commits = [
|
|
374
|
+
lead_commit,
|
|
375
|
+
*map(
|
|
376
|
+
lambda parsed_result, mr=lead_commit.linked_merge_request: ( # type: ignore[misc]
|
|
377
|
+
add_linked_merge_request(parsed_result, mr)
|
|
378
|
+
),
|
|
379
|
+
parsed_commits[1:],
|
|
380
|
+
),
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
elif isinstance(lead_commit, ParseError) and (
|
|
384
|
+
mr_match := self.mr_selector.search(force_str(lead_commit.message))
|
|
385
|
+
):
|
|
386
|
+
# Handle BitBucket Squash Merge Commits (see #1085), which have non angular commit
|
|
387
|
+
# format but include the PR number in the commit subject that we want to extract
|
|
388
|
+
linked_merge_request = mr_match.group("mr_number")
|
|
389
|
+
|
|
390
|
+
# apply the linked MR to all commits
|
|
391
|
+
parsed_commits = [
|
|
392
|
+
add_linked_merge_request(parsed_result, linked_merge_request)
|
|
393
|
+
for parsed_result in parsed_commits
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
return parsed_commits
|
|
397
|
+
|
|
398
|
+
def unsquash_commit(self, commit: Commit) -> list[Commit]:
|
|
399
|
+
# GitHub EXAMPLE:
|
|
400
|
+
# feat(changelog): add autofit_text_width filter to template environment (#1062)
|
|
401
|
+
#
|
|
402
|
+
# This change adds an equivalent style formatter that can apply a text alignment
|
|
403
|
+
# to a maximum width and also maintain an indent over paragraphs of text
|
|
404
|
+
#
|
|
405
|
+
# * docs(changelog-templates): add definition & usage of autofit_text_width template filter
|
|
406
|
+
#
|
|
407
|
+
# * test(changelog-context): add test cases to check autofit_text_width filter use
|
|
408
|
+
#
|
|
409
|
+
# `git merge --squash` EXAMPLE:
|
|
410
|
+
# Squashed commit of the following:
|
|
411
|
+
#
|
|
412
|
+
# commit 63ec09b9e844e616dcaa7bae35a0b66671b59fbb
|
|
413
|
+
# Author: codejedi365 <codejedi365@gmail.com>
|
|
414
|
+
# Date: Sun Oct 13 12:05:23 2024 -0600
|
|
415
|
+
#
|
|
416
|
+
# feat(release-config): some commit subject
|
|
417
|
+
#
|
|
418
|
+
|
|
419
|
+
# Return a list of artificial commits (each with a single commit message)
|
|
420
|
+
return [
|
|
421
|
+
# create a artificial commit object (copy of original but with modified message)
|
|
422
|
+
Commit(
|
|
423
|
+
**{
|
|
424
|
+
**deep_copy_commit(commit),
|
|
425
|
+
"message": commit_msg,
|
|
426
|
+
}
|
|
277
427
|
)
|
|
428
|
+
for commit_msg in self.unsquash_commit_message(force_str(commit.message))
|
|
429
|
+
] or [commit]
|
|
278
430
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
431
|
+
def unsquash_commit_message(self, message: str) -> list[str]:
|
|
432
|
+
normalized_message = message.replace("\r", "").strip()
|
|
433
|
+
|
|
434
|
+
# split by obvious separate commits (applies to manual git squash merges)
|
|
435
|
+
obvious_squashed_commits = self.filters["git-header-commit"][0].split(
|
|
436
|
+
normalized_message
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
separate_commit_msgs: list[str] = reduce(
|
|
440
|
+
lambda all_msgs, msgs: all_msgs + msgs,
|
|
441
|
+
map(self._find_squashed_commits_in_str, obvious_squashed_commits),
|
|
442
|
+
[],
|
|
283
443
|
)
|
|
284
444
|
|
|
285
|
-
return
|
|
445
|
+
return separate_commit_msgs
|
|
446
|
+
|
|
447
|
+
def _find_squashed_commits_in_str(self, message: str) -> list[str]:
|
|
448
|
+
separate_commit_msgs: list[str] = []
|
|
449
|
+
current_msg = ""
|
|
450
|
+
|
|
451
|
+
for paragraph in filter(None, message.strip().split("\n\n")):
|
|
452
|
+
# Apply filters to normalize the paragraph
|
|
453
|
+
clean_paragraph = reduce(text_reducer, self.filters.values(), paragraph)
|
|
454
|
+
|
|
455
|
+
# remove any filtered (and now empty) paragraphs (ie. the git headers)
|
|
456
|
+
if not clean_paragraph.strip():
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Check if the paragraph is the start of a new angular commit
|
|
460
|
+
if not self.commit_prefix.search(clean_paragraph):
|
|
461
|
+
if not separate_commit_msgs and not current_msg:
|
|
462
|
+
# if there are no separate commit messages and no current message
|
|
463
|
+
# then this is the first commit message
|
|
464
|
+
current_msg = dedent(clean_paragraph)
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
# append the paragraph as part of the previous commit message
|
|
468
|
+
if current_msg:
|
|
469
|
+
current_msg += f"\n\n{dedent(clean_paragraph)}"
|
|
470
|
+
# else: drop the paragraph
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
# Since we found the start of the new commit, store any previous commit
|
|
474
|
+
# message separately and start the new commit message
|
|
475
|
+
if current_msg:
|
|
476
|
+
separate_commit_msgs.append(current_msg)
|
|
477
|
+
|
|
478
|
+
current_msg = clean_paragraph
|
|
479
|
+
|
|
480
|
+
# Store the last commit message (if its not empty)
|
|
481
|
+
if current_msg:
|
|
482
|
+
separate_commit_msgs.append(current_msg)
|
|
483
|
+
|
|
484
|
+
return separate_commit_msgs
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
from functools import reduce
|
|
8
8
|
from itertools import zip_longest
|
|
9
9
|
from re import compile as regexp
|
|
10
|
+
from textwrap import dedent
|
|
10
11
|
from typing import Tuple
|
|
11
12
|
|
|
12
13
|
from git.objects.commit import Commit
|
|
@@ -18,9 +19,14 @@ from semantic_release.commit_parser.token import (
|
|
|
18
19
|
ParsedMessageResult,
|
|
19
20
|
ParseResult,
|
|
20
21
|
)
|
|
21
|
-
from semantic_release.commit_parser.util import
|
|
22
|
+
from semantic_release.commit_parser.util import (
|
|
23
|
+
deep_copy_commit,
|
|
24
|
+
force_str,
|
|
25
|
+
parse_paragraphs,
|
|
26
|
+
)
|
|
22
27
|
from semantic_release.enums import LevelBump
|
|
23
28
|
from semantic_release.errors import InvalidParserOptions
|
|
29
|
+
from semantic_release.helpers import sort_numerically, text_reducer
|
|
24
30
|
|
|
25
31
|
logger = logging.getLogger(__name__)
|
|
26
32
|
|
|
@@ -60,7 +66,7 @@ class EmojiParserOptions(ParserOptions):
|
|
|
60
66
|
)
|
|
61
67
|
"""Commit-type prefixes that should result in a patch release bump."""
|
|
62
68
|
|
|
63
|
-
other_allowed_tags: Tuple[str, ...] = ()
|
|
69
|
+
other_allowed_tags: Tuple[str, ...] = (":memo:", ":checkmark:")
|
|
64
70
|
"""Commit-type prefixes that are allowed but do not result in a version bump."""
|
|
65
71
|
|
|
66
72
|
allowed_tags: Tuple[str, ...] = (
|
|
@@ -74,11 +80,6 @@ class EmojiParserOptions(ParserOptions):
|
|
|
74
80
|
default_bump_level: LevelBump = LevelBump.NO_RELEASE
|
|
75
81
|
"""The minimum bump level to apply to valid commit message."""
|
|
76
82
|
|
|
77
|
-
@property
|
|
78
|
-
def tag_to_level(self) -> dict[str, LevelBump]:
|
|
79
|
-
"""A mapping of commit tags to the level bump they should result in."""
|
|
80
|
-
return self._tag_to_level
|
|
81
|
-
|
|
82
83
|
parse_linked_issues: bool = False
|
|
83
84
|
"""
|
|
84
85
|
Whether to parse linked issues from the commit message.
|
|
@@ -92,6 +93,15 @@ class EmojiParserOptions(ParserOptions):
|
|
|
92
93
|
a whitespace separator.
|
|
93
94
|
"""
|
|
94
95
|
|
|
96
|
+
# TODO: breaking change v10, change default to True
|
|
97
|
+
parse_squash_commits: bool = False
|
|
98
|
+
"""Toggle flag for whether or not to parse squash commits"""
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def tag_to_level(self) -> dict[str, LevelBump]:
|
|
102
|
+
"""A mapping of commit tags to the level bump they should result in."""
|
|
103
|
+
return self._tag_to_level
|
|
104
|
+
|
|
95
105
|
def __post_init__(self) -> None:
|
|
96
106
|
self._tag_to_level: dict[str, LevelBump] = {
|
|
97
107
|
str(tag): level
|
|
@@ -132,7 +142,7 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
132
142
|
emojis_in_precedence_order = list(self.options.tag_to_level.keys())[::-1]
|
|
133
143
|
|
|
134
144
|
try:
|
|
135
|
-
|
|
145
|
+
highest_emoji_pattern = regexp(
|
|
136
146
|
r"(?P<type>%s)" % str.join("|", emojis_in_precedence_order)
|
|
137
147
|
)
|
|
138
148
|
except re.error as err:
|
|
@@ -147,6 +157,16 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
147
157
|
)
|
|
148
158
|
) from err
|
|
149
159
|
|
|
160
|
+
self.emoji_selector = regexp(
|
|
161
|
+
str.join(
|
|
162
|
+
"",
|
|
163
|
+
[
|
|
164
|
+
f"^{highest_emoji_pattern.pattern}",
|
|
165
|
+
r"(?:\((?P<scope>[^)]+)\))?:?",
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
150
170
|
# GitHub & Gitea use (#123), GitLab uses (!123), and BitBucket uses (pull request #123)
|
|
151
171
|
self.mr_selector = regexp(
|
|
152
172
|
r"[\t ]+\((?:pull request )?(?P<mr_number>[#!]\d+)\)[\t ]*$"
|
|
@@ -163,6 +183,44 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
163
183
|
flags=re.MULTILINE | re.IGNORECASE,
|
|
164
184
|
)
|
|
165
185
|
|
|
186
|
+
self.filters = {
|
|
187
|
+
"typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"),
|
|
188
|
+
"git-header-commit": (
|
|
189
|
+
regexp(r"^[\t ]*commit [0-9a-f]+$\n?", flags=re.MULTILINE),
|
|
190
|
+
"",
|
|
191
|
+
),
|
|
192
|
+
"git-header-author": (
|
|
193
|
+
regexp(r"^[\t ]*Author: .+$\n?", flags=re.MULTILINE),
|
|
194
|
+
"",
|
|
195
|
+
),
|
|
196
|
+
"git-header-date": (
|
|
197
|
+
regexp(r"^[\t ]*Date: .+$\n?", flags=re.MULTILINE),
|
|
198
|
+
"",
|
|
199
|
+
),
|
|
200
|
+
"git-squash-heading": (
|
|
201
|
+
regexp(
|
|
202
|
+
r"^[\t ]*Squashed commit of the following:.*$\n?",
|
|
203
|
+
flags=re.MULTILINE,
|
|
204
|
+
),
|
|
205
|
+
"",
|
|
206
|
+
),
|
|
207
|
+
"git-squash-commit-prefix": (
|
|
208
|
+
regexp(
|
|
209
|
+
str.join(
|
|
210
|
+
"",
|
|
211
|
+
[
|
|
212
|
+
r"^(?:[\t ]*[*-][\t ]+|[\t ]+)?", # bullet points or indentation
|
|
213
|
+
highest_emoji_pattern.pattern
|
|
214
|
+
+ r"(\W)", # prior to commit type
|
|
215
|
+
],
|
|
216
|
+
),
|
|
217
|
+
flags=re.MULTILINE,
|
|
218
|
+
),
|
|
219
|
+
# move commit type to the start of the line
|
|
220
|
+
r"\1\2",
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
|
|
166
224
|
@staticmethod
|
|
167
225
|
def get_default_options() -> EmojiParserOptions:
|
|
168
226
|
return EmojiParserOptions()
|
|
@@ -209,11 +267,9 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
209
267
|
# subject = self.mr_selector.sub("", subject).strip()
|
|
210
268
|
|
|
211
269
|
# Search for emoji of the highest importance in the subject
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
else "Other"
|
|
216
|
-
)
|
|
270
|
+
match = self.emoji_selector.search(subject)
|
|
271
|
+
primary_emoji = match.group("type") if match else "Other"
|
|
272
|
+
parsed_scope = (match.group("scope") if match else None) or ""
|
|
217
273
|
|
|
218
274
|
level_bump = self.options.tag_to_level.get(
|
|
219
275
|
primary_emoji, self.options.default_bump_level
|
|
@@ -235,7 +291,7 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
235
291
|
bump=level_bump,
|
|
236
292
|
type=primary_emoji,
|
|
237
293
|
category=primary_emoji,
|
|
238
|
-
scope=
|
|
294
|
+
scope=parsed_scope,
|
|
239
295
|
# TODO: breaking change v10, removes breaking change footers from descriptions
|
|
240
296
|
# descriptions=(
|
|
241
297
|
# descriptions[:1] if level_bump is LevelBump.MAJOR else descriptions
|
|
@@ -248,17 +304,149 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
248
304
|
linked_merge_request=linked_merge_request,
|
|
249
305
|
)
|
|
250
306
|
|
|
251
|
-
def
|
|
307
|
+
def parse_commit(self, commit: Commit) -> ParseResult:
|
|
308
|
+
return ParsedCommit.from_parsed_message_result(
|
|
309
|
+
commit, self.parse_message(force_str(commit.message))
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def parse(self, commit: Commit) -> ParseResult | list[ParseResult]:
|
|
252
313
|
"""
|
|
253
|
-
|
|
254
|
-
|
|
314
|
+
Parse a commit message
|
|
315
|
+
|
|
316
|
+
If the commit message is a squashed merge commit, it will be split into
|
|
317
|
+
multiple commits, each of which will be parsed separately. Single commits
|
|
318
|
+
will be returned as a list of a single ParseResult.
|
|
255
319
|
"""
|
|
256
|
-
|
|
320
|
+
separate_commits: list[Commit] = (
|
|
321
|
+
self.unsquash_commit(commit)
|
|
322
|
+
if self.options.parse_squash_commits
|
|
323
|
+
else [commit]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Parse each commit individually if there were more than one
|
|
327
|
+
parsed_commits: list[ParseResult] = list(
|
|
328
|
+
map(self.parse_commit, separate_commits)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def add_linked_merge_request(
|
|
332
|
+
parsed_result: ParseResult, mr_number: str
|
|
333
|
+
) -> ParseResult:
|
|
334
|
+
return (
|
|
335
|
+
parsed_result
|
|
336
|
+
if not isinstance(parsed_result, ParsedCommit)
|
|
337
|
+
else ParsedCommit(
|
|
338
|
+
**{
|
|
339
|
+
**parsed_result._asdict(),
|
|
340
|
+
"linked_merge_request": mr_number,
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# TODO: improve this for other VCS systems other than GitHub & BitBucket
|
|
346
|
+
# Github works as the first commit in a squash merge commit has the PR number
|
|
347
|
+
# appended to the first line of the commit message
|
|
348
|
+
lead_commit = next(iter(parsed_commits))
|
|
349
|
+
|
|
350
|
+
if isinstance(lead_commit, ParsedCommit) and lead_commit.linked_merge_request:
|
|
351
|
+
# If the first commit has linked merge requests, assume all commits
|
|
352
|
+
# are part of the same PR and add the linked merge requests to all
|
|
353
|
+
# parsed commits
|
|
354
|
+
parsed_commits = [
|
|
355
|
+
lead_commit,
|
|
356
|
+
*map(
|
|
357
|
+
lambda parsed_result, mr=lead_commit.linked_merge_request: ( # type: ignore[misc]
|
|
358
|
+
add_linked_merge_request(parsed_result, mr)
|
|
359
|
+
),
|
|
360
|
+
parsed_commits[1:],
|
|
361
|
+
),
|
|
362
|
+
]
|
|
257
363
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
364
|
+
return parsed_commits
|
|
365
|
+
|
|
366
|
+
def unsquash_commit(self, commit: Commit) -> list[Commit]:
|
|
367
|
+
# GitHub EXAMPLE:
|
|
368
|
+
# ✨(changelog): add autofit_text_width filter to template environment (#1062)
|
|
369
|
+
#
|
|
370
|
+
# This change adds an equivalent style formatter that can apply a text alignment
|
|
371
|
+
# to a maximum width and also maintain an indent over paragraphs of text
|
|
372
|
+
#
|
|
373
|
+
# * 🌐 Support Japanese language
|
|
374
|
+
#
|
|
375
|
+
# * ✅(changelog-context): add test cases to check autofit_text_width filter use
|
|
376
|
+
#
|
|
377
|
+
# `git merge --squash` EXAMPLE:
|
|
378
|
+
# Squashed commit of the following:
|
|
379
|
+
#
|
|
380
|
+
# commit 63ec09b9e844e616dcaa7bae35a0b66671b59fbb
|
|
381
|
+
# Author: codejedi365 <codejedi365@gmail.com>
|
|
382
|
+
# Date: Sun Oct 13 12:05:23 2024 -0000
|
|
383
|
+
#
|
|
384
|
+
# ⚡️ (homepage): Lazyload home screen images
|
|
385
|
+
#
|
|
386
|
+
#
|
|
387
|
+
# Return a list of artificial commits (each with a single commit message)
|
|
388
|
+
return [
|
|
389
|
+
# create a artificial commit object (copy of original but with modified message)
|
|
390
|
+
Commit(
|
|
391
|
+
**{
|
|
392
|
+
**deep_copy_commit(commit),
|
|
393
|
+
"message": commit_msg,
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
for commit_msg in self.unsquash_commit_message(force_str(commit.message))
|
|
397
|
+
] or [commit]
|
|
398
|
+
|
|
399
|
+
def unsquash_commit_message(self, message: str) -> list[str]:
|
|
400
|
+
normalized_message = message.replace("\r", "").strip()
|
|
401
|
+
|
|
402
|
+
# split by obvious separate commits (applies to manual git squash merges)
|
|
403
|
+
obvious_squashed_commits = self.filters["git-header-commit"][0].split(
|
|
404
|
+
normalized_message
|
|
262
405
|
)
|
|
263
406
|
|
|
264
|
-
|
|
407
|
+
separate_commit_msgs: list[str] = reduce(
|
|
408
|
+
lambda all_msgs, msgs: all_msgs + msgs,
|
|
409
|
+
map(self._find_squashed_commits_in_str, obvious_squashed_commits),
|
|
410
|
+
[],
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return separate_commit_msgs
|
|
414
|
+
|
|
415
|
+
def _find_squashed_commits_in_str(self, message: str) -> list[str]:
|
|
416
|
+
separate_commit_msgs: list[str] = []
|
|
417
|
+
current_msg = ""
|
|
418
|
+
|
|
419
|
+
for paragraph in filter(None, message.strip().split("\n\n")):
|
|
420
|
+
# Apply filters to normalize the paragraph
|
|
421
|
+
clean_paragraph = reduce(text_reducer, self.filters.values(), paragraph)
|
|
422
|
+
|
|
423
|
+
# remove any filtered (and now empty) paragraphs (ie. the git headers)
|
|
424
|
+
if not clean_paragraph.strip():
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
# Check if the paragraph is the start of a new angular commit
|
|
428
|
+
if not self.emoji_selector.search(clean_paragraph):
|
|
429
|
+
if not separate_commit_msgs and not current_msg:
|
|
430
|
+
# if there are no separate commit messages and no current message
|
|
431
|
+
# then this is the first commit message
|
|
432
|
+
current_msg = dedent(clean_paragraph)
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
# append the paragraph as part of the previous commit message
|
|
436
|
+
if current_msg:
|
|
437
|
+
current_msg += f"\n\n{dedent(clean_paragraph)}"
|
|
438
|
+
# else: drop the paragraph
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
# Since we found the start of the new commit, store any previous commit
|
|
442
|
+
# message separately and start the new commit message
|
|
443
|
+
if current_msg:
|
|
444
|
+
separate_commit_msgs.append(current_msg)
|
|
445
|
+
|
|
446
|
+
current_msg = clean_paragraph
|
|
447
|
+
|
|
448
|
+
# Store the last commit message (if its not empty)
|
|
449
|
+
if current_msg:
|
|
450
|
+
separate_commit_msgs.append(current_msg)
|
|
451
|
+
|
|
452
|
+
return separate_commit_msgs
|
|
@@ -74,21 +74,21 @@ def _logged_parse_error(commit: Commit, error: str) -> ParseError:
|
|
|
74
74
|
|
|
75
75
|
tag_to_section = {
|
|
76
76
|
"API": "breaking",
|
|
77
|
-
"BENCH": "
|
|
77
|
+
"BENCH": "none",
|
|
78
78
|
"BLD": "fix",
|
|
79
79
|
"BUG": "fix",
|
|
80
80
|
"DEP": "breaking",
|
|
81
|
-
"DEV": "
|
|
81
|
+
"DEV": "none",
|
|
82
82
|
"DOC": "documentation",
|
|
83
83
|
"ENH": "feature",
|
|
84
84
|
"MAINT": "fix",
|
|
85
|
-
"REV": "
|
|
86
|
-
"STY": "
|
|
87
|
-
"TST": "
|
|
88
|
-
"REL": "
|
|
85
|
+
"REV": "other",
|
|
86
|
+
"STY": "none",
|
|
87
|
+
"TST": "none",
|
|
88
|
+
"REL": "none",
|
|
89
89
|
# strictly speaking not part of the standard
|
|
90
90
|
"FEAT": "feature",
|
|
91
|
-
"TEST": "
|
|
91
|
+
"TEST": "none",
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Legacy commit parser from Python Semantic Release 1.0"""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import logging
|
|
4
6
|
import re
|
|
5
7
|
|
|
@@ -41,7 +43,7 @@ class TagCommitParser(CommitParser[ParseResult, TagParserOptions]):
|
|
|
41
43
|
def get_default_options() -> TagParserOptions:
|
|
42
44
|
return TagParserOptions()
|
|
43
45
|
|
|
44
|
-
def parse(self, commit: Commit) -> ParseResult:
|
|
46
|
+
def parse(self, commit: Commit) -> ParseResult | list[ParseResult]:
|
|
45
47
|
message = str(commit.message)
|
|
46
48
|
|
|
47
49
|
# Attempt to parse the commit message with a regular expression
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from copy import deepcopy
|
|
3
5
|
from functools import reduce
|
|
4
6
|
from re import MULTILINE, compile as regexp
|
|
5
7
|
from typing import TYPE_CHECKING
|
|
6
8
|
|
|
9
|
+
# TODO: remove in v10
|
|
10
|
+
from semantic_release.helpers import (
|
|
11
|
+
sort_numerically, # noqa: F401 # TODO: maintained for compatibility
|
|
12
|
+
)
|
|
13
|
+
|
|
7
14
|
if TYPE_CHECKING: # pragma: no cover
|
|
8
15
|
from re import Pattern
|
|
9
|
-
from typing import
|
|
16
|
+
from typing import Any, TypedDict
|
|
17
|
+
|
|
18
|
+
from git import Commit
|
|
10
19
|
|
|
11
20
|
class RegexReplaceDef(TypedDict):
|
|
12
21
|
pattern: Pattern
|
|
13
22
|
repl: str
|
|
14
23
|
|
|
15
24
|
|
|
16
|
-
number_pattern = regexp(r"(\d+)")
|
|
17
|
-
|
|
18
25
|
breaking_re = regexp(r"BREAKING[ -]CHANGE:\s?(.*)")
|
|
19
26
|
|
|
20
27
|
un_word_wrap: RegexReplaceDef = {
|
|
@@ -73,5 +80,42 @@ def parse_paragraphs(text: str) -> list[str]:
|
|
|
73
80
|
)
|
|
74
81
|
|
|
75
82
|
|
|
76
|
-
def
|
|
77
|
-
|
|
83
|
+
def force_str(msg: str | bytes | bytearray | memoryview) -> str:
|
|
84
|
+
# This shouldn't be a thing but typing is being weird around what
|
|
85
|
+
# git.commit.message returns and the memoryview type won't go away
|
|
86
|
+
message = msg.tobytes() if isinstance(msg, memoryview) else msg
|
|
87
|
+
return (
|
|
88
|
+
message.decode("utf-8")
|
|
89
|
+
if isinstance(message, (bytes, bytearray))
|
|
90
|
+
else str(message)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def deep_copy_commit(commit: Commit) -> dict[str, Any]:
|
|
95
|
+
keys = [
|
|
96
|
+
"repo",
|
|
97
|
+
"binsha",
|
|
98
|
+
"author",
|
|
99
|
+
"authored_date",
|
|
100
|
+
"committer",
|
|
101
|
+
"committed_date",
|
|
102
|
+
"message",
|
|
103
|
+
"tree",
|
|
104
|
+
"parents",
|
|
105
|
+
"encoding",
|
|
106
|
+
"gpgsig",
|
|
107
|
+
"author_tz_offset",
|
|
108
|
+
"committer_tz_offset",
|
|
109
|
+
]
|
|
110
|
+
kwargs = {}
|
|
111
|
+
for key in keys:
|
|
112
|
+
with suppress(ValueError):
|
|
113
|
+
if hasattr(commit, key) and (value := getattr(commit, key)) is not None:
|
|
114
|
+
if key in ["parents", "repo", "tree"]:
|
|
115
|
+
# These tend to have circular references so don't deepcopy them
|
|
116
|
+
kwargs[key] = value
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
kwargs[key] = deepcopy(value)
|
|
120
|
+
|
|
121
|
+
return kwargs
|
semantic_release/helpers.py
CHANGED
|
@@ -1,16 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import importlib.util
|
|
2
4
|
import logging
|
|
3
5
|
import os
|
|
4
6
|
import re
|
|
5
7
|
import string
|
|
6
8
|
import sys
|
|
7
|
-
from functools import lru_cache, wraps
|
|
9
|
+
from functools import lru_cache, reduce, wraps
|
|
8
10
|
from pathlib import Path, PurePosixPath
|
|
9
|
-
from
|
|
11
|
+
from re import IGNORECASE, compile as regexp
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar
|
|
10
13
|
from urllib.parse import urlsplit
|
|
11
14
|
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
from re import Pattern
|
|
17
|
+
from typing import Iterable
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
log = logging.getLogger(__name__)
|
|
13
21
|
|
|
22
|
+
number_pattern = regexp(r"(?P<prefix>\S*?)(?P<number>\d[\d,]*)\b")
|
|
23
|
+
hex_number_pattern = regexp(
|
|
24
|
+
r"(?P<prefix>\S*?)(?:0x)?(?P<number>[0-9a-f]+)\b", IGNORECASE
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_number_from_str(
|
|
29
|
+
string: str, default: int = -1, interpret_hex: bool = False
|
|
30
|
+
) -> int:
|
|
31
|
+
if interpret_hex and (match := hex_number_pattern.search(string)):
|
|
32
|
+
return abs(int(match.group("number"), 16))
|
|
33
|
+
|
|
34
|
+
if match := number_pattern.search(string):
|
|
35
|
+
return int(match.group("number"))
|
|
36
|
+
|
|
37
|
+
return default
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sort_numerically(
|
|
41
|
+
iterable: Iterable[str], reverse: bool = False, allow_hex: bool = False
|
|
42
|
+
) -> list[str]:
|
|
43
|
+
# Alphabetically sort prefixes first, then sort by number
|
|
44
|
+
alphabetized_list = sorted(iterable)
|
|
45
|
+
|
|
46
|
+
# Extract prefixes in order to group items by prefix
|
|
47
|
+
unmatched_items = []
|
|
48
|
+
prefixes: dict[str, list[str]] = {}
|
|
49
|
+
for item in alphabetized_list:
|
|
50
|
+
if not (
|
|
51
|
+
pattern_match := (
|
|
52
|
+
(hex_number_pattern.search(item) if allow_hex else None)
|
|
53
|
+
or number_pattern.search(item)
|
|
54
|
+
)
|
|
55
|
+
):
|
|
56
|
+
unmatched_items.append(item)
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
prefix = prefix if (prefix := pattern_match.group("prefix")) else ""
|
|
60
|
+
|
|
61
|
+
if prefix not in prefixes:
|
|
62
|
+
prefixes[prefix] = []
|
|
63
|
+
|
|
64
|
+
prefixes[prefix].append(item)
|
|
65
|
+
|
|
66
|
+
# Sort prefixes and items by number mixing in unmatched items as alphabetized with other prefixes
|
|
67
|
+
return reduce(
|
|
68
|
+
lambda acc, next_item: acc + next_item,
|
|
69
|
+
[
|
|
70
|
+
(
|
|
71
|
+
sorted(
|
|
72
|
+
prefixes[prefix],
|
|
73
|
+
key=lambda x: get_number_from_str(
|
|
74
|
+
x, default=-1, interpret_hex=allow_hex
|
|
75
|
+
),
|
|
76
|
+
reverse=reverse,
|
|
77
|
+
)
|
|
78
|
+
if prefix in prefixes
|
|
79
|
+
else [prefix]
|
|
80
|
+
)
|
|
81
|
+
for prefix in sorted([*prefixes.keys(), *unmatched_items])
|
|
82
|
+
],
|
|
83
|
+
[],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def text_reducer(text: str, filter_pair: tuple[Pattern[str], str]) -> str:
|
|
88
|
+
"""Reduce function to apply mulitple filters to a string"""
|
|
89
|
+
if not text: # abort if the paragraph is empty
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
filter_pattern, replacement = filter_pair
|
|
93
|
+
return filter_pattern.sub(replacement, text)
|
|
94
|
+
|
|
14
95
|
|
|
15
96
|
def format_arg(value: Any) -> str:
|
|
16
97
|
"""Helper to format an argument an argument for logging"""
|
|
@@ -69,7 +150,6 @@ def dynamic_import(import_path: str) -> Any:
|
|
|
69
150
|
Dynamically import an object from a conventionally formatted "module:attribute"
|
|
70
151
|
string
|
|
71
152
|
"""
|
|
72
|
-
log.debug("Trying to import %s", import_path)
|
|
73
153
|
module_name, attr = import_path.split(":", maxsplit=1)
|
|
74
154
|
|
|
75
155
|
# Check if the module is a file path, if it can be resolved and exists on disk then import as a file
|
|
@@ -78,10 +158,11 @@ def dynamic_import(import_path: str) -> Any:
|
|
|
78
158
|
module_path = (
|
|
79
159
|
module_filepath.stem
|
|
80
160
|
if Path(module_name).is_absolute()
|
|
81
|
-
else str(Path(module_name).with_suffix("")).replace(os.sep, ".")
|
|
161
|
+
else str(Path(module_name).with_suffix("")).replace(os.sep, ".").lstrip(".")
|
|
82
162
|
)
|
|
83
163
|
|
|
84
164
|
if module_path not in sys.modules:
|
|
165
|
+
log.debug("Loading '%s' from file '%s'", module_path, module_filepath)
|
|
85
166
|
spec = importlib.util.spec_from_file_location(
|
|
86
167
|
module_path, str(module_filepath)
|
|
87
168
|
)
|
|
@@ -96,7 +177,9 @@ def dynamic_import(import_path: str) -> Any:
|
|
|
96
177
|
|
|
97
178
|
# Otherwise, import as a module
|
|
98
179
|
try:
|
|
180
|
+
log.debug("Importing module '%s'", module_name)
|
|
99
181
|
module = importlib.import_module(module_name)
|
|
182
|
+
log.debug("Loading '%s' from module '%s'", attr, module_name)
|
|
100
183
|
return getattr(module, attr)
|
|
101
184
|
except TypeError as err:
|
|
102
185
|
raise ImportError(
|
semantic_release/hvcs/_base.py
CHANGED
|
@@ -30,7 +30,7 @@ class HvcsBase(metaclass=ABCMeta):
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
def __init__(self, remote_url: str, *args: Any, **kwargs: Any) -> None:
|
|
33
|
-
self._remote_url = remote_url
|
|
33
|
+
self._remote_url = remote_url if parse_git_url(remote_url) else ""
|
|
34
34
|
self._name: str | None = None
|
|
35
35
|
self._owner: str | None = None
|
|
36
36
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from contextlib import suppress
|
|
5
|
+
from functools import reduce
|
|
5
6
|
from queue import LifoQueue
|
|
6
7
|
from typing import TYPE_CHECKING, Iterable
|
|
7
8
|
|
|
@@ -89,7 +90,6 @@ def _traverse_graph_for_commits(
|
|
|
89
90
|
# Add all parent commits to the stack from left to right so that the rightmost is popped first
|
|
90
91
|
# as the left side is generally the merged into branch
|
|
91
92
|
for parent in node.parents:
|
|
92
|
-
logger.debug("queuing parent commit %s", parent.hexsha[:7])
|
|
93
93
|
stack.put(parent)
|
|
94
94
|
|
|
95
95
|
return commits
|
|
@@ -347,11 +347,25 @@ def next_version(
|
|
|
347
347
|
)
|
|
348
348
|
|
|
349
349
|
# Step 5. Parse the commits to determine the bump level that should be applied
|
|
350
|
-
parsed_levels: set[LevelBump] = {
|
|
350
|
+
parsed_levels: set[LevelBump] = { # type: ignore[var-annotated] # too complex for type checkers
|
|
351
351
|
parsed_result.bump # type: ignore[union-attr] # too complex for type checkers
|
|
352
352
|
for parsed_result in filter(
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
# Filter out any non-ParsedCommit results (i.e. ParseErrors)
|
|
354
|
+
lambda parsed_result: isinstance(parsed_result, ParsedCommit), # type: ignore[arg-type]
|
|
355
|
+
reduce(
|
|
356
|
+
# Accumulate all parsed results into a single list
|
|
357
|
+
lambda accumulated_results, parsed_results: [
|
|
358
|
+
*accumulated_results,
|
|
359
|
+
*(
|
|
360
|
+
parsed_results
|
|
361
|
+
if isinstance(parsed_results, Iterable)
|
|
362
|
+
else [parsed_results] # type: ignore[list-item]
|
|
363
|
+
),
|
|
364
|
+
],
|
|
365
|
+
# apply the parser to each commit in the history (could return multiple results per commit)
|
|
366
|
+
map(commit_parser.parse, commits_since_last_release),
|
|
367
|
+
[],
|
|
368
|
+
),
|
|
355
369
|
)
|
|
356
370
|
}
|
|
357
371
|
|
{python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/AUTHORS.rst
RENAMED
|
File without changes
|
{python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_semantic_release-9.16.0.dist-info → python_semantic_release-9.17.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|