瀏覽代碼

Merge branch 'master' into 2.0

Jordi Boggiano 6 年之前
父節點
當前提交
b89720b52a

+ 3 - 1
doc/03-cli.md

@@ -22,6 +22,8 @@ The following options are available with every command:
 * **--quiet (-q):** Do not output any message.
 * **--no-interaction (-n):** Do not ask any interactive question.
 * **--no-plugins:** Disables plugins.
+* **--no-cache:** Disables the use of the cache directory. Same as setting the COMPOSER_CACHE_DIR
+  env var to /dev/null (or NUL on Windows).
 * **--working-dir (-d):** If specified, use the given directory as working directory.
 * **--profile:** Display timing and memory usage information
 * **--ansi:** Force ANSI output.
@@ -491,7 +493,7 @@ php composer.phar validate
 
 ### Options
 
-* **--no-check-all:** Do not emit a warning if requirements in `composer.json` use unbound version constraints.
+* **--no-check-all:** Do not emit a warning if requirements in `composer.json` use unbound or overly strict version constraints.
 * **--no-check-lock:** Do not emit an error if `composer.lock` exists and is not up to date.
 * **--no-check-publish:** Do not emit an error if `composer.json` is unsuitable for publishing as a package on Packagist but is otherwise valid.
 * **--with-dependencies:** Also validate the composer.json of all installed dependencies.

+ 6 - 2
src/Composer/Autoload/AutoloadGenerator.php

@@ -940,9 +940,13 @@ INITIALIZER;
             $packageMap,
             function ($item) use ($include) {
                 $package = $item[0];
-                $name = $package->getName();
+                foreach ($package->getNames() as $name) {
+                    if (isset($include[$name])) {
+                        return true;
+                    }
+                }
 
-                return isset($include[$name]);
+                return false;
             }
         );
     }

+ 6 - 1
src/Composer/Cache.php

@@ -44,7 +44,7 @@ class Cache
         $this->whitelist = $whitelist;
         $this->filesystem = $filesystem ?: new Filesystem();
 
-        if (preg_match('{(^|[\\\\/])(\$null|NUL|/dev/null)([\\\\/]|$)}', $cacheDir)) {
+        if (!self::isUsable($cacheDir)) {
             $this->enabled = false;
 
             return;
@@ -59,6 +59,11 @@ class Cache
         }
     }
 
+    public static function isUsable($path)
+    {
+        return !preg_match('{(^|[\\\\/])(\$null|nul|NUL|/dev/null)([\\\\/]|$)}', $path);
+    }
+
     public function isEnabled()
     {
         return $this->enabled;

+ 6 - 1
src/Composer/Command/InitCommand.php

@@ -557,7 +557,12 @@ EOT
         $finder = new ExecutableFinder();
         $gitBin = $finder->find('git');
 
-        $cmd = new Process(sprintf('%s config -l', ProcessExecutor::escape($gitBin)));
+        // TODO in v3 always call with an array
+        if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
+            $cmd = new Process(array($gitBin, 'config', '-l'));
+        } else {
+            $cmd = new Process(sprintf('%s config -l', ProcessExecutor::escape($gitBin)));
+        }
         $cmd->run();
 
         if ($cmd->isSuccessful()) {

+ 1 - 1
src/Composer/Command/ValidateCommand.php

@@ -39,7 +39,7 @@ class ValidateCommand extends BaseCommand
             ->setName('validate')
             ->setDescription('Validates a composer.json and composer.lock.')
             ->setDefinition(array(
-                new InputOption('no-check-all', null, InputOption::VALUE_NONE, 'Do not make a complete validation'),
+                new InputOption('no-check-all', null, InputOption::VALUE_NONE, 'Do not validate requires for overly strict/loose constraints'),
                 new InputOption('no-check-lock', null, InputOption::VALUE_NONE, 'Do not check if lock file is up to date'),
                 new InputOption('no-check-publish', null, InputOption::VALUE_NONE, 'Do not check for publish errors'),
                 new InputOption('with-dependencies', 'A', InputOption::VALUE_NONE, 'Also validate the composer.json of all installed dependencies'),

+ 4 - 0
src/Composer/Config/JsonConfigSource.php

@@ -193,6 +193,10 @@ class JsonConfigSource implements ConfigSourceInterface
     {
         $this->manipulateJson('removeSubNode', $type, $name, function (&$config, $type, $name) {
             unset($config[$type][$name]);
+
+            if (0 === count($config[$type])) {
+                unset($config[$type]);
+            }
         });
     }
 

+ 7 - 1
src/Composer/Console/Application.php

@@ -118,6 +118,11 @@ class Application extends BaseApplication
         )));
         ErrorHandler::register($io);
 
+        if ($input->hasParameterOption('--no-cache')) {
+            $io->writeError('Disabling cache usage', true, IOInterface::DEBUG);
+            putenv('COMPOSER_CACHE_DIR='.(Platform::isWindows() ? 'nul' : '/dev/null'));
+        }
+
         // switch working dir
         if ($newWorkDir = $this->getNewWorkingDir($input)) {
             $oldWorkingDir = getcwd();
@@ -272,7 +277,7 @@ class Application extends BaseApplication
             }
 
             if (isset($startTime)) {
-                $io->writeError('<info>Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MB), time: '.round(microtime(true) - $startTime, 2).'s');
+                $io->writeError('<info>Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s');
             }
 
             restore_error_handler();
@@ -457,6 +462,7 @@ class Application extends BaseApplication
         $definition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Display timing and memory usage information'));
         $definition->addOption(new InputOption('--no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.'));
         $definition->addOption(new InputOption('--working-dir', '-d', InputOption::VALUE_REQUIRED, 'If specified, use the given directory as working directory.'));
+        $definition->addOption(new InputOption('--no-cache', null, InputOption::VALUE_NONE, 'Prevent use of the cache'));
 
         return $definition;
     }

+ 2 - 1
src/Composer/Downloader/GitDownloader.php

@@ -19,6 +19,7 @@ use Composer\Util\Filesystem;
 use Composer\Util\Git as GitUtil;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
+use Composer\Cache;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -51,7 +52,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
         $msg = "Cloning ".$this->getShortHash($ref);
 
         $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer';
-        if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=')) {
+        if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) {
             $this->io->writeError('', true, IOInterface::DEBUG);
             $this->io->writeError(sprintf('    Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG);
             try {

+ 1 - 1
src/Composer/IO/ConsoleIO.php

@@ -153,7 +153,7 @@ class ConsoleIO extends BaseIO
             $memoryUsage = memory_get_usage() / 1024 / 1024;
             $timeSpent = microtime(true) - $this->startTime;
             $messages = array_map(function ($message) use ($memoryUsage, $timeSpent) {
-                return sprintf('[%.1fMB/%.2fs] %s', $memoryUsage, $timeSpent, $message);
+                return sprintf('[%.1fMiB/%.2fs] %s', $memoryUsage, $timeSpent, $message);
             }, (array) $messages);
         }
 

+ 14 - 9
src/Composer/Installer.php

@@ -1294,11 +1294,6 @@ class Installer
 
         $rootRequires = array_merge($rootRequires, $rootDevRequires);
 
-        $requiredPackageNames = array();
-        foreach ($rootRequires as $require) {
-            $requiredPackageNames[] = $require->getTarget();
-        }
-
         $skipPackages = array();
         if (!$this->whitelistAllDependencies) {
             foreach ($rootRequires as $require) {
@@ -1317,11 +1312,17 @@ class Installer
             $packageQueue = new \SplQueue;
 
             $depPackages = $repositorySet->findPackages($packageName, null, false);
-
-            $nameMatchesRequiredPackage = in_array($packageName, $requiredPackageNames, true);
+            $matchesByPattern = array();
 
             // check if the name is a glob pattern that did not match directly
-            if (!$nameMatchesRequiredPackage) {
+            if (empty($depPackages)) {
+                // add any installed package matching the whitelisted name/pattern
+                $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$');
+                foreach ($localOrLockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) {
+                    $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, false);
+                }
+
+                // add root requirements which match the whitelisted name/pattern
                 $whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName);
                 foreach ($rootRequiredPackageNames as $rootRequiredPackageName) {
                     if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) {
@@ -1331,6 +1332,10 @@ class Installer
                 }
             }
 
+            if (!empty($matchesByPattern)) {
+                $depPackages = array_merge($depPackages, call_user_func_array('array_merge', $matchesByPattern));
+            }
+
             if (count($depPackages) == 0 && !$nameMatchesRequiredPackage && !in_array($packageName, array('nothing', 'lock', 'mirrors'))) {
                 $this->io->writeError('<warning>Package "' . $packageName . '" listed for update is not installed. Ignoring.</warning>');
             }
@@ -1362,7 +1367,7 @@ class Installer
                             continue;
                         }
 
-                        if (isset($skipPackages[$requirePackage->getName()])) {
+                        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;
                         }

+ 3 - 2
src/Composer/Package/BasePackage.php

@@ -239,12 +239,13 @@ abstract class BasePackage implements PackageInterface
      * Build a regexp from a package name, expanding * globs as required
      *
      * @param  string $whiteListedPattern
+     * @param  bool $wrap Wrap the cleaned string by the given string
      * @return string
      */
-    public static function packageNameToRegexp($whiteListedPattern)
+    public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i')
     {
         $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern));
 
-        return "{^" . $cleanedWhiteListedPattern . "$}i";
+        return sprintf($wrap, $cleanedWhiteListedPattern);
     }
 }

+ 5 - 0
src/Composer/Repository/Vcs/FossilDriver.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Repository\Vcs;
 
+use Composer\Cache;
 use Composer\Config;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\Filesystem;
@@ -45,6 +46,10 @@ class FossilDriver extends VcsDriver
         if (Filesystem::isLocalPath($this->url) && is_dir($this->url)) {
             $this->checkoutDir = $this->url;
         } else {
+            if (!Cache::isUsable($this->config->get('cache-repo-dir')) || !Cache::isUsable($this->config->get('cache-vcs-dir'))) {
+                throw new \RuntimeException('FossilDriver requires a usable cache directory, and it looks like you set it to be disabled');
+            }
+
             $localName = preg_replace('{[^a-z0-9]}i', '-', $this->url);
             $this->repoFile = $this->config->get('cache-repo-dir') . '/' . $localName . '.fossil';
             $this->checkoutDir = $this->config->get('cache-vcs-dir') . '/' . $localName . '/';

+ 4 - 0
src/Composer/Repository/Vcs/GitDriver.php

@@ -41,6 +41,10 @@ class GitDriver extends VcsDriver
             $this->repoDir = $this->url;
             $cacheUrl = realpath($this->url);
         } else {
+            if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) {
+                throw new \RuntimeException('GitDriver requires a usable cache directory, and it looks like you set it to be disabled');
+            }
+
             $this->repoDir = $this->config->get('cache-vcs-dir') . '/' . preg_replace('{[^a-z0-9.]}i', '-', $this->url) . '/';
 
             GitUtil::cleanEnv();

+ 5 - 0
src/Composer/Repository/Vcs/HgDriver.php

@@ -13,6 +13,7 @@
 namespace Composer\Repository\Vcs;
 
 use Composer\Config;
+use Composer\Cache;
 use Composer\Util\Hg as HgUtils;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\Filesystem;
@@ -37,6 +38,10 @@ class HgDriver extends VcsDriver
         if (Filesystem::isLocalPath($this->url)) {
             $this->repoDir = $this->url;
         } else {
+            if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) {
+                throw new \RuntimeException('HgDriver requires a usable cache directory, and it looks like you set it to be disabled');
+            }
+
             $cacheDir = $this->config->get('cache-vcs-dir');
             $this->repoDir = $cacheDir . '/' . preg_replace('{[^a-z0-9]}i', '-', $this->url) . '/';
 

+ 5 - 0
src/Composer/Repository/Vcs/PerforceDriver.php

@@ -13,6 +13,7 @@
 namespace Composer\Repository\Vcs;
 
 use Composer\Config;
+use Composer\Cache;
 use Composer\IO\IOInterface;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\Perforce;
@@ -54,6 +55,10 @@ class PerforceDriver extends VcsDriver
             return;
         }
 
+        if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) {
+            throw new \RuntimeException('PerforceDriver requires a usable cache directory, and it looks like you set it to be disabled');
+        }
+
         $repoDir = $this->config->get('cache-vcs-dir') . '/' . $this->depot;
         $this->perforce = Perforce::create($repoConfig, $this->getUrl(), $repoDir, $this->process, $this->io);
     }

+ 7 - 1
src/Composer/Util/Perforce.php

@@ -370,7 +370,13 @@ class Perforce
     public function windowsLogin($password)
     {
         $command = $this->generateP4Command(' login -a');
-        $process = new Process($command, null, null, $password);
+
+        // TODO in v3 generate command as an array
+        if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
+            $process = Process::fromShellCommandline($command, null, null, $password);
+        } else {
+            $process = new Process($command, null, null, $password);
+        }
 
         return $process->run();
     }

+ 7 - 1
src/Composer/Util/ProcessExecutor.php

@@ -62,7 +62,13 @@ class ProcessExecutor
 
         $this->captureOutput = func_num_args() > 1;
         $this->errorOutput = null;
-        $process = new Process($command, $cwd, null, null, static::getTimeout());
+
+        // TODO in v3, commands should be passed in as arrays of cmd + args
+        if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
+            $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout());
+        } else {
+            $process = new Process($command, $cwd, null, null, static::getTimeout());
+        }
 
         $callback = is_callable($output) ? $output : array($this, 'outputHandler');
         $process->run($callback);

+ 10 - 0
tests/Composer/Test/ApplicationTest.php

@@ -31,6 +31,11 @@ class ApplicationTest extends TestCase
             ->with($this->equalTo('--no-plugins'))
             ->will($this->returnValue(true));
 
+        $inputMock->expects($this->at($index++))
+            ->method('hasParameterOption')
+            ->with($this->equalTo('--no-cache'))
+            ->will($this->returnValue(false));
+
         $inputMock->expects($this->at($index++))
             ->method('getParameterOption')
             ->with($this->equalTo(array('--working-dir', '-d')))
@@ -84,6 +89,11 @@ class ApplicationTest extends TestCase
             ->with($this->equalTo('--no-plugins'))
             ->will($this->returnValue(true));
 
+        $inputMock->expects($this->at($index++))
+            ->method('hasParameterOption')
+            ->with($this->equalTo('--no-cache'))
+            ->will($this->returnValue(false));
+
         $inputMock->expects($this->at($index++))
             ->method('getParameterOption')
             ->with($this->equalTo(array('--working-dir', '-d')))

+ 67 - 0
tests/Composer/Test/Autoload/AutoloadGeneratorTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test\Autoload;
 
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Package\Link;
+use Composer\Semver\Constraint\Constraint;
 use Composer\Util\Filesystem;
 use Composer\Package\AliasPackage;
 use Composer\Package\Package;
@@ -419,6 +420,72 @@ class AutoloadGeneratorTest extends TestCase
         $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty.");
     }
 
+    public function testNonDevAutoloadShouldIncludeReplacedPackages()
+    {
+        $package = new Package('a', '1.0', '1.0');
+        $package->setRequires(array(new Link('a', 'a/a')));
+
+        $packages = array();
+        $packages[] = $a = new Package('a/a', '1.0', '1.0');
+        $packages[] = $b = new Package('b/b', '1.0', '1.0');
+
+        $a->setRequires(array(new Link('a/a', 'b/c')));
+
+        $b->setAutoload(array('psr-4' => array('B\\' => 'src/')));
+        $b->setReplaces(
+            array(new Link('b/b', 'b/c', new Constraint('==', '1.0'), 'replaces'))
+        );
+
+        $this->repository->expects($this->once())
+            ->method('getCanonicalPackages')
+            ->will($this->returnValue($packages));
+
+        $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src/C');
+        file_put_contents($this->vendorDir.'/b/b/src/C/C.php', '<?php namespace B\\C; class C {}');
+
+        $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_5');
+
+        $this->assertEquals(
+            array(
+                'B\\C\\C' => $this->vendorDir.'/b/b/src/C/C.php',
+            ),
+            include $this->vendorDir.'/composer/autoload_classmap.php'
+        );
+    }
+
+    public function testNonDevAutoloadExclusionWithRecursionReplace()
+    {
+        $package = new Package('a', '1.0', '1.0');
+        $package->setRequires(array(
+            new Link('a', 'a/a'),
+        ));
+
+        $packages = array();
+        $packages[] = $a = new Package('a/a', '1.0', '1.0');
+        $packages[] = $b = new Package('b/b', '1.0', '1.0');
+        $a->setAutoload(array('psr-0' => array('A' => 'src/', 'A\\B' => 'lib/')));
+        $a->setRequires(array(
+            new Link('a/a', 'c/c'),
+        ));
+        $b->setAutoload(array('psr-0' => array('B\\Sub\\Name' => 'src/')));
+        $b->setReplaces(array(
+            new Link('b/b', 'c/c'),
+        ));
+
+        $this->repository->expects($this->once())
+            ->method('getCanonicalPackages')
+            ->will($this->returnValue($packages));
+
+        $this->fs->ensureDirectoryExists($this->vendorDir.'/composer');
+        $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/src');
+        $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/lib');
+        $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src');
+
+        $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_5');
+        $this->assertAutoloadFiles('vendors', $this->vendorDir.'/composer');
+        $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty.");
+    }
+
     public function testPSRToClassMapIgnoresNonExistingDir()
     {
         $package = new Package('a', '1.0', '1.0');

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

@@ -0,0 +1,46 @@
+--TEST--
+Update with a package whitelist pattern and all-dependencies flag updates packages and their dependencies, even if defined as root dependency, matching the pattern
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "fixed", "version": "1.1.0" },
+                { "name": "fixed", "version": "1.0.0" },
+                { "name": "whitelisted-component1", "version": "1.1.0" },
+                { "name": "whitelisted-component1", "version": "1.0.0" },
+                { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } },
+                { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } },
+                { "name": "dependency", "version": "1.1.0" },
+                { "name": "dependency", "version": "1.0.0" },
+                { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated-dependency", "version": "1.1.0" },
+                { "name": "unrelated-dependency", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "fixed": "1.*",
+        "whitelisted-component1": "1.*",
+        "whitelisted-component2": "1.*",
+        "dependency": "1.*",
+        "unrelated": "1.*"
+    }
+}
+--INSTALLED--
+[
+    { "name": "fixed", "version": "1.0.0" },
+    { "name": "whitelisted-component1", "version": "1.0.0" },
+    { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+    { "name": "dependency", "version": "1.0.0" },
+    { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+    { "name": "unrelated-dependency", "version": "1.0.0" }
+]
+--RUN--
+update whitelisted-* --with-all-dependencies
+--EXPECT--
+Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0)
+Updating dependency (1.0.0) to dependency (1.1.0)
+Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0)

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

@@ -0,0 +1,49 @@
+--TEST--
+Update with a package whitelist only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "fixed", "version": "1.1.0" },
+                { "name": "fixed", "version": "1.0.0" },
+                { "name": "whitelisted-component1", "version": "1.1.0" },
+                { "name": "whitelisted-component1", "version": "1.0.0" },
+                { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } },
+                { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } },
+                { "name": "dependency", "version": "1.1.0" },
+                { "name": "dependency", "version": "1.0.0" },
+                { "name": "root-dependency", "version": "1.1.0" },
+                { "name": "root-dependency", "version": "1.0.0" },
+                { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated-dependency", "version": "1.1.0" },
+                { "name": "unrelated-dependency", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "fixed": "1.*",
+        "whitelisted-component1": "1.*",
+        "whitelisted-component2": "1.*",
+        "root-dependency": "1.*",
+        "unrelated": "1.*"
+    }
+}
+--INSTALLED--
+[
+    { "name": "fixed", "version": "1.0.0" },
+    { "name": "whitelisted-component1", "version": "1.0.0" },
+    { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+    { "name": "root-dependency", "version": "1.0.0" },
+    { "name": "dependency", "version": "1.0.0" },
+    { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+    { "name": "unrelated-dependency", "version": "1.0.0" }
+]
+--RUN--
+update whitelisted-* --with-dependencies
+--EXPECT--
+Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0)
+Updating dependency (1.0.0) to dependency (1.1.0)
+Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0)

+ 55 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test

@@ -0,0 +1,55 @@
+--TEST--
+Update with a package whitelist only updates those packages and their dependencies matching the pattern
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "fixed", "version": "1.1.0" },
+                { "name": "fixed", "version": "1.0.0" },
+                { "name": "whitelisted-component1", "version": "1.1.0", "require": { "whitelisted-component2": "1.1.0" } },
+                { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } },
+                { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.1.0", "whitelisted-component5": "1.0.0" } },
+                { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+                { "name": "whitelisted-component3", "version": "1.1.0", "require": { "whitelisted-component4": "1.1.0" } },
+                { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } },
+                { "name": "whitelisted-component4", "version": "1.1.0" },
+                { "name": "whitelisted-component4", "version": "1.0.0" },
+                { "name": "whitelisted-component5", "version": "1.1.0" },
+                { "name": "whitelisted-component5", "version": "1.0.0" },
+                { "name": "dependency", "version": "1.1.0" },
+                { "name": "dependency", "version": "1.0.0" },
+                { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated-dependency", "version": "1.1.0" },
+                { "name": "unrelated-dependency", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "fixed": "1.*",
+        "whitelisted-component1": "1.*",
+        "whitelisted-component2": "1.*",
+        "whitelisted-component3": "1.0.0",
+        "unrelated": "1.*"
+    }
+}
+--INSTALLED--
+[
+    { "name": "fixed", "version": "1.0.0" },
+    { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } },
+    { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+    { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } },
+    { "name": "whitelisted-component4", "version": "1.0.0" },
+    { "name": "whitelisted-component5", "version": "1.0.0" },
+    { "name": "dependency", "version": "1.0.0" },
+    { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+    { "name": "unrelated-dependency", "version": "1.0.0" }
+]
+--RUN--
+update whitelisted-* --with-dependencies
+--EXPECT--
+Updating dependency (1.0.0) to dependency (1.1.0)
+Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0)
+Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0)

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

@@ -0,0 +1,44 @@
+--TEST--
+Update with a package whitelist only updates those packages matching the pattern
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "fixed", "version": "1.1.0" },
+                { "name": "fixed", "version": "1.0.0" },
+                { "name": "whitelisted-component1", "version": "1.1.0" },
+                { "name": "whitelisted-component1", "version": "1.0.0" },
+                { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } },
+                { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } },
+                { "name": "dependency", "version": "1.1.0" },
+                { "name": "dependency", "version": "1.0.0" },
+                { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" }  },
+                { "name": "unrelated-dependency", "version": "1.1.0" },
+                { "name": "unrelated-dependency", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "fixed": "1.*",
+        "whitelisted-component1": "1.*",
+        "whitelisted-component2": "1.*",
+        "unrelated": "1.*"
+    }
+}
+--INSTALLED--
+[
+    { "name": "fixed", "version": "1.0.0" },
+    { "name": "whitelisted-component1", "version": "1.0.0" },
+    { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+    { "name": "dependency", "version": "1.0.0" },
+    { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+    { "name": "unrelated-dependency", "version": "1.0.0" }
+]
+--RUN--
+update whitelisted-*
+--EXPECT--
+Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0)
+Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0)