diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 4a3880581..aa44da021 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install, upgrade or remove one dependency, you can whitelist them: +If you only want to install, upgrade or remove one dependency, you can explicitly list it as an argument: ```sh php composer.phar update monolog/monolog [...] diff --git a/doc/03-cli.md b/doc/03-cli.md index e0ae7487a..725be430b 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -155,8 +155,8 @@ php composer.phar update "vendor/*" * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements. -* **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements. +* **--with-dependencies:** Update also dependencies of packages in the argument list, except those which are root requirements. +* **--with-all-dependencies:** Update also dependencies of packages in the argument list, including those which are root requirements. * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 06c6a0996..2a4e7756f 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -28,20 +28,20 @@ class Cache private $io; private $root; private $enabled = true; - private $whitelist; + private $allowlist; private $filesystem; /** * @param IOInterface $io * @param string $cacheDir location of the cache - * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) + * @param string $allowlist List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance */ - public function __construct(IOInterface $io, $cacheDir, $whitelist = 'a-z0-9.', Filesystem $filesystem = null) + public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; - $this->whitelist = $whitelist; + $this->allowlist = $allowlist; $this->filesystem = $filesystem ?: new Filesystem(); if (!self::isUsable($cacheDir)) { @@ -77,7 +77,7 @@ class Cache public function read($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); @@ -91,7 +91,7 @@ class Cache public function write($file, $contents) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); @@ -129,7 +129,7 @@ class Cache public function copyFrom($file, $source) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); if (!file_exists($source)) { @@ -150,7 +150,7 @@ class Cache public function copyTo($file, $target) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { try { touch($this->root . $file, filemtime($this->root . $file), time()); @@ -177,7 +177,7 @@ class Cache public function remove($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return $this->filesystem->unlink($this->root . $file); } @@ -229,7 +229,7 @@ class Cache public function sha1($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return sha1_file($this->root . $file); } @@ -241,7 +241,7 @@ class Cache public function sha256($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return hash_file('sha256', $this->root . $file); } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index d653ad61c..8f101515c 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -86,8 +86,8 @@ EOT { $io = $this->getIO(); - $whitelist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); - $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); + $allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist))); if (isset($options['author'])) { $options['authors'] = $this->formatAuthors($options['author']); diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index b95af9f72..7ae8abbe2 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Config\JsonConfigSource; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; @@ -179,8 +180,8 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($input->getOption('no-update-with-dependencies') ? Request::UPDATE_ONLY_LISTED : Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) ->setDryRun($dryRun) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 97fbfe5f2..357127925 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\DependencyResolver\Request; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -248,6 +249,13 @@ EOT $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('update-with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } elseif ($input->getOption('update-with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + } + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -264,8 +272,7 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies')) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) @@ -275,7 +282,7 @@ EOT // if no lock is present, or the file is brand new, we do not do a // partial update as this is not supported by the Installer if (!$this->firstRequire && $composer->getConfig()->get('lock')) { - $install->setUpdateWhitelist(array_keys($requirements)); + $install->setUpdateAllowList(array_keys($requirements)); } $status = $install->run(); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index a6a5dc4f7..840f7ae28 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Composer; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\IO\IOInterface; use Composer\Plugin\CommandEvent; @@ -48,8 +49,8 @@ class UpdateCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'), - new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'), + new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, except those which are root requirements.'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, including those which are root requirements.'), 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('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), @@ -145,6 +146,13 @@ EOT $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } elseif ($input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + } + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) @@ -158,9 +166,8 @@ EOT ->setApcuAutoloader($apcu) ->setUpdate(true) ->setUpdateMirrors($updateMirrors) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index b74cfbc78..dbbad01d6 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -112,4 +112,22 @@ class LockTransaction extends Transaction return $packages; } + + /** + * Checks which of the given aliases from composer.json are actually in use for the lock file + */ + public function getAliases($aliases) + { + $usedAliases = array(); + + foreach ($this->resultPackages['all'] as $package) { + if ($package instanceof AliasPackage) { + if (isset($aliases[$package->getName()])) { + $usedAliases[$package->getName()] = $aliases[$package->getName()]; + } + } + } + + return $usedAliases; + } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 718f340e7..305ad16b6 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -12,6 +12,7 @@ namespace Composer\DependencyResolver; +use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; @@ -36,24 +37,46 @@ class PoolBuilder private $rootAliases; private $rootReferences; private $eventDispatcher; + private $io; private $aliasMap = array(); private $nameConstraints = array(); private $loadedNames = array(); private $packages = array(); private $unacceptableFixedPackages = array(); + private $updateAllowList = array(); + private $skippedLoad = array(); + private $updateAllowWarned = array(); - public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, EventDispatcher $eventDispatcher = null) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; $this->eventDispatcher = $eventDispatcher; + $this->io = $io; } public function buildPool(array $repositories, Request $request) { + if ($request->getUpdateAllowList()) { + $this->updateAllowList = $request->getUpdateAllowList(); + $this->warnAboutNonMatchingUpdateAllowList($request); + + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { + if (!$this->isUpdateAllowed($lockedPackage)) { + $request->fixPackage($lockedPackage); + $lockedName = $lockedPackage->getName(); + // remember which packages we skipped loading remote content for in this partial update + $this->skippedLoad[$lockedPackage->getName()] = $lockedName; + foreach ($lockedPackage->getReplaces() as $link) { + $this->skippedLoad[$link->getTarget()] = $lockedName; + } + } + } + } + $loadNames = array(); foreach ($request->getFixedPackages() as $package) { $this->nameConstraints[$package->getName()] = null; @@ -73,7 +96,7 @@ class PoolBuilder || $package->getRepository() instanceof PlatformRepository || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) ) { - $loadNames += $this->loadPackage($request, $package); + $loadNames += $this->loadPackage($request, $package, false); } else { $this->unacceptableFixedPackages[] = $package; } @@ -108,7 +131,6 @@ class PoolBuilder if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) { continue; } - $result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags); foreach ($result['namesFound'] as $name) { @@ -177,9 +199,10 @@ class PoolBuilder return $pool; } - private function loadPackage(Request $request, PackageInterface $package) + private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true) { - $index = count($this->packages); + end($this->packages); + $index = key($this->packages) + 1; $this->packages[] = $package; if ($package instanceof AliasPackage) { @@ -198,7 +221,9 @@ class PoolBuilder } } - if (isset($this->rootAliases[$name][$package->getVersion()])) { + // if propogateUpdate is false we are loading a fixed package, root aliases do not apply as they are manually + // loaded as separate packages in this case + if ($propagateUpdate && isset($this->rootAliases[$name][$package->getVersion()])) { $alias = $this->rootAliases[$name][$package->getVersion()]; if ($package instanceof AliasPackage) { $basePackage = $package->getAliasOf(); @@ -217,6 +242,16 @@ class PoolBuilder $require = $link->getTarget(); if (!isset($this->loadedNames[$require])) { $loadNames[$require] = null; + // if this is a partial update with transitive dependencies we need to unfix the package we now know is a + // dependency of another package which we are trying to update, and then attempt to load it again + } elseif ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies() && isset($this->skippedLoad[$require])) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$require])) { + $this->unfixPackage($request, $require); + $loadNames[$require] = null; + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) { + $this->updateAllowWarned[$require] = true; + $this->io->writeError('Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.'); + } } $linkConstraint = $link->getConstraint(); @@ -233,7 +268,109 @@ class PoolBuilder } } + // if we're doing a partial update with deps and we're not loading an initial fixed package + // we also need to trigger an update for transitive deps which are being replaced + if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { + foreach ($package->getReplaces() as $link) { + $replace = $link->getTarget(); + if (isset($this->loadedNames[$replace]) && isset($this->skippedLoad[$replace])) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$replace])) { + $this->unfixPackage($request, $replace); + $loadNames[$replace] = null; + // TODO should we try to merge constraints here? + $this->nameConstraints[$replace] = null; + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require])) { + $this->updateAllowWarned[$replace] = true; + $this->io->writeError('Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.'); + } + } + } + } + return $loadNames; } + + /** + * Checks if a particular name is required directly in the request + * + * @return bool + */ + private function isRootRequire(Request $request, $name) + { + $rootRequires = $request->getRequires(); + return isset($rootRequires[$name]); + } + + /** + * Checks whether the update allow list allows this package in the lock file to be updated + * @return bool + */ + private function isUpdateAllowed(PackageInterface $package) + { + foreach ($this->updateAllowList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + if (preg_match($patternRegexp, $package->getName())) { + return true; + } + } + + return false; + } + + private function warnAboutNonMatchingUpdateAllowList(Request $request) + { + foreach ($this->updateAllowList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + // update pattern matches a locked package? => all good + foreach ($request->getLockedRepository()->getPackages() as $package) { + if (preg_match($patternRegexp, $package->getName())) { + continue 2; + } + } + // update pattern matches a root require? => all good, probably a new package + foreach ($request->getRequires() as $packageName => $constraint) { + if (preg_match($patternRegexp, $packageName)) { + continue 2; + } + } + if (strpos($pattern, '*') !== false) { + $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); + } else { + $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); + } + } + } + + /** + * Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies + * found that this package actually needs to be updated + */ + private function unfixPackage(Request $request, $name) + { + // remove locked package by this name which was already initialized + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { + if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) { + if (false !== $index = array_search($lockedPackage, $this->packages, true)) { + $request->unfixPackage($lockedPackage); + unset($this->packages[$index]); + if (isset($this->aliasMap[spl_object_hash($lockedPackage)])) { + foreach ($this->aliasMap[spl_object_hash($lockedPackage)] as $aliasIndex => $aliasPackage) { + $request->unfixPackage($aliasPackage); + unset($this->packages[$aliasIndex]); + } + unset($this->aliasMap[spl_object_hash($lockedPackage)]); + } + } + } + } + + // if we unfixed a replaced package name, we also need to unfix the replacer itself + if ($this->skippedLoad[$name] !== $name) { + $this->unfixPackage($request, $this->skippedLoad[$name]); + } + + unset($this->skippedLoad[$name]); + unset($this->loadedNames[$name]); + } } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index ad9989d19..a26cc47de 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -182,7 +182,7 @@ class Problem if ($package->getName() === $packageName) { $fixedPackage = $package; if ($pool->isUnacceptableFixedPackage($package)) { - return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update.'); + return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.'); } break; } @@ -207,7 +207,7 @@ class Problem return $fixedConstraint->matches(new Constraint('==', $p->getVersion())); }); if (0 === count($filtered)) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.'); } } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index d4f1b0523..5782c3ff1 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -23,10 +23,29 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class Request { + /** + * Identifies a partial update for listed packages only, all dependencies will remain at locked versions + */ + const UPDATE_ONLY_LISTED = 0; + + /** + * Identifies a partial update for listed packages and recursively all their dependencies, however dependencies + * also directly required by the root composer.json and their dependencies will remain at the locked version. + */ + const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE = 1; + + /** + * Identifies a partial update for listed packages and recursively all their dependencies, even dependencies + * also directly required by the root composer.json will be updated. + */ + const UPDATE_LISTED_WITH_TRANSITIVE_DEPS = 2; + protected $lockedRepository; protected $requires = array(); protected $fixedPackages = array(); protected $unlockables = array(); + protected $updateAllowList = array(); + protected $updateAllowTransitiveDependencies = false; public function __construct(LockArrayRepository $lockedRepository = null) { @@ -49,10 +68,37 @@ class Request $this->fixedPackages[spl_object_hash($package)] = $package; if (!$lockable) { - $this->unlockables[] = $package; + $this->unlockables[spl_object_hash($package)] = $package; } } + public function unfixPackage(PackageInterface $package) + { + unset($this->fixedPackages[spl_object_hash($package)]); + unset($this->unlockables[spl_object_hash($package)]); + } + + public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies) + { + $this->updateAllowList = $updateAllowList; + $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; + } + + public function getUpdateAllowList() + { + return $this->updateAllowList; + } + + public function getUpdateAllowTransitiveDependencies() + { + return $this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED; + } + + public function getUpdateAllowTransitiveRootDependencies() + { + return $this->updateAllowTransitiveDependencies === self::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } + public function getRequires() { return $this->requires; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 72273f11d..15cfb78e1 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -142,9 +142,8 @@ class Installer * @var array|null */ protected $updateMirrors = false; - protected $updateWhitelist = null; - protected $whitelistTransitiveDependencies = false; - protected $whitelistAllDependencies = false; + protected $updateAllowList = null; + protected $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; /** * @var SuggestedPackagesReporter @@ -199,8 +198,8 @@ class Installer gc_collect_cycles(); gc_disable(); - if ($this->updateWhitelist && $this->updateMirrors) { - throw new \RuntimeException("The installer options updateMirrors and updateWhitelist are mutually exclusive."); + if ($this->updateAllowList && $this->updateMirrors) { + throw new \RuntimeException("The installer options updateMirrors and updateAllowList are mutually exclusive."); } // Force update if there is no lock file present @@ -352,16 +351,11 @@ class Installer $lockedRepository = $this->locker->getLockedRepository(true); } - if ($this->updateWhitelist) { + if ($this->updateAllowList) { if (!$lockedRepository) { $this->io->writeError('Cannot update only a partial set of packages without a lock file present.', true, IOInterface::QUIET); return 1; } - $this->whitelistUpdateDependencies( - $lockedRepository, - $this->package->getRequires(), - $this->package->getDevRequires() - ); } $this->io->writeError('Loading composer repositories with package information'); @@ -394,17 +388,12 @@ class Installer } } - // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock - if ($this->updateWhitelist && $lockedRepository) { - foreach ($lockedRepository->getPackages() as $lockedPackage) { - if (!$this->isUpdateable($lockedPackage)) { - $request->fixPackage($lockedPackage); - } - } + // pass the allow list into the request, so the pool builder can apply it + if ($this->updateAllowList) { + $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -508,7 +497,7 @@ class Installer $lockTransaction->getNewLockPackages(true, $this->updateMirrors), $platformReqs, $platformDevReqs, - $aliases, + $lockTransaction->getAliases($aliases), $this->package->getMinimumStability(), $this->package->getStabilityFlags(), $this->preferStable || $this->package->getPreferStable(), @@ -623,7 +612,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -847,26 +836,6 @@ class Installer return $normalizedAliases; } - /** - * @param PackageInterface $package - * @return bool - */ - private function isUpdateable(PackageInterface $package) - { - if (!$this->updateWhitelist) { - throw new \LogicException('isUpdateable should only be called when a whitelist is present'); - } - - foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $patternRegexp = BasePackage::packageNameToRegexp($whiteListedPattern); - if (preg_match($patternRegexp, $package->getName())) { - return true; - } - } - - return false; - } - /** * @param array $links * @return array @@ -883,108 +852,6 @@ class Installer return $platformReqs; } - /** - * Adds all dependencies of the update whitelist to the whitelist, too. - * - * Packages which are listed as requirements in the root package will be - * skipped including their dependencies, unless they are listed in the - * update whitelist themselves or $whitelistAllDependencies is true. - * - * @param RepositoryInterface $lockRepo Use the locked repo - * As we want the most accurate package list to work with, and installed - * repo might be empty but locked repo will always be current. - * @param array $rootRequires An array of links to packages in require of the root package - * @param array $rootDevRequires An array of links to packages in require-dev of the root package - */ - private function whitelistUpdateDependencies($lockRepo, array $rootRequires, array $rootDevRequires) - { - $rootRequires = array_merge($rootRequires, $rootDevRequires); - - $skipPackages = array(); - if (!$this->whitelistAllDependencies) { - foreach ($rootRequires as $require) { - $skipPackages[$require->getTarget()] = true; - } - } - - $installedRepo = new InstalledRepository(array($lockRepo)); - - $seen = array(); - - $rootRequiredPackageNames = array_keys($rootRequires); - - foreach ($this->updateWhitelist as $packageName => $void) { - $packageQueue = new \SplQueue; - $nameMatchesRequiredPackage = false; - - $depPackages = $installedRepo->findPackagesWithReplacersAndProviders($packageName); - $matchesByPattern = array(); - - // check if the name is a glob pattern that did not match directly - if (empty($depPackages)) { - // add any installed package matching the whitelisted name/pattern - $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); - foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { - $matchesByPattern[] = $installedRepo->findPackages($installedPackage['name']); - } - - // add root requirements which match the whitelisted name/pattern - $whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName); - foreach ($rootRequiredPackageNames as $rootRequiredPackageName) { - if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) { - $nameMatchesRequiredPackage = true; - break; - } - } - } - - if (!empty($matchesByPattern)) { - $depPackages = array_merge($depPackages, call_user_func_array('array_merge', $matchesByPattern)); - } - - if (count($depPackages) == 0 && !$nameMatchesRequiredPackage) { - $this->io->writeError('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); - } - - foreach ($depPackages as $depPackage) { - $packageQueue->enqueue($depPackage); - } - - while (!$packageQueue->isEmpty()) { - $package = $packageQueue->dequeue(); - if (isset($seen[spl_object_hash($package)])) { - continue; - } - - $seen[spl_object_hash($package)] = true; - $this->updateWhitelist[$package->getName()] = true; - - if (!$this->whitelistTransitiveDependencies && !$this->whitelistAllDependencies) { - continue; - } - - $requires = $package->getRequires(); - - foreach ($requires as $require) { - $requirePackages = $installedRepo->findPackagesWithReplacersAndProviders($require->getTarget()); - - foreach ($requirePackages as $requirePackage) { - if (isset($this->updateWhitelist[$requirePackage->getName()])) { - continue; - } - - if (isset($skipPackages[$requirePackage->getName()]) && !preg_match(BasePackage::packageNameToRegexp($packageName), $requirePackage->getName())) { - $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.'); - continue; - } - - $packageQueue->enqueue($requirePackage); - } - } - } - } - } - /** * Replace local repositories with InstalledArrayRepository instances * @@ -1265,41 +1132,29 @@ class Installer * @param array $packages * @return Installer */ - public function setUpdateWhitelist(array $packages) + public function setUpdateAllowList(array $packages) { - $this->updateWhitelist = array_flip(array_map('strtolower', $packages)); + $this->updateAllowList = array_flip(array_map('strtolower', $packages)); return $this; } /** - * Should dependencies of whitelisted packages (but not direct dependencies) be updated? + * Should dependencies of packages marked for update be updated? * - * This will NOT whitelist any dependencies that are also directly defined - * in the root package. + * Depending on the chosen constant this will either only update the directly named packages, all transitive + * dependencies which are not root requirement or all transitive dependencies including root requirements * - * @param bool $updateTransitiveDependencies + * @param int $updateAllowTransitiveDependencies One of the UPDATE_ constants on the Request class * @return Installer */ - public function setWhitelistTransitiveDependencies($updateTransitiveDependencies = true) + public function setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) { - $this->whitelistTransitiveDependencies = (bool) $updateTransitiveDependencies; + if (!in_array($updateAllowTransitiveDependencies, array(Request::UPDATE_ONLY_LISTED, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS), true)) { + throw new \RuntimeException("Invalid value for updateAllowTransitiveDependencies supplied"); + } - return $this; - } - - /** - * Should all dependencies of whitelisted packages be updated recursively? - * - * This will whitelist any dependencies of the whitelisted packages, including - * those defined in the root package. - * - * @param bool $updateAllDependencies - * @return Installer - */ - public function setWhitelistAllDependencies($updateAllDependencies = true) - { - $this->whitelistAllDependencies = (bool) $updateAllDependencies; + $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; return $this; } diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 0c40a3016..9ecbcb332 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -250,14 +250,14 @@ abstract class BasePackage implements PackageInterface /** * Build a regexp from a package name, expanding * globs as required * - * @param string $whiteListedPattern + * @param string $allowPattern * @param string $wrap Wrap the cleaned string by the given string * @return string */ - public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i') + public static function packageNameToRegexp($allowPattern, $wrap = '{^%s$}i') { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + $cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern)); - return sprintf($wrap, $cleanedWhiteListedPattern); + return sprintf($wrap, $cleanedAllowPattern); } } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index a2efbdc67..b0489e99b 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -16,6 +16,8 @@ use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\PoolBuilder; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\EventDispatcher; +use Composer\IO\IOInterface; +use Composer\IO\NullIO; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; @@ -185,9 +187,9 @@ class RepositorySet * * @return Pool */ - public function createPool(Request $request, EventDispatcher $eventDispatcher = null) + public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher); foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { @@ -236,6 +238,6 @@ class RepositorySet $request->requireName($packageName); } - return $this->createPool($request); + return $this->createPool($request, new NullIO()); } } diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php index c55431b6e..f51661db6 100644 --- a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -89,7 +89,7 @@ class PoolBuilderTest extends TestCase $request->fixPackage($loadPackage($fixedPackage)); } - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request, new NullIO()); for ($i = 1, $count = count($pool); $i <= $count; $i++) { $result[] = $pool->packageById($i); } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 46d882e9f..3090d9d83 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -890,8 +890,9 @@ class SolverTest extends TestCase protected function createSolver() { - $this->pool = $this->repoSet->createPool($this->request); - $this->solver = new Solver($this->policy, $this->pool, new NullIO()); + $io = new NullIO(); + $this->pool = $this->repoSet->createPool($this->request, $io); + $this->solver = new Solver($this->policy, $this->pool, $io); } protected function checkSolverResult(array $expected) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 8e5d17dfe..dc722c379 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -14,7 +14,7 @@ dependency of one the requirements that is whitelisted for update. { "name": "a/a", "version": "1.0.0" }, { "name": "a/a", "version": "1.1.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }, - { "name": "b/b", "version": "1.1.0", "require": { "a/b": "~1.1" } } + { "name": "b/b", "version": "1.1.0", "require": { "a/a": "~1.1" } } ] } ], @@ -49,9 +49,9 @@ dependency of one the requirements that is whitelisted for update. update b/b --with-dependencies --EXPECT-OUTPUT-- -Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information Updating dependencies +Dependency "a/a" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies. Nothing to modify in lock file Writing lock file Installing dependencies from lock file (including require-dev) 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-allow-listed-unstable.test similarity index 96% rename from tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test rename to tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test index 8e0b18e05..25bd4a9c6 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test @@ -59,4 +59,4 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update. + - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command. diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 005047127..9c3d0e534 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -117,7 +117,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 3 - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 4 - - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. Problem 5 - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. Problem 6 diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test similarity index 98% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test index 02f544577..55a07b118 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test @@ -70,7 +70,7 @@ Update with a package whitelist only updates those packages and their dependenci "platform-dev": [] } --RUN-- -update whitelisted/pkg-* --with-dependencies +update whitelisted/pkg-* foobar --with-dependencies --EXPECT-- Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test new file mode 100644 index 000000000..d4d258112 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test @@ -0,0 +1,58 @@ +--TEST-- +Verify that partial updates warn about using patterns in the argument which have no matches +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" }, + { "name": "b/b", "version": "1.1.0" } + ] + } + ], + "require": { + "a/a": "~1.0", + "b/b": "~1.0" + } +} + +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } +] + +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update b/b foo/bar baz/* --with-dependencies + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Package "foo/bar" listed for update is not locked. +Pattern "baz/*" listed for update does not match any locked packages. +Lock file operations: 0 installs, 1 update, 0 removals + - Upgrading b/b (1.0.0 => 1.1.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 1 update, 0 removals +Generating autoload files + +--EXPECT-- +Upgrading b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test new file mode 100644 index 000000000..2b68c6c69 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -0,0 +1,99 @@ +--TEST-- +Verify that a partial update with deps correctly keeps track of all aliases. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.1.0", "require": {"current/dep2": "*"} }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "2.x-dev"}}}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "^1.1"} }, + { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "current/dep": "dev-master as 1.1.0", + "current/dep2": "dev-master as 1.1.2", + "current/pkg": "1.0.0 as 2.0.0", + "new/pkg": "1.*" + }, + "minimum-stability": "dev" +} +--INSTALLED-- +[ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } +] +--LOCK-- +{ + "packages": [ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } + ], + "packages-dev": [], + "aliases": [ + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep" + } + ], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-all-dependencies +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "2.x-dev"}}, "type": "library"}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" }, "type": "library"}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "^1.1"}, "type": "library"} + ], + "packages-dev": [], + "aliases": [ + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep" + }, + { + "alias": "1.1.2", + "alias_normalized": "1.1.2.0", + "version": "dev-master", + "package": "current/dep2" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "current/dep": 20, + "current/dep2": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Marking current/dep (1.1.0) as installed, alias of current/dep (dev-master) +Upgrading current/dep2 (dev-foo => dev-master) +Marking current/dep2 (1.1.2) as installed, alias of current/dep2 (dev-master) +Marking current/dep2 (2.x-dev) as installed, alias of current/dep2 (dev-master) +Installing new/pkg (1.0.0) +Marking current/dep2 (1.0.x-dev) as uninstalled, alias of current/dep2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test new file mode 100644 index 000000000..ff1257498 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test @@ -0,0 +1,49 @@ +--TEST-- +When partially updating a package to a newer version and the new version has a new requirement for a package we already have installed, mark it for update +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "root/pkg1", "version": "1.2.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "root/pkg2", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.2.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "root/pkg1": "1.*", + "root/pkg2": "1.*" + } +} +--INSTALLED-- +[ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update root/pkg2 --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.2.0) +Upgrading root/pkg2 (1.0.0 => 1.2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test new file mode 100644 index 000000000..0cb5ad97f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test @@ -0,0 +1,50 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*", "mutual/target-provide": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } }, + { "name": "new/pkg-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*", + "new/pkg-provide": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Removing current/dep (1.0.0) +Installing new/pkg (1.0.0) +Installing new/pkg-provide (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test new file mode 100644 index 000000000..8bb676e76 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test @@ -0,0 +1,44 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "current/dep": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Removing current/dep (1.0.0) +Installing new/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test new file mode 100644 index 000000000..24eb95538 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test @@ -0,0 +1,48 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should update locked dependencies as far as possible +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/pkg", "version": "1.1.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.1.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1" } }, + { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.1.0) +Installing new/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-allow-list.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list.test diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 71a955748..3c6c35de6 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -12,6 +12,7 @@ namespace Composer\Test; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\Console\Application; use Composer\IO\BufferIO; @@ -279,14 +280,20 @@ class InstallerTest extends TestCase $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); $packages = $filteredPackages; + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } elseif ($input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + } + $installer ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) ->setUpdateMirrors($updateMirrors) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'));