diff --git a/doc/03-cli.md b/doc/03-cli.md index 60599e5cd..26e70f54a 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -140,7 +140,9 @@ php composer.phar update vendor/* * **--lock:** Only updates the lock file hash to suppress warning about the lock file being out of date. * **--with-dependencies** Add also all dependencies of whitelisted packages to the whitelist. - So all packages with their dependencies are updated recursively. +* **--prefer-stable:** Prefer stable versions of dependencies. +* **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal + versions of requirements, generally used with `--prefer-stable`. ## require diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index ebfd4cb2a..5e83658c9 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -47,6 +47,8 @@ class UpdateCommand extends Command new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), + new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), )) ->setHelp(<<update command reads the composer.json file from the @@ -121,6 +123,8 @@ EOT ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $input->getArgument('packages')) ->setWhitelistDependencies($input->getOption('with-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ->setPreferStable($input->getOption('prefer-stable')) + ->setPreferLowest($input->getOption('prefer-lowest')) ; if ($input->getOption('no-plugins')) { diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 10cbe101e..5ae6b2c31 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -24,10 +24,12 @@ use Composer\Package\LinkConstraint\VersionConstraint; class DefaultPolicy implements PolicyInterface { private $preferStable; + private $preferLowest; - public function __construct($preferStable = false) + public function __construct($preferStable = false, $preferLowest = false) { $this->preferStable = $preferStable; + $this->preferLowest = $preferLowest; } public function versionCompare(PackageInterface $a, PackageInterface $b, $operator) @@ -195,6 +197,7 @@ class DefaultPolicy implements PolicyInterface protected function pruneToBestVersion(Pool $pool, $literals) { + $operator = $this->preferLowest ? '<' : '>'; $bestLiterals = array($literals[0]); $bestPackage = $pool->literalToPackage($literals[0]); foreach ($literals as $i => $literal) { @@ -204,7 +207,7 @@ class DefaultPolicy implements PolicyInterface $package = $pool->literalToPackage($literal); - if ($this->versionCompare($package, $bestPackage, '>')) { + if ($this->versionCompare($package, $bestPackage, $operator)) { $bestPackage = $package; $bestLiterals = array($literal); } elseif ($this->versionCompare($package, $bestPackage, '==')) { diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9e2c07021..35df7e61b 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -107,6 +107,8 @@ class Installer protected $update = false; protected $runScripts = true; protected $ignorePlatformReqs = false; + protected $preferStable = false; + protected $preferLowest = false; /** * Array of package names/globs flagged for update * @@ -307,7 +309,8 @@ class Installer $aliases, $this->package->getMinimumStability(), $this->package->getStabilityFlags(), - $this->package->getPreferStable() + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest ); if ($updatedLock) { $this->io->write('Writing lock file'); @@ -694,16 +697,21 @@ class Installer private function createPolicy() { $preferStable = null; + $preferLowest = null; if (!$this->update && $this->locker->isLocked()) { $preferStable = $this->locker->getPreferStable(); + $preferLowest = $this->locker->getPreferLowest(); } - // old lock file without prefer stable will return null + // old lock file without prefer stable/lowest will return null // so in this case we use the composer.json info if (null === $preferStable) { - $preferStable = $this->package->getPreferStable(); + $preferStable = $this->preferStable || $this->package->getPreferStable(); + } + if (null === $preferLowest) { + $preferLowest = $this->preferLowest; } - return new DefaultPolicy($preferStable); + return new DefaultPolicy($preferStable, $preferLowest); } private function createRequest(Pool $pool, RootPackageInterface $rootPackage, PlatformRepository $platformRepo) @@ -1244,6 +1252,32 @@ class Installer return $this; } + /** + * Should packages be prefered in a stable version when updating? + * + * @param boolean $preferStable + * @return Installer + */ + public function setPreferStable($preferStable = true) + { + $this->preferStable = (boolean) $preferStable; + + return $this; + } + + /** + * Should packages be prefered in a lowest version when updating? + * + * @param boolean $preferLowest + * @return Installer + */ + public function setPreferLowest($preferLowest = true) + { + $this->preferLowest = (boolean) $preferLowest; + + return $this; + } + /** * Disables plugins. * diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 9d49f6eaf..40ff8907c 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -182,6 +182,15 @@ class Locker return isset($lockData['prefer-stable']) ? $lockData['prefer-stable'] : null; } + public function getPreferLowest() + { + $lockData = $this->getLockData(); + + // return null if not set to allow caller logic to choose the + // right behavior since old lock files have no prefer-lowest + return isset($lockData['prefer-lowest']) ? $lockData['prefer-lowest'] : null; + } + public function getAliases() { $lockData = $this->getLockData(); @@ -213,10 +222,11 @@ class Locker * @param string $minimumStability * @param array $stabilityFlags * @param bool $preferStable + * @param bool $preferLowest * * @return bool */ - public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest) { $lock = array( '_readme' => array('This file locks the dependencies of your project to a known state', @@ -229,6 +239,7 @@ class Locker 'minimum-stability' => $minimumStability, 'stability-flags' => $stabilityFlags, 'prefer-stable' => $preferStable, + 'prefer-lowest' => $preferLowest, ); foreach ($aliases as $package => $versions) { diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 9e9952228..f06ba4437 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -247,4 +247,20 @@ class DefaultPolicyTest extends TestCase return $map; } + + public function testSelectLowest() + { + $policy = new DefaultPolicy(false, true); + + $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0')); + $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); + $this->pool->addRepository($this->repo); + + $literals = array($packageA1->getId(), $packageA2->getId()); + $expected = array($packageA1->getId()); + + $selected = $policy->selectPreferedPackages($this->pool, array(), $literals); + + $this->assertEquals($expected, $selected); + } } diff --git a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test index af4eed811..ccc9ead1a 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -48,6 +48,7 @@ install --prefer-dist "a/a": 20 }, "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test index 88c3e8fa7..7bb69f131 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -25,7 +25,8 @@ Requirements from the composer file are not installed if the lock file is presen "aliases": [], "minimum-stability": "stable", "stability-flags": [], - "prefer-stable": false + "prefer-stable": false, + "prefer-lowest": false } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test index 298846609..5acb7a069 100644 --- a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test @@ -33,7 +33,8 @@ Installing an old alias that doesn't exist anymore from a lock is possible "aliases": [], "minimum-stability": "dev", "stability-flags": [], - "prefer-stable": false + "prefer-stable": false, + "prefer-lowest": false } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index f9fd5058a..4b2c62a53 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -36,6 +36,7 @@ Partial update from lock file should apply lock file and downgrade unstable pack "b/unstable": 15 }, "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } @@ -59,6 +60,7 @@ update c/uptodate "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index 5b904f9b5..5c3508840 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -36,6 +36,7 @@ Partial update from lock file should update everything to the state of the lock, "b/unstable": 15 }, "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } @@ -59,6 +60,7 @@ update b/unstable "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index 224e58f7d..e931a1f7d 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -43,6 +43,7 @@ update b/unstable "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index bfe0bb8a8..920321a20 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -39,7 +39,8 @@ Update aliased package does not mess up the lock file "aliases": [], "minimum-stability": "dev", "stability-flags": [], - "prefer-stable": false + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ @@ -66,6 +67,7 @@ update "minimum-stability": "dev", "stability-flags": [], "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test new file mode 100644 index 000000000..00efd5688 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test @@ -0,0 +1,40 @@ +--TEST-- +Updates packages to their lowest stable version +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0-rc1" }, + { "name": "a/a", "version": "1.0.1" }, + { "name": "a/a", "version": "1.1.0" }, + + { "name": "a/b", "version": "1.0.0" }, + { "name": "a/b", "version": "1.0.1" }, + { "name": "a/b", "version": "2.0.0" }, + + { "name": "a/c", "version": "1.0.0" }, + { "name": "a/c", "version": "2.0.0" } + ] + } + ], + "require": { + "a/a": "~1.0@dev", + "a/c": "2.*" + }, + "require-dev": { + "a/b": "*" + } +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0-rc1" }, + { "name": "a/c", "version": "2.0.0" }, + { "name": "a/b", "version": "1.0.1" } +] +--RUN-- +update --prefer-lowest --prefer-stable +--EXPECT-- +Updating a/a (1.0.0-rc1) to a/a (1.0.1) +Updating a/b (1.0.1) to a/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test index 3bc189015..96036e479 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -32,7 +32,8 @@ Limited update takes rules from lock if available, and not from the installed re "aliases": [], "minimum-stability": "stable", "stability-flags": [], - "prefer-stable": false + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index bd94617bc..04624561d 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -20,7 +20,8 @@ Installing locked dev packages should remove old dependencies "aliases": [], "minimum-stability": "dev", "stability-flags": [], - "prefer-stable": false + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index 849296850..c5c838517 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -32,7 +32,8 @@ Updating a dev package for new reference updates the url and reference "aliases": [], "minimum-stability": "dev", "stability-flags": {"a/a":20}, - "prefer-stable": false + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ @@ -59,6 +60,7 @@ update "minimum-stability": "dev", "stability-flags": {"a/a":20}, "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 4dc0bf511..9bf2c01a7 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -217,6 +217,8 @@ class InstallerTest extends TestCase ->setDryRun($input->getOption('dry-run')) ->setUpdateWhitelist($input->getArgument('packages')) ->setWhitelistDependencies($input->getOption('with-dependencies')) + ->setPreferStable($input->getOption('prefer-stable')) + ->setPreferLowest($input->getOption('prefer-lowest')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); return $installer->run(); diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 7e1d2d3b1..913868cd1 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -135,9 +135,10 @@ class LockerTest extends \PHPUnit_Framework_TestCase 'platform' => array(), 'platform-dev' => array(), 'prefer-stable' => false, + 'prefer-lowest' => false, )); - $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array(), false); + $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array(), false, false); } public function testLockBadPackages() @@ -156,7 +157,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $this->setExpectedException('LogicException'); - $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array(), false); + $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array(), false, false); } public function testIsFresh()