app/classes/ReleaseInsights/Bugzilla.php (99 lines of code) (raw):
<?php
declare(strict_types=1);
namespace ReleaseInsights;
class Bugzilla
{
/**
* Create a bugzilla link for multiple bugs
*
* @param array<mixed> $bug_numbers List of bug numbers
*
* @return string Link
*/
public static function getBugListLink(array $bug_numbers): string
{
$bug_numbers = array_unique($bug_numbers);
$bug_numbers = array_filter($bug_numbers, 'is_numeric');
return URL::Bugzilla->value . 'buglist.cgi?bug_id=' . implode('%2C', $bug_numbers);
}
/**
* Turn bug numbers in a string into Bugzilla links
*/
public static function linkify(string $text): ?string
{
return preg_replace_callback(
"/bug +\d+/i",
fn(array $matches) => '<a href="'
. URL::Bugzilla->value
. trim(str_ireplace('bug', '', (string) $matches[0]))
. '">'
. $matches[0]
. '</a>',
$text
);
}
/**
* Extract bug numbers from a mozilla mercurial log generated by the Web interface
*
* @param string $query The web url to get information from
* @param bool $detect_backouts Decide if we want to detect backouts separately, false by default
* @param int $cache_ttl Caching time in seconds, by default set to zero
*
* @return array<mixed>
*/
public static function getBugsFromHgWeb(string $query, bool $detect_backouts = false, int $cache_ttl = 0): array
{
if (json_validate($query)) {
// We sometimes pass directly the result of a JSON response
$results = Json::toArray($query)['pushes'] ?? null;
} else {
$results = Json::load($query, $cache_ttl)['pushes'] ?? null;
}
// Handle the lack of data from HG
if (empty($results)) {
return [
'bug_fixes' => [],
'backouts' => [],
'total' => [],
'no_data' => true,
];
}
$changesets = array_column($results, 'changesets');
$bug_fixes = [];
$backouts = [];
// Extract bug number from commit message
$get_bugs = function (string $str): array {
if (preg_match_all("/bug \d+/", $str, $matches)) {
$matches[0] = array_map(
fn (string $str) => str_replace('bug', '', $str),
$matches[0]
);
$matches[0] = array_map('trim', $matches[0]);
}
return count($matches[0]) > 1 ? [$matches[0][0]] : $matches[0];
};
foreach ($changesets as $items) {
foreach ($items as $subitem) {
$subitem = explode("\n", (string) $subitem['desc'])[0];
$subitem = strtolower(Utils::mtrim($subitem));
if (Utils::startsWith($subitem, ['no bug', 'automatic version bump'])) {
continue;
}
// Commits can be ignored if they contain one of these strings
$ignore_list = [
'a=test-only', 'a=release', 'a=npotb', 'a=searchfox-only',
'try-staging', 'taskcluster', 'a=tomprince', 'a=aki', 'a=testing',
'[mozharness]', 'r=aki', 'r=tomprince', 'r=mtabara', 'a=jorgk',
'beetmover', '[taskgraph]', 'a=testonly', 'a=bustage',
'a=expectation-update-for-worker-image',
'a=repo-update',
];
if (Utils::inString($subitem, $ignore_list)) {
continue;
}
if (Utils::inString($subitem, ['Backed out', 'backed out', 'back out', 'Back out']) && $detect_backouts === true) {
$counter = count($bug_fixes);
$bug_fixes = array_diff($bug_fixes, $get_bugs($subitem));
if ($counter === count($bug_fixes)) {
$backouts[] = $subitem;
}
continue;
}
// We only include the first bug number mentionned for normal cases
$bug_fixes = array_merge($bug_fixes, array_slice($get_bugs($subitem), 0, 1));
}
}
$bug_fixes = array_unique($bug_fixes);
$backed_out_bugs = [];
foreach ($backouts as $backout) {
$backed_out_bugs = [...$backed_out_bugs, ...$get_bugs($backout)];
}
$backed_out_bugs = array_unique($backed_out_bugs);
// Substract bug_fixes that were backed out later
$clean_bug_fixes = $bug_fixes;
$clean_backed_out_bugs = array_diff($backed_out_bugs, $bug_fixes);
$clean_bug_fixes = array_map('intval', $clean_bug_fixes);
$clean_backed_out_bugs = array_map('intval', $clean_backed_out_bugs);
return [
'bug_fixes' => array_values($clean_bug_fixes),
'backouts' => array_values($clean_backed_out_bugs),
'total' => array_values([...$clean_bug_fixes, ...$clean_backed_out_bugs]),
'no_data' => false,
];
}
}