diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index d5b483788..f31c49889 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -16,10 +16,15 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Downloader\ChangeReportInterface; +use Composer\Downloader\DvcsDownloaderInterface; +use Composer\Downloader\VcsCapableDownloaderInterface; +use Composer\Package\Dumper\ArrayDumper; +use Composer\Package\Version\VersionGuesser; +use Composer\Package\Version\VersionParser; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Script\ScriptEvents; -use Composer\Downloader\DvcsDownloaderInterface; +use Composer\Util\ProcessExecutor; /** * @author Tiago Ribeiro @@ -27,6 +32,11 @@ use Composer\Downloader\DvcsDownloaderInterface; */ class StatusCommand extends BaseCommand { + + const EXIT_CODE_ERRORS = 1; + const EXIT_CODE_UNPUSHED_CHANGES = 2; + const EXIT_CODE_VERSION_CHANGES = 4; + protected function configure() { $this @@ -63,14 +73,18 @@ EOT $errors = array(); $io = $this->getIO(); $unpushedChanges = array(); + $vcsVersionChanges = array(); + + $parser = new VersionParser; + $guesser = new VersionGuesser($composer->getConfig(), new ProcessExecutor($io), $parser); + $dumper = new ArrayDumper; // list packages foreach ($installedRepo->getCanonicalPackages() as $package) { $downloader = $dm->getDownloaderForInstalledPackage($package); + $targetDir = $im->getInstallPath($package); if ($downloader instanceof ChangeReportInterface) { - $targetDir = $im->getInstallPath($package); - if (is_link($targetDir)) { $errors[$targetDir] = $targetDir . ' is a symbolic link.'; } @@ -78,31 +92,64 @@ EOT if ($changes = $downloader->getLocalChanges($package, $targetDir, true)) { $errors[$targetDir] = $changes; } + } + + if ($downloader instanceof VcsCapableDownloaderInterface) { + if ($currentRef = $downloader->getVcsReference($package, $targetDir)) { + switch ($package->getInstallationSource()) { + case 'source': + $previousRef = $package->getSourceReference(); + break; + case 'dist': + $previousRef = $package->getDistReference(); + break; + default: + $previousRef = null; + } - if ($downloader instanceof DvcsDownloaderInterface) { - if ($unpushed = $downloader->getUnpushedChanges($package, $targetDir)) { - $unpushedChanges[$targetDir] = $unpushed; + $currentVersion = $guesser->guessVersion($dumper->dump($package), $targetDir); + + if ($previousRef && $currentVersion['commit'] !== $previousRef) { + $vcsVersionChanges[$targetDir] = array( + 'previous' => array( + 'version' => $package->getPrettyVersion(), + 'ref' => $previousRef + ), + 'current' => array( + 'version' => $currentVersion['version'], + 'ref' => $currentVersion['commit'], + ), + ); } } } + + if ($downloader instanceof DvcsDownloaderInterface) { + if ($unpushed = $downloader->getUnpushedChanges($package, $targetDir)) { + $unpushedChanges[$targetDir] = $unpushed; + } + } } // output errors/warnings - if (!$errors && !$unpushedChanges) { + if (!$errors && !$unpushedChanges && !$vcsVersionChanges) { $io->writeError('No local changes'); - } elseif ($errors) { - $io->writeError('You have changes in the following dependencies:'); + return 0; } - foreach ($errors as $path => $changes) { - if ($input->getOption('verbose')) { - $indentedChanges = implode("\n", array_map(function ($line) { - return ' ' . ltrim($line); - }, explode("\n", $changes))); - $io->write(''.$path.':'); - $io->write($indentedChanges); - } else { - $io->write($path); + if ($errors) { + $io->writeError('You have changes in the following dependencies:'); + + foreach ($errors as $path => $changes) { + if ($input->getOption('verbose')) { + $indentedChanges = implode("\n", array_map(function ($line) { + return ' ' . ltrim($line); + }, explode("\n", $changes))); + $io->write(''.$path.':'); + $io->write($indentedChanges); + } else { + $io->write($path); + } } } @@ -122,13 +169,36 @@ EOT } } - if (($errors || $unpushedChanges) && !$input->getOption('verbose')) { + if ($vcsVersionChanges) { + $io->writeError('You have version variations in the following dependencies:'); + + foreach ($vcsVersionChanges as $path => $changes) { + if ($input->getOption('verbose')) { + // If we don't can't find a version, use the ref instead. + $currentVersion = $changes['current']['version'] ?: $changes['current']['ref']; + $previousVersion = $changes['previous']['version'] ?: $changes['previous']['ref']; + + if ($io->isVeryVerbose()) { + // Output the ref regardless of whether or not it's being used as the version + $currentVersion .= sprintf(' (%s)', $changes['current']['ref']); + $previousVersion .= sprintf(' (%s)', $changes['previous']['ref']); + } + + $io->write(''.$path.':'); + $io->write(sprintf(' From %s to %s', $previousVersion, $currentVersion)); + } else { + $io->write($path); + } + } + } + + if (($errors || $unpushedChanges || $vcsVersionChanges) && !$input->getOption('verbose')) { $io->writeError('Use --verbose (-v) to see a list of files'); } // Dispatch post-status-command $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_STATUS_CMD, true); - return ($errors ? 1 : 0) + ($unpushedChanges ? 2 : 0); + return ($errors ? self::EXIT_CODE_ERRORS : 0) + ($unpushedChanges ? self::EXIT_CODE_UNPUSHED_CHANGES : 0) + ($vcsVersionChanges ? self::EXIT_CODE_VERSION_CHANGES : 0); } } diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index e7e934544..bc18f88d8 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -12,8 +12,12 @@ namespace Composer\Downloader; +use Composer\Package\Dumper\ArrayDumper; use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionGuesser; +use Composer\Package\Version\VersionParser; use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; @@ -23,7 +27,7 @@ use Symfony\Component\Filesystem\Filesystem; * @author Samuel Roze * @author Johann Reinke */ -class PathDownloader extends FileDownloader +class PathDownloader extends FileDownloader implements VcsCapableDownloaderInterface { const STRATEGY_SYMLINK = 10; const STRATEGY_MIRROR = 20; @@ -127,4 +131,19 @@ class PathDownloader extends FileDownloader parent::remove($package, $path); } } + + /** + * {@inheritDoc} + */ + public function getVcsReference(PackageInterface $package, $path) + { + $parser = new VersionParser; + $guesser = new VersionGuesser($this->config, new ProcessExecutor($this->io), $parser); + $dumper = new ArrayDumper; + + $packageConfig = $dumper->dump($package); + if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { + return $packageVersion['commit']; + } + } } diff --git a/src/Composer/Downloader/VcsCapableDownloaderInterface.php b/src/Composer/Downloader/VcsCapableDownloaderInterface.php new file mode 100644 index 000000000..fa4ddf485 --- /dev/null +++ b/src/Composer/Downloader/VcsCapableDownloaderInterface.php @@ -0,0 +1,33 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +use Composer\Package\PackageInterface; + +/** + * VCS Capable Downloader interface. + * + * @author Steve Buzonas + */ +interface VcsCapableDownloaderInterface +{ + + /** + * Gets the VCS Reference for the package at path + * + * @param PackageInterface $package package directory + * @param string $path package directory + * @return string|null reference or null + */ + public function getVcsReference(PackageInterface $package, $path); +} diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 30323856f..e4fe312d7 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -13,7 +13,10 @@ namespace Composer\Downloader; use Composer\Config; +use Composer\Package\Dumper\ArrayDumper; use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionGuesser; +use Composer\Package\Version\VersionParser; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; @@ -21,7 +24,7 @@ use Composer\Util\Filesystem; /** * @author Jordi Boggiano */ -abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterface +abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterface, VcsCapableDownloaderInterface { /** @var IOInterface */ protected $io; @@ -193,6 +196,21 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa return $this; } + /** + * {@inheritDoc} + */ + public function getVcsReference(PackageInterface $package, $path) + { + $parser = new VersionParser; + $guesser = new VersionGuesser($this->config, $this->process, $parser); + $dumper = new ArrayDumper; + + $packageConfig = $dumper->dump($package); + if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { + return $packageVersion['commit']; + } + } + /** * Prompt the user to check if changes should be stashed/removed or the operation aborted * diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 010aa7fcc..d5535dcd5 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -130,6 +130,13 @@ class VersionGuesser $version = $this->versionFromGitTags($path); } + if (!$commit) { + $command = 'git log --pretty="%H" -n1 HEAD'; + if (0 === $this->process->execute($command, $output, $path)) { + $commit = trim($output) ?: null; + } + } + return array('version' => $version, 'commit' => $commit); }