Преглед изворни кода

Merge pull request #8717 from naderman/t/pool-builder-allow-list

Move processing of partial update argument list into the pool builder
Jordi Boggiano пре 5 година
родитељ
комит
379baa1560
37 измењених фајлова са 649 додато и 220 уклоњено
  1. 1 1
      doc/01-basic-usage.md
  2. 2 2
      doc/03-cli.md
  3. 11 11
      src/Composer/Cache.php
  4. 2 2
      src/Composer/Command/InitCommand.php
  5. 3 2
      src/Composer/Command/RemoveCommand.php
  6. 10 3
      src/Composer/Command/RequireCommand.php
  7. 12 5
      src/Composer/Command/UpdateCommand.php
  8. 18 0
      src/Composer/DependencyResolver/LockTransaction.php
  9. 143 6
      src/Composer/DependencyResolver/PoolBuilder.php
  10. 2 2
      src/Composer/DependencyResolver/Problem.php
  11. 47 1
      src/Composer/DependencyResolver/Request.php
  12. 22 167
      src/Composer/Installer.php
  13. 4 4
      src/Composer/Package/BasePackage.php
  14. 5 3
      src/Composer/Repository/RepositorySet.php
  15. 1 1
      tests/Composer/Test/DependencyResolver/PoolBuilderTest.php
  16. 3 2
      tests/Composer/Test/DependencyResolver/SolverTest.php
  17. 2 2
      tests/Composer/Test/Fixtures/installer/github-issues-4795.test
  18. 1 1
      tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test
  19. 1 1
      tests/Composer/Test/Fixtures/installer/solver-problems.test
  20. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test
  21. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test
  22. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test
  23. 1 1
      tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test
  24. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test
  25. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test
  26. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test
  27. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test
  28. 58 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test
  29. 99 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test
  30. 49 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test
  31. 50 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test
  32. 44 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test
  33. 48 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test
  34. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test
  35. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test
  36. 0 0
      tests/Composer/Test/Fixtures/installer/update-allow-list.test
  37. 10 3
      tests/Composer/Test/InstallerTest.php

+ 1 - 1
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 [...]

+ 2 - 2
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.

+ 11 - 11
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);
             }

+ 2 - 2
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']);

+ 3 - 2
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)

+ 10 - 3
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();

+ 12 - 5
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'))

+ 18 - 0
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;
+    }
 }

+ 143 - 6
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('<warning>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.</warning>');
+                }
             }
 
             $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('<warning>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.</warning>');
+                    }
+                }
+            }
+        }
+
         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('<warning>Pattern "' . $pattern . '" listed for update does not match any locked packages.</warning>');
+            } else {
+                $this->io->writeError('<warning>Package "' . $pattern . '" listed for update is not locked.</warning>');
+            }
+        }
+    }
+
+    /**
+     * 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]);
+    }
 }
 

+ 2 - 2
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.');
                 }
             }
 

+ 47 - 1
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;

+ 22 - 167
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('<error>Cannot update only a partial set of packages without a lock file present.</error>', true, IOInterface::QUIET);
                 return 1;
             }
-            $this->whitelistUpdateDependencies(
-                $lockedRepository,
-                $this->package->getRequires(),
-                $this->package->getDevRequires()
-            );
         }
 
         $this->io->writeError('<info>Loading composer repositories with package information</info>');
@@ -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('<warning>Package "' . $packageName . '" listed for update is not installed. Ignoring.</warning>');
-            }
-
-            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('<warning>Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.</warning>');
-                            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;
-
-        return $this;
-    }
+        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");
+        }
 
-    /**
-     * 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;
     }

+ 4 - 4
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);
     }
 }

+ 5 - 3
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());
     }
 }

+ 1 - 1
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);
         }

+ 3 - 2
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)

+ 2 - 2
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--
-<warning>Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring.</warning>
 Loading composer repositories with package information
 Updating dependencies
+<warning>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.</warning>
 Nothing to modify in lock file
 Writing lock file
 Installing dependencies from lock file (including require-dev)

+ 1 - 1
tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test → 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.

+ 1 - 1
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

+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test → tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test → tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test → tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test


+ 1 - 1
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test → 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)

+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test → tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test → tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test → tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test → tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test


+ 58 - 0
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
+<warning>Package "foo/bar" listed for update is not locked.</warning>
+<warning>Pattern "baz/*" listed for update does not match any locked packages.</warning>
+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)

+ 99 - 0
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)

+ 49 - 0
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)

+ 50 - 0
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)

+ 44 - 0
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)

+ 48 - 0
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)

+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test → tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test → tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test


+ 0 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist.test → tests/Composer/Test/Fixtures/installer/update-allow-list.test


+ 10 - 3
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'));