diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index f17124226..6eca70e60 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -19,6 +19,8 @@ use Composer\Downloader\ChangeReportInterface; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Script\ScriptEvents; +use Composer\Downloader\VcsDownloader; +use Composer\Downloader\DvcsDownloaderInterface; /** * @author Tiago Ribeiro @@ -60,6 +62,7 @@ EOT $composer->getEventDispatcher()->dispatchScript(ScriptEvents::PRE_STATUS_CMD, true); $errors = array(); + $unpushedChanges = array(); // list packages foreach ($installedRepo->getPackages() as $package) { @@ -75,13 +78,19 @@ EOT if ($changes = $downloader->getLocalChanges($package, $targetDir)) { $errors[$targetDir] = $changes; } + + if ($downloader instanceof DvcsDownloaderInterface) { + if ($unpushed = $downloader->getUnpushedChanges($targetDir)) { + $unpushedChanges[$targetDir] = $unpushed; + } + } } } // output errors/warnings - if (!$errors) { + if (!$errors && !$unpushed) { $this->getIO()->writeError('No local changes'); - } else { + } elseif ($errors) { $this->getIO()->writeError('You have changes in the following dependencies:'); } @@ -97,6 +106,22 @@ EOT } } + if ($unpushedChanges) { + $this->getIO()->writeError('You have unpushed changes on the current branch in the following dependencies:'); + + foreach ($unpushedChanges as $path => $changes) { + if ($input->getOption('verbose')) { + $indentedChanges = implode("\n", array_map(function ($line) { + return ' ' . ltrim($line); + }, explode("\n", $changes))); + $this->getIO()->write(''.$path.':'); + $this->getIO()->write($indentedChanges); + } else { + $this->getIO()->write($path); + } + } + } + if ($errors && !$input->getOption('verbose')) { $this->getIO()->writeError('Use --verbose (-v) to see modified files'); } @@ -104,6 +129,6 @@ EOT // Dispatch post-status-command $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_STATUS_CMD, true); - return $errors ? 1 : 0; + return ($errors || $unpushedChanges) ? 1 : 0; } } diff --git a/src/Composer/Downloader/DvcsDownloaderInterface.php b/src/Composer/Downloader/DvcsDownloaderInterface.php new file mode 100644 index 000000000..d4cbe37e1 --- /dev/null +++ b/src/Composer/Downloader/DvcsDownloaderInterface.php @@ -0,0 +1,29 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +/** + * DVCS Downloader interface. + * + * @author James Titcumb + */ +interface DvcsDownloaderInterface +{ + /** + * Checks for unpushed changes to a current branch + * + * @param string $path package directory + * @return string|null changes or null + */ + public function getUnpushedChanges($path); +} diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 82552c009..33870ed01 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -22,7 +22,7 @@ use Composer\Config; /** * @author Jordi Boggiano */ -class GitDownloader extends VcsDownloader +class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface { private $hasStashedChanges = false; private $gitUtil; @@ -112,6 +112,29 @@ class GitDownloader extends VcsDownloader return trim($output) ?: null; } + public function getUnpushedChanges($path) + { + GitUtil::cleanEnv(); + $path = $this->normalizePath($path); + if (!is_dir($path.'/.git')) { + return; + } + + $command = 'git rev-parse --abbrev-ref HEAD'; + if (0 !== $this->process->execute($command, $output, $path)) { + throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + } + + $branch = trim($output); + + $command = sprintf('git diff --name-status %s..composer/%s', $branch, $branch); + if (0 !== $this->process->execute($command, $output, $path)) { + throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + } + + return trim($output) ?: null; + } + /** * {@inheritDoc} */ @@ -119,6 +142,11 @@ class GitDownloader extends VcsDownloader { GitUtil::cleanEnv(); $path = $this->normalizePath($path); + + if (null !== $this->getUnpushedChanges($path)) { + throw new \RuntimeException('Source directory ' . $path . ' has unpushed changes on the current branch.'); + } + if (!$changes = $this->getLocalChanges($package, $path)) { return; } diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 7521ac51a..676cf1169 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -250,21 +250,29 @@ class GitDownloaderTest extends \PHPUnit_Framework_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 diff --name-status ..composer/"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($expectedGitUpdateCommand)) + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo('git branch -r')) + ->with($this->equalTo($this->winCompat("git remote -v"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(4)) + ->method('execute') + ->with($this->equalTo($expectedGitUpdateCommand)) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(5)) + ->method('execute') + ->with($this->equalTo('git branch -r')) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(6)) ->method('execute') ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($tmpDir))) ->will($this->returnValue(0)); @@ -293,13 +301,21 @@ class GitDownloaderTest extends \PHPUnit_Framework_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 diff --name-status ..composer/"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(3)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git remote -v"))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(4)) ->method('execute') ->with($this->equalTo($expectedGitUpdateCommand)) ->will($this->returnValue(1));