Browse Source

Fix show/depends commands to display and abort when a circular dep was reached, fixes #4983

Jordi Boggiano 9 years ago
parent
commit
27e1c4358e

+ 36 - 5
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('<info>There is no installed package depending on "%s"%s</info>',
                 $needle, $extra));
         } elseif ($renderTree) {
+            $this->initStyles($output);
             $root = $packages[0];
             $this->getIO()->write(sprintf('<info>%s</info> %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</%1$s> %s', $color, $package->getPrettyName(), $versionText));
+            $linkText = sprintf('%s <%s>%s</%2$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);
+            }
         }
     }
 

+ 8 - 9
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('<info>%s</info>', $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> %s', $treeBar, $color, $requireName, $color, $require->getPrettyConstraint());
+
+                $circularWarn = in_array($requireName, $currentTree) ? '(circular dependency aborted here)' : '';
+                $info = rtrim(sprintf('%s──<%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);
                 }
             }
         }

+ 23 - 7
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);
                         }
                     }