2020-05-22 21:52:12 +00:00
|
|
|
#!/usr/bin/env php
|
|
|
|
<?php
|
|
|
|
|
2020-05-27 11:03:32 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2020-05-22 21:52:12 +00:00
|
|
|
/**
|
2020-05-27 11:03:32 +00:00
|
|
|
* Usage: git close-pull-request -t <target>
|
2020-05-22 21:52:12 +00:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
{
|
2020-05-27 09:53:33 +00:00
|
|
|
private $targetBranch;
|
2020-05-22 21:52:12 +00:00
|
|
|
private $localBranch;
|
|
|
|
private $remoteBranch;
|
|
|
|
|
2020-05-22 23:12:21 +00:00
|
|
|
private const CI_ERROR = 'error';
|
2020-05-22 22:11:37 +00:00
|
|
|
private const CI_PENDING = 'pending';
|
|
|
|
private const CI_SUCCESS = 'success';
|
|
|
|
|
2020-05-27 16:56:15 +00:00
|
|
|
private const RUN_TYPE_COMMAND = 'command';
|
|
|
|
private const RUN_TYPE_QUERY = 'query';
|
|
|
|
|
2020-05-22 21:52:12 +00:00
|
|
|
public function __construct()
|
|
|
|
{
|
2020-05-27 16:56:15 +00:00
|
|
|
$this->localBranch = $this->run(
|
|
|
|
'git rev-parse --abbrev-ref HEAD',
|
|
|
|
self::RUN_TYPE_QUERY
|
|
|
|
);
|
2020-05-27 11:03:32 +00:00
|
|
|
$this->targetBranch = $this->getTargetBranchFromArgs();
|
2020-05-27 09:53:33 +00:00
|
|
|
|
2020-05-27 16:56:15 +00:00
|
|
|
$this->remoteBranch = $this->run(
|
|
|
|
'git rev-parse --abbrev-ref --symbolic-full-name @{u}',
|
|
|
|
self::RUN_TYPE_QUERY
|
|
|
|
);
|
2020-05-22 21:52:12 +00:00
|
|
|
$this->remoteBranch = str_replace('origin/', '', $this->remoteBranch);
|
|
|
|
}
|
|
|
|
|
2020-05-27 08:48:14 +00:00
|
|
|
public function __invoke(): void
|
|
|
|
{
|
|
|
|
$this->confirmCiStatusIsPassing();
|
|
|
|
// TODO: Check that the current branch has a tracking branch.
|
2020-06-01 18:54:57 +00:00
|
|
|
$this->ensureWorkingDirectoryAndIndexAreClean();
|
2020-05-27 08:48:14 +00:00
|
|
|
$this->fetchOrigin();
|
2020-05-27 09:55:46 +00:00
|
|
|
$this->ensureFeatureBranchInSync();
|
|
|
|
$this->ensureTargetBranchInSync();
|
2020-05-27 08:48:14 +00:00
|
|
|
$this->checkoutTargetBranch();
|
|
|
|
$this->mergeLocalBranch();
|
|
|
|
$this->pushTargetBranch();
|
|
|
|
$this->deleteRemoteBranch();
|
|
|
|
$this->deleteLocalBranch();
|
|
|
|
}
|
|
|
|
|
2020-06-01 18:54:57 +00:00
|
|
|
private function ensureWorkingDirectoryAndIndexAreClean(): void
|
|
|
|
{
|
|
|
|
echo 'Ensuring that index and working directory are clean...' . PHP_EOL;
|
|
|
|
|
|
|
|
$isIndexClean = $this->run('git diff --cached --exit-code', self::RUN_TYPE_COMMAND);
|
|
|
|
$isWorkingDirClean = $this->run('git diff --exit-code', self::RUN_TYPE_COMMAND);
|
|
|
|
|
|
|
|
if (!$isIndexClean || !$isWorkingDirClean) {
|
|
|
|
$this->dieWithMessage('Index or working dir not clean. Aborting.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-27 11:03:32 +00:00
|
|
|
private function getTargetBranchFromArgs(): string
|
|
|
|
{
|
2020-05-27 16:35:01 +00:00
|
|
|
if (!$targetBranchName = $this->getArg('t:', ['target:'])) {
|
2020-05-27 17:44:03 +00:00
|
|
|
$this->dieWithMessage('Invalid target branch specified. Aborting.');
|
2020-05-27 11:03:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $targetBranchName;
|
|
|
|
}
|
|
|
|
|
2020-05-27 09:54:00 +00:00
|
|
|
private function confirmCiStatusIsPassing(): void
|
2020-05-22 22:11:37 +00:00
|
|
|
{
|
2020-05-30 02:46:54 +00:00
|
|
|
if ($this->isForce()) {
|
|
|
|
echo 'Forced. Skipping ci-status check...' . PHP_EOL;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-22 22:11:37 +00:00
|
|
|
echo 'Confirming ci-status on PR is green...' . PHP_EOL;
|
|
|
|
|
|
|
|
// TODO: Check for failures, or skip if there is no CI.
|
2020-05-22 23:05:24 +00:00
|
|
|
$errors = [
|
2020-05-22 23:12:21 +00:00
|
|
|
self::CI_ERROR => 'Aborting: CI error',
|
2020-05-22 23:05:24 +00:00
|
|
|
self::CI_PENDING => 'Aborting: CI pending',
|
|
|
|
];
|
|
|
|
|
2020-05-27 17:44:03 +00:00
|
|
|
$ciStatus = $this->run('hub ci-status', self::RUN_TYPE_QUERY);
|
|
|
|
|
|
|
|
switch ($ciStatus) {
|
|
|
|
case self::CI_PENDING:
|
|
|
|
$this->exitWithWarning($errors[$ciStatus]);
|
|
|
|
|
|
|
|
case self::CI_ERROR:
|
|
|
|
$this->dieWithMessage($errors[$ciStatus]);
|
2020-05-22 22:11:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-27 09:54:00 +00:00
|
|
|
private function fetchOrigin(): void
|
2020-05-22 21:52:12 +00:00
|
|
|
{
|
|
|
|
print 'Fetching origin to confirm local and remote in sync...'
|
|
|
|
. PHP_EOL;
|
2020-05-27 16:56:15 +00:00
|
|
|
|
|
|
|
$this->run('git fetch origin', self::RUN_TYPE_COMMAND);
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 09:55:46 +00:00
|
|
|
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) {
|
2020-05-27 17:44:03 +00:00
|
|
|
$this->dieWithMessage(sprintf(
|
2020-05-27 09:55:46 +00:00
|
|
|
'Branch %s was out of date, needs rebasing. Aborting.',
|
|
|
|
$localBranch
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function tipCommitOfBranch(string $branchName): string
|
|
|
|
{
|
2020-05-27 16:56:15 +00:00
|
|
|
return $this->run(
|
|
|
|
sprintf('git rev-parse %s', $branchName),
|
|
|
|
self::RUN_TYPE_QUERY
|
|
|
|
);
|
2020-05-27 09:55:46 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 09:54:00 +00:00
|
|
|
private function checkoutTargetBranch(): void
|
2020-05-22 21:52:12 +00:00
|
|
|
{
|
2020-05-27 16:56:15 +00:00
|
|
|
echo sprintf('Checking out %s...' . PHP_EOL, $this->targetBranch);
|
|
|
|
|
|
|
|
$this->run(
|
|
|
|
sprintf('git checkout %s', $this->targetBranch),
|
|
|
|
self::RUN_TYPE_COMMAND
|
|
|
|
);
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 09:54:00 +00:00
|
|
|
private function mergeLocalBranch(): void
|
2020-05-22 21:52:12 +00:00
|
|
|
{
|
|
|
|
echo sprintf(
|
|
|
|
'Merging %s into %s...' . PHP_EOL,
|
|
|
|
$this->localBranch,
|
|
|
|
$this->targetBranch
|
|
|
|
);
|
|
|
|
|
2020-05-27 16:56:15 +00:00
|
|
|
$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);
|
|
|
|
|
2020-05-27 17:44:03 +00:00
|
|
|
$this->dieWithMessage(sprintf(
|
2020-05-27 16:56:15 +00:00
|
|
|
'Branch %s is not fast-forwardable.',
|
|
|
|
$this->localBranch
|
|
|
|
));
|
|
|
|
}
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function pushTargetBranch(): void
|
|
|
|
{
|
|
|
|
print(sprintf('Pushing updated %s branch...', $this->targetBranch));
|
2020-05-27 16:56:15 +00:00
|
|
|
|
|
|
|
$this->run(
|
|
|
|
sprintf('git push origin %s', $this->targetBranch),
|
|
|
|
self::RUN_TYPE_COMMAND
|
|
|
|
);
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function deleteRemoteBranch(): void
|
|
|
|
{
|
|
|
|
echo 'Deleting remote branch...' . PHP_EOL;
|
2020-05-27 16:56:15 +00:00
|
|
|
|
|
|
|
$this->run(
|
|
|
|
sprintf('git push origin :%s', $this->remoteBranch),
|
|
|
|
self::RUN_TYPE_COMMAND
|
|
|
|
);
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function deleteLocalBranch(): void
|
|
|
|
{
|
|
|
|
echo 'Deleting local branch...' . PHP_EOL;
|
2020-05-27 16:56:15 +00:00
|
|
|
|
|
|
|
$this->run(
|
|
|
|
sprintf('git branch -d %s', $this->localBranch),
|
|
|
|
self::RUN_TYPE_COMMAND
|
|
|
|
);
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
2020-05-27 11:03:32 +00:00
|
|
|
|
2020-05-27 16:35:01 +00:00
|
|
|
private function getArg(string $shortOpts, array $longOpts = []): ?string
|
2020-05-27 11:03:32 +00:00
|
|
|
{
|
2020-05-27 16:35:01 +00:00
|
|
|
if (!$values = getopt($shortOpts, $longOpts)) {
|
|
|
|
return NULL;
|
2020-05-27 11:03:32 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 16:35:01 +00:00
|
|
|
return current($values);
|
2020-05-27 11:03:32 +00:00
|
|
|
}
|
2020-05-27 16:56:15 +00:00
|
|
|
|
2020-05-30 02:46:54 +00:00
|
|
|
private function hasArg(string $shortOpts, array $longOpts = []): bool
|
|
|
|
{
|
|
|
|
return !empty(getopt($shortOpts, $longOpts));
|
|
|
|
}
|
|
|
|
|
|
|
|
private function isForce(): bool
|
|
|
|
{
|
|
|
|
return $this->hasArg('f::', ['force::']);
|
|
|
|
}
|
|
|
|
|
2020-05-27 16:56:15 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
2020-05-27 17:44:03 +00:00
|
|
|
|
|
|
|
private function dieWithMessage(string $message): void
|
|
|
|
{
|
|
|
|
echo sprintf("\e[31m%s\e[0m", $message);
|
|
|
|
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function exitWithWarning(string $message): void
|
|
|
|
{
|
|
|
|
echo sprintf("\e[33m%s\e[0m", $message);
|
|
|
|
|
|
|
|
exit(2);
|
|
|
|
}
|
2020-05-22 21:52:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
(new ClosesPullRequests())->__invoke();
|