Skip to content

Commit 34615cd

Browse files
committed
Added conventional commit actions.
- ParseConventionalCommit - ParseBreakingChangeFooter
1 parent 217b898 commit 34615cd

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

generate_changelog/actions/metadata.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from generate_changelog.actions import register_builtin
1010
from generate_changelog.data_merge import comprehensive_merge
1111

12-
REGEX_RFC822_KEY_VALUE = r"(?:^|\n)(?P<key>[-\w]*)\s*:\s*(?P<value>[^\n]*(?:\n\s+[^\n]*)*)"
12+
RFC822_KEY_VALUE_PATTERN = r"(?:^|\n)(?P<key>[-\w]*)\s*:\s*(?P<value>[^\n]*(?:\n\s+[^\n]*)*)"
13+
BREAKING_CHANGE_PATTERN = r"(?:^|\n)BREAKING[-_ ]CHANGE\s*:\s*(?P<description>[^\n]*(?:\n\s+[^\n]*)*)"
14+
CONV_COMMIT_PATTERN = r"(?i)^(?P<type>[\w]+)(\((?P<scope>[\\,/\w\-]+)\))?(?P<breaking>!)?: (?P<description>.*)"
1315

1416

1517
@dataclass
@@ -39,11 +41,13 @@ def __call__(self, message: str) -> str:
3941
"""Parse and extract trailers from a commit message."""
4042
pos = len(message)
4143
trailers = defaultdict(list)
42-
for match in re.finditer(REGEX_RFC822_KEY_VALUE, message, re.MULTILINE | re.IGNORECASE):
44+
for match in re.finditer(RFC822_KEY_VALUE_PATTERN, message, re.MULTILINE | re.IGNORECASE):
4345
pos = min(pos, match.start())
4446
dct = match.groupdict()
4547
key = dct["key"].lower()
4648
value = dct["value"]
49+
50+
# Convert a multiline description to a single line.
4751
if "\n" in value:
4852
first_line, remaining = value.split("\n", 1)
4953
value = f"{first_line}\n{textwrap.dedent(remaining)}"
@@ -75,7 +79,8 @@ def __call__(self, message: str) -> str:
7579
Returns:
7680
The commit message for later processing.
7781
"""
78-
if matches := self.issue_pattern.findall(message):
82+
matches = self.issue_pattern.findall(message)
83+
if matches:
7984
self.commit_metadata(issue=matches)
8085
return message
8186

@@ -120,3 +125,98 @@ class ParseAzureBoardIssue(ParseIssue):
120125
"""
121126

122127
issue_pattern = re.compile(r"(?im)AB#(\d+)")
128+
129+
130+
@register_builtin
131+
class ParseBreakingChangeFooter:
132+
"""Parse a breaking change footer."""
133+
134+
def __init__(self, commit_metadata: Callable):
135+
self.commit_metadata = commit_metadata
136+
137+
def __call__(self, message: str) -> str:
138+
"""
139+
Parse a BREAKING CHANGE footer.
140+
141+
Args:
142+
message: The commit message
143+
144+
Returns:
145+
The commit message for later processing.
146+
"""
147+
from more_itertools import chunked
148+
149+
msg_length = len(message)
150+
breaking_changes = []
151+
message_cut_points = [0] # a list of start and stop positions
152+
153+
for match in re.finditer(BREAKING_CHANGE_PATTERN, message, re.MULTILINE | re.IGNORECASE):
154+
start_pos = min(msg_length, match.start())
155+
end_pos = min(msg_length, match.end())
156+
message_cut_points.extend([start_pos, end_pos])
157+
158+
value = match.groupdict()["description"]
159+
160+
# Convert a multiline description to a single line.
161+
if "\n" in value:
162+
first_line, remaining = value.split("\n", 1)
163+
value = f"{first_line}\n{textwrap.dedent(remaining)}"
164+
165+
breaking_changes.append(value)
166+
167+
message_cut_points.append(msg_length)
168+
169+
message_spans = [
170+
message[start_pos:end_pos] for start_pos, end_pos in chunked(message_cut_points, 2, strict=True)
171+
]
172+
if breaking_changes:
173+
self.commit_metadata(has_breaking_change=True, breaking_changes=" ".join(breaking_changes))
174+
175+
return "".join(message_spans)
176+
177+
178+
@register_builtin
179+
class ParseConventionalCommit:
180+
"""
181+
Parse a line of text using the conventional commit syntax.
182+
183+
The metadata will contain ``commit_type``, a string and ``scopes``, an empty list or a list of strings.
184+
185+
If a breaking change is indicated (with the ``!``), metadata will also contain ``has_breaking_change`` set
186+
to ``True``.
187+
188+
The description is returned for further processing.
189+
190+
If the summary does not match a conventional commit, the whole line is returned.
191+
"""
192+
193+
def __init__(self, commit_metadata: Callable):
194+
self.commit_metadata = commit_metadata
195+
196+
def __call__(self, message: str) -> str:
197+
"""
198+
Parse a line of text using the conventional commit syntax.
199+
200+
Args:
201+
message: The commit message
202+
203+
Returns:
204+
The description for later processing.
205+
"""
206+
match = re.match(CONV_COMMIT_PATTERN, message)
207+
if not match:
208+
return message
209+
210+
grp_dict = match.groupdict()
211+
metadata = {
212+
"commit_type": grp_dict["type"],
213+
"scope": [],
214+
}
215+
216+
if grp_dict["breaking"]:
217+
metadata["has_breaking_change"] = True
218+
if grp_dict["scope"]:
219+
scopes = re.split(r"[\\,/]\s*", grp_dict["scope"])
220+
metadata["scope"] = scopes
221+
self.commit_metadata(**metadata)
222+
return grp_dict["description"]

requirements/prod.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
GitPython
22
jinja2
3+
more-itertools
34
pyyaml
45
typer

0 commit comments

Comments
 (0)