Add support for two different run types: - Command: executes the command silently and returns whether or not there was an error. This is used to check if the merge was successful. - Query: executes the command and returns the output. This is used for retrieving the tip commits of the branch.
236 lines
6.4 KiB
PHP
Executable file
236 lines
6.4 KiB
PHP
Executable file
#!/usr/bin/env php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Usage: git close-pull-request -t <target>
|
|
*
|
|
* Run this from a branch which has an upstream remote branch, and an associated
|
|
* pull request.
|
|
*
|
|
* The script will merge the branch into master, push master (which will
|
|
* automatically close the pull request), and delete both the local and remote
|
|
* branches.
|
|
*
|
|
* Based on a script by @christoomey. Translated into PHP.
|
|
*/
|
|
|
|
class ClosesPullRequests
|
|
{
|
|
private $targetBranch;
|
|
private $localBranch;
|
|
private $remoteBranch;
|
|
|
|
private const CI_ERROR = 'error';
|
|
private const CI_PENDING = 'pending';
|
|
private const CI_SUCCESS = 'success';
|
|
|
|
private const RUN_TYPE_COMMAND = 'command';
|
|
private const RUN_TYPE_QUERY = 'query';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->localBranch = $this->run(
|
|
'git rev-parse --abbrev-ref HEAD',
|
|
self::RUN_TYPE_QUERY
|
|
);
|
|
$this->targetBranch = $this->getTargetBranchFromArgs();
|
|
|
|
$this->remoteBranch = $this->run(
|
|
'git rev-parse --abbrev-ref --symbolic-full-name @{u}',
|
|
self::RUN_TYPE_QUERY
|
|
);
|
|
$this->remoteBranch = str_replace('origin/', '', $this->remoteBranch);
|
|
}
|
|
|
|
public function __invoke(): void
|
|
{
|
|
$this->confirmCiStatusIsPassing();
|
|
// TODO: Check that the current branch has a tracking branch.
|
|
$this->fetchOrigin();
|
|
$this->ensureFeatureBranchInSync();
|
|
$this->ensureTargetBranchInSync();
|
|
$this->checkoutTargetBranch();
|
|
$this->mergeLocalBranch();
|
|
$this->pushTargetBranch();
|
|
$this->deleteRemoteBranch();
|
|
$this->deleteLocalBranch();
|
|
}
|
|
|
|
private function getTargetBranchFromArgs(): string
|
|
{
|
|
if (!$targetBranchName = $this->getArg('t:', ['target:'])) {
|
|
die('Invalid target branch specified. Aborting.');
|
|
}
|
|
|
|
return $targetBranchName;
|
|
}
|
|
|
|
private function confirmCiStatusIsPassing(): void
|
|
{
|
|
echo 'Confirming ci-status on PR is green...' . PHP_EOL;
|
|
|
|
// TODO: Check for failures, or skip if there is no CI.
|
|
$errors = [
|
|
self::CI_ERROR => 'Aborting: CI error',
|
|
self::CI_PENDING => 'Aborting: CI pending',
|
|
];
|
|
|
|
if (array_key_exists($status = exec('hub ci-status'), $errors)) {
|
|
die($errors[$status]);
|
|
}
|
|
}
|
|
|
|
private function fetchOrigin(): void
|
|
{
|
|
print 'Fetching origin to confirm local and remote in sync...'
|
|
. PHP_EOL;
|
|
|
|
$this->run('git fetch origin', self::RUN_TYPE_COMMAND);
|
|
}
|
|
|
|
private function ensureTargetBranchInSync(): void
|
|
{
|
|
$this->ensureBranchInSyncWithUpstream(
|
|
$this->targetBranch,
|
|
$this->targetBranch
|
|
);
|
|
}
|
|
|
|
private function ensureFeatureBranchInSync(): void
|
|
{
|
|
$this->ensureBranchInSyncWithUpstream(
|
|
$this->localBranch,
|
|
$this->remoteBranch
|
|
);
|
|
}
|
|
|
|
private function ensureBranchInSyncWithUpstream(
|
|
string $localBranch,
|
|
string $remoteBranch
|
|
): void {
|
|
echo sprintf(
|
|
'Ensuring that %s is in sync with its upstream...',
|
|
$localBranch
|
|
) . PHP_EOL;
|
|
|
|
$localCommitTip = $this->tipCommitOfBranch($localBranch);
|
|
$remoteCommitTip = $this->tipCommitOfBranch(sprintf(
|
|
'origin/%s',
|
|
$remoteBranch
|
|
));
|
|
|
|
if ($localCommitTip != $remoteCommitTip) {
|
|
die(sprintf(
|
|
'Branch %s was out of date, needs rebasing. Aborting.',
|
|
$localBranch
|
|
));
|
|
}
|
|
}
|
|
|
|
private function tipCommitOfBranch(string $branchName): string
|
|
{
|
|
return $this->run(
|
|
sprintf('git rev-parse %s', $branchName),
|
|
self::RUN_TYPE_QUERY
|
|
);
|
|
}
|
|
|
|
private function checkoutTargetBranch(): void
|
|
{
|
|
echo sprintf('Checking out %s...' . PHP_EOL, $this->targetBranch);
|
|
|
|
$this->run(
|
|
sprintf('git checkout %s', $this->targetBranch),
|
|
self::RUN_TYPE_COMMAND
|
|
);
|
|
}
|
|
|
|
private function mergeLocalBranch(): void
|
|
{
|
|
echo sprintf(
|
|
'Merging %s into %s...' . PHP_EOL,
|
|
$this->localBranch,
|
|
$this->targetBranch
|
|
);
|
|
|
|
$mergeCommand = sprintf('git merge --ff-only %s', $this->localBranch);
|
|
if (!$this->run($mergeCommand, self::RUN_TYPE_COMMAND)) {
|
|
// Switch back to the previous branch.
|
|
$this->run('git checkout -', self::RUN_TYPE_COMMAND);
|
|
|
|
die(sprintf(
|
|
'Branch %s is not fast-forwardable.',
|
|
$this->localBranch
|
|
));
|
|
}
|
|
}
|
|
|
|
public function pushTargetBranch(): void
|
|
{
|
|
print(sprintf('Pushing updated %s branch...', $this->targetBranch));
|
|
|
|
$this->run(
|
|
sprintf('git push origin %s', $this->targetBranch),
|
|
self::RUN_TYPE_COMMAND
|
|
);
|
|
}
|
|
|
|
public function deleteRemoteBranch(): void
|
|
{
|
|
echo 'Deleting remote branch...' . PHP_EOL;
|
|
|
|
$this->run(
|
|
sprintf('git push origin :%s', $this->remoteBranch),
|
|
self::RUN_TYPE_COMMAND
|
|
);
|
|
}
|
|
|
|
public function deleteLocalBranch(): void
|
|
{
|
|
echo 'Deleting local branch...' . PHP_EOL;
|
|
|
|
$this->run(
|
|
sprintf('git branch -d %s', $this->localBranch),
|
|
self::RUN_TYPE_COMMAND
|
|
);
|
|
}
|
|
|
|
private function getArg(string $shortOpts, array $longOpts = []): ?string
|
|
{
|
|
if (!$values = getopt($shortOpts, $longOpts)) {
|
|
return NULL;
|
|
}
|
|
|
|
return current($values);
|
|
}
|
|
|
|
/**
|
|
* Run the command.
|
|
*
|
|
* @return bool|string
|
|
* If the type is 'command', the method will return if there were any
|
|
* errors when running the command based on its return code.
|
|
*
|
|
* If the type is 'query', then the output of the command will be returned
|
|
* as a string.
|
|
*/
|
|
private function run(string $command, string $type)
|
|
{
|
|
switch ($type) {
|
|
case self::RUN_TYPE_COMMAND:
|
|
// Perform the command, hiding the original output and return
|
|
// whether or not there were errors.
|
|
@exec("$command", $output, $return);
|
|
|
|
return $return == 0;
|
|
|
|
case self::RUN_TYPE_QUERY:
|
|
// Perform the command and return the output.
|
|
return exec($command, $output);
|
|
}
|
|
}
|
|
}
|
|
|
|
(new ClosesPullRequests())->__invoke();
|