Skip to content

Commit 877d7d5

Browse files
build: improve changelog parsing and order (#768)
2 parents 282c0e0 + 8e2b104 commit 877d7d5

File tree

1 file changed

+125
-30
lines changed

1 file changed

+125
-30
lines changed

buildtools/changelog.php

+125-30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
// D++ changelog generator, saves 15 minutes for each release :-)
44

5+
// Categories, in display order
6+
$catgroup = [
7+
'💣 Breaking Changes' => [],
8+
'✨ New Features' => [],
9+
'🐞 Bug Fixes' => [],
10+
'🚀 Performance Improvements' => [],
11+
'♻️ Refactoring' => [],
12+
'🚨 Testing' => [],
13+
'📚 Documentation' => [],
14+
'💎 Style Changes' => [],
15+
'🔧 Chore' => [],
16+
'📜 Miscellaneous Changes' => []
17+
];
18+
519
// Pattern list
620
$categories = [
721
'break' => '💣 Breaking Changes',
@@ -42,23 +56,16 @@
4256
'updated' => '📜 Miscellaneous Changes',
4357
];
4458

45-
$catgroup = [];
4659
$changelog = [];
4760
$githubstyle = true;
4861
if (count($argv) > 2 && $argv[1] == '--discord') {
4962
$githubstyle = false;
5063
}
64+
$errors = [];
5165

5266
// Magic sauce
5367
exec("git log --format=\"%s\" $(git log --no-walk --tags | head -n1 | cut -d ' ' -f 2)..HEAD | grep -v '^Merge '", $changelog);
5468

55-
// Leadin
56-
if ($githubstyle) {
57-
echo "The changelog is listed below:\n\nRelease Changelog\n===========\n";
58-
} else {
59-
echo "The changelog is listed below:\n\n__**Release Changelog**__\n";
60-
}
61-
6269
// Case insensitive removal of duplicates
6370
$changelog = array_intersect_key($changelog, array_unique(array_map("strtolower", $changelog)));
6471

@@ -78,6 +85,20 @@
7885
}
7986
}
8087

88+
function add_change(string $change, string $category, string $scope = null) {
89+
global $catgroup;
90+
91+
if (isset($scope) && !empty($scope)) {
92+
// Group by feature inside the section
93+
if (!isset($catgroup[$category][$scope])) {
94+
$catgroup[$category][$scope] = [];
95+
}
96+
$catgroup[$category][$scope][] = $change;
97+
return;
98+
}
99+
$catgroup[$category][] = $change;
100+
}
101+
81102
foreach ($changelog as $change) {
82103

83104
// Wrap anything that looks like a symbol name in backticks
@@ -92,41 +113,111 @@
92113
$change = preg_replace("/\b(was|is|wo)nt\b/i", '$1n\'t', $change);
93114
$change = preg_replace("/\bfreebsd\b/", 'FreeBSD', $change);
94115
$change = preg_replace("/``/", "`", $change);
116+
$change = trim($change);
95117

96-
// Match keywords against categories
97118
$matched = false;
98-
foreach ($categories as $cat => $header) {
99-
// Purposefully ignored: comments that are one word, merge commits, and version bumps
100-
if (strpos($change, ' ') === false || preg_match("/^Merge (branch|pull request|remote-tracking branch) /", $change) || preg_match("/version bump/i", $change)) {
101-
$matched = true;
102-
continue;
103-
}
104-
// Groupings
105-
if ((preg_match("/^" . $cat . ":/i", $change)) || (preg_match("/^\[" . $cat . "\//i", $change)) || (preg_match("/^\[" . $cat . "\]/i", $change)) || (preg_match("/^\[" . $cat . ":/i", $change)) || (preg_match("/^" . $cat . "\//i", $change)) || (preg_match("/^" . $cat . ":/i", $change))) {
106-
if (!isset($catgroup[$header])) {
107-
$catgroup[$header] = [];
119+
$matches = [];
120+
// Extract leading category section
121+
if (preg_match("/^((?:(?:[\w_]+(?:\([\w_]+\))?+)(?:[\s]*[,\/][\s]*)?)+):/i", $change, $matches) || preg_match("/^\[((?:(?:[\w_]+(?:\([\w_]+\))?+)(?:[\s]*[,\/][\s]*)?)+)\](?:\s*:)?/i", $change, $matches)) {
122+
$categorysection = $matches[0];
123+
$changecategories = $matches[1];
124+
$matchflags = PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL;
125+
// Extract each category and scope
126+
if (preg_match_all("/(?:[\s]*)([\w_]+)(?:\(([\w_]+)\))?(?:[\s]*)(?:[,\/]+)?/i", $changecategories, $matches, $matchflags) !== false) {
127+
/**
128+
* Given a commit "foo, bar(foobar): add many foos and bars" :
129+
* $matches is [
130+
* 0 => [[0] => 'foo,', [1] => 'foo', [2] => null],
131+
* 1 => [[0] => ' bar(foobar)', [1] => 'bar', [2] => 'foobar'],
132+
* ]
133+
* In other words, for a matched category N, matches[N][1] is the category, matches[N][2] is the scope
134+
*/
135+
$header = $matches[0][1];
136+
$scope = $matches[0][2];
137+
$change = trim(substr($change, strlen($categorysection)));
138+
// List in breaking if present
139+
foreach ($matches as $nb => $match) {
140+
if ($nb == 0) // Skip the first category which will be added anyways
141+
continue;
142+
$category = $match[1];
143+
if (isset($categories[$category]) && $categories[$category] === '💣 Breaking Changes')
144+
add_change($change, $categories[$category], $scope);
145+
}
146+
if (!isset($categories[$header])) {
147+
$errors[] = "could not find category \"" . $header . "\" for commit \"" . $change . "\", adding it to misc";
148+
$header = $categories['misc'];
149+
}
150+
else {
151+
$header = $categories[$header];
108152
}
109-
$matched = true;
110-
$catgroup[$header][] = preg_replace("/^\S+\s+/", "", $change);
111-
break;
112-
} else if (preg_match("/^" . $cat . " /i", $change)) {
113153
if (!isset($catgroup[$header])) {
114154
$catgroup[$header] = [];
115155
}
116156
$matched = true;
117-
$catgroup[$header][] = $change;
118-
break;
157+
// Ignore version bumps
158+
if (!preg_match("/^(version )?bump/i", $change)) {
159+
add_change($change, $header, $scope);
160+
}
161+
}
162+
}
163+
if (!$matched) { // Could not parse category section, try keywords
164+
// Match keywords against categories
165+
foreach ($categories as $cat => $header) {
166+
// Purposefully ignored: comments that are one word, merge commits, and version bumps
167+
if (strpos($change, ' ') === false || preg_match("/^Merge (branch|pull request|remote-tracking branch) /", $change) || preg_match("/^(version )?bump/i", $change)) {
168+
$matched = true;
169+
break;
170+
}
171+
if (preg_match("/^" . $cat . " /i", $change)) {
172+
if (!isset($catgroup[$header])) {
173+
$catgroup[$header] = [];
174+
}
175+
$matched = true;
176+
$catgroup[$header][] = $change;
177+
break;
178+
}
119179
}
120180
}
181+
if (!$matched) {
182+
$errors[] = "could not guess category for commit \"" . $change . "\", adding it to misc";
183+
$header = $categories['misc'];
184+
if (!isset($catgroup[$header])) {
185+
$catgroup[$header] = [];
186+
}
187+
$matched = true;
188+
$catgroup[$header][] = $change;
189+
}
190+
}
191+
192+
// Leadin
193+
if ($githubstyle) {
194+
echo "The changelog is listed below:\n\nRelease Changelog\n===========\n";
195+
} else {
196+
echo "The changelog is listed below:\n\n__**Release Changelog**__\n";
197+
}
198+
199+
function print_change(string $change) {
200+
global $githubstyle;
201+
202+
// Exclude bad commit messages like 'typo fix', 'test push' etc by pattern
203+
if (!preg_match("/^(typo|test|fix)\s\w+$/", $change) && strpos($change, ' ') !== false) {
204+
echo ($githubstyle ? '-' : '') . ' ' . ucfirst(str_replace('@', '', $change)) . "\n";
205+
}
121206
}
122207

123208
// Output tidy formatting
124209
foreach ($catgroup as $cat => $list) {
125-
echo "\n" . ($githubstyle ? '## ' : '__**') . $cat . ($githubstyle ? '' : '**__') . "\n";
126-
foreach ($list as $item) {
127-
// Exclude bad commit messages like 'typo fix', 'test push' etc by pattern
128-
if (!preg_match("/^(typo|test|fix)\s\w+$/", $item) && strpos($item, ' ') !== false) {
129-
echo ($githubstyle ? '-' : '') . ' ' . ucfirst(str_replace('@', '', $item)) . "\n";
210+
if (!empty($list)) {
211+
echo "\n" . ($githubstyle ? '## ' : '__**') . $cat . ($githubstyle ? '' : '**__') . "\n";
212+
foreach ($list as $key => $item) {
213+
if (is_array($item)) {
214+
foreach ($item as $change) {
215+
print_change("$key: $change");
216+
}
217+
}
218+
else {
219+
print_change($item);
220+
}
130221
}
131222
}
132223
}
@@ -138,3 +229,7 @@
138229
echo 'The ' . $version . ' download can be found here: <https://dl.dpp.dev/' . $version . '>';
139230
echo "\n";
140231
}
232+
233+
foreach ($errors as $err) {
234+
trigger_error($err, E_USER_WARNING);
235+
}

0 commit comments

Comments
 (0)