diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index b90ba2edc..6a492fd80 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -14,6 +14,7 @@ namespace Composer\Command; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\DefaultPolicy; +use Composer\Json\JsonFile; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\BasePackage; @@ -74,6 +75,7 @@ class ShowCommand extends BaseCommand new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), )) ->setHelp(<<getOption('format'); + if (!in_array($format, array('text', 'json'))) { + $io->writeError(sprintf('Unsupported format "%s". See help for supported formats.', $format)); + + return 1; + } + // init repos $platformOverrides = array(); if ($composer) { @@ -163,6 +172,9 @@ EOT // show single package or single version if (($packageFilter && false === strpos($packageFilter, '*')) || !empty($package)) { + if ('json' === $format) { + $io->writeError('Format "json" is only supported for package listings, falling back to format "text"'); + } if (empty($package)) { list($package, $versions) = $this->getPackage($installedRepo, $repos, $input->getArgument('package'), $input->getArgument('version')); @@ -205,6 +217,9 @@ EOT // show tree view if requested if ($input->getOption('tree')) { + if ('json' === $format) { + $io->writeError('Format "json" is only supported for package listings, falling back to format "text"'); + } $rootRequires = $this->getRootRequires(); foreach ($installedRepo->getPackages() as $package) { if (in_array($package->getName(), $rootRequires, true)) { @@ -232,16 +247,31 @@ EOT $packageListFilter = $this->getRootRequires(); } + list($width) = $this->getApplication()->getTerminalDimensions(); + if (null === $width) { + // In case the width is not detected, we're probably running the command + // outside of a real terminal, use space without a limit + $width = PHP_INT_MAX; + } + if (Platform::isWindows()) { + $width--; + } + + if ($input->getOption('path') && null === $composer) { + $io->writeError('No composer.json found in the current directory, disabling "path" option'); + $input->setOption('path', false); + } + foreach ($repos as $repo) { if ($repo === $platformRepo) { - $type = 'platform:'; + $type = 'platform'; } elseif ( $repo === $installedRepo || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true)) ) { - $type = 'installed:'; + $type = 'installed'; } else { - $type = 'available:'; + $type = 'available'; } if ($repo instanceof ComposerRepository && $repo->hasProviders()) { foreach ($repo->getProviderNames() as $name) { @@ -270,11 +300,11 @@ EOT $showMinorOnly = $input->getOption('minor-only'); $indent = $showAllTypes ? ' ' : ''; $latestPackages = array(); - foreach (array('platform:' => true, 'available:' => false, 'installed:' => true) as $type => $showVersion) { + $exitCode = 0; + $viewData = array(); + $viewMetaData = array(); + foreach (array('platform' => true, 'available' => false, 'installed' => true) as $type => $showVersion) { if (isset($packages[$type])) { - if ($showAllTypes) { - $io->write($type); - } ksort($packages[$type]); $nameLength = $versionLength = $latestLength = 0; @@ -298,101 +328,137 @@ EOT $nameLength = max($nameLength, strlen($package)); } } - list($width) = $this->getApplication()->getTerminalDimensions(); - if (null === $width) { - // In case the width is not detected, we're probably running the command - // outside of a real terminal, use space without a limit - $width = PHP_INT_MAX; - } - if (Platform::isWindows()) { - $width--; - } - - if ($input->getOption('path') && null === $composer) { - $io->writeError('No composer.json found in the current directory, disabling "path" option'); - $input->setOption('path', false); - } $writePath = !$input->getOption('name-only') && $input->getOption('path'); - $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion && ($nameLength + $versionLength + 3 <= $width); - $writeLatest = $writeVersion && $showLatest && ($nameLength + $versionLength + $latestLength + 3 <= $width); - $writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + $versionLength + $latestLength + 24 <= $width); - if ($writeLatest && !$io->isDecorated()) { - $latestLength += 2; - } + $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion; + $writeLatest = $writeVersion && $showLatest; + $writeDescription = !$input->getOption('name-only') && !$input->getOption('path'); + $hasOutdatedPackages = false; + + $viewData[$type] = array(); + $viewMetaData[$type] = array( + 'nameLength' => $nameLength, + 'versionLength' => $versionLength, + 'latestLength' => $latestLength, + ); foreach ($packages[$type] as $package) { + $packageViewData = array(); if (is_object($package)) { - $latestPackackage = null; + $latestPackage = null; if ($showLatest && isset($latestPackages[$package->getPrettyName()])) { - $latestPackackage = $latestPackages[$package->getPrettyName()]; + $latestPackage = $latestPackages[$package->getPrettyName()]; } - if ($input->getOption('outdated') && $latestPackackage && $latestPackackage->getFullPrettyVersion() === $package->getFullPrettyVersion() && !$latestPackackage->isAbandoned()) { + if ($input->getOption('outdated') && $latestPackage && $latestPackage->getFullPrettyVersion() === $package->getFullPrettyVersion() && !$latestPackage->isAbandoned()) { continue; } elseif ($input->getOption('outdated')) { $hasOutdatedPackages = true; } - $io->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); - + $packageViewData['name'] = $package->getPrettyName(); if ($writeVersion) { - $io->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false); + $packageViewData['version'] = $package->getFullPrettyVersion(); } - - if ($writeLatest && $latestPackackage) { - $latestVersion = $latestPackackage->getFullPrettyVersion(); - $style = $this->getVersionStyle($latestPackackage, $package); - if (!$io->isDecorated()) { - $latestVersion = str_replace(array('info', 'highlight', 'comment'), array('=', '!', '~'), $style) . ' ' . $latestVersion; - } - $io->write(' <'.$style.'>' . str_pad($latestVersion, $latestLength, ' ') . '', false); + if ($writeLatest && $latestPackage) { + $packageViewData['latest'] = $latestPackage->getFullPrettyVersion(); + $packageViewData['status'] = $this->getUpdateStatus($latestPackage, $package); } - if ($writeDescription) { - $description = strtok($package->getDescription(), "\r\n"); - $remaining = $width - $nameLength - $versionLength - 4; - if ($writeLatest) { - $remaining -= $latestLength; - } - if (strlen($description) > $remaining) { - $description = substr($description, 0, $remaining - 3) . '...'; - } - $io->write(' ' . $description, false); + $packageViewData['description'] = $package->getDescription(); } - if ($writePath) { - $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); - $io->write(' ' . $path, false); + $packageViewData['path'] = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); } - if ($latestPackackage && $latestPackackage->isAbandoned()) { - $replacement = (is_string($latestPackackage->getReplacementPackage())) - ? 'Use ' . $latestPackackage->getReplacementPackage() . ' instead' + if ($latestPackage && $latestPackage->isAbandoned()) { + $replacement = (is_string($latestPackage->getReplacementPackage())) + ? 'Use ' . $latestPackage->getReplacementPackage() . ' instead' : 'No replacement was suggested'; - - $io->writeError(''); - $io->writeError( - sprintf( - "Package %s is abandoned, you should avoid using it. %s.", - $package->getPrettyName(), - $replacement - ), - false + $packageWarning = sprintf( + 'Package %s is abandoned, you should avoid using it. %s.', + $package->getPrettyName(), + $replacement ); + $packageViewData['warning'] = $packageWarning; } } else { - $io->write($indent . $package, false); + $packageViewData['name'] = $package; } - $io->write(''); - } - if ($showAllTypes) { - $io->write(''); + $viewData[$type][] = $packageViewData; } if ($input->getOption('strict') && $hasOutdatedPackages) { - return 1; + $exitCode = 1; + break; } } } + + if ('json' === $format) { + $io->write(JsonFile::encode($viewData)); + } else { + foreach ($viewData as $type => $packages) { + $nameLength = $viewMetaData[$type]['nameLength']; + $versionLength = $viewMetaData[$type]['versionLength']; + $latestLength = $viewMetaData[$type]['latestLength']; + + $writeVersion = $nameLength + $versionLength + 3 <= $width; + $writeLatest = $nameLength + $versionLength + $latestLength + 3 <= $width; + $writeDescription = $nameLength + $versionLength + $latestLength + 24 <= $width; + + if ($writeLatest && !$io->isDecorated()) { + $latestLength += 2; + } + + if ($showAllTypes) { + if ('available' === $type) { + $io->write('' . $type . ':'); + } else { + $io->write('' . $type . ':'); + } + } + + foreach ($packages as $package) { + $io->write($indent . str_pad($package['name'], $nameLength, ' '), false); + if (isset($package['version']) && $writeVersion) { + $io->write(' ' . str_pad($package['version'], $versionLength, ' '), false); + } + if (isset($package['latest']) && $writeLatest) { + $latestVersion = $package['latest']; + $updateStatus = $package['status']; + $style = $this->updateStatusToVersionStyle($updateStatus); + if (!$io->isDecorated()) { + $latestVersion = str_replace(array('up-to-date', 'update-recommended', 'update-possible'), array('=', '!', '~'), $updateStatus) . ' ' . $latestVersion; + } + $io->write(' <' . $style . '>' . str_pad($latestVersion, $latestLength, ' ') . '', false); + } + if (isset($package['description']) && $writeDescription) { + $description = strtok($package['description'], "\r\n"); + $remaining = $width - $nameLength - $versionLength - 4; + if ($writeLatest) { + $remaining -= $latestLength; + } + if (strlen($description) > $remaining) { + $description = substr($description, 0, $remaining - 3) . '...'; + } + $io->write(' ' . $description, false); + } + if (isset($package['path'])) { + $io->write(' ' . $package['path'], false); + } + if (isset($package['warning'])) { + $io->writeError(''); + $io->writeError('' . $package['warning'] . '', false); + } + $io->write(''); + } + + if ($showAllTypes) { + $io->write(''); + } + } + } + + return $exitCode; } protected function getRootRequires() @@ -406,22 +472,7 @@ EOT protected function getVersionStyle(PackageInterface $latestPackage, PackageInterface $package) { - if ($latestPackage->getFullPrettyVersion() === $package->getFullPrettyVersion()) { - // print green as it's up to date - return 'info'; - } - - $constraint = $package->getVersion(); - if (0 !== strpos($constraint, 'dev-')) { - $constraint = '^'.$constraint; - } - if ($latestPackage->getVersion() && Semver::satisfies($latestPackage->getVersion(), $constraint)) { - // print red as it needs an immediate semver-compliant upgrade - return 'highlight'; - } - - // print yellow as it needs an upgrade but has potential BC breaks so is not urgent - return 'comment'; + return $this->updateStatusToVersionStyle($this->getUpdateStatus($latestPackage, $package)); } /** @@ -716,6 +767,32 @@ EOT } } + private function updateStatusToVersionStyle($updateStatus) + { + // 'up-to-date' is printed green + // 'update-recommended' is printed red + // 'upgrade-possible' is printed yellow + return str_replace(array('up-to-date', 'update-recommended', 'update-possible'), array('info', 'highlight', 'comment'), $updateStatus); + } + + private function getUpdateStatus(PackageInterface $latestPackage, PackageInterface $package) { + if ($latestPackage->getFullPrettyVersion() === $package->getFullPrettyVersion()) { + return 'up-to-date'; + } + + $constraint = $package->getVersion(); + if (0 !== strpos($constraint, 'dev-')) { + $constraint = '^'.$constraint; + } + if ($latestPackage->getVersion() && Semver::satisfies($latestPackage->getVersion(), $constraint)) { + // it needs an immediate semver-compliant upgrade + return 'update-recommended'; + } + + // it needs an upgrade but has potential BC breaks so is not urgent + return 'update-possible'; + } + private function writeTreeLine($line) { $io = $this->getIO();