python-semantic-release 9.12.2__py3-none-any.whl → 9.13.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.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/METADATA +1 -1
- {python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/RECORD +18 -17
- {python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/WHEEL +1 -1
- semantic_release/__init__.py +1 -1
- semantic_release/commit_parser/angular.py +60 -33
- semantic_release/commit_parser/emoji.py +86 -39
- semantic_release/commit_parser/scipy.py +30 -81
- semantic_release/commit_parser/token.py +30 -0
- semantic_release/commit_parser/util.py +35 -4
- semantic_release/data/templates/angular/md/.components/changes.md.j2 +40 -12
- semantic_release/data/templates/angular/md/.components/macros.md.j2 +48 -0
- semantic_release/data/templates/angular/md/CHANGELOG.md.j2 +3 -2
- semantic_release/data/templates/angular/rst/.components/changes.rst.j2 +59 -19
- semantic_release/data/templates/angular/rst/.components/macros.rst.j2 +71 -2
- {python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/AUTHORS.rst +0 -0
- {python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/LICENSE +0 -0
- {python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/entry_points.txt +0 -0
- {python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/top_level.txt +0 -0
{python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/RECORD
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
semantic_release/__init__.py,sha256=
|
|
1
|
+
semantic_release/__init__.py,sha256=HkSV-d4oTywSXoJB6atMrEkQnimWPdJJGqSSrFeYjQQ,1229
|
|
2
2
|
semantic_release/__main__.py,sha256=kuotDU7aFKrCBeAJUPWrbIxgJWAmrXUMnztCqWMDMPY,1292
|
|
3
3
|
semantic_release/const.py,sha256=Z1o2QNh60wSLeF-_1TemMBjU3ZXbV0XghnUFsbTVfOs,831
|
|
4
4
|
semantic_release/enums.py,sha256=D5B_reQGGKQQT22HO5PUtvn2Bok3fkht6TfJtXkmAUg,1020
|
|
@@ -26,26 +26,27 @@ semantic_release/cli/commands/publish.py,sha256=SZQlIewvqyIC14dkIIVVFetE0tPsKbO1
|
|
|
26
26
|
semantic_release/cli/commands/version.py,sha256=PKNoP_b8puzcScKkQEbeB3DviJv49cQ-vjq6v25nG9Q,23931
|
|
27
27
|
semantic_release/commit_parser/__init__.py,sha256=cv5HFBdw7OJd4Laj4Ex8ZZ5Tml8GwXgQcXW6Pasr2Ao,615
|
|
28
28
|
semantic_release/commit_parser/_base.py,sha256=t-Z9ALgAe7aZpYXz1mk3Fe-uAvipgKdNrq4Okg_WW9c,3026
|
|
29
|
-
semantic_release/commit_parser/angular.py,sha256=
|
|
30
|
-
semantic_release/commit_parser/emoji.py,sha256=
|
|
31
|
-
semantic_release/commit_parser/scipy.py,sha256=
|
|
29
|
+
semantic_release/commit_parser/angular.py,sha256=a1AmgstkclzJnRWqSsF4cuO8awuOJe7VAG__1W7WF7U,7276
|
|
30
|
+
semantic_release/commit_parser/emoji.py,sha256=4OQ5yyHBNC-pA7d6KP_pMjKxutwVaBvO_ydqCT1KtJA,5849
|
|
31
|
+
semantic_release/commit_parser/scipy.py,sha256=u_5OWOeM2V7_jZzYfEicKqx0GJBsAqIOfzRX_4cmOqk,3973
|
|
32
32
|
semantic_release/commit_parser/tag.py,sha256=4uwIKBqUM2SE6UTGIw-a7B6Jg1OONXmGwXsTyL3yZBA,3490
|
|
33
|
-
semantic_release/commit_parser/token.py,sha256
|
|
34
|
-
semantic_release/commit_parser/util.py,sha256=
|
|
33
|
+
semantic_release/commit_parser/token.py,sha256=-C1ZKG7pdbcGT2nc3-L2APLUUDGTXkbDeNi5mvvUwjk,2621
|
|
34
|
+
semantic_release/commit_parser/util.py,sha256=b9lud_FBjsmimLrvILf_NXvZ2wg8JPDmA364hcfM6is,1710
|
|
35
35
|
semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=bJeaCDzbva8ntefPPeAS3YD0GXkZv-j0_fIbVmnAKbQ,52
|
|
36
|
-
semantic_release/data/templates/angular/md/CHANGELOG.md.j2,sha256=
|
|
36
|
+
semantic_release/data/templates/angular/md/CHANGELOG.md.j2,sha256=zsf5C75jBP2JUdl_7dJvEXgPURvFp6GxPKGRelqLghM,854
|
|
37
37
|
semantic_release/data/templates/angular/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
|
|
38
38
|
semantic_release/data/templates/angular/md/.components/changelog_init.md.j2,sha256=d3tS_nCe_ttNQRGl8Jan4H42iJ8hKu03HrfdEdGAh5M,599
|
|
39
39
|
semantic_release/data/templates/angular/md/.components/changelog_update.md.j2,sha256=JhXF-vYlqd4qNI8rO2Hte3Jbemo17wdeZ7w3G2nwjus,2384
|
|
40
|
-
semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=
|
|
40
|
+
semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=qA190vxbADim4M1b2f-oWF4uWT7Vx8ToSjKrRkPeJfA,1638
|
|
41
|
+
semantic_release/data/templates/angular/md/.components/macros.md.j2,sha256=cUrl4tqjhR6Ur-gl_gHsuRz1DVU7X_-0QYnswahoTm0,1709
|
|
41
42
|
semantic_release/data/templates/angular/md/.components/unreleased_changes.md.j2,sha256=HRLj6cyRfPZXC0s-0Av6s0Gp3jKxWg9AIEtIXBVqJuY,177
|
|
42
43
|
semantic_release/data/templates/angular/md/.components/versioned_changes.md.j2,sha256=2Hky2mBrC4jltz3mvaiPDD0KQP0ELe5Ag75HgaLpaIE,257
|
|
43
44
|
semantic_release/data/templates/angular/rst/CHANGELOG.rst.j2,sha256=OQPAKgqTnYGXFyDTm8hwYsMWXfi_z8I5Fsgtw09sWBQ,840
|
|
44
45
|
semantic_release/data/templates/angular/rst/.components/changelog_header.rst.j2,sha256=c9xN1SEYLFwMvPYXYKt-ZbYPn2-Ss0V7zepEtFFj3Os,200
|
|
45
46
|
semantic_release/data/templates/angular/rst/.components/changelog_init.rst.j2,sha256=dOICVsTMVLewBDC9QMHhX6Ub9rUFlBAzHOGvsFtNzYY,596
|
|
46
47
|
semantic_release/data/templates/angular/rst/.components/changelog_update.rst.j2,sha256=L44PJyCb1xge2wC2-w9cAPCLoJDFVIsm8BnZeQOzg94,2381
|
|
47
|
-
semantic_release/data/templates/angular/rst/.components/changes.rst.j2,sha256=
|
|
48
|
-
semantic_release/data/templates/angular/rst/.components/macros.rst.j2,sha256=
|
|
48
|
+
semantic_release/data/templates/angular/rst/.components/changes.rst.j2,sha256=0JDnsFghFqHezOim9IuQhlVig5mHBKf8Aq7_Y9M2UQE,2852
|
|
49
|
+
semantic_release/data/templates/angular/rst/.components/macros.rst.j2,sha256=C_5V80wQu-uCLE_Qp3ACwGS-4jQDGc7NmKaDi3C6m1s,2774
|
|
49
50
|
semantic_release/data/templates/angular/rst/.components/unreleased_changes.rst.j2,sha256=ARBhc1ZpKwehGKDvOMqukmN59mTJiHzHsS7rOfKYCt8,202
|
|
50
51
|
semantic_release/data/templates/angular/rst/.components/versioned_changes.rst.j2,sha256=NZfn1W14QochiAJ43oNKmcrCn_vgfbkKtvOTAw1jEc8,530
|
|
51
52
|
semantic_release/hvcs/__init__.py,sha256=JwoaLOF-12L-OBo_9-tOXXhdiHKeVungA9865to2oZk,494
|
|
@@ -62,10 +63,10 @@ semantic_release/version/algorithm.py,sha256=ofx_bIWq6ptJVr-ekI11IzxzDEctDKFiVwa
|
|
|
62
63
|
semantic_release/version/declaration.py,sha256=f6Ld7hIhrqvDrRBapJHr-KDimuyo-4IG8009Zu9BIgU,7357
|
|
63
64
|
semantic_release/version/translator.py,sha256=P1noIsVBn8u6zNOFjG0xKYOWapxqf_PHSMvMeLJ9kXg,3050
|
|
64
65
|
semantic_release/version/version.py,sha256=6PCtSbLP88U1daoxnCwHc--YguZo4waGNLqJ5JfeczE,14175
|
|
65
|
-
python_semantic_release-9.
|
|
66
|
-
python_semantic_release-9.
|
|
67
|
-
python_semantic_release-9.
|
|
68
|
-
python_semantic_release-9.
|
|
69
|
-
python_semantic_release-9.
|
|
70
|
-
python_semantic_release-9.
|
|
71
|
-
python_semantic_release-9.
|
|
66
|
+
python_semantic_release-9.13.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
|
|
67
|
+
python_semantic_release-9.13.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
|
|
68
|
+
python_semantic_release-9.13.0.dist-info/METADATA,sha256=aFjXFtGi0j7sN3G2CfwdCPBGENzFlgkU9a2oDp_2Jeo,3571
|
|
69
|
+
python_semantic_release-9.13.0.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
|
|
70
|
+
python_semantic_release-9.13.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
|
|
71
|
+
python_semantic_release-9.13.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
|
|
72
|
+
python_semantic_release-9.13.0.dist-info/RECORD,,
|
semantic_release/__init__.py
CHANGED
|
@@ -8,19 +8,26 @@ from __future__ import annotations
|
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
10
|
from functools import reduce
|
|
11
|
+
from itertools import zip_longest
|
|
11
12
|
from re import compile as regexp
|
|
12
13
|
from typing import TYPE_CHECKING, Tuple
|
|
13
14
|
|
|
14
15
|
from pydantic.dataclasses import dataclass
|
|
15
16
|
|
|
16
17
|
from semantic_release.commit_parser._base import CommitParser, ParserOptions
|
|
17
|
-
from semantic_release.commit_parser.token import
|
|
18
|
+
from semantic_release.commit_parser.token import (
|
|
19
|
+
ParsedCommit,
|
|
20
|
+
ParsedMessageResult,
|
|
21
|
+
ParseError,
|
|
22
|
+
ParseResult,
|
|
23
|
+
)
|
|
18
24
|
from semantic_release.commit_parser.util import breaking_re, parse_paragraphs
|
|
19
25
|
from semantic_release.enums import LevelBump
|
|
20
26
|
|
|
21
27
|
if TYPE_CHECKING:
|
|
22
28
|
from git.objects.commit import Commit
|
|
23
29
|
|
|
30
|
+
|
|
24
31
|
logger = logging.getLogger(__name__)
|
|
25
32
|
|
|
26
33
|
|
|
@@ -65,11 +72,16 @@ class AngularParserOptions(ParserOptions):
|
|
|
65
72
|
default_bump_level: LevelBump = LevelBump.NO_RELEASE
|
|
66
73
|
|
|
67
74
|
def __post_init__(self) -> None:
|
|
68
|
-
self.tag_to_level
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
self.tag_to_level: dict[str, LevelBump] = dict(
|
|
76
|
+
[
|
|
77
|
+
# we have to do a type ignore as zip_longest provides a type that is not specific enough
|
|
78
|
+
# for our expected output. Due to the empty second array, we know the first is always longest
|
|
79
|
+
# and that means no values in the first entry of the tuples will ever be a LevelBump.
|
|
80
|
+
*zip_longest(self.allowed_tags, (), fillvalue=self.default_bump_level), # type: ignore[list-item]
|
|
81
|
+
*zip_longest(self.patch_tags, (), fillvalue=LevelBump.PATCH), # type: ignore[list-item]
|
|
82
|
+
*zip_longest(self.minor_tags, (), fillvalue=LevelBump.MINOR), # type: ignore[list-item]
|
|
83
|
+
]
|
|
84
|
+
)
|
|
73
85
|
|
|
74
86
|
|
|
75
87
|
class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
@@ -98,6 +110,10 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
98
110
|
),
|
|
99
111
|
flags=re.DOTALL,
|
|
100
112
|
)
|
|
113
|
+
# GitHub & Gitea use (#123), GitLab uses (!123), and BitBucket uses (pull request #123)
|
|
114
|
+
self.mr_selector = regexp(
|
|
115
|
+
r"[\t ]+\((?:pull request )?(?P<mr_number>[#!]\d+)\)[\t ]*$"
|
|
116
|
+
)
|
|
101
117
|
|
|
102
118
|
@staticmethod
|
|
103
119
|
def get_default_options() -> AngularParserOptions:
|
|
@@ -114,27 +130,23 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
114
130
|
accumulator["descriptions"].append(text)
|
|
115
131
|
return accumulator
|
|
116
132
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def parse(self, commit: Commit) -> ParseResult:
|
|
122
|
-
"""
|
|
123
|
-
Attempt to parse the commit message with a regular expression into a
|
|
124
|
-
ParseResult
|
|
125
|
-
"""
|
|
126
|
-
message = str(commit.message)
|
|
127
|
-
parsed = self.re_parser.match(message)
|
|
128
|
-
if not parsed:
|
|
129
|
-
return _logged_parse_error(
|
|
130
|
-
commit, f"Unable to parse commit message: {message}"
|
|
131
|
-
)
|
|
133
|
+
def parse_message(self, message: str) -> ParsedMessageResult | None:
|
|
134
|
+
if not (parsed := self.re_parser.match(message)):
|
|
135
|
+
return None
|
|
136
|
+
|
|
132
137
|
parsed_break = parsed.group("break")
|
|
133
138
|
parsed_scope = parsed.group("scope")
|
|
134
139
|
parsed_subject = parsed.group("subject")
|
|
135
140
|
parsed_text = parsed.group("text")
|
|
136
141
|
parsed_type = parsed.group("type")
|
|
137
142
|
|
|
143
|
+
linked_merge_request = ""
|
|
144
|
+
if mr_match := self.mr_selector.search(parsed_subject):
|
|
145
|
+
linked_merge_request = mr_match.group("mr_number")
|
|
146
|
+
# TODO: breaking change v10, removes PR number from subject/descriptions
|
|
147
|
+
# expects changelog template to format the line accordingly
|
|
148
|
+
# parsed_subject = self.pr_selector.sub("", parsed_subject).strip()
|
|
149
|
+
|
|
138
150
|
body_components: dict[str, list[str]] = reduce(
|
|
139
151
|
self.commit_body_components_separator,
|
|
140
152
|
[
|
|
@@ -157,19 +169,34 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
|
|
|
157
169
|
)
|
|
158
170
|
)
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
parsed_type
|
|
172
|
+
return ParsedMessageResult(
|
|
173
|
+
bump=level_bump,
|
|
174
|
+
type=parsed_type,
|
|
175
|
+
category=LONG_TYPE_NAMES.get(parsed_type, parsed_type),
|
|
176
|
+
scope=parsed_scope,
|
|
177
|
+
descriptions=tuple(body_components["descriptions"]),
|
|
178
|
+
breaking_descriptions=tuple(body_components["breaking_descriptions"]),
|
|
179
|
+
linked_merge_request=linked_merge_request,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Maybe this can be cached as an optimization, similar to how
|
|
183
|
+
# mypy/pytest use their own caching directories, for very large commit
|
|
184
|
+
# histories?
|
|
185
|
+
# The problem is the cache likely won't be present in CI environments
|
|
186
|
+
def parse(self, commit: Commit) -> ParseResult:
|
|
187
|
+
"""
|
|
188
|
+
Attempt to parse the commit message with a regular expression into a
|
|
189
|
+
ParseResult
|
|
190
|
+
"""
|
|
191
|
+
if not (pmsg_result := self.parse_message(str(commit.message))):
|
|
192
|
+
return _logged_parse_error(
|
|
193
|
+
commit, f"Unable to parse commit message: {commit.message!r}"
|
|
194
|
+
)
|
|
163
195
|
|
|
164
196
|
logger.debug(
|
|
165
|
-
"commit %s introduces a %s level_bump",
|
|
197
|
+
"commit %s introduces a %s level_bump",
|
|
198
|
+
commit.hexsha[:8],
|
|
199
|
+
pmsg_result.bump,
|
|
166
200
|
)
|
|
167
201
|
|
|
168
|
-
return ParsedCommit(
|
|
169
|
-
bump=level_bump,
|
|
170
|
-
type=LONG_TYPE_NAMES.get(parsed_type, parsed_type),
|
|
171
|
-
scope=parsed_scope,
|
|
172
|
-
descriptions=body_components["descriptions"],
|
|
173
|
-
breaking_descriptions=body_components["breaking_descriptions"],
|
|
174
|
-
commit=commit,
|
|
175
|
-
)
|
|
202
|
+
return ParsedCommit.from_parsed_message_result(commit, pmsg_result)
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
"""Commit parser which looks for emojis to determine the type of commit"""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import logging
|
|
6
|
+
from itertools import zip_longest
|
|
7
|
+
from re import compile as regexp
|
|
4
8
|
from typing import Tuple
|
|
5
9
|
|
|
6
10
|
from git.objects.commit import Commit
|
|
7
11
|
from pydantic.dataclasses import dataclass
|
|
8
12
|
|
|
9
13
|
from semantic_release.commit_parser._base import CommitParser, ParserOptions
|
|
10
|
-
from semantic_release.commit_parser.token import
|
|
14
|
+
from semantic_release.commit_parser.token import (
|
|
15
|
+
ParsedCommit,
|
|
16
|
+
ParsedMessageResult,
|
|
17
|
+
ParseResult,
|
|
18
|
+
)
|
|
11
19
|
from semantic_release.commit_parser.util import parse_paragraphs
|
|
12
20
|
from semantic_release.enums import LevelBump
|
|
13
21
|
|
|
@@ -41,8 +49,26 @@ class EmojiParserOptions(ParserOptions):
|
|
|
41
49
|
":robot:",
|
|
42
50
|
":green_apple:",
|
|
43
51
|
)
|
|
52
|
+
allowed_tags: Tuple[str, ...] = (
|
|
53
|
+
*major_tags,
|
|
54
|
+
*minor_tags,
|
|
55
|
+
*patch_tags,
|
|
56
|
+
)
|
|
44
57
|
default_bump_level: LevelBump = LevelBump.NO_RELEASE
|
|
45
58
|
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
self.tag_to_level: dict[str, LevelBump] = dict(
|
|
61
|
+
[
|
|
62
|
+
# we have to do a type ignore as zip_longest provides a type that is not specific enough
|
|
63
|
+
# for our expected output. Due to the empty second array, we know the first is always longest
|
|
64
|
+
# and that means no values in the first entry of the tuples will ever be a LevelBump.
|
|
65
|
+
*zip_longest(self.allowed_tags, (), fillvalue=self.default_bump_level), # type: ignore[list-item]
|
|
66
|
+
*zip_longest(self.patch_tags, (), fillvalue=LevelBump.PATCH), # type: ignore[list-item]
|
|
67
|
+
*zip_longest(self.minor_tags, (), fillvalue=LevelBump.MINOR), # type: ignore[list-item]
|
|
68
|
+
*zip_longest(self.major_tags, (), fillvalue=LevelBump.MAJOR), # type: ignore[list-item]
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
46
72
|
|
|
47
73
|
class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
48
74
|
"""
|
|
@@ -60,56 +86,77 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
|
|
|
60
86
|
# TODO: Deprecate in lieu of get_default_options()
|
|
61
87
|
parser_options = EmojiParserOptions
|
|
62
88
|
|
|
89
|
+
def __init__(self, options: EmojiParserOptions | None = None) -> None:
|
|
90
|
+
super().__init__(options)
|
|
91
|
+
prcedence_order_regex = str.join(
|
|
92
|
+
"|",
|
|
93
|
+
[
|
|
94
|
+
*self.options.major_tags,
|
|
95
|
+
*self.options.minor_tags,
|
|
96
|
+
*self.options.patch_tags,
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
self.emoji_selector = regexp(r"(?P<type>%s)" % prcedence_order_regex)
|
|
100
|
+
|
|
101
|
+
# GitHub & Gitea use (#123), GitLab uses (!123), and BitBucket uses (pull request #123)
|
|
102
|
+
self.mr_selector = regexp(
|
|
103
|
+
r"[\t ]+\((?:pull request )?(?P<mr_number>[#!]\d+)\)[\t ]*$"
|
|
104
|
+
)
|
|
105
|
+
|
|
63
106
|
@staticmethod
|
|
64
107
|
def get_default_options() -> EmojiParserOptions:
|
|
65
108
|
return EmojiParserOptions()
|
|
66
109
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
def parse_message(self, message: str) -> ParsedMessageResult:
|
|
111
|
+
subject = message.split("\n", maxsplit=1)[0]
|
|
112
|
+
|
|
113
|
+
linked_merge_request = ""
|
|
114
|
+
if mr_match := self.mr_selector.search(subject):
|
|
115
|
+
linked_merge_request = mr_match.group("mr_number")
|
|
116
|
+
# TODO: breaking change v10, removes PR number from subject/descriptions
|
|
117
|
+
# expects changelog template to format the line accordingly
|
|
118
|
+
# subject = self.mr_selector.sub("", subject).strip()
|
|
119
|
+
|
|
120
|
+
# Search for emoji of the highest importance in the subject
|
|
121
|
+
primary_emoji = (
|
|
122
|
+
match.group("type")
|
|
123
|
+
if (match := self.emoji_selector.search(subject))
|
|
124
|
+
else "Other"
|
|
70
125
|
)
|
|
71
126
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Loop over emojis from most important to least important
|
|
76
|
-
# Therefore, we find the highest level emoji first
|
|
77
|
-
primary_emoji = "Other"
|
|
78
|
-
for emoji in all_emojis:
|
|
79
|
-
if emoji in subject:
|
|
80
|
-
primary_emoji = emoji
|
|
81
|
-
break
|
|
82
|
-
logger.debug("Selected %s as the primary emoji", primary_emoji)
|
|
83
|
-
|
|
84
|
-
# Find which level this commit was from
|
|
85
|
-
level_bump = LevelBump.NO_RELEASE
|
|
86
|
-
if primary_emoji in self.options.major_tags:
|
|
87
|
-
level_bump = LevelBump.MAJOR
|
|
88
|
-
elif primary_emoji in self.options.minor_tags:
|
|
89
|
-
level_bump = LevelBump.MINOR
|
|
90
|
-
elif primary_emoji in self.options.patch_tags:
|
|
91
|
-
level_bump = LevelBump.PATCH
|
|
92
|
-
else:
|
|
93
|
-
level_bump = self.options.default_bump_level
|
|
94
|
-
logger.debug(
|
|
95
|
-
"commit %s introduces a level bump of %s due to the default_bump_level",
|
|
96
|
-
commit.hexsha[:8],
|
|
97
|
-
level_bump,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
logger.debug(
|
|
101
|
-
"commit %s introduces a %s level_bump", commit.hexsha[:8], level_bump
|
|
127
|
+
level_bump = self.options.tag_to_level.get(
|
|
128
|
+
primary_emoji, self.options.default_bump_level
|
|
102
129
|
)
|
|
103
130
|
|
|
104
131
|
# All emojis will remain part of the returned description
|
|
105
|
-
descriptions = parse_paragraphs(message)
|
|
106
|
-
return
|
|
132
|
+
descriptions = tuple(parse_paragraphs(message))
|
|
133
|
+
return ParsedMessageResult(
|
|
107
134
|
bump=level_bump,
|
|
108
135
|
type=primary_emoji,
|
|
109
|
-
|
|
136
|
+
category=primary_emoji,
|
|
137
|
+
scope="", # TODO: add scope support
|
|
138
|
+
# TODO: breaking change v10, removes breaking change footers from descriptions
|
|
139
|
+
# descriptions=(
|
|
140
|
+
# descriptions[:1] if level_bump is LevelBump.MAJOR else descriptions
|
|
141
|
+
# )
|
|
110
142
|
descriptions=descriptions,
|
|
111
143
|
breaking_descriptions=(
|
|
112
|
-
descriptions[1:] if level_bump is LevelBump.MAJOR else
|
|
144
|
+
descriptions[1:] if level_bump is LevelBump.MAJOR else ()
|
|
113
145
|
),
|
|
114
|
-
|
|
146
|
+
linked_merge_request=linked_merge_request,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def parse(self, commit: Commit) -> ParseResult:
|
|
150
|
+
"""
|
|
151
|
+
Attempt to parse the commit message with a regular expression into a
|
|
152
|
+
ParseResult
|
|
153
|
+
"""
|
|
154
|
+
pmsg_result = self.parse_message(str(commit.message))
|
|
155
|
+
|
|
156
|
+
logger.debug(
|
|
157
|
+
"commit %s introduces a %s level_bump",
|
|
158
|
+
commit.hexsha[:8],
|
|
159
|
+
pmsg_result.bump,
|
|
115
160
|
)
|
|
161
|
+
|
|
162
|
+
return ParsedCommit.from_parsed_message_result(commit, pmsg_result)
|
|
@@ -47,13 +47,18 @@ Supported Changelog Sections::
|
|
|
47
47
|
from __future__ import annotations
|
|
48
48
|
|
|
49
49
|
import logging
|
|
50
|
-
import re
|
|
51
50
|
from typing import TYPE_CHECKING, Tuple
|
|
52
51
|
|
|
53
52
|
from pydantic.dataclasses import dataclass
|
|
54
53
|
|
|
55
|
-
from semantic_release.commit_parser.
|
|
56
|
-
|
|
54
|
+
from semantic_release.commit_parser.angular import (
|
|
55
|
+
AngularCommitParser,
|
|
56
|
+
AngularParserOptions,
|
|
57
|
+
)
|
|
58
|
+
from semantic_release.commit_parser.token import (
|
|
59
|
+
ParsedMessageResult,
|
|
60
|
+
ParseError,
|
|
61
|
+
)
|
|
57
62
|
from semantic_release.enums import LevelBump
|
|
58
63
|
|
|
59
64
|
if TYPE_CHECKING:
|
|
@@ -86,11 +91,16 @@ tag_to_section = {
|
|
|
86
91
|
"TEST": "None",
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
_COMMIT_FILTER = "|".join(tag_to_section)
|
|
90
|
-
|
|
91
94
|
|
|
92
95
|
@dataclass
|
|
93
|
-
class ScipyParserOptions(
|
|
96
|
+
class ScipyParserOptions(AngularParserOptions):
|
|
97
|
+
"""
|
|
98
|
+
Options dataclass for ScipyCommitParser
|
|
99
|
+
|
|
100
|
+
Scipy-style commit messages follow the same format as Angular-style commit
|
|
101
|
+
just with different tag names.
|
|
102
|
+
"""
|
|
103
|
+
|
|
94
104
|
major_tags: Tuple[str, ...] = ("API",)
|
|
95
105
|
minor_tags: Tuple[str, ...] = ("DEP", "DEV", "ENH", "REV", "FEAT")
|
|
96
106
|
patch_tags: Tuple[str, ...] = ("BLD", "BUG", "MAINT")
|
|
@@ -105,19 +115,18 @@ class ScipyParserOptions(ParserOptions):
|
|
|
105
115
|
"REL",
|
|
106
116
|
"TEST",
|
|
107
117
|
)
|
|
118
|
+
# TODO: breaking v10, make consistent with AngularParserOptions
|
|
108
119
|
default_level_bump: LevelBump = LevelBump.NO_RELEASE
|
|
109
120
|
|
|
110
121
|
def __post_init__(self) -> None:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for tag in self.minor_tags:
|
|
115
|
-
self.tag_to_level[tag] = LevelBump.MINOR
|
|
122
|
+
# TODO: breaking v10, remove as the name is now consistent
|
|
123
|
+
self.default_bump_level = self.default_level_bump
|
|
124
|
+
super().__post_init__()
|
|
116
125
|
for tag in self.major_tags:
|
|
117
126
|
self.tag_to_level[tag] = LevelBump.MAJOR
|
|
118
127
|
|
|
119
128
|
|
|
120
|
-
class ScipyCommitParser(
|
|
129
|
+
class ScipyCommitParser(AngularCommitParser):
|
|
121
130
|
"""Parser for scipy-style commit messages"""
|
|
122
131
|
|
|
123
132
|
# TODO: Deprecate in lieu of get_default_options()
|
|
@@ -125,79 +134,19 @@ class ScipyCommitParser(CommitParser[ParseResult, ScipyParserOptions]):
|
|
|
125
134
|
|
|
126
135
|
def __init__(self, options: ScipyParserOptions | None = None) -> None:
|
|
127
136
|
super().__init__(options)
|
|
128
|
-
self.re_parser = re.compile(
|
|
129
|
-
rf"(?P<tag>{_COMMIT_FILTER})?"
|
|
130
|
-
r"(?:\((?P<scope>[^\n]+)\))?"
|
|
131
|
-
r":? "
|
|
132
|
-
r"(?P<subject>[^\n]+):?"
|
|
133
|
-
r"(\n\n(?P<text>.*))?",
|
|
134
|
-
re.DOTALL,
|
|
135
|
-
)
|
|
136
137
|
|
|
137
138
|
@staticmethod
|
|
138
139
|
def get_default_options() -> ScipyParserOptions:
|
|
139
140
|
return ScipyParserOptions()
|
|
140
141
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
def parse_message(self, message: str) -> ParsedMessageResult | None:
|
|
143
|
+
return (
|
|
144
|
+
None
|
|
145
|
+
if not (pmsg_result := super().parse_message(message))
|
|
146
|
+
else ParsedMessageResult(
|
|
147
|
+
**{
|
|
148
|
+
**pmsg_result._asdict(),
|
|
149
|
+
"category": tag_to_section.get(pmsg_result.type, "None"),
|
|
150
|
+
}
|
|
148
151
|
)
|
|
149
|
-
|
|
150
|
-
if parsed.group("subject"):
|
|
151
|
-
subject = parsed.group("subject")
|
|
152
|
-
else:
|
|
153
|
-
return _logged_parse_error(commit, f"Commit has no subject {message!r}")
|
|
154
|
-
|
|
155
|
-
if parsed.group("text"):
|
|
156
|
-
blocks = parsed.group("text").split("\n\n")
|
|
157
|
-
blocks = [x for x in blocks if x]
|
|
158
|
-
blocks.insert(0, subject)
|
|
159
|
-
else:
|
|
160
|
-
blocks = [subject]
|
|
161
|
-
|
|
162
|
-
for tag in self.options.allowed_tags:
|
|
163
|
-
if tag == parsed.group("tag"):
|
|
164
|
-
section = tag_to_section.get(tag, "None")
|
|
165
|
-
level_bump = self.options.tag_to_level.get(
|
|
166
|
-
tag, self.options.default_level_bump
|
|
167
|
-
)
|
|
168
|
-
logger.debug(
|
|
169
|
-
"commit %s introduces a %s level_bump",
|
|
170
|
-
commit.hexsha[:8],
|
|
171
|
-
level_bump,
|
|
172
|
-
)
|
|
173
|
-
break
|
|
174
|
-
else:
|
|
175
|
-
# some commits may not have a tag, e.g. if they belong to a PR that
|
|
176
|
-
# wasn't squashed (for maintainability) ignore them
|
|
177
|
-
section, level_bump = "None", self.options.default_level_bump
|
|
178
|
-
logger.debug(
|
|
179
|
-
"commit %s introduces a level bump of %s due to the default bump level",
|
|
180
|
-
commit.hexsha[:8],
|
|
181
|
-
level_bump,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Look for descriptions of breaking changes
|
|
185
|
-
migration_instructions = [
|
|
186
|
-
block for block in blocks if block.startswith("BREAKING CHANGE")
|
|
187
|
-
]
|
|
188
|
-
if migration_instructions:
|
|
189
|
-
level_bump = LevelBump.MAJOR
|
|
190
|
-
logger.debug(
|
|
191
|
-
"commit %s upgraded to a %s level bump due to included migration instructions",
|
|
192
|
-
commit.hexsha[:8],
|
|
193
|
-
level_bump,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
return ParsedCommit(
|
|
197
|
-
bump=level_bump,
|
|
198
|
-
type=section,
|
|
199
|
-
scope=parsed.group("scope"),
|
|
200
|
-
descriptions=blocks,
|
|
201
|
-
breaking_descriptions=migration_instructions,
|
|
202
|
-
commit=commit,
|
|
203
152
|
)
|
|
@@ -10,6 +10,16 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from semantic_release.enums import LevelBump
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class ParsedMessageResult(NamedTuple):
|
|
14
|
+
bump: LevelBump
|
|
15
|
+
type: str
|
|
16
|
+
category: str
|
|
17
|
+
scope: str
|
|
18
|
+
descriptions: tuple[str, ...]
|
|
19
|
+
breaking_descriptions: tuple[str, ...] = ()
|
|
20
|
+
linked_merge_request: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
class ParsedCommit(NamedTuple):
|
|
14
24
|
bump: LevelBump
|
|
15
25
|
type: str
|
|
@@ -17,6 +27,7 @@ class ParsedCommit(NamedTuple):
|
|
|
17
27
|
descriptions: list[str]
|
|
18
28
|
breaking_descriptions: list[str]
|
|
19
29
|
commit: Commit
|
|
30
|
+
linked_merge_request: str = ""
|
|
20
31
|
|
|
21
32
|
@property
|
|
22
33
|
def message(self) -> str:
|
|
@@ -32,6 +43,25 @@ class ParsedCommit(NamedTuple):
|
|
|
32
43
|
def short_hash(self) -> str:
|
|
33
44
|
return self.commit.hexsha[:7]
|
|
34
45
|
|
|
46
|
+
@property
|
|
47
|
+
def linked_pull_request(self) -> str:
|
|
48
|
+
return self.linked_merge_request
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def from_parsed_message_result(
|
|
52
|
+
commit: Commit, parsed_message_result: ParsedMessageResult
|
|
53
|
+
) -> ParsedCommit:
|
|
54
|
+
return ParsedCommit(
|
|
55
|
+
bump=parsed_message_result.bump,
|
|
56
|
+
# TODO: breaking v10, swap back to type rather than category
|
|
57
|
+
type=parsed_message_result.category,
|
|
58
|
+
scope=parsed_message_result.scope,
|
|
59
|
+
descriptions=list(parsed_message_result.descriptions),
|
|
60
|
+
breaking_descriptions=list(parsed_message_result.breaking_descriptions),
|
|
61
|
+
commit=commit,
|
|
62
|
+
linked_merge_request=parsed_message_result.linked_merge_request,
|
|
63
|
+
)
|
|
64
|
+
|
|
35
65
|
|
|
36
66
|
class ParseError(NamedTuple):
|
|
37
67
|
commit: Commit
|
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from functools import reduce
|
|
4
|
+
from re import compile as regexp
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from re import Pattern
|
|
9
|
+
from typing import TypedDict
|
|
10
|
+
|
|
11
|
+
class RegexReplaceDef(TypedDict):
|
|
12
|
+
pattern: Pattern
|
|
13
|
+
repl: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
breaking_re = regexp(r"BREAKING[ -]CHANGE:\s?(.*)")
|
|
17
|
+
un_word_wrap: RegexReplaceDef = {
|
|
18
|
+
# Match a line ending where the next line is not indented, or a bullet
|
|
19
|
+
"pattern": regexp(r"((?<!-)\n(?![\s*-]))"),
|
|
20
|
+
"repl": r" ", # Replace with a space
|
|
21
|
+
}
|
|
22
|
+
un_word_wrap_hyphen: RegexReplaceDef = {
|
|
23
|
+
"pattern": regexp(r"((?<=\w)-\n(?=\w))"),
|
|
24
|
+
"repl": r"-", # Replace with single hyphen
|
|
25
|
+
}
|
|
26
|
+
trim_line_endings: RegexReplaceDef = {
|
|
27
|
+
# Match line endings with optional whitespace
|
|
28
|
+
"pattern": regexp(r"[\r\t\f\v ]*\r?\n"),
|
|
29
|
+
"repl": "\n", # remove the optional whitespace & remove windows newlines
|
|
30
|
+
}
|
|
6
31
|
|
|
7
32
|
|
|
8
33
|
def parse_paragraphs(text: str) -> list[str]:
|
|
@@ -16,12 +41,18 @@ def parse_paragraphs(text: str) -> list[str]:
|
|
|
16
41
|
:param text: The text string to be divided.
|
|
17
42
|
:return: A list of condensed paragraphs, as strings.
|
|
18
43
|
"""
|
|
44
|
+
adjusted_text = reduce(
|
|
45
|
+
lambda txt, adj: adj["pattern"].sub(adj["repl"], txt),
|
|
46
|
+
[trim_line_endings, un_word_wrap_hyphen],
|
|
47
|
+
text,
|
|
48
|
+
)
|
|
49
|
+
|
|
19
50
|
return list(
|
|
20
51
|
filter(
|
|
21
52
|
None,
|
|
22
53
|
[
|
|
23
|
-
|
|
24
|
-
for paragraph in
|
|
54
|
+
un_word_wrap["pattern"].sub(un_word_wrap["repl"], paragraph).strip()
|
|
55
|
+
for paragraph in adjusted_text.split("\n\n")
|
|
25
56
|
],
|
|
26
57
|
)
|
|
27
58
|
)
|
|
@@ -1,16 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
"\n### %s\n" | format(type_ | title)
|
|
1
|
+
{% from 'macros.md.j2' import format_commit_summary_line
|
|
2
|
+
%}{#
|
|
3
|
+
EXAMPLE:
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
%}{{
|
|
8
|
-
"\n* %s ([`%s`](%s))\n" | format(
|
|
9
|
-
commit.message.rstrip(),
|
|
10
|
-
commit.short_hash,
|
|
11
|
-
commit.hexsha | commit_hash_url,
|
|
12
|
-
)
|
|
5
|
+
### Features
|
|
13
6
|
|
|
7
|
+
- Add new feature ([#10](https://domain.com/namespace/repo/pull/10),
|
|
8
|
+
[`abcdef0`](https://domain.com/namespace/repo/commit/HASH))
|
|
9
|
+
|
|
10
|
+
- Add new feature ([`abcdef0`](https://domain.com/namespace/repo/commit/HASH))
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- Fix bug ([#11](https://domain.com/namespace/repo/pull/11),
|
|
15
|
+
[`abcdef1`](https://domain.com/namespace/repo/commit/HASH))
|
|
16
|
+
|
|
17
|
+
#}{% set max_line_width = 100
|
|
18
|
+
%}{% set hanging_indent = 2
|
|
19
|
+
%}{#
|
|
20
|
+
#}{% for type_, commits in commit_objects if type_ != "unknown"
|
|
21
|
+
%}{# PREPROCESS COMMITS (order by description & format description line)
|
|
22
|
+
#}{% set commit_descriptions = []
|
|
23
|
+
%}{% for commit in commits
|
|
24
|
+
%}{# # Update the first line with reference links and if commit description
|
|
25
|
+
# has more than one line, add the rest of the lines
|
|
26
|
+
# NOTE: This is specifically to make sure to not hide contents
|
|
27
|
+
# of squash commits (until parse support is added)
|
|
28
|
+
#}{% set description = "- %s" | format(format_commit_summary_line(commit))
|
|
29
|
+
%}{% if commit.descriptions | length > 1
|
|
30
|
+
%}{% set description = "%s\n\n%s" | format(
|
|
31
|
+
description, commit.descriptions[1:] | join("\n\n")
|
|
32
|
+
)
|
|
33
|
+
%}{% endif
|
|
34
|
+
%}{% set description = description | autofit_text_width(max_line_width, hanging_indent)
|
|
35
|
+
%}{{ commit_descriptions.append(description) | default("", true)
|
|
14
36
|
}}{% endfor
|
|
15
|
-
%}{
|
|
37
|
+
%}{#
|
|
38
|
+
# # PRINT SECTION (header & commits)
|
|
39
|
+
#}{{ "\n"
|
|
40
|
+
}}{{ "### %s\n" | format(type_ | title)
|
|
41
|
+
}}{{ "\n"
|
|
42
|
+
}}{{ "%s\n" | format(commit_descriptions | unique | join("\n\n"))
|
|
43
|
+
}}{% endfor
|
|
16
44
|
%}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{#
|
|
2
|
+
MACRO: commit message links or PR/MR links of commit
|
|
3
|
+
#}{% macro commit_msg_links(commit)
|
|
4
|
+
%}{% if commit.error is undefined
|
|
5
|
+
%}{% set commit_hash_link = "[`%s`](%s)" | format(
|
|
6
|
+
commit.short_hash, commit.hexsha | commit_hash_url
|
|
7
|
+
)
|
|
8
|
+
%}{#
|
|
9
|
+
#}{% set summary_line = commit.descriptions[0] | safe
|
|
10
|
+
%}{% set summary_line = [
|
|
11
|
+
summary_line.split(" ", maxsplit=1)[0] | capitalize,
|
|
12
|
+
summary_line.split(" ", maxsplit=1)[1]
|
|
13
|
+
] | join(" ")
|
|
14
|
+
%}{#
|
|
15
|
+
#}{% if commit.linked_merge_request != ""
|
|
16
|
+
%}{# # Add PR references with a link to the PR
|
|
17
|
+
#}{% set pr_num = commit.linked_merge_request
|
|
18
|
+
%}{% set pr_link = "[%s](%s)" | format(pr_num, pr_num | pull_request_url)
|
|
19
|
+
%}{#
|
|
20
|
+
# TODO: breaking change v10, remove summary line replacers as PSR will do it for us
|
|
21
|
+
#}{% set summary_line = summary_line | replace("(pull request", "(") | replace("(" ~ pr_num ~ ")", "") | trim
|
|
22
|
+
%}{% set summary_line = "%s (%s, %s)" | format(
|
|
23
|
+
summary_line,
|
|
24
|
+
pr_link,
|
|
25
|
+
commit_hash_link,
|
|
26
|
+
)
|
|
27
|
+
%}{#
|
|
28
|
+
# DEFAULT: No PR identifier found, so just append commit hash as url to the commit summary_line
|
|
29
|
+
#}{% else
|
|
30
|
+
%}{% set summary_line = "%s (%s)" | format(summary_line, commit_hash_link)
|
|
31
|
+
%}{% endif
|
|
32
|
+
%}{#
|
|
33
|
+
# Return the modified summary_line
|
|
34
|
+
#}{{ summary_line
|
|
35
|
+
}}{% endif
|
|
36
|
+
%}{% endmacro
|
|
37
|
+
%}
|
|
38
|
+
|
|
39
|
+
{#
|
|
40
|
+
MACRO: format commit summary line
|
|
41
|
+
#}{% macro format_commit_summary_line(commit)
|
|
42
|
+
%}{% if commit.error is undefined
|
|
43
|
+
%}{{ commit_msg_links(commit)
|
|
44
|
+
}}{% else
|
|
45
|
+
%}{{ (commit.commit.message | string).split("\n", maxsplit=1)[0]
|
|
46
|
+
}}{% endif
|
|
47
|
+
%}{% endmacro
|
|
48
|
+
%}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
#}{% set insertion_flag = ctx.changelog_insertion_flag
|
|
10
10
|
%}{% set unreleased_commits = ctx.history.unreleased | dictsort
|
|
11
|
+
%}{% set releases = ctx.history.released.values() | list
|
|
11
12
|
%}{#
|
|
12
13
|
#}{% if ctx.changelog_mode == "init"
|
|
13
14
|
%}{% include ".components/changelog_init.md.j2"
|
|
@@ -15,8 +16,8 @@
|
|
|
15
16
|
#}{% elif ctx.changelog_mode == "update"
|
|
16
17
|
%}{% set prev_changelog_file = ctx.prev_changelog_file
|
|
17
18
|
%}{% set new_releases = []
|
|
18
|
-
%}{% if
|
|
19
|
-
%}{% set new_releases = [
|
|
19
|
+
%}{% if releases | length > 0
|
|
20
|
+
%}{% set new_releases = [releases | first]
|
|
20
21
|
%}{% endif
|
|
21
22
|
%}{% include ".components/changelog_update.md.j2"
|
|
22
23
|
%}{#
|
|
@@ -1,36 +1,76 @@
|
|
|
1
|
-
{%
|
|
1
|
+
{% from 'macros.rst.j2' import extract_pr_link_reference, format_link_reference
|
|
2
|
+
%}{% from 'macros.rst.j2' import format_commit_summary_line, generate_heading_underline
|
|
2
3
|
%}{#
|
|
3
4
|
|
|
4
5
|
Features
|
|
5
6
|
--------
|
|
6
7
|
|
|
7
|
-
*
|
|
8
|
+
* Add new feature (`#10`_, `8a7b8ec`_)
|
|
9
|
+
|
|
10
|
+
* Add another feature (`abcdef0`_)
|
|
8
11
|
|
|
9
12
|
Fixes
|
|
10
13
|
-----
|
|
11
14
|
|
|
12
|
-
*
|
|
15
|
+
* Fix bug (`#11`_, `8a7b8ec`_)
|
|
13
16
|
|
|
14
|
-
..
|
|
17
|
+
.. _10: https://domain.com/namespace/repo/pull/10
|
|
18
|
+
.. _8a7B8ec: https://domain.com/owner/repo/commit/8a7b8ec
|
|
19
|
+
.. _abcdef0: https://domain.com/owner/repo/commit/abcdef0
|
|
20
|
+
.. _11: https://domain.com/namespace/repo/pull/11
|
|
15
21
|
|
|
16
|
-
#}{%
|
|
22
|
+
#}{% set max_line_width = 100
|
|
23
|
+
%}{% set hanging_indent = 2
|
|
24
|
+
%}{#
|
|
25
|
+
#}{% set post_paragraph_links = []
|
|
26
|
+
%}{#
|
|
27
|
+
#}{% for type_, commits in commit_objects if type_ != "unknown"
|
|
28
|
+
%}{# PREPARE SECTION HEADER
|
|
29
|
+
#}{% set section_header = "%s" | format(type_ | title)
|
|
30
|
+
%}{# PREPROCESS COMMITS
|
|
31
|
+
#}{% set commit_descriptions = []
|
|
32
|
+
%}{#
|
|
33
|
+
#}{% for commit in commits
|
|
34
|
+
%}{# # Extract PR/MR reference if it exists and store it for later
|
|
35
|
+
#}{% set pr_link_reference = extract_pr_link_reference(commit) | default("", true)
|
|
36
|
+
%}{% if pr_link_reference != ""
|
|
37
|
+
%}{{ post_paragraph_links.append(pr_link_reference) | default("", true)
|
|
38
|
+
}}{% endif
|
|
39
|
+
%}{#
|
|
40
|
+
# # Always generate a commit hash reference link and store it for later
|
|
41
|
+
#}{% set commit_hash_link_reference = format_link_reference(
|
|
42
|
+
commit.hexsha | commit_hash_url,
|
|
43
|
+
commit.short_hash
|
|
44
|
+
)
|
|
45
|
+
%}{{ post_paragraph_links.append(commit_hash_link_reference) | default("", true)
|
|
46
|
+
}}{#
|
|
47
|
+
# Generate the commit summary line and format it for RST
|
|
48
|
+
# Update the first line with reference links and if commit description
|
|
49
|
+
# has more than one line, add the rest of the lines
|
|
50
|
+
# NOTE: This is specifically to make sure to not hide contents
|
|
51
|
+
# of squash commits (until parse support is added)
|
|
52
|
+
#}{% set description = "* %s" | format(format_commit_summary_line(commit))
|
|
53
|
+
%}{% if commit.descriptions | length > 1
|
|
54
|
+
%}{% set description = "%s\n\n%s" | format(
|
|
55
|
+
description, commit.descriptions[1:] | join("\n\n") | trim
|
|
56
|
+
)
|
|
57
|
+
%}{% endif
|
|
58
|
+
%}{% set description = description | convert_md_to_rst
|
|
59
|
+
%}{% set description = description | autofit_text_width(max_line_width, hanging_indent)
|
|
60
|
+
%}{{ commit_descriptions.append(description) | default("", true)
|
|
61
|
+
}}{% endfor
|
|
17
62
|
%}{#
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
%}{{ "\n"
|
|
63
|
+
# # PRINT SECTION (Header & Commits)
|
|
64
|
+
#}{{ "\n"
|
|
21
65
|
}}{{ section_header ~ "\n"
|
|
22
66
|
}}{{ generate_heading_underline(section_header, '-') ~ "\n"
|
|
23
|
-
}}{#
|
|
24
|
-
#}{% for commit in commits
|
|
25
|
-
%}{% set commit_link_reference = format_link_reference(commit.hexsha | commit_hash_url, commit.short_hash)
|
|
26
|
-
%}{{ post_paragraph_links.append(commit_link_reference) | default("", true)
|
|
27
67
|
}}{{
|
|
28
|
-
|
|
29
|
-
commit.message.rstrip() | convert_md_to_rst,
|
|
30
|
-
commit.short_hash,
|
|
31
|
-
)
|
|
68
|
+
"\n%s\n" | format(commit_descriptions | unique | join("\n\n"))
|
|
32
69
|
|
|
33
|
-
}}{%
|
|
34
|
-
%}{
|
|
70
|
+
}}{% endfor
|
|
71
|
+
%}{#
|
|
72
|
+
#}{% if post_paragraph_links | length > 0
|
|
73
|
+
%}{# # Print out any PR/MR or Issue URL references that were found in the commit messages
|
|
74
|
+
#}{{ "\n%s\n" | format(post_paragraph_links | unique | sort | join("\n"))
|
|
75
|
+
}}{% endif
|
|
35
76
|
%}
|
|
36
|
-
{{ post_paragraph_links | join("\n") ~ "\n" }}
|
|
@@ -1,9 +1,78 @@
|
|
|
1
|
-
{#
|
|
2
|
-
|
|
1
|
+
{#
|
|
2
|
+
MACRO: format a post-paragraph link reference in RST
|
|
3
|
+
#}{% macro format_link_reference(link, label)
|
|
3
4
|
%}{{ ".. _%s: %s" | format(label, link)
|
|
4
5
|
}}{% endmacro
|
|
5
6
|
%}
|
|
6
7
|
|
|
8
|
+
|
|
9
|
+
{#
|
|
10
|
+
MACRO: format commit summary line
|
|
11
|
+
#}{% macro format_commit_summary_line(commit)
|
|
12
|
+
%}{% if commit.error is undefined
|
|
13
|
+
%}{{ commit_msg_links(commit)
|
|
14
|
+
}}{% else
|
|
15
|
+
%}{{ (commit.commit.message | string).split("\n", maxsplit=1)[0]
|
|
16
|
+
}}{% endif
|
|
17
|
+
%}{% endmacro
|
|
18
|
+
%}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
{#
|
|
22
|
+
MACRO: Create & return an non-inline RST link from a commit message
|
|
23
|
+
- Returns empty string if no PR/MR identifier is found
|
|
24
|
+
#}{% macro extract_pr_link_reference(commit)
|
|
25
|
+
%}{% if commit.error is undefined
|
|
26
|
+
%}{% set summary_line = commit.descriptions[0]
|
|
27
|
+
%}{#
|
|
28
|
+
#}{% if commit.linked_merge_request != ""
|
|
29
|
+
%}{# # Create a PR/MR reference url
|
|
30
|
+
#}{{ format_link_reference(
|
|
31
|
+
commit.linked_merge_request | pull_request_url,
|
|
32
|
+
commit.linked_merge_request,
|
|
33
|
+
)
|
|
34
|
+
}}{% endif
|
|
35
|
+
%}{% endif
|
|
36
|
+
%}{% endmacro
|
|
37
|
+
%}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
{#
|
|
41
|
+
MACRO: formats a commit message for a non-inline RST link for a commit hash and/or PR/MR
|
|
42
|
+
#}{% macro commit_msg_links(commit, hvcs_type)
|
|
43
|
+
%}{% if commit.error is undefined
|
|
44
|
+
%}{% set commit_hash_link = "`%s`_" | format(commit.short_hash)
|
|
45
|
+
%}{#
|
|
46
|
+
#}{% set summary_line = commit.descriptions[0] | safe
|
|
47
|
+
%}{% set summary_line = [
|
|
48
|
+
summary_line.split(" ", maxsplit=1)[0] | capitalize,
|
|
49
|
+
summary_line.split(" ", maxsplit=1)[1]
|
|
50
|
+
] | join(" ")
|
|
51
|
+
%}{#
|
|
52
|
+
#}{% if commit.linked_merge_request != ""
|
|
53
|
+
%}{# # Add PR references with a link to the PR
|
|
54
|
+
#}{% set pr_link = "`%s`_" | format(commit.linked_merge_request)
|
|
55
|
+
%}{#
|
|
56
|
+
# TODO: breaking change v10, remove summary line replacers as PSR will do it for us
|
|
57
|
+
#}{% set summary_line = summary_line | replace("(pull request ", "(") | replace("(" ~ commit.linked_merge_request ~ ")", "") | trim
|
|
58
|
+
%}{% set summary_line = "%s (%s, %s)" | format(
|
|
59
|
+
summary_line,
|
|
60
|
+
pr_link,
|
|
61
|
+
commit_hash_link,
|
|
62
|
+
)
|
|
63
|
+
%}{#
|
|
64
|
+
# DEFAULT: No PR identifier found, so just append a commit hash as url to the commit summary_line
|
|
65
|
+
#}{% else
|
|
66
|
+
%}{% set summary_line = "%s (%s)" | format(summary_line, commit_hash_link)
|
|
67
|
+
%}{% endif
|
|
68
|
+
%}{#
|
|
69
|
+
# Return the modified summary_line
|
|
70
|
+
#}{{ summary_line
|
|
71
|
+
}}{% endif
|
|
72
|
+
%}{% endmacro
|
|
73
|
+
%}
|
|
74
|
+
|
|
75
|
+
|
|
7
76
|
{# MACRO: generate a heading underline that matches the exact length of the header #}
|
|
8
77
|
{% macro generate_heading_underline(header, underline_char)
|
|
9
78
|
%}{% set header_underline = []
|
{python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/AUTHORS.rst
RENAMED
|
File without changes
|
{python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
{python_semantic_release-9.12.2.dist-info → python_semantic_release-9.13.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|