Last active
July 6, 2021 15:57
-
-
Save M1ke/2ed2087e1bf4ebab80b6d04d2951599a to your computer and use it in GitHub Desktop.
Handy PHP script to examine and merge lists of BitBucket PRs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* Declare the following consts: | |
* | |
* REPO_DEFAULT - Set this to avoid having to type each time | |
* TEAM - Find this in URLs | |
* USER, PASS - Bitbucket credentials | |
* | |
* Install Guzzle and require './vendor/autoload.php' | |
*/ | |
const BASE_URI = 'https://api.bitbucket.org/2.0/repositories/'; | |
const URI = BASE_URI.TEAM.'/'; | |
const ROLE_REVIEWER = 'REVIEWER'; | |
const FLAG_MERGE = '--merge'; | |
const FLAG_REPO = '--repo'; | |
const FLAG_SHOW = '--show'; | |
const FLAG_FORCE = '--force'; | |
const STATE_APPROVED = 'approved'; | |
// This one is quite subjective | |
const NOSQUASH = '[nosquash]'; | |
$client = new Client([ | |
'auth' => [USER, PASS,], | |
]); | |
function getPr(Client $client, string $id, string $repo, bool $permit_one = false): ?object{ | |
$response = clientGet($client, URI.$repo.'/pullrequests/'.$id); | |
$pr = json_decode($response->getBody() | |
->getContents(), false); | |
$is_reviewed = null; | |
foreach ($pr->participants as $participant){ | |
if ($participant->role!==ROLE_REVIEWER){ | |
continue; | |
} | |
$approved = $participant->state===STATE_APPROVED; | |
if (is_null($is_reviewed)){ | |
$is_reviewed = $approved; | |
} | |
else { | |
$is_reviewed = $permit_one ? ($approved || $is_reviewed) : ($approved && $is_reviewed); | |
} | |
} | |
if ($is_reviewed){ | |
return $pr; | |
} | |
return null; | |
} | |
function clientGet(Client $client, string $url): ResponseInterface{ | |
try { | |
return $client->get($url, [ | |
'headers' => [ | |
'Accept' => 'application/json', | |
], | |
]); | |
} | |
catch (ServerException $e){ | |
die("Something went wrong when fetching PRs from Bitbucket:\n{$e->getResponse()->getBody()->getContents()}"); | |
} | |
} | |
function getPrs(Client $client, string $url, string $repo, bool $permit_one = false): array{ | |
$response = clientGet($client, $url); | |
$prs = json_decode($response->getBody() | |
->getContents(), false); | |
$merge = []; | |
foreach ($prs->values as $pr){ | |
if ($pr->destination->branch->name!=='master'){ | |
continue; | |
} | |
$result = getPr($client, $pr->id, $repo, $permit_one); | |
if ($result){ | |
$merge[] = $result; | |
} | |
} | |
if ($prs->next){ | |
$merge = array_merge($merge, getPrs($client, $prs->next, $repo, $permit_one)); | |
} | |
return $merge; | |
} | |
function doMerge(Client $client, object $pr): void{ | |
echo "--- MERGING ---\n"; | |
[$message, $no_squash] = commitMsgFromPr($pr); | |
$merge = [ | |
'message' => $message, | |
'close_source_branch' => true, | |
'merge_strategy' => $no_squash ? 'merge_commit' : 'squash', | |
]; | |
$url = $pr->links->merge->href; | |
try { | |
$client->post($url, [ | |
GuzzleHttp\RequestOptions::JSON => $merge, | |
]); | |
echo "--- MERGED ---\n"; | |
} | |
catch (RequestException $e){ | |
$response = $e->getResponse(); | |
if (!is_null($response)){ | |
$data = json_decode($response->getBody() | |
->getContents(), true); | |
echo "ERROR: {$data['error']['message']}\n"; | |
} | |
} | |
} | |
/** | |
* @psalm-return array{string, bool} | |
*/ | |
function commitMsgFromPr(object $pr): array{ | |
$description = str_replace("\r\n", "\n", $pr->description); | |
$description = trim($description); | |
$no_squash = false; | |
$commit_msg = "Merged in {$pr->source->branch->name} (pull request #$pr->id)"; | |
if ($description){ | |
if (strpos($description, NOSQUASH)!==false){ | |
$no_squash = true; | |
$description = str_replace(NOSQUASH, '', $description); | |
} | |
$commit_msg .= "\n\n$description\n"; | |
} | |
foreach ($pr->participants as $participant){ | |
if ($participant->role!==ROLE_REVIEWER || $participant->state!==STATE_APPROVED){ | |
continue; | |
} | |
$commit_msg .= "\nApproved-by: {$participant->user->display_name}"; | |
} | |
return [$commit_msg, $no_squash]; | |
} | |
function outputPr(object $pr, bool $show_commit = false): void{ | |
$title = $pr->title; | |
$merge = $pr->links->merge->href; | |
[$commit_msg, $no_squash] = commitMsgFromPr($pr); | |
$no_squash_msg = $no_squash ? ' [no squash]' : ''; | |
echo "{$title}{$no_squash_msg}: $merge\n"; | |
if ($show_commit){ | |
echo "$commit_msg\n\n"; | |
} | |
} | |
echo "Polling Bitbucket for approved PRs to master\n"; | |
$is_repo = array_search(FLAG_REPO, $argv, true); | |
if ($is_repo){ | |
$repo = $argv[$is_repo + 1]; | |
} | |
if (!$repo){ | |
$repo = REPO_DEFAULT; | |
} | |
// Force allows just one approval, out of possibly many | |
$permit_one = in_array(FLAG_FORCE, $argv, true); | |
$prs = getPrs($client, URI.$repo.'/pullrequests?state=OPEN', $repo, $permit_one); | |
rsort($prs); | |
$count = count($prs); | |
$do_merge = array_search(FLAG_MERGE, $argv, true); | |
$merge_num = (int) $argv[$do_merge + 1]; | |
if ($do_merge && $merge_num!==$count){ | |
echo "There are $count PRs to merge but the value passed in was '{$merge_num}'. To avoid accidental merges the flag must match the number of PRs, which you can report on by removing the ".FLAG_MERGE." flag\n"; | |
return; | |
} | |
$show_msg = $do_merge || in_array(FLAG_SHOW, $argv, true); | |
foreach ($prs as $pr){ | |
outputPr($pr, $show_msg); | |
if ($do_merge){ | |
sleep(5); | |
doMerge($client, $pr); | |
echo "\n"; | |
} | |
} | |
if (!$do_merge){ | |
echo "\n"; | |
if (!$show_msg){ | |
echo "To see example commit messages, run with '".FLAG_SHOW."'\n"; | |
} | |
echo "PRs to merge: $count. Run again with '".FLAG_MERGE." $count' to carry these out\n"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment