Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: improve changelog parsing and order #768

Merged
merged 3 commits into from
Aug 10, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 125 additions & 30 deletions buildtools/changelog.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

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

// Categories, in display order
$catgroup = [
'💣 Breaking Changes' => [],
'✨ New Features' => [],
'🐞 Bug Fixes' => [],
'🚀 Performance Improvements' => [],
'♻️ Refactoring' => [],
'🚨 Testing' => [],
'📚 Documentation' => [],
'💎 Style Changes' => [],
'🔧 Chore' => [],
'📜 Miscellaneous Changes' => []
];

// Pattern list
$categories = [
'break' => '💣 Breaking Changes',
Expand Down Expand Up @@ -42,23 +56,16 @@
'updated' => '📜 Miscellaneous Changes',
];

$catgroup = [];
$changelog = [];
$githubstyle = true;
if (count($argv) > 2 && $argv[1] == '--discord') {
$githubstyle = false;
}
$errors = [];

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

// Leadin
if ($githubstyle) {
echo "The changelog is listed below:\n\nRelease Changelog\n===========\n";
} else {
echo "The changelog is listed below:\n\n__**Release Changelog**__\n";
}

// Case insensitive removal of duplicates
$changelog = array_intersect_key($changelog, array_unique(array_map("strtolower", $changelog)));

Expand All @@ -78,6 +85,20 @@
}
}

function add_change(string $change, string $category, string $scope = null) {
global $catgroup;

if (isset($scope) && !empty($scope)) {
// Group by feature inside the section
if (!isset($catgroup[$category][$scope])) {
$catgroup[$category][$scope] = [];
}
$catgroup[$category][$scope][] = $change;
return;
}
$catgroup[$category][] = $change;
}

foreach ($changelog as $change) {

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

// Match keywords against categories
$matched = false;
foreach ($categories as $cat => $header) {
// Purposefully ignored: comments that are one word, merge commits, and version bumps
if (strpos($change, ' ') === false || preg_match("/^Merge (branch|pull request|remote-tracking branch) /", $change) || preg_match("/version bump/i", $change)) {
$matched = true;
continue;
}
// Groupings
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))) {
if (!isset($catgroup[$header])) {
$catgroup[$header] = [];
$matches = [];
// Extract leading category section
if (preg_match("/^((?:(?:[\w_]+(?:\([\w_]+\))?+)(?:[\s]*[,\/][\s]*)?)+):/i", $change, $matches) || preg_match("/^\[((?:(?:[\w_]+(?:\([\w_]+\))?+)(?:[\s]*[,\/][\s]*)?)+)\](?:\s*:)?/i", $change, $matches)) {
$categorysection = $matches[0];
$changecategories = $matches[1];
$matchflags = PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL;
// Extract each category and scope
if (preg_match_all("/(?:[\s]*)([\w_]+)(?:\(([\w_]+)\))?(?:[\s]*)(?:[,\/]+)?/i", $changecategories, $matches, $matchflags) !== false) {
/**
* Given a commit "foo, bar(foobar): add many foos and bars" :
* $matches is [
* 0 => [[0] => 'foo,', [1] => 'foo', [2] => null],
* 1 => [[0] => ' bar(foobar)', [1] => 'bar', [2] => 'foobar'],
* ]
* In other words, for a matched category N, matches[N][1] is the category, matches[N][2] is the scope
*/
$header = $matches[0][1];
$scope = $matches[0][2];
$change = trim(substr($change, strlen($categorysection)));
// List in breaking if present
foreach ($matches as $nb => $match) {
if ($nb == 0) // Skip the first category which will be added anyways
continue;
$category = $match[1];
if (isset($categories[$category]) && $categories[$category] === '💣 Breaking Changes')
add_change($change, $categories[$category], $scope);
}
if (!isset($categories[$header])) {
$errors[] = "could not find category \"" . $header . "\" for commit \"" . $change . "\", adding it to misc";
$header = $categories['misc'];
}
else {
$header = $categories[$header];
}
$matched = true;
$catgroup[$header][] = preg_replace("/^\S+\s+/", "", $change);
break;
} else if (preg_match("/^" . $cat . " /i", $change)) {
if (!isset($catgroup[$header])) {
$catgroup[$header] = [];
}
$matched = true;
$catgroup[$header][] = $change;
break;
// Ignore version bumps
if (!preg_match("/^(version )?bump/i", $change)) {
add_change($change, $header, $scope);
}
}
}
if (!$matched) { // Could not parse category section, try keywords
// Match keywords against categories
foreach ($categories as $cat => $header) {
// Purposefully ignored: comments that are one word, merge commits, and version bumps
if (strpos($change, ' ') === false || preg_match("/^Merge (branch|pull request|remote-tracking branch) /", $change) || preg_match("/^(version )?bump/i", $change)) {
$matched = true;
break;
}
if (preg_match("/^" . $cat . " /i", $change)) {
if (!isset($catgroup[$header])) {
$catgroup[$header] = [];
}
$matched = true;
$catgroup[$header][] = $change;
break;
}
}
}
if (!$matched) {
$errors[] = "could not guess category for commit \"" . $change . "\", adding it to misc";
$header = $categories['misc'];
if (!isset($catgroup[$header])) {
$catgroup[$header] = [];
}
$matched = true;
$catgroup[$header][] = $change;
}
}

// Leadin
if ($githubstyle) {
echo "The changelog is listed below:\n\nRelease Changelog\n===========\n";
} else {
echo "The changelog is listed below:\n\n__**Release Changelog**__\n";
}

function print_change(string $change) {
global $githubstyle;

// Exclude bad commit messages like 'typo fix', 'test push' etc by pattern
if (!preg_match("/^(typo|test|fix)\s\w+$/", $change) && strpos($change, ' ') !== false) {
echo ($githubstyle ? '-' : '•') . ' ' . ucfirst(str_replace('@', '', $change)) . "\n";
}
}

// Output tidy formatting
foreach ($catgroup as $cat => $list) {
echo "\n" . ($githubstyle ? '## ' : '__**') . $cat . ($githubstyle ? '' : '**__') . "\n";
foreach ($list as $item) {
// Exclude bad commit messages like 'typo fix', 'test push' etc by pattern
if (!preg_match("/^(typo|test|fix)\s\w+$/", $item) && strpos($item, ' ') !== false) {
echo ($githubstyle ? '-' : '•') . ' ' . ucfirst(str_replace('@', '', $item)) . "\n";
if (!empty($list)) {
echo "\n" . ($githubstyle ? '## ' : '__**') . $cat . ($githubstyle ? '' : '**__') . "\n";
foreach ($list as $key => $item) {
if (is_array($item)) {
foreach ($item as $change) {
print_change("$key: $change");
}
}
else {
print_change($item);
}
}
}
}
Expand All @@ -138,3 +229,7 @@
echo 'The ' . $version . ' download can be found here: <https://dl.dpp.dev/' . $version . '>';
echo "\n";
}

foreach ($errors as $err) {
trigger_error($err, E_USER_WARNING);
}
Loading