diff --git a/src/Command/PullRequest/PullRequestMergeCommand.php b/src/Command/PullRequest/PullRequestMergeCommand.php index aefaf354..69253302 100644 --- a/src/Command/PullRequest/PullRequestMergeCommand.php +++ b/src/Command/PullRequest/PullRequestMergeCommand.php @@ -40,6 +40,8 @@ protected function configure() ->addOption('fast-forward', null, InputOption::VALUE_NONE, 'Merge pull-request using fast forward (no merge commit will be created)') ->addOption('squash', null, InputOption::VALUE_NONE, 'Squash the PR before merging') ->addOption('force-squash', null, InputOption::VALUE_NONE, 'Force squashing the PR, even if there are multiple authors (this will implicitly use --squash)') + ->addOption('rebase', null, InputOption::VALUE_NONE, 'Rebase the PR before merging') + ->addOption('ensure-sync', null, InputOption::VALUE_NONE, 'Ensure that the pull request history is up to date before merging') ->addOption('switch', null, InputOption::VALUE_REQUIRED, 'Switch the base of the pull request before merging') ->addOption('pat', null, InputOption::VALUE_REQUIRED, 'Give the PR\'s author a pat on the back after the merge') ->setHelp( @@ -79,6 +81,16 @@ protected function configure() $ gush %command.name% --fast-forward 12 +If you want to perform an automatic rebase against the base branch before merging, the --rebase option can be used +in order to try that operation: + + $ gush %command.name% --rebase 12 + +A synchronization check against the base branch can be done before the merge, passing the --ensure-sync option; so +if this check fails, the operation will be aborted: + + $ gush %command.name% --ensure-sync 12 + After the pull request is merged, you can give a pat on the back to its author using the --pat. This option accepts the name of any configured pat's name: @@ -173,6 +185,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $mergeOperation->setTarget($targetRemote, $targetBranch); $mergeOperation->setSource($sourceRemote, $sourceBranch); $mergeOperation->squashCommits($squash, $input->getOption('force-squash')); + $mergeOperation->guardSync($input->getOption('ensure-sync')); + $mergeOperation->rebase($input->getOption('rebase')); $mergeOperation->switchBase($input->getOption('switch')); $mergeOperation->setMergeMessage($messageCallback); $mergeOperation->useFastForward($input->getOption('fast-forward')); diff --git a/src/Helper/GitHelper.php b/src/Helper/GitHelper.php index 80bea7c0..e2258e5d 100644 --- a/src/Helper/GitHelper.php +++ b/src/Helper/GitHelper.php @@ -12,6 +12,7 @@ namespace Gush\Helper; use Gush\Exception\CannotSquashMultipleAuthors; +use Gush\Exception\MergeWorkflowException; use Gush\Exception\UserException; use Gush\Exception\WorkingTreeIsNotReady; use Gush\Operation\RemoteMergeOperation; @@ -342,7 +343,7 @@ public function clearTempBranches() */ public function createRemoteMergeOperation() { - return new RemoteMergeOperation($this, $this->filesystemHelper); + return new RemoteMergeOperation($this, $this->filesystemHelper, $this->processHelper); } /** @@ -386,7 +387,7 @@ public function mergeBranch($base, $sourceBranch, $commitMessage, $fastForward = 'allow_failures' => false, ], [ - 'line' => ['git', 'commit', '-F', $tmpName], + 'line' => ['git', 'commit', '--file', $tmpName], 'allow_failures' => false, ], ] @@ -433,7 +434,7 @@ public function mergeBranchWithLog($base, $sourceBranch, $commitMessage, $source $tmpName = $this->filesystemHelper->newTempFilename(); file_put_contents($tmpName, $commitMessage); - $this->processHelper->runCommand(['git', 'commit', '-F', $tmpName]); + $this->processHelper->runCommand(['git', 'commit', '--file', $tmpName]); return trim($this->processHelper->runCommand('git rev-parse HEAD')); } @@ -443,15 +444,7 @@ public function addNotes($notes, $commitHash, $ref) $tmpName = $this->filesystemHelper->newTempFilename(); file_put_contents($tmpName, $notes); - $commands = [ - 'git', - 'notes', - '--ref='.$ref, - 'add', - '-F', - $tmpName, - $commitHash, - ]; + $commands = ['git', 'notes', '--ref', $ref, 'add', '--file', $tmpName, $commitHash]; $this->processHelper->runCommand($commands, true); } @@ -596,21 +589,19 @@ public function squashCommits($base, $branchName, $ignoreMultipleAuthors = false // Get commits only in the branch but not in base (in reverse order) // we can't use --max-count here because that is applied before the reversing! // - // using git-log works better then finding the fork-point with git-merge-base + // using git-log works better than finding the fork-point with git-merge-base // because this protects against edge cases were there is no valid fork-point - $firstCommitHash = StringUtil::splitLines($this->processHelper->runCommand( - [ - 'git', - '--no-pager', - 'log', - '--oneline', - '--no-color', - '--format=%H', - '--reverse', - $base.'..'.$branchName, - ] - ))[0]; + $firstCommitHash = StringUtil::splitLines($this->processHelper->runCommand([ + 'git', + '--no-pager', + 'log', + '--oneline', + '--no-color', + '--format=%H', + '--reverse', + $base.'..'.$branchName, + ]))[0]; // 0=author anything higher then 0 is the full body $commitData = StringUtil::splitLines( @@ -631,13 +622,7 @@ public function squashCommits($base, $branchName, $ignoreMultipleAuthors = false $message = implode("\n", $commitData); $this->reset($base); - $this->commit( - $message, - [ - 'a', - '-author' => $author, - ] - ); + $this->commit($message, ['all', 'author' => $author]); } public function syncWithRemote($remote, $branchName = null) @@ -671,9 +656,17 @@ public function commit($message, array $options = []) foreach ($options as $option => $value) { if (is_int($option)) { - $params[] = '-'.$value; + if (1 === strlen($value)) { + $params[] = '-'.$value; + } else { + $params[] = '--'.$value; + } } else { - $params[] = '-'.$option; + if (1 === strlen($option)) { + $params[] = '-'.$option; + } else { + $params[] = '--'.$option; + } $params[] = $value; } } @@ -681,7 +674,7 @@ public function commit($message, array $options = []) $tmpName = $this->filesystemHelper->newTempFilename(); file_put_contents($tmpName, $message); - $this->processHelper->runCommand(array_merge(['git', 'commit', '-F', $tmpName], $params)); + $this->processHelper->runCommand(array_merge(['git', 'commit', '--file', $tmpName], $params)); } /** diff --git a/src/Operation/RemoteMergeOperation.php b/src/Operation/RemoteMergeOperation.php index 2bc4969b..ef22fec8 100644 --- a/src/Operation/RemoteMergeOperation.php +++ b/src/Operation/RemoteMergeOperation.php @@ -13,11 +13,13 @@ use Gush\Helper\FilesystemHelper; use Gush\Helper\GitHelper; +use Gush\Helper\ProcessHelper; class RemoteMergeOperation { private $gitHelper; private $filesystemHelper; + private $processHelper; private $sourceBranch; private $sourceRemote; @@ -31,11 +33,14 @@ class RemoteMergeOperation private $performed = false; private $fastForward = false; private $withLog = false; + private $rebase = false; + private $guardSync = false; - public function __construct(GitHelper $gitHelper, FilesystemHelper $filesystemHelper) + public function __construct(GitHelper $gitHelper, FilesystemHelper $filesystemHelper, ProcessHelper $processHelper) { $this->gitHelper = $gitHelper; $this->filesystemHelper = $filesystemHelper; + $this->processHelper = $processHelper; } public function setSource($remote, $branch) @@ -80,6 +85,8 @@ public function setMergeMessage($message, $withLog = false) public function useFastForward($fastForward = true) { $this->fastForward = (bool) $fastForward; + + return $this; } public function performMerge() @@ -143,6 +150,20 @@ public function pushToRemote() $this->gitHelper->pushToRemote($this->targetRemote, $target); } + public function rebase(bool $rebase = false) + { + $this->rebase = $rebase; + + return $this; + } + + public function guardSync(bool $guardSync = false) + { + $this->guardSync = $guardSync; + + return $this; + } + private function createBaseBranch() { $targetBranch = null !== $this->switchBase ? $this->switchBase : $this->targetBranch; @@ -170,6 +191,24 @@ private function createSourceBranch() $this->targetBranch = $this->switchBase; } + $currentBaseHeadCommit = $this->processHelper->runCommand(['git', 'rev-parse', $this->targetBase]); + $lastKnownCommonCommit = $this->processHelper->runCommand(['git', 'merge-base', '--fork-point', $this->targetBase, $sourceBranch]); + + if ($currentBaseHeadCommit !== $lastKnownCommonCommit) { + if ($this->rebase) { + try { + $this->processHelper->runCommand(['git', 'pull', '--rebase', $this->targetBase]); + } catch (\Exception $e) { + // Error, abort the rebase operation + $this->processHelper->runCommand(['git', 'rebase', '--abort'], true); + + throw new MergeWorkflowException(sprintf('Git rebase failed while trying to synchronize history against "%s".', $this->targetBase), 0, $e); + } + } elseif ($this->guardSync) { + throw new MergeWorkflowException(sprintf('Failed while trying to perform merge against "%s", history is out of sync.', $this->targetBase)); + } + } + if ($this->squash) { $this->gitHelper->squashCommits($this->targetBase, $sourceBranch, $this->forceSquash); } diff --git a/tests/Command/PullRequest/PullRequestMergeCommandTest.php b/tests/Command/PullRequest/PullRequestMergeCommandTest.php index aef6b1a2..7761135f 100644 --- a/tests/Command/PullRequest/PullRequestMergeCommandTest.php +++ b/tests/Command/PullRequest/PullRequestMergeCommandTest.php @@ -455,7 +455,7 @@ protected function getGitConfigHelper($notes = true) return $helper; } - private function getLocalGitHelper($message = null, $squash = false, $forceSquash = false, $switch = null, $withComments = true, $fastForward = false) + private function getLocalGitHelper($message = null, $squash = false, $forceSquash = false, $switch = null, $withComments = true, $fastForward = false, $guardSync = false, $rebase = false) { $helper = parent::getGitHelper(); @@ -470,6 +470,8 @@ private function getLocalGitHelper($message = null, $squash = false, $forceSquas $mergeOperation->setTarget('gushphp', 'base_ref')->shouldBeCalled(); $mergeOperation->setSource('cordoval', 'head_ref')->shouldBeCalled(); $mergeOperation->squashCommits($squash, $forceSquash)->shouldBeCalled(); + $mergeOperation->guardSync($guardSync)->shouldBeCalled(); + $mergeOperation->rebase($rebase)->shouldBeCalled(); $mergeOperation->switchBase($switch)->shouldBeCalled(); $mergeOperation->useFastForward($fastForward)->shouldBeCalled(); $mergeOperation->setMergeMessage( diff --git a/tests/Helper/GitHelperTest.php b/tests/Helper/GitHelperTest.php index d0f9f82b..bbe0492f 100644 --- a/tests/Helper/GitHelperTest.php +++ b/tests/Helper/GitHelperTest.php @@ -176,7 +176,7 @@ public function merges_remote_branch_in_clean_wc() 'allow_failures' => false, ], [ - 'line' => ['git', 'commit', '-F', $tmpName], + 'line' => ['git', 'commit', '--file', $tmpName], 'allow_failures' => false, ], ]