Skip to content

Instantly share code, notes, and snippets.

@M1ke
Last active July 6, 2021 15:57
Show Gist options
  • Save M1ke/2ed2087e1bf4ebab80b6d04d2951599a to your computer and use it in GitHub Desktop.
Save M1ke/2ed2087e1bf4ebab80b6d04d2951599a to your computer and use it in GitHub Desktop.
Handy PHP script to examine and merge lists of BitBucket PRs
<?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