From e38fe673339dd5d846de34ab251fcb34f6bc8e52 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Mon, 15 Feb 2016 00:49:00 +0100 Subject: [PATCH] Rewrote DependsCommand --- src/Composer/Command/DependsCommand.php | 165 ++++++++++++++++-------- src/Composer/Package/Link.php | 8 ++ 2 files changed, 119 insertions(+), 54 deletions(-) diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index abdce5a86..185d3bb7e 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -15,12 +15,15 @@ namespace Composer\Command; use Composer\DependencyResolver\Pool; use Composer\Package\Link; use Composer\Package\PackageInterface; +use Composer\Package\RootPackage; use Composer\Repository\ArrayRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\VersionParser; +use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -32,22 +35,19 @@ use Symfony\Component\Console\Output\OutputInterface; */ class DependsCommand extends Command { - protected $linkTypes = array( - 'require' => array('requires', 'requires'), - 'require-dev' => array('devRequires', 'requires (dev)'), - ); + /** @var CompositeRepository */ + private $repository; protected function configure() { $this ->setName('depends') + ->setAliases(array('why')) ->setDescription('Shows which packages depend on the given package') ->setDefinition(array( new InputArgument('package', InputArgument::REQUIRED, 'Package to inspect'), - new InputOption('link-type', '', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Link types to show (require, require-dev)', array_keys($this->linkTypes)), new InputOption('match-constraint', 'm', InputOption::VALUE_REQUIRED, 'Filters the dependencies shown using this constraint', '*'), new InputOption('invert-match-constraint', 'i', InputOption::VALUE_NONE, 'Turns --match-constraint around into a blacklist instead of whitelist'), - new InputOption('with-replaces', '', InputOption::VALUE_NONE, 'Search for replaced packages as well'), )) ->setHelp(<<getComposer(); - $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'depends', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + // Prepare repositories and set up a pool $platformOverrides = $composer->getConfig()->get('platform') ?: array(); - $repo = new CompositeRepository(array( + $this->repository = new CompositeRepository(array( new ArrayRepository(array($composer->getPackage())), $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); - $needle = $input->getArgument('package'); - $pool = new Pool(); - $pool->addRepository($repo); + $pool->addRepository($this->repository); + // Find packages that are or provide the requested package first + $needle = $input->getArgument('package'); $packages = $pool->whatProvides($needle); if (empty($packages)) { - throw new \InvalidArgumentException('Could not find package "'.$needle.'" in your project.'); + throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } - $linkTypes = $this->linkTypes; + // Parse options that are only relevant for the initial needle(s) + if ('*' !== ($textConstraint = $input->getOption('match-constraint'))) { + $versionParser = new VersionParser(); + $constraint = $versionParser->parseConstraints($textConstraint); + } else { + $constraint = null; + } + $matchInvert = $input->getOption('invert-match-constraint'); + $recursive = true; + $tree = true; + + // Resolve dependencies + $results = $this->getDependers($needle, $constraint, $matchInvert, $recursive); + if (empty($results)) { + $extra = isset($constraint) ? sprintf(' in versions %smatching %s', $matchInvert ? 'not ' : '', $textConstraint) : ''; + $this->getIO()->writeError(sprintf('There is no installed package depending on "%s"%s', + $needle, $extra)); + } elseif ($tree) { + $root = $packages[0]; + $this->getIO()->write(sprintf('%s %s %s', $root->getPrettyName(), $root->getPrettyVersion(), $root->getDescription())); + $this->printTree($output, $results); + } else { + $this->printTable($output, $results); + } + } - $types = array_map(function ($type) use ($linkTypes) { - $type = rtrim($type, 's'); - if (!isset($linkTypes[$type])) { - throw new \InvalidArgumentException('Unexpected link type: '.$type.', valid types: '.implode(', ', array_keys($linkTypes))); + private function printTable(OutputInterface $output, $results) + { + $table = array(); + $doubles = array(); + do { + $queue = array(); + $rows = array(); + foreach($results as $result) { + /** + * @var PackageInterface $package + * @var Link $link + */ + list($package, $link, $children) = $result; + $unique = (string)$link; + if (isset($doubles[$unique])) { + continue; + } + $doubles[$unique] = true; + $realVersion = (strpos($package->getPrettyVersion(), 'No version set') === 0) ? '-' : $package->getPrettyVersion(); + $rows[] = array($package->getPrettyName(), $realVersion, $link->getDescription(), sprintf('%s (%s)', $link->getTarget(), $link->getPrettyConstraint())); + $queue = array_merge($queue, $children); } + $results = $queue; + $table = array_merge($rows, $table); + } while(!empty($results)); - return $type; - }, $input->getOption('link-type')); + // Render table + $renderer = new Table($output); + $renderer->setStyle('compact')->setRows($table)->render(); + } - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($input->getOption('match-constraint')); - $matchInvert = $input->getOption('invert-match-constraint'); + public function printTree(OutputInterface $output, $results, $prefix = '') + { + $count = count($results); + $idx = 0; + foreach($results as $key => $result) { + /** + * @var PackageInterface $package + * @var Link $link + */ + list($package, $link, $children) = $result; + $isLast = (++$idx == $count); + $output->write(sprintf("%s%s %s %s %s (%s)\n", $prefix, $isLast ? '`-' : '|-', $link->getSource(), $link->getDescription(), $link->getTarget(), $link->getPrettyConstraint())); + $this->printTree($output, $children, $prefix . ($isLast ? ' ' : '| ')); + } + } - $needles = array($needle); - if (true === $input->getOption('with-replaces')) { - foreach ($packages as $package) { - $needles = array_merge($needles, array_map(function (Link $link) { - return $link->getTarget(); - }, $package->getReplaces())); + /** + * @param string $needle The package to inspect. + * @param ConstraintInterface|null $constraint Optional constraint to filter by. + * @param bool $invert Whether to invert matches on the previous constraint. + * @param bool $recurse Whether to recursively expand the requirement tree. + * @return array An array with dependers as key, and as values an array containing the source package and the link respectively + */ + private function getDependers($needle, $constraint = null, $invert = false, $recurse = true) + { + $needles = is_array($needle) ? $needle : array($needle); + $results = array(); + + /** + * Loop over all currently installed packages. + * @var PackageInterface $package + */ + foreach ($this->repository->getPackages() as $package) { + // Retrieve all requirements, but dev only for the root package + $links = $package->getRequires(); + $links += $package->getReplaces(); + if ($package instanceof RootPackage) { + $links += $package->getDevRequires(); } - } - $messages = array(); - $outputPackages = array(); - $io = $this->getIO(); - /** @var PackageInterface $package */ - foreach ($repo->getPackages() as $package) { - foreach ($types as $type) { - /** @var Link $link */ - foreach ($package->{'get'.$linkTypes[$type][0]}() as $link) { - foreach ($needles as $needle) { - if ($link->getTarget() === $needle && ($link->getConstraint()->matches($constraint) ? !$matchInvert : $matchInvert)) { - if (!isset($outputPackages[$package->getName()])) { - $messages[] = ''.$package->getPrettyName() . ' ' . $linkTypes[$type][1] . ' ' . $needle .' (' . $link->getPrettyConstraint() . ')'; - $outputPackages[$package->getName()] = true; - } + // Cross-reference all discovered links to the needles + foreach ($links as $link) { + foreach ($needles as $needle) { + if ($link->getTarget() === $needle) { + if (is_null($constraint) || (($link->getConstraint()->matches($constraint) === !$invert))) { + $results[$link->getSource()] = array($package, $link, $recurse ? $this->getDependers($link->getSource(), null, false, true) : array()); } } } } } - - if ($messages) { - sort($messages); - $io->write($messages); - } else { - $matchText = ''; - if ($input->getOption('match-constraint') !== '*') { - $matchText = ' in versions '.($matchInvert ? 'not ' : '').'matching ' . $input->getOption('match-constraint'); - } - $io->writeError('There is no installed package depending on "'.$needle.'"'.$matchText.'.'); - } + ksort($results); + return $results; } } diff --git a/src/Composer/Package/Link.php b/src/Composer/Package/Link.php index e695822fd..217da0713 100644 --- a/src/Composer/Package/Link.php +++ b/src/Composer/Package/Link.php @@ -64,6 +64,14 @@ class Link $this->prettyConstraint = $prettyConstraint; } + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + /** * @return string */