diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php
index f49b00bc3..47d139144 100644
--- a/src/Composer/Command/ClearCacheCommand.php
+++ b/src/Composer/Command/ClearCacheCommand.php
@@ -40,21 +40,30 @@ EOT
{
$config = Factory::createConfig();
$io = $this->getIO();
-
- $cachePath = realpath($config->get('cache-repo-dir'));
- if (!$cachePath) {
- $io->write('Cache directory does not exist.');
- return;
+
+ $cachePaths = array(
+ 'cache-dir' => $config->get('cache-dir'),
+ 'cache-files-dir' => $config->get('cache-files-dir'),
+ 'cache-repo-dir' => $config->get('cache-repo-dir'),
+ 'cache-vcs-dir' => $config->get('cache-vcs-dir'),
+ );
+
+ foreach ($cachePaths as $key => $cachePath) {
+ $cachePath = realpath($cachePath);
+ if (!$cachePath) {
+ $io->write("Cache directory does not exist ($key): $cachePath");
+ return;
+ }
+ $cache = new Cache($io, $cachePath);
+ if (!$cache->isEnabled()) {
+ $io->write("Cache is not enabled ($key): $cachePath");
+ return;
+ }
+
+ $io->write("Clearing cache ($key): $cachePath");
+ $cache->gc(0, 0);
}
- $cache = new Cache($io, $cachePath);
- if (!$cache->isEnabled()) {
- $io->write('Cache is not enabled.');
- return;
- }
-
- $io->write('Clearing cache in: '.$cachePath.'');
- $cache->gc(0, 0);
- $io->write('Cache cleared.');
+ $io->write('All caches cleared.');
}
}
diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php
index 632e869df..9928692eb 100644
--- a/src/Composer/Command/CreateProjectCommand.php
+++ b/src/Composer/Command/CreateProjectCommand.php
@@ -21,6 +21,7 @@ use Composer\IO\IOInterface;
use Composer\Package\BasePackage;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\Package\Version\VersionSelector;
use Composer\Repository\ComposerRepository;
use Composer\Repository\CompositeRepository;
use Composer\Repository\FilesystemRepository;
@@ -242,7 +243,6 @@ EOT
}
$parser = new VersionParser();
- $candidates = array();
$requirements = $parser->parseNameVersionPairs(array($packageName));
$name = strtolower($requirements[0]['name']);
if (!$packageVersion && isset($requirements[0]['version'])) {
@@ -266,15 +266,11 @@ EOT
$pool = new Pool($stability);
$pool->addRepository($sourceRepo);
- $constraint = $packageVersion ? $parser->parseConstraints($packageVersion) : null;
- $candidates = $pool->whatProvides($name, $constraint);
- foreach ($candidates as $key => $candidate) {
- if ($candidate->getName() !== $name) {
- unset($candidates[$key]);
- }
- }
+ // find the latest version if there are multiple
+ $versionSelector = new VersionSelector($pool);
+ $package = $versionSelector->findBestCandidate($name, $packageVersion);
- if (!$candidates) {
+ if (!$package) {
throw new \InvalidArgumentException("Could not find package $name" . ($packageVersion ? " with version $packageVersion." : " with stability $stability."));
}
@@ -283,15 +279,6 @@ EOT
$directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts);
}
- // select highest version if we have many
- $package = reset($candidates);
- foreach ($candidates as $candidate) {
- if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) {
- $package = $candidate;
- }
- }
- unset($candidates);
-
$io->write('Installing ' . $package->getName() . ' (' . VersionParser::formatVersion($package, false) . ')');
if ($disablePlugins) {
diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php
index a44546b73..d7ee60677 100644
--- a/src/Composer/Command/InitCommand.php
+++ b/src/Composer/Command/InitCommand.php
@@ -12,9 +12,11 @@
namespace Composer\Command;
+use Composer\DependencyResolver\Pool;
use Composer\Json\JsonFile;
use Composer\Factory;
use Composer\Package\BasePackage;
+use Composer\Package\Version\VersionSelector;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\Package\Version\VersionParser;
@@ -32,6 +34,7 @@ class InitCommand extends Command
{
private $gitConfig;
private $repos;
+ private $pool;
public function parseAuthorString($author)
{
@@ -284,9 +287,11 @@ EOT
protected function findPackages($name)
{
- $packages = array();
+ return $this->getRepos()->search($name);
+ }
- // init repos
+ protected function getRepos()
+ {
if (!$this->repos) {
$this->repos = new CompositeRepository(array_merge(
array(new PlatformRepository),
@@ -294,7 +299,7 @@ EOT
));
}
- return $this->repos->search($name);
+ return $this->repos;
}
protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array())
@@ -306,15 +311,18 @@ EOT
$requires = $this->normalizeRequirements($requires);
$result = array();
- foreach ($requires as $key => $requirement) {
- if (!isset($requirement['version']) && $input->isInteractive()) {
- $question = $dialog->getQuestion('Please provide a version constraint for the '.$requirement['name'].' requirement');
- if ($constraint = $dialog->ask($output, $question)) {
- $requirement['version'] = $constraint;
- }
- }
+ foreach ($requires as $requirement) {
if (!isset($requirement['version'])) {
- throw new \InvalidArgumentException('The requirement '.$requirement['name'].' must contain a version constraint');
+
+ // determine the best version automatically
+ $version = $this->findBestVersionForPackage($input, $requirement['name']);
+ $requirement['version'] = $version;
+
+ $output->writeln(sprintf(
+ 'Using version %s for %s',
+ $requirement['version'],
+ $requirement['name']
+ ));
}
$result[] = $requirement['name'] . ' ' . $requirement['version'];
@@ -369,7 +377,7 @@ EOT
$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
+ // no constraint yet, determine the best version automatically
if (false !== $package && false === strpos($package, ' ')) {
$validator = function ($input) {
$input = trim($input);
@@ -377,9 +385,20 @@ EOT
return $input ?: false;
};
- $constraint = $dialog->askAndValidate($output, $dialog->getQuestion('Enter the version constraint to require', false, ':'), $validator, 3);
+ $constraint = $dialog->askAndValidate(
+ $output,
+ $dialog->getQuestion('Enter the version constraint to require (or leave blank to use the latest version)', false, ':'),
+ $validator,
+ 3)
+ ;
if (false === $constraint) {
- continue;
+ $constraint = $this->findBestVersionForPackage($input, $package);
+
+ $output->writeln(sprintf(
+ 'Using version %s for %s',
+ $constraint,
+ $package
+ ));
}
$package .= ' '.$constraint;
@@ -504,4 +523,57 @@ EOT
return false !== filter_var($email, FILTER_VALIDATE_EMAIL);
}
+
+ private function getPool(InputInterface $input)
+ {
+ if (!$this->pool) {
+ $this->pool = new Pool($this->getMinimumStability($input));
+ $this->pool->addRepository($this->getRepos());
+ }
+
+ return $this->pool;
+ }
+
+ private function getMinimumStability(InputInterface $input)
+ {
+ if ($input->hasOption('stability')) {
+ return $input->getOption('stability') ?: 'stable';
+ }
+
+ $file = Factory::getComposerFile();
+ if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) {
+ if (!empty($composer['minimum-stability'])) {
+ return $composer['minimum-stability'];
+ }
+ }
+
+ return 'stable';
+ }
+
+ /**
+ * Given a package name, this determines the best version to use in the require key.
+ *
+ * This returns a version with the ~ operator prefixed when possible.
+ *
+ * @param InputInterface $input
+ * @param string $name
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ private function findBestVersionForPackage(InputInterface $input, $name)
+ {
+ // find the latest version allowed in this pool
+ $versionSelector = new VersionSelector($this->getPool($input));
+ $package = $versionSelector->findBestCandidate($name);
+
+ if (!$package) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Could not find package %s at any version for your minimum-stability (%s). Check the package spelling or your minimum-stability',
+ $name,
+ $this->getMinimumStability($input)
+ ));
+ }
+
+ return $versionSelector->findRecommendedRequireVersion($package);
+ }
}
diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php
index 636a78c0a..d0c660f82 100644
--- a/src/Composer/DependencyResolver/Pool.php
+++ b/src/Composer/DependencyResolver/Pool.php
@@ -15,7 +15,6 @@ namespace Composer\DependencyResolver;
use Composer\Package\BasePackage;
use Composer\Package\AliasPackage;
use Composer\Package\Version\VersionParser;
-use Composer\Package\Link;
use Composer\Package\LinkConstraint\LinkConstraintInterface;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\LinkConstraint\EmptyConstraint;
@@ -25,6 +24,7 @@ use Composer\Repository\ComposerRepository;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Repository\StreamableRepositoryInterface;
use Composer\Repository\PlatformRepository;
+use Composer\Package\PackageInterface;
/**
* A package pool contains repositories that provide packages.
@@ -232,7 +232,7 @@ class Pool
* packages must match or null to return all
* @param bool $mustMatchName Whether the name of returned packages
* must match the given name
- * @return array A set of packages
+ * @return PackageInterface[] A set of packages
*/
public function whatProvides($name, LinkConstraintInterface $constraint = null, $mustMatchName = false)
{
diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php
index 3176ece17..5191d8558 100644
--- a/src/Composer/Installer.php
+++ b/src/Composer/Installer.php
@@ -597,9 +597,19 @@ class Installer
continue;
}
- if ($package->getRequires() === array() && ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer')) {
- $installerOps[] = $op;
- unset($operations[$idx]);
+ if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') {
+ // ignore requirements to platform or composer-plugin-api
+ $requires = array_keys($package->getRequires());
+ foreach ($requires as $index => $req) {
+ if ($req === 'composer-plugin-api' || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) {
+ unset($requires[$index]);
+ }
+ }
+ // if there are no other requirements, move the plugin to the top of the op list
+ if (!count($requires)) {
+ $installerOps[] = $op;
+ unset($operations[$idx]);
+ }
}
}
diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php
index 812ccf5ad..3825426a8 100644
--- a/src/Composer/Package/Version/VersionParser.php
+++ b/src/Composer/Package/Version/VersionParser.php
@@ -122,6 +122,12 @@ class VersionParser
} elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)'.self::$modifierRegex.'$}i', $version, $matches)) { // match date-based versioning
$version = preg_replace('{\D}', '-', $matches[1]);
$index = 2;
+ } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?'.self::$modifierRegex.'$}i', $version, $matches)) {
+ $version = $matches[1]
+ .(!empty($matches[2]) ? $matches[2] : '.0')
+ .(!empty($matches[3]) ? $matches[3] : '.0')
+ .(!empty($matches[4]) ? $matches[4] : '.0');
+ $index = 5;
}
// add version modifiers if a version was matched
diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php
new file mode 100644
index 000000000..29358027f
--- /dev/null
+++ b/src/Composer/Package/Version/VersionSelector.php
@@ -0,0 +1,131 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Package\Version;
+
+use Composer\DependencyResolver\Pool;
+use Composer\Package\PackageInterface;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\Dumper\ArrayDumper;
+
+/**
+ * Selects the best possible version for a package
+ *
+ * @author Ryan Weaver
+ */
+class VersionSelector
+{
+ private $pool;
+
+ private $parser;
+
+ public function __construct(Pool $pool)
+ {
+ $this->pool = $pool;
+ }
+
+ /**
+ * Given a package name and optional version, returns the latest PackageInterface
+ * that matches.
+ *
+ * @param string $packageName
+ * @param string $targetPackageVersion
+ * @return PackageInterface|bool
+ */
+ public function findBestCandidate($packageName, $targetPackageVersion = null)
+ {
+ $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null;
+ $candidates = $this->pool->whatProvides($packageName, $constraint, true);
+
+ if (!$candidates) {
+ return false;
+ }
+
+ // select highest version if we have many
+ $package = reset($candidates);
+ foreach ($candidates as $candidate) {
+ if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) {
+ $package = $candidate;
+ }
+ }
+
+ return $package;
+ }
+
+ /**
+ * Given a concrete version, this returns a ~ constraint (when possible)
+ * that should be used, for example, in composer.json.
+ *
+ * For example:
+ * * 1.2.1 -> ~1.2
+ * * 1.2 -> ~1.2
+ * * v3.2.1 -> ~3.2
+ * * 2.0-beta.1 -> ~2.0@beta
+ * * dev-master -> ~2.1@dev (dev version with alias)
+ * * dev-master -> dev-master (dev versions are untouched)
+ *
+ * @param PackageInterface $package
+ * @return string
+ */
+ public function findRecommendedRequireVersion(PackageInterface $package)
+ {
+ $version = $package->getVersion();
+ if (!$package->isDev()) {
+ return $this->transformVersion($version, $package->getPrettyVersion(), $package->getStability());
+ }
+
+ $loader = new ArrayLoader($this->getParser());
+ $dumper = new ArrayDumper();
+ $extra = $loader->getBranchAlias($dumper->dump($package));
+ if ($extra) {
+ $extra = preg_replace('{^(\d+\.\d+\.\d+)(\.9999999)-dev$}', '$1.0', $extra, -1, $count);
+ if ($count) {
+ $extra = str_replace('.9999999', '.0', $extra);
+ return $this->transformVersion($extra, $extra, 'dev');
+ }
+ }
+
+ return $package->getPrettyVersion();
+ }
+
+ private function transformVersion($version, $prettyVersion, $stability)
+ {
+ // attempt to transform 2.1.1 to 2.1
+ // this allows you to upgrade through minor versions
+ $semanticVersionParts = explode('.', $version);
+ // check to see if we have a semver-looking version
+ if (count($semanticVersionParts) == 4 && preg_match('{^0\D?}', $semanticVersionParts[3])) {
+ // remove the last parts (i.e. the patch version number and any extra)
+ unset($semanticVersionParts[2], $semanticVersionParts[3]);
+ $version = implode('.', $semanticVersionParts);
+ } else {
+ return $prettyVersion;
+ }
+
+ // append stability flag if not default
+ if ($stability != 'stable') {
+ $version .= '@'.$stability;
+ }
+
+ // 2.1 -> ~2.1
+ return '~'.$version;
+ }
+
+ private function getParser()
+ {
+ if ($this->parser === null) {
+ $this->parser = new VersionParser();
+ }
+
+ return $this->parser;
+ }
+}
diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php
index f37be1e9a..20cfa93fc 100644
--- a/src/Composer/Repository/Vcs/GitDriver.php
+++ b/src/Composer/Repository/Vcs/GitDriver.php
@@ -56,16 +56,22 @@ class GitDriver extends VcsDriver
throw new \InvalidArgumentException('The source URL '.$this->url.' is invalid, ssh URLs should have a port number after ":".'."\n".'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.');
}
+ $gitUtil = new GitUtil($this->io, $this->config, $this->process, $fs);
+
// update the repo if it is a valid git repository
if (is_dir($this->repoDir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $this->repoDir) && trim($output) === '.') {
- if (0 !== $this->process->execute('git remote update --prune origin', $output, $this->repoDir)) {
- $this->io->write('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')');
+ try {
+ $commandCallable = function ($url) {
+ return sprintf('git remote set-url origin %s && git remote update --prune origin', escapeshellarg($url));
+ };
+ $gitUtil->runCommand($commandCallable, $this->url, $this->repoDir);
+ } catch (\Exception $e) {
+ $this->io->write('Failed to update '.$this->url.', package information from this repository may be outdated ('.$e->getMessage().')');
}
} else {
// clean up directory and do a fresh clone into it
$fs->removeDirectory($this->repoDir);
- $gitUtil = new GitUtil($this->io, $this->config, $this->process, $fs);
$repoDir = $this->repoDir;
$commandCallable = function ($url) use ($repoDir) {
return sprintf('git clone --mirror %s %s', escapeshellarg($url), escapeshellarg($repoDir));
diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php
index 182004205..6044c25d6 100644
--- a/src/Composer/Util/Filesystem.php
+++ b/src/Composer/Util/Filesystem.php
@@ -234,6 +234,13 @@ class Filesystem
*/
public function copyThenRemove($source, $target)
{
+ if (!is_dir($source)) {
+ copy($source, $target);
+ $this->unlink($source);
+
+ return;
+ }
+
$it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS);
$ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST);
$this->ensureDirectoryExists($target);
diff --git a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test
index c57a36d35..ad34e9c02 100644
--- a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test
+++ b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test
@@ -1,5 +1,5 @@
--TEST--
-Composer installers are installed first if they have no requirements
+Composer installers are installed first if they have no meaningful requirements
--COMPOSER--
{
"repositories": [
@@ -9,20 +9,23 @@ Composer installers are installed first if they have no requirements
{ "name": "pkg", "version": "1.0.0" },
{ "name": "pkg2", "version": "1.0.0" },
{ "name": "inst", "version": "1.0.0", "type": "composer-plugin" },
- { "name": "inst2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } }
+ { "name": "inst-with-req", "version": "1.0.0", "type": "composer-plugin", "require": { "php": ">=5", "ext-json": "*", "composer-plugin-api": "*" } },
+ { "name": "inst-with-req2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } }
]
}
],
"require": {
"pkg": "1.0.0",
"inst": "1.0.0",
- "inst2": "1.0.0"
+ "inst-with-req2": "1.0.0",
+ "inst-with-req": "1.0.0"
}
}
--RUN--
install
--EXPECT--
Installing inst (1.0.0)
+Installing inst-with-req (1.0.0)
Installing pkg (1.0.0)
Installing pkg2 (1.0.0)
-Installing inst2 (1.0.0)
+Installing inst-with-req2 (1.0.0)
diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php
index 464779d83..f54371d65 100644
--- a/tests/Composer/Test/Package/Version/VersionParserTest.php
+++ b/tests/Composer/Test/Package/Version/VersionParserTest.php
@@ -79,34 +79,35 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
public function successfulNormalizedVersions()
{
return array(
- 'none' => array('1.0.0', '1.0.0.0'),
- 'none/2' => array('1.2.3.4', '1.2.3.4'),
- 'parses state' => array('1.0.0RC1dev', '1.0.0.0-RC1-dev'),
- 'CI parsing' => array('1.0.0-rC15-dev', '1.0.0.0-RC15-dev'),
- 'delimiters' => array('1.0.0.RC.15-dev', '1.0.0.0-RC15-dev'),
- 'RC uppercase' => array('1.0.0-rc1', '1.0.0.0-RC1'),
- 'patch replace' => array('1.0.0.pl3-dev', '1.0.0.0-patch3-dev'),
- 'forces w.x.y.z' => array('1.0-dev', '1.0.0.0-dev'),
- 'forces w.x.y.z/2' => array('0', '0.0.0.0'),
- 'parses long' => array('10.4.13-beta', '10.4.13.0-beta'),
- 'expand shorthand' => array('10.4.13-b', '10.4.13.0-beta'),
- 'expand shorthand2' => array('10.4.13-b5', '10.4.13.0-beta5'),
- 'strips leading v' => array('v1.0.0', '1.0.0.0'),
- 'strips v/datetime' => array('v20100102', '20100102'),
- 'parses dates y-m' => array('2010.01', '2010-01'),
- 'parses dates w/ .' => array('2010.01.02', '2010-01-02'),
- 'parses dates w/ -' => array('2010-01-02', '2010-01-02'),
- 'parses numbers' => array('2010-01-02.5', '2010-01-02-5'),
- 'parses datetime' => array('20100102-203040', '20100102-203040'),
- 'parses dt+number' => array('20100102203040-10', '20100102203040-10'),
- 'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'),
- 'parses master' => array('dev-master', '9999999-dev'),
- 'parses trunk' => array('dev-trunk', '9999999-dev'),
- 'parses branches' => array('1.x-dev', '1.9999999.9999999.9999999-dev'),
- 'parses arbitrary' => array('dev-feature-foo', 'dev-feature-foo'),
- 'parses arbitrary2' => array('DEV-FOOBAR', 'dev-FOOBAR'),
- 'parses arbitrary3' => array('dev-feature/foo', 'dev-feature/foo'),
- 'ignores aliases' => array('dev-master as 1.0.0', '9999999-dev'),
+ 'none' => array('1.0.0', '1.0.0.0'),
+ 'none/2' => array('1.2.3.4', '1.2.3.4'),
+ 'parses state' => array('1.0.0RC1dev', '1.0.0.0-RC1-dev'),
+ 'CI parsing' => array('1.0.0-rC15-dev', '1.0.0.0-RC15-dev'),
+ 'delimiters' => array('1.0.0.RC.15-dev', '1.0.0.0-RC15-dev'),
+ 'RC uppercase' => array('1.0.0-rc1', '1.0.0.0-RC1'),
+ 'patch replace' => array('1.0.0.pl3-dev', '1.0.0.0-patch3-dev'),
+ 'forces w.x.y.z' => array('1.0-dev', '1.0.0.0-dev'),
+ 'forces w.x.y.z/2' => array('0', '0.0.0.0'),
+ 'parses long' => array('10.4.13-beta', '10.4.13.0-beta'),
+ 'expand shorthand' => array('10.4.13-b', '10.4.13.0-beta'),
+ 'expand shorthand2' => array('10.4.13-b5', '10.4.13.0-beta5'),
+ 'strips leading v' => array('v1.0.0', '1.0.0.0'),
+ 'strips v/datetime' => array('v20100102', '20100102'),
+ 'parses dates y-m' => array('2010.01', '2010-01'),
+ 'parses dates w/ .' => array('2010.01.02', '2010-01-02'),
+ 'parses dates w/ -' => array('2010-01-02', '2010-01-02'),
+ 'parses numbers' => array('2010-01-02.5', '2010-01-02-5'),
+ 'parses dates y.m.Y' => array('2010.1.555', '2010.1.555.0'),
+ 'parses datetime' => array('20100102-203040', '20100102-203040'),
+ 'parses dt+number' => array('20100102203040-10', '20100102203040-10'),
+ 'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'),
+ 'parses master' => array('dev-master', '9999999-dev'),
+ 'parses trunk' => array('dev-trunk', '9999999-dev'),
+ 'parses branches' => array('1.x-dev', '1.9999999.9999999.9999999-dev'),
+ 'parses arbitrary' => array('dev-feature-foo', 'dev-feature-foo'),
+ 'parses arbitrary2' => array('DEV-FOOBAR', 'dev-FOOBAR'),
+ 'parses arbitrary3' => array('dev-feature/foo', 'dev-feature/foo'),
+ 'ignores aliases' => array('dev-master as 1.0.0', '9999999-dev'),
);
}
diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php
new file mode 100644
index 000000000..2a9cb45a0
--- /dev/null
+++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php
@@ -0,0 +1,132 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Package\Version;
+
+use Composer\Package\Version\VersionSelector;
+use Composer\Package\Version\VersionParser;
+
+class VersionSelectorTest extends \PHPUnit_Framework_TestCase
+{
+ // A) multiple versions, get the latest one
+ // B) targetPackageVersion will pass to pool
+ // C) No results, throw exception
+
+ public function testLatestVersionIsReturned()
+ {
+ $packageName = 'foobar';
+
+ $package1 = $this->createMockPackage('1.2.1');
+ $package2 = $this->createMockPackage('1.2.2');
+ $package3 = $this->createMockPackage('1.2.0');
+ $packages = array($package1, $package2, $package3);
+
+ $pool = $this->createMockPool();
+ $pool->expects($this->once())
+ ->method('whatProvides')
+ ->with($packageName, null, true)
+ ->will($this->returnValue($packages));
+
+ $versionSelector = new VersionSelector($pool);
+ $best = $versionSelector->findBestCandidate($packageName);
+
+ // 1.2.2 should be returned because it's the latest of the returned versions
+ $this->assertEquals($package2, $best, 'Latest version should be 1.2.2');
+ }
+
+ public function testFalseReturnedOnNoPackages()
+ {
+ $pool = $this->createMockPool();
+ $pool->expects($this->once())
+ ->method('whatProvides')
+ ->will($this->returnValue(array()));
+
+ $versionSelector = new VersionSelector($pool);
+ $best = $versionSelector->findBestCandidate('foobaz');
+ $this->assertFalse($best, 'No versions are available returns false');
+ }
+
+ /**
+ * @dataProvider getRecommendedRequireVersionPackages
+ */
+ public function testFindRecommendedRequireVersion($prettyVersion, $isDev, $stability, $expectedVersion, $branchAlias = null)
+ {
+ $pool = $this->createMockPool();
+ $versionSelector = new VersionSelector($pool);
+ $versionParser = new VersionParser();
+
+ $package = $this->getMock('\Composer\Package\PackageInterface');
+ $package->expects($this->any())
+ ->method('getPrettyVersion')
+ ->will($this->returnValue($prettyVersion));
+ $package->expects($this->any())
+ ->method('getVersion')
+ ->will($this->returnValue($versionParser->normalize($prettyVersion)));
+ $package->expects($this->any())
+ ->method('isDev')
+ ->will($this->returnValue($isDev));
+ $package->expects($this->any())
+ ->method('getStability')
+ ->will($this->returnValue($stability));
+
+ $branchAlias = $branchAlias === null ? array() : array('branch-alias' => array($prettyVersion => $branchAlias));
+ $package->expects($this->any())
+ ->method('getExtra')
+ ->will($this->returnValue($branchAlias));
+
+ $recommended = $versionSelector->findRecommendedRequireVersion($package);
+
+ // assert that the recommended version is what we expect
+ $this->assertEquals($expectedVersion, $recommended);
+ }
+
+ public function getRecommendedRequireVersionPackages()
+ {
+ return array(
+ // real version, is dev package, stability, expected recommendation, [branch-alias]
+ array('1.2.1', false, 'stable', '~1.2'),
+ array('1.2', false, 'stable', '~1.2'),
+ array('v1.2.1', false, 'stable', '~1.2'),
+ array('3.1.2-pl2', false, 'stable', '~3.1'),
+ array('3.1.2-patch', false, 'stable', '~3.1'),
+ // for non-stable versions, we add ~, but don't try the (1.2.1 -> 1.2) transformation
+ array('2.0-beta.1', false, 'beta', '~2.0@beta'),
+ array('3.1.2-alpha5', false, 'alpha', '~3.1@alpha'),
+ array('3.0-RC2', false, 'RC', '~3.0@RC'),
+ // date-based versions are not touched at all
+ array('v20121020', false, 'stable', 'v20121020'),
+ array('v20121020.2', false, 'stable', 'v20121020.2'),
+ // dev packages without alias are not touched at all
+ array('dev-master', true, 'dev', 'dev-master'),
+ array('3.1.2-dev', true, 'dev', '3.1.2-dev'),
+ // dev packages with alias inherit the alias
+ array('dev-master', true, 'dev', '~2.1@dev', '2.1.x-dev'),
+ array('dev-master', true, 'dev', '~2.1@dev', '2.1.3.x-dev'),
+ array('dev-master', true, 'dev', '~2.0@dev', '2.x-dev'),
+ );
+ }
+
+ private function createMockPackage($version)
+ {
+ $package = $this->getMock('\Composer\Package\PackageInterface');
+ $package->expects($this->any())
+ ->method('getVersion')
+ ->will($this->returnValue($version));
+
+ return $package;
+ }
+
+ private function createMockPool()
+ {
+ return $this->getMock('Composer\DependencyResolver\Pool', array(), array(), '', true);
+ }
+}