dotfiles/bin/git-close-pull-request

285 lines
7.8 KiB
Plaintext
Raw Normal View History

2020-05-22 21:52:12 +00:00
#!/usr/bin/env php
<?php
declare(strict_types=1);
2020-05-22 21:52:12 +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
{
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';
private const CI_PENDING = 'pending';
private const CI_SUCCESS = 'success';
private const RUN_TYPE_COMMAND = 'command';
private const RUN_TYPE_QUERY = 'query';
2020-05-22 21:52:12 +00:00
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
);
2020-05-22 21:52:12 +00:00
$this->remoteBranch = str_replace('origin/', '', $this->remoteBranch);
}
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();
$this->fetchOrigin();
$this->ensureFeatureBranchInSync();
$this->ensureTargetBranchInSync();
$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.');
}
}
private function getTargetBranchFromArgs(): string
{
if (!$targetBranchName = $this->getArg('t:', ['target:'])) {
$this->dieWithMessage('Invalid target branch specified. Aborting.');
}
return $targetBranchName;
}
private function confirmCiStatusIsPassing(): void
{
if ($this->isForce()) {
echo 'Forced. Skipping ci-status check...' . PHP_EOL;
return;
}
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',
];
$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]);
}
}
private function fetchOrigin(): void
2020-05-22 21:52:12 +00:00
{
print 'Fetching origin to confirm local and remote in sync...'
. PHP_EOL;
$this->run('git fetch origin', self::RUN_TYPE_COMMAND);
2020-05-22 21:52:12 +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) {
$this->dieWithMessage(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
2020-05-22 21:52:12 +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
}
private function mergeLocalBranch(): void
2020-05-22 21:52:12 +00:00
{
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);
$this->dieWithMessage(sprintf(
'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));
$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;
$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;
$this->run(
sprintf('git branch -d %s', $this->localBranch),
self::RUN_TYPE_COMMAND
);
2020-05-22 21:52:12 +00:00
}
private function getArg(string $shortOpts, array $longOpts = []): ?string
{
if (!$values = getopt($shortOpts, $longOpts)) {
return NULL;
}
return current($values);
}
private function hasArg(string $shortOpts, array $longOpts = []): bool
{
return !empty(getopt($shortOpts, $longOpts));
}
private function isForce(): bool
{
return $this->hasArg('f::', ['force::']);
}
/**
* 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);
}
}
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();