1
0
Fork 0

Remove filterPackages and add RepositoryInterface::search, refactor all commands to use new methods and remove all usage of the full package list for Composer repositories that support providers, fixes #1646

pull/1681/head
Jordi Boggiano 2013-03-10 13:32:59 +01:00
parent 095852933e
commit be861f090a
11 changed files with 233 additions and 205 deletions

View File

@ -73,9 +73,8 @@ EOT
}, $input->getOption('link-type')); }, $input->getOption('link-type'));
$messages = array(); $messages = array();
$repo->filterPackages(function ($package) use ($needle, $types, $linkTypes, &$messages) { $outputPackages = array();
static $outputPackages = array(); foreach ($repo->getPackages() as $package) {
foreach ($types as $type) { foreach ($types as $type) {
foreach ($package->{'get'.$linkTypes[$type][0]}() as $link) { foreach ($package->{'get'.$linkTypes[$type][0]}() as $link) {
if ($link->getTarget() === $needle) { if ($link->getTarget() === $needle) {
@ -86,7 +85,7 @@ EOT
} }
} }
} }
}); }
if ($messages) { if ($messages) {
sort($messages); sort($messages);

View File

@ -292,15 +292,7 @@ EOT
)); ));
} }
$token = strtolower($name); return $this->repos->search($name);
$this->repos->filterPackages(function ($package) use ($token, &$packages) {
if (false !== strpos($package->getName(), $token)) {
$packages[] = $package;
}
});
return $packages;
} }
protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array()) protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array())
@ -339,31 +331,57 @@ EOT
'' ''
)); ));
$exactMatch = null;
$choices = array();
foreach ($matches as $position => $package) { foreach ($matches as $position => $package) {
$output->writeln(sprintf(' <info>%5s</info> %s <comment>%s</comment>', "[$position]", $package->getPrettyName(), $package->getPrettyVersion())); $choices[] = sprintf(' <info>%5s</info> %s', "[$position]", $package['name']);
if ($package['name'] === $package) {
$exactMatch = true;
break;
}
} }
$output->writeln(''); // no match, prompt which to pick
if (!$exactMatch) {
$output->writeln($choices);
$output->writeln('');
$validator = function ($selection) use ($matches) { $validator = function ($selection) use ($matches) {
if ('' === $selection) { if ('' === $selection) {
return false; return false;
}
if (!is_numeric($selection) && preg_match('{^\s*(\S+)\s+(\S.*)\s*$}', $selection, $matches)) {
return $matches[1].' '.$matches[2];
}
if (!isset($matches[(int) $selection])) {
throw new \Exception('Not a valid selection');
}
$package = $matches[(int) $selection];
return $package['name'];
};
$package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or the complete package name if it is not listed', false, ':'), $validator, 3);
}
// no constraint yet, prompt user
if (false !== $package && false === strpos($package, ' ')) {
$validator = function ($input) {
$input = trim($input);
return $input ?: false;
};
$constraint = $dialog->askAndValidate($output, $dialog->getQuestion('Enter the version constraint to require', false, ':'), $validator, 3);
if (false === $constraint) {
continue;
} }
if (!is_numeric($selection) && preg_match('{^\s*(\S+) +(\S.*)\s*}', $selection, $matches)) { $package .= ' '.$constraint;
return $matches[1].' '.$matches[2]; }
}
if (!isset($matches[(int) $selection])) {
throw new \Exception('Not a valid selection');
}
$package = $matches[(int) $selection];
return sprintf('%s %s', $package->getName(), $package->getPrettyVersion());
};
$package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a "[package] [version]" couple if it is not listed', false, ':'), $validator, 3);
if (false !== $package) { if (false !== $package) {
$requires[] = $package; $requires[] = $package;

View File

@ -109,7 +109,7 @@ EOT
->setPreferDist($input->getOption('prefer-dist')) ->setPreferDist($input->getOption('prefer-dist'))
->setDevMode($input->getOption('dev')) ->setDevMode($input->getOption('dev'))
->setUpdate(true) ->setUpdate(true)
->setUpdateWhitelist($requirements); ->setUpdateWhitelist(array_keys($requirements));
; ;
if (!$install->run()) { if (!$install->run()) {

View File

@ -18,6 +18,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Factory; use Composer\Factory;
@ -66,79 +67,13 @@ EOT
$repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
} }
$this->onlyName = $input->getOption('only-name'); $onlyName = $input->getOption('only-name');
$this->tokens = $input->getArgument('tokens');
$this->output = $output;
$repos->filterPackages(array($this, 'processPackage'), 'Composer\Package\CompletePackage');
foreach ($this->lowMatches as $details) { $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT;
$output->writeln($details['name'] . '<comment>:</comment> '. $details['description']); $results = $repos->search(implode(' ', $input->getArgument('tokens')), $flags);
foreach ($results as $result) {
$output->writeln($result['name'] . (isset($result['description']) ? ' '. $result['description'] : ''));
} }
} }
public function processPackage($package)
{
if ($package instanceof AliasPackage || isset($this->matches[$package->getName()])) {
return;
}
foreach ($this->tokens as $token) {
if (!$score = $this->matchPackage($package, $token)) {
continue;
}
if (false !== ($pos = stripos($package->getName(), $token))) {
$name = substr($package->getPrettyName(), 0, $pos)
. '<highlight>' . substr($package->getPrettyName(), $pos, strlen($token)) . '</highlight>'
. substr($package->getPrettyName(), $pos + strlen($token));
} else {
$name = $package->getPrettyName();
}
$description = strtok($package->getDescription(), "\r\n");
if (false !== ($pos = stripos($description, $token))) {
$description = substr($description, 0, $pos)
. '<highlight>' . substr($description, $pos, strlen($token)) . '</highlight>'
. substr($description, $pos + strlen($token));
}
if ($score >= 3) {
$this->output->writeln($name . '<comment>:</comment> '. $description);
$this->matches[$package->getName()] = true;
} else {
$this->lowMatches[$package->getName()] = array(
'name' => $name,
'description' => $description,
);
}
return;
}
}
/**
* tries to find a token within the name/keywords/description
*
* @param CompletePackageInterface $package
* @param string $token
* @return boolean
*/
private function matchPackage(CompletePackageInterface $package, $token)
{
$score = 0;
if (false !== stripos($package->getName(), $token)) {
$score += 5;
}
if (!$this->onlyName && false !== stripos(join(',', $package->getKeywords() ?: array()), $token)) {
$score += 3;
}
if (!$this->onlyName && false !== stripos($package->getDescription(), $token)) {
$score += 1;
}
return $score;
}
} }

View File

@ -13,8 +13,11 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Composer; use Composer\Composer;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\DefaultPolicy;
use Composer\Factory; use Composer\Factory;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -22,6 +25,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Repository\ArrayRepository; use Composer\Repository\ArrayRepository;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\ComposerRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryInterface;
@ -122,20 +126,39 @@ EOT
// list packages // list packages
$packages = array(); $packages = array();
$repos->filterPackages(function ($package) use (&$packages, $platformRepo, $installedRepo) {
if ($platformRepo->hasPackage($package)) { if ($repos instanceof CompositeRepository) {
$repos = $repos->getRepositories();
} elseif (!is_array($repos)) {
$repos = array($repos);
}
foreach ($repos as $repo) {
if ($repo === $platformRepo) {
$type = '<info>platform</info>:'; $type = '<info>platform</info>:';
} elseif ($installedRepo->hasPackage($package)) { } elseif (
$repo === $installedRepo
|| ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true))
) {
$type = '<info>installed</info>:'; $type = '<info>installed</info>:';
} else { } else {
$type = '<comment>available</comment>:'; $type = '<comment>available</comment>:';
} }
if (!isset($packages[$type][$package->getName()]) if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
|| version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') foreach ($repo->getProviderNames() as $name) {
) { $packages[$type][$name] = $name;
$packages[$type][$package->getName()] = $package; }
} else {
foreach ($repo->getPackages() as $package) {
if (!isset($packages[$type][$package->getName()])
|| !is_object($packages[$type][$package->getName()])
|| version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<')
) {
$packages[$type][$package->getName()] = $package;
}
}
} }
}, 'Composer\Package\CompletePackage'); }
$tree = !$input->getOption('platform') && !$input->getOption('installed') && !$input->getOption('available'); $tree = !$input->getOption('platform') && !$input->getOption('installed') && !$input->getOption('available');
$indent = $tree ? ' ' : ''; $indent = $tree ? ' ' : '';
@ -148,8 +171,12 @@ EOT
$nameLength = $versionLength = 0; $nameLength = $versionLength = 0;
foreach ($packages[$type] as $package) { foreach ($packages[$type] as $package) {
$nameLength = max($nameLength, strlen($package->getPrettyName())); if (is_object($package)) {
$versionLength = max($versionLength, strlen($this->versionParser->formatVersion($package))); $nameLength = max($nameLength, strlen($package->getPrettyName()));
$versionLength = max($versionLength, strlen($this->versionParser->formatVersion($package)));
} else {
$nameLength = max($nameLength, $package);
}
} }
list($width) = $this->getApplication()->getTerminalDimensions(); list($width) = $this->getApplication()->getTerminalDimensions();
if (defined('PHP_WINDOWS_VERSION_BUILD')) { if (defined('PHP_WINDOWS_VERSION_BUILD')) {
@ -159,19 +186,23 @@ EOT
$writeVersion = !$input->getOption('name-only') && $showVersion && ($nameLength + $versionLength + 3 <= $width); $writeVersion = !$input->getOption('name-only') && $showVersion && ($nameLength + $versionLength + 3 <= $width);
$writeDescription = !$input->getOption('name-only') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width); $writeDescription = !$input->getOption('name-only') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width);
foreach ($packages[$type] as $package) { foreach ($packages[$type] as $package) {
$output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); if (is_object($package)) {
$output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
if ($writeVersion) { if ($writeVersion) {
$output->write(' ' . str_pad($this->versionParser->formatVersion($package), $versionLength, ' '), false); $output->write(' ' . str_pad($this->versionParser->formatVersion($package), $versionLength, ' '), false);
}
if ($writeDescription) {
$description = strtok($package->getDescription(), "\r\n");
$remaining = $width - $nameLength - $versionLength - 4;
if (strlen($description) > $remaining) {
$description = substr($description, 0, $remaining - 3) . '...';
} }
$output->write(' ' . $description);
if ($writeDescription) {
$description = strtok($package->getDescription(), "\r\n");
$remaining = $width - $nameLength - $versionLength - 4;
if (strlen($description) > $remaining) {
$description = substr($description, 0, $remaining - 3) . '...';
}
$output->write(' ' . $description);
}
} else {
$output->write($indent . $package);
} }
$output->writeln(''); $output->writeln('');
} }
@ -195,51 +226,46 @@ EOT
protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null) protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null)
{ {
$name = strtolower($name); $name = strtolower($name);
$constraint = null;
if ($version) { if ($version) {
$version = $this->versionParser->normalize($version); $version = $this->versionParser->normalize($version);
$constraint = new VersionConstraint('=', $version);
} }
$match = null; $policy = new DefaultPolicy();
$matches = array(); $pool = new Pool('dev');
$repos->filterPackages(function ($package) use ($name, $version, &$matches) { $pool->addRepository($repos);
if ($package->getName() === $name) {
$matches[] = $package;
}
}, 'Composer\Package\CompletePackage');
if (null === $version) { $matchedPackage = null;
// search for a locally installed version $matches = $pool->whatProvides($name, $constraint);
foreach ($matches as $package) { foreach ($matches as $index => $package) {
if ($installedRepo->hasPackage($package)) { // skip providers/replacers
$match = $package; if ($package->getName() !== $name) {
break; unset($matches[$index]);
} continue;
} }
if (!$match) { // select an exact match if it is in the installed repo and no specific version was required
// fallback to the highest version if (null === $version && $installedRepo->hasPackage($package)) {
foreach ($matches as $package) { $matchedPackage = $package;
if (null === $match || version_compare($package->getVersion(), $match->getVersion(), '>=')) {
$match = $package;
}
}
}
} else {
// select the specified version
foreach ($matches as $package) {
if ($package->getVersion() === $version) {
$match = $package;
}
} }
$matches[$index] = $package->getId();
}
// select prefered package according to policy rules
if (!$matchedPackage && $matches && $prefered = $policy->selectPreferedPackages($pool, array(), $matches)) {
$matchedPackage = $pool->literalToPackage($prefered[0]);
} }
// build versions array // build versions array
$versions = array(); $versions = array();
foreach ($matches as $package) { foreach ($matches as $package) {
$package = $pool->literalToPackage($package);
$versions[$package->getPrettyVersion()] = $package->getVersion(); $versions[$package->getPrettyVersion()] = $package->getVersion();
} }
return array($match, $versions); return array($matchedPackage, $versions);
} }
/** /**

View File

@ -214,13 +214,13 @@ class Installer
// output suggestions // output suggestions
foreach ($this->suggestedPackages as $suggestion) { foreach ($this->suggestedPackages as $suggestion) {
$target = $suggestion['target']; $target = $suggestion['target'];
if ($installedRepo->filterPackages(function (PackageInterface $package) use ($target) { foreach ($installedRepo->getPackages() as $package) {
if (in_array($target, $package->getNames())) { if (in_array($target, $package->getNames())) {
return false; continue 2;
} }
})) {
$this->io->write($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')');
} }
$this->io->write($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')');
} }
if (!$this->dryRun) { if (!$this->dryRun) {

View File

@ -74,6 +74,27 @@ class ArrayRepository implements RepositoryInterface
return $packages; return $packages;
} }
/**
* {@inheritDoc}
*/
public function search($query, $mode = 0)
{
$regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i';
$matches = array();
foreach ($this->getPackages() as $package) {
// TODO implement SEARCH_FULLTEXT handling with keywords/description matching
if (preg_match($regex, $package->getName())) {
$matches[] = array(
'name' => $package->getName(),
'description' => $package->getDescription(),
);
}
}
return $matches;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -112,20 +133,6 @@ class ArrayRepository implements RepositoryInterface
} }
} }
/**
* {@inheritDoc}
*/
public function filterPackages($callback, $class = 'Composer\Package\Package')
{
foreach ($this->getPackages() as $package) {
if (false === call_user_func($callback, $package)) {
return false;
}
}
return true;
}
protected function createAliasPackage(PackageInterface $package, $alias = null, $prettyAlias = null) protected function createAliasPackage(PackageInterface $package, $alias = null, $prettyAlias = null)
{ {
return new AliasPackage($package, $alias ?: $package->getAlias(), $prettyAlias ?: $package->getPrettyAlias()); return new AliasPackage($package, $alias ?: $package->getAlias(), $prettyAlias ?: $package->getPrettyAlias());

View File

@ -135,24 +135,54 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function filterPackages($callback, $class = 'Composer\Package\Package') public function search($query, $mode = 0)
{ {
if (null === $this->rawData) { $this->loadRootServerFile();
$this->rawData = $this->loadDataFromServer();
if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) {
$url = str_replace('%query%', $query, $this->searchUrl);
$json = $this->rfs->getContents($url, $url, false);
$results = JsonFile::parseJson($json, $url);
return $results['results'];
} }
foreach ($this->rawData as $package) { if ($this->hasProviders()) {
if (false === call_user_func($callback, $package = $this->createPackage($package, $class))) { $results = array();
return false; $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i';
}
if ($package->getAlias()) { foreach ($this->getProviderNames() as $name) {
if (false === call_user_func($callback, $this->createAliasPackage($package))) { if (preg_match($regex, $name)) {
return false; $results[] = array('name' => $name);
} }
} }
return $results;
} }
return true; return parent::search($query, $mode);
}
public function getProviderNames()
{
$this->loadRootServerFile();
if (null === $this->providerListing) {
$this->loadProviderListings($this->loadRootServerFile());
}
if ($this->providersUrl) {
return array_keys($this->providerListing);
}
// BC handling for old providers-includes
$providers = array();
foreach (array_keys($this->providerListing) as $provider) {
$providers[] = substr($provider, 2, -5);
}
return $providers;
} }
/** /**
@ -196,15 +226,15 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository
public function whatProvides(Pool $pool, $name) public function whatProvides(Pool $pool, $name)
{ {
// skip platform packages
if ($name === 'php' || in_array(substr($name, 0, 4), array('ext-', 'lib-'), true) || $name === '__root__') {
return array();
}
if (isset($this->providers[$name])) { if (isset($this->providers[$name])) {
return $this->providers[$name]; return $this->providers[$name];
} }
// skip platform packages
if (preg_match('{^(?:php(?:-64bit)?|(?:ext|lib)-[^/]+)$}i', $name) || '__root__' === $name) {
return array();
}
if (null === $this->providerListing) { if (null === $this->providerListing) {
$this->loadProviderListings($this->loadRootServerFile()); $this->loadProviderListings($this->loadRootServerFile());
} }

View File

@ -94,6 +94,20 @@ class CompositeRepository implements RepositoryInterface
return call_user_func_array('array_merge', $packages); return call_user_func_array('array_merge', $packages);
} }
/**
* {@inheritdoc}
*/
public function search($query, $mode = 0)
{
$matches = array();
foreach ($this->repositories as $repository) {
/* @var $repository RepositoryInterface */
$matches[] = $repository->search($query, $mode);
}
return call_user_func_array('array_merge', $matches);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -19,9 +19,13 @@ use Composer\Package\PackageInterface;
* *
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
* @author Konstantin Kudryashov <ever.zet@gmail.com> * @author Konstantin Kudryashov <ever.zet@gmail.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/ */
interface RepositoryInterface extends \Countable interface RepositoryInterface extends \Countable
{ {
const SEARCH_FULLTEXT = 0;
const SEARCH_NAME = 1;
/** /**
* Checks if specified package registered (installed). * Checks if specified package registered (installed).
* *
@ -51,24 +55,19 @@ interface RepositoryInterface extends \Countable
*/ */
public function findPackages($name, $version = null); public function findPackages($name, $version = null);
/**
* Filters all the packages through a callback
*
* The packages are not guaranteed to be instances in the repository
* and this can only be used for streaming through a list of packages.
*
* If the callback returns false, the process stops
*
* @param callable $callback
* @param string $class
* @return bool false if the process was interrupted, true otherwise
*/
public function filterPackages($callback, $class = 'Composer\Package\Package');
/** /**
* Returns list of registered packages. * Returns list of registered packages.
* *
* @return array * @return array
*/ */
public function getPackages(); public function getPackages();
/**
* Searches the repository for packages containing the query
*
* @param string $query search query
* @param int $mode a set of SEARCH_* constants to search on, implementations should do a best effort only
* @return array[] an array of array('name' => '...', 'description' => '...')
*/
public function search($query, $mode = 0);
} }

View File

@ -42,7 +42,7 @@ class ComposerRepositoryTest extends TestCase
); );
$repository $repository
->expects($this->once()) ->expects($this->exactly(2))
->method('loadRootServerFile') ->method('loadRootServerFile')
->will($this->returnValue($repoPackages)); ->will($this->returnValue($repoPackages));
@ -50,7 +50,7 @@ class ComposerRepositoryTest extends TestCase
$stubPackage = $this->getPackage('stub/stub', '1.0.0'); $stubPackage = $this->getPackage('stub/stub', '1.0.0');
$repository $repository
->expects($this->at($at + 1)) ->expects($this->at($at + 2))
->method('createPackage') ->method('createPackage')
->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage')) ->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage'))
->will($this->returnValue($stubPackage)); ->will($this->returnValue($stubPackage));