From 27e1c4358eae1e896d157d651c4fdab77d1340df Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 2 Mar 2016 13:24:07 +0000 Subject: [PATCH] Fix show/depends commands to display and abort when a circular dep was reached, fixes #4983 --- .../Command/BaseDependencyCommand.php | 41 ++++++++++++++++--- src/Composer/Command/ShowCommand.php | 17 ++++---- src/Composer/Repository/BaseRepository.php | 30 ++++++++++---- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 561975bd1..faa08764d 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -20,6 +20,7 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; @@ -39,6 +40,8 @@ class BaseDependencyCommand extends BaseCommand const OPTION_RECURSIVE = 'recursive'; const OPTION_TREE = 'tree'; + protected $colors; + /** * Set common options and arguments. */ @@ -119,6 +122,7 @@ class BaseDependencyCommand extends BaseCommand $this->getIO()->writeError(sprintf('There is no installed package depending on "%s"%s', $needle, $extra)); } elseif ($renderTree) { + $this->initStyles($output); $root = $packages[0]; $this->getIO()->write(sprintf('%s %s %s', $root->getPrettyName(), $root->getPrettyVersion(), $root->getDescription())); $this->printTree($results); @@ -169,13 +173,34 @@ class BaseDependencyCommand extends BaseCommand $renderer->setRows($table)->render(); } + /** + * Init styles for tree + * + * @param OutputInterface $output + */ + protected function initStyles(OutputInterface $output) + { + $this->colors = array( + 'green', + 'yellow', + 'cyan', + 'magenta', + 'blue', + ); + + foreach ($this->colors as $color) { + $style = new OutputFormatterStyle($color); + $output->getFormatter()->setStyle($color, $style); + } + } + /** * Recursively prints a tree of the selected results. * * @param array $results * @param string $prefix */ - protected function printTree($results, $prefix = '') + protected function printTree($results, $prefix = '', $level = 1) { $count = count($results); $idx = 0; @@ -185,12 +210,18 @@ class BaseDependencyCommand extends BaseCommand * @var Link $link */ list($package, $link, $children) = $result; + + $color = $this->colors[$level % count($this->colors)]; + $prevColor = $this->colors[($level - 1) % count($this->colors)]; $isLast = (++$idx == $count); $versionText = (strpos($package->getPrettyVersion(), 'No version set') === 0) ? '' : $package->getPrettyVersion(); - $packageText = rtrim(sprintf('%s %s', $package->getPrettyName(), $versionText)); - $linkText = implode(' ', array($link->getDescription(), $link->getTarget(), $link->getPrettyConstraint())); - $this->writeTreeLine(sprintf("%s%s%s (%s)", $prefix, $isLast ? '└──' : '├──', $packageText, $linkText)); - $this->printTree($children, $prefix . ($isLast ? ' ' : '│ ')); + $packageText = rtrim(sprintf('<%s>%s %s', $color, $package->getPrettyName(), $versionText)); + $linkText = sprintf('%s <%s>%s %s', $link->getDescription(), $prevColor, $link->getTarget(), $link->getPrettyConstraint()); + $circularWarn = $children === false ? '(circular dependency aborted here)' : ''; + $this->writeTreeLine(rtrim(sprintf("%s%s%s (%s) %s", $prefix, $isLast ? '└──' : '├──', $packageText, $linkText, $circularWarn))); + if ($children) { + $this->printTree($children, $prefix . ($isLast ? ' ' : '│ '), $level + 1); + } } } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index f8e031c37..b5d80cf05 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -491,9 +491,6 @@ EOT */ protected function displayPackageTree(PackageInterface $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos) { - $packagesInTree = array(); - $packagesInTree[] = $package; - $io = $this->getIO(); $io->write(sprintf('%s', $package->getPrettyName()), false); $io->write(' ' . $package->getPrettyVersion(), false); @@ -518,8 +515,7 @@ EOT $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); - - $packagesInTree[] = $requireName; + $packagesInTree = array($package->getName(), $requireName); $this->displayTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree, $treeBar, $level + 1); } @@ -547,19 +543,22 @@ EOT $i = 0; $total = count($requires); foreach ($requires as $requireName => $require) { + $currentTree = $packagesInTree; $i++; if ($i == $total) { $treeBar = $previousTreeBar . ' └'; } $colorIdent = $level % count($this->colors); $color = $this->colors[$colorIdent]; - $info = sprintf('%s──<%s>%s %s', $treeBar, $color, $requireName, $color, $require->getPrettyConstraint()); + + $circularWarn = in_array($requireName, $currentTree) ? '(circular dependency aborted here)' : ''; + $info = rtrim(sprintf('%s──<%s>%s %s %s', $treeBar, $color, $requireName, $color, $require->getPrettyConstraint(), $circularWarn)); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); - if (!in_array($requireName, $packagesInTree)) { - $packagesInTree[] = $requireName; - $this->displayTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree, $treeBar, $level + 1); + if (!in_array($requireName, $currentTree)) { + $currentTree[] = $requireName; + $this->displayTree($requireName, $require, $installedRepo, $distantRepos, $currentTree, $treeBar, $level + 1); } } } diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index e29d87f6d..79814eca8 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -26,23 +26,33 @@ abstract class BaseRepository implements RepositoryInterface * Returns a list of links causing the requested needle packages to be installed, as an associative array with the * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship * as values. If recursive lookup was requested a third value is returned containing an identically formed array up - * to the root package. + * to the root package. That third value will be false in case a circular recursion was detected. * - * @param string|string[] $needle The package name(s) to inspect. - * @param ConstraintInterface|null $constraint Optional constraint to filter by. - * @param bool $invert Whether to invert matches to discover reasons for the package *NOT* to be installed. - * @param bool $recurse Whether to recursively expand the requirement tree up to the root package. + * @param string|string[] $needle The package name(s) to inspect. + * @param ConstraintInterface|null $constraint Optional constraint to filter by. + * @param bool $invert Whether to invert matches to discover reasons for the package *NOT* to be installed. + * @param bool $recurse Whether to recursively expand the requirement tree up to the root package. + * @param string[] $packagesFound Used internally when recurring * @return array An associative array of arrays as described above. */ - public function getDependents($needle, $constraint = null, $invert = false, $recurse = true) + public function getDependents($needle, $constraint = null, $invert = false, $recurse = true, $packagesFound = null) { $needles = (array) $needle; $results = array(); + // initialize the array with the needles before any recursion occurs + if (null === $packagesFound) { + $packagesFound = $needles; + } + // Loop over all currently installed packages. foreach ($this->getPackages() as $package) { $links = $package->getRequires(); + // each loop needs its own "tree" as we want to show the complete dependent set of every needle + // without warning all the time about finding circular deps + $packagesInTree = $packagesFound; + // Replacements are considered valid reasons for a package to be installed during forward resolution if (!$invert) { $links += $package->getReplaces(); @@ -58,7 +68,13 @@ abstract class BaseRepository implements RepositoryInterface foreach ($needles as $needle) { if ($link->getTarget() === $needle) { if (is_null($constraint) || (($link->getConstraint()->matches($constraint) === !$invert))) { - $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true) : array(); + // already displayed this node's dependencies, cutting short + if (in_array($link->getSource(), $packagesInTree)) { + $results[$link->getSource()] = array($package, $link, false); + continue; + } + $packagesInTree[] = $link->getSource(); + $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true, $packagesInTree) : array(); $results[$link->getSource()] = array($package, $link, $dependents); } }