diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index 65c62ad31..eaf127feb 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -81,7 +81,7 @@ EOT } if ($downloader instanceof DvcsDownloaderInterface) { - if ($unpushed = $downloader->getUnpushedChanges($targetDir)) { + if ($unpushed = $downloader->getUnpushedChanges($package, $targetDir)) { $unpushedChanges[$targetDir] = $unpushed; } } @@ -123,8 +123,8 @@ EOT } } - if ($errors && !$input->getOption('verbose')) { - $io->writeError('Use --verbose (-v) to see modified files'); + if (($errors || $unpushedChanges) && !$input->getOption('verbose')) { + $io->writeError('Use --verbose (-v) to see a list of files'); } // Dispatch post-status-command diff --git a/src/Composer/Downloader/DvcsDownloaderInterface.php b/src/Composer/Downloader/DvcsDownloaderInterface.php index d4cbe37e1..d27dcbcd1 100644 --- a/src/Composer/Downloader/DvcsDownloaderInterface.php +++ b/src/Composer/Downloader/DvcsDownloaderInterface.php @@ -12,6 +12,8 @@ namespace Composer\Downloader; +use Composer\Package\PackageInterface; + /** * DVCS Downloader interface. * @@ -22,8 +24,9 @@ interface DvcsDownloaderInterface /** * Checks for unpushed changes to a current branch * - * @param string $path package directory - * @return string|null changes or null + * @param PackageInterface $package package directory + * @param string $path package directory + * @return string|null changes or null */ - public function getUnpushedChanges($path); + public function getUnpushedChanges(PackageInterface $package, $path); } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 726a86a2d..6da37d824 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -112,7 +112,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface return trim($output) ?: null; } - public function getUnpushedChanges($path) + public function getUnpushedChanges(PackageInterface $package, $path) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); @@ -127,11 +127,41 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $branch = trim($output); - $command = sprintf('git diff --name-status %s..composer/%s', $branch, $branch); + // we are on a detached HEAD tag so no need to check for changes + if ($branch === 'HEAD') { + return; + } + + // check that the branch exists in composer remote, if not then we should assume it is an unpushed branch + $command = sprintf('git rev-parse --verify composer/%s', $branch); + if (0 !== $this->process->execute($command, $output, $path)) { + $composerBranch = preg_replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $package->getPrettyVersion()); + $branches = ''; + if (0 === $this->process->execute('git branch -r', $output, $path)) { + $branches = $output; + } + // add 'v' in front of the branch if it was stripped when generating the pretty name + if (!preg_match('{^\s+composer/'.preg_quote($composerBranch).'$}m', $branches) && preg_match('{^\s+composer/v'.preg_quote($composerBranch).'$}m', $branches)) { + $composerBranch = 'v' . $composerBranch; + } + } else { + $composerBranch = $branch; + } + + $command = sprintf('git diff --name-status composer/%s...%s', $composerBranch, $branch); if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } + if (trim($output)) { + // fetch from both to make sure we have up to date remotes + $this->process->execute('git fetch composer && git fetch origin', $output, $path); + + if (0 !== $this->process->execute($command, $output, $path)) { + throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + } + } + return trim($output) ?: null; } @@ -143,8 +173,9 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface GitUtil::cleanEnv(); $path = $this->normalizePath($path); - if (null !== $this->getUnpushedChanges($path)) { - throw new \RuntimeException('Source directory ' . $path . ' has unpushed changes on the current branch.'); + $unpushed = $this->getUnpushedChanges($package, $path); + if ($unpushed && ($this->io->isInteractive() || $this->config->get('discard-changes') !== true)) { + throw new \RuntimeException('Source directory ' . $path . ' has unpushed changes on the current branch: '."\n".$unpushed); } if (!$changes = $this->getLocalChanges($package, $path)) { diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 0c58b2ad4..80cce9e9a 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -271,25 +271,29 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->winCompat("git diff --name-status ..composer/"))) + ->with($this->equalTo($this->winCompat("git rev-parse --verify composer/"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->with($this->equalTo($this->winCompat("git diff --name-status composer/..."))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(4)) ->method('execute') - ->with($this->equalTo($this->winCompat($expectedGitUpdateCommand)), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) + ->with($this->equalTo($this->winCompat("git remote -v"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(5)) ->method('execute') - ->with($this->equalTo('git branch -r')) + ->with($this->equalTo($this->winCompat($expectedGitUpdateCommand)), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(6)) + ->method('execute') + ->with($this->equalTo('git branch -r')) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(7)) ->method('execute') ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->will($this->returnValue(0)); @@ -321,17 +325,21 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->winCompat("git diff --name-status ..composer/"))) + ->with($this->equalTo($this->winCompat("git rev-parse --verify composer/"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->with($this->equalTo($this->winCompat("git diff --name-status composer/..."))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(4)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git remote -v"))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(5)) ->method('execute') ->with($this->equalTo($expectedGitUpdateCommand)) ->will($this->returnValue(1)); @@ -356,33 +364,45 @@ class GitDownloaderTest extends TestCase $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->with($this->equalTo($this->winCompat("git rev-parse --abbrev-ref HEAD"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) + ->with($this->equalTo($this->winCompat("git rev-parse --verify composer/"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git diff --name-status composer/..."))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(3)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(4)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git remote -v"))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(5)) ->method('execute') ->with($this->equalTo($expectedFirstGitUpdateCommand)) ->will($this->returnValue(1)); - $processExecutor->expects($this->at(4)) + $processExecutor->expects($this->at(7)) ->method('execute') ->with($this->equalTo($this->winCompat("git --version"))) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(5)) + $processExecutor->expects($this->at(8)) ->method('execute') ->with($this->equalTo($this->winCompat("git remote -v"))) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(6)) + $processExecutor->expects($this->at(9)) ->method('execute') ->with($this->equalTo($expectedSecondGitUpdateCommand)) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(7)) + $processExecutor->expects($this->at(11)) ->method('execute') ->with($this->equalTo('git branch -r')) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(8)) + $processExecutor->expects($this->at(13)) ->method('execute') ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->will($this->returnValue(0));