Sfoglia il codice sorgente

Merge branch '2.0'

Jordi Boggiano 5 anni fa
parent
commit
87757de6bc
100 ha cambiato i file con 4111 aggiunte e 2546 eliminazioni
  1. 1 0
      .gitattributes
  2. 30 13
      .travis.yml
  3. 10 1
      appveyor.yml
  4. 11 5
      composer.json
  5. 122 9
      composer.lock
  6. 1 1
      doc/01-basic-usage.md
  7. 15 9
      doc/03-cli.md
  8. 2 2
      doc/articles/plugins.md
  9. 5 3
      doc/articles/scripts.md
  10. 46 0
      phpstan/Rules/src/AnonymousFunctionWithThisRule.php
  11. 28 0
      phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php
  12. 34 0
      phpstan/Rules/tests/data/method-with-this.php
  13. 5 0
      phpstan/autoload.php
  14. 39 0
      phpstan/config.neon
  15. 8 13
      src/Composer/Autoload/ClassMapGenerator.php
  16. 11 11
      src/Composer/Cache.php
  17. 4 2
      src/Composer/Command/ArchiveCommand.php
  18. 4 2
      src/Composer/Command/BaseCommand.php
  19. 9 11
      src/Composer/Command/BaseDependencyCommand.php
  20. 58 45
      src/Composer/Command/CheckPlatformReqsCommand.php
  21. 1 1
      src/Composer/Command/ConfigCommand.php
  22. 12 23
      src/Composer/Command/CreateProjectCommand.php
  23. 16 31
      src/Composer/Command/DiagnoseCommand.php
  24. 2 2
      src/Composer/Command/HomeCommand.php
  25. 12 12
      src/Composer/Command/InitCommand.php
  26. 0 3
      src/Composer/Command/InstallCommand.php
  27. 42 7
      src/Composer/Command/RemoveCommand.php
  28. 69 11
      src/Composer/Command/RequireCommand.php
  29. 5 5
      src/Composer/Command/SelfUpdateCommand.php
  30. 66 68
      src/Composer/Command/ShowCommand.php
  31. 1 1
      src/Composer/Command/StatusCommand.php
  32. 43 90
      src/Composer/Command/SuggestsCommand.php
  33. 25 8
      src/Composer/Command/UpdateCommand.php
  34. 1 0
      src/Composer/Compiler.php
  35. 1 1
      src/Composer/Composer.php
  36. 1 1
      src/Composer/Config/JsonConfigSource.php
  37. 7 1
      src/Composer/Console/Application.php
  38. 43 112
      src/Composer/DependencyResolver/DefaultPolicy.php
  39. 5 6
      src/Composer/DependencyResolver/GenericRule.php
  40. 36 0
      src/Composer/DependencyResolver/LocalRepoTransaction.php
  41. 133 0
      src/Composer/DependencyResolver/LockTransaction.php
  42. 105 0
      src/Composer/DependencyResolver/MultiConflictRule.php
  43. 11 3
      src/Composer/DependencyResolver/Operation/InstallOperation.php
  44. 11 3
      src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php
  45. 11 3
      src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php
  46. 10 2
      src/Composer/DependencyResolver/Operation/OperationInterface.php
  47. 5 4
      src/Composer/DependencyResolver/Operation/SolverOperation.php
  48. 11 3
      src/Composer/DependencyResolver/Operation/UninstallOperation.php
  49. 24 6
      src/Composer/DependencyResolver/Operation/UpdateOperation.php
  50. 1 4
      src/Composer/DependencyResolver/PolicyInterface.php
  51. 34 193
      src/Composer/DependencyResolver/Pool.php
  52. 376 0
      src/Composer/DependencyResolver/PoolBuilder.php
  53. 187 123
      src/Composer/DependencyResolver/Problem.php
  54. 104 31
      src/Composer/DependencyResolver/Request.php
  55. 127 77
      src/Composer/DependencyResolver/Rule.php
  56. 2 3
      src/Composer/DependencyResolver/Rule2Literals.php
  57. 7 5
      src/Composer/DependencyResolver/RuleSet.php
  58. 76 117
      src/Composer/DependencyResolver/RuleSetGenerator.php
  59. 44 21
      src/Composer/DependencyResolver/RuleWatchGraph.php
  60. 1 1
      src/Composer/DependencyResolver/RuleWatchNode.php
  61. 65 141
      src/Composer/DependencyResolver/Solver.php
  62. 18 9
      src/Composer/DependencyResolver/SolverProblemsException.php
  63. 214 152
      src/Composer/DependencyResolver/Transaction.php
  64. 50 55
      src/Composer/Downloader/ArchiveDownloader.php
  65. 210 88
      src/Composer/Downloader/DownloadManager.php
  66. 37 6
      src/Composer/Downloader/DownloaderInterface.php
  67. 164 98
      src/Composer/Downloader/FileDownloader.php
  68. 10 2
      src/Composer/Downloader/FossilDownloader.php
  69. 77 48
      src/Composer/Downloader/GitDownloader.php
  70. 7 13
      src/Composer/Downloader/GzipDownloader.php
  71. 10 2
      src/Composer/Downloader/HgDownloader.php
  72. 28 11
      src/Composer/Downloader/PathDownloader.php
  73. 11 3
      src/Composer/Downloader/PerforceDownloader.php
  74. 3 1
      src/Composer/Downloader/PharDownloader.php
  75. 6 4
      src/Composer/Downloader/RarDownloader.php
  76. 15 7
      src/Composer/Downloader/SvnDownloader.php
  77. 3 1
      src/Composer/Downloader/TarDownloader.php
  78. 111 47
      src/Composer/Downloader/VcsDownloader.php
  79. 5 12
      src/Composer/Downloader/XzDownloader.php
  80. 8 6
      src/Composer/Downloader/ZipDownloader.php
  81. 51 63
      src/Composer/EventDispatcher/EventDispatcher.php
  82. 40 39
      src/Composer/Factory.php
  83. 1 2
      src/Composer/IO/BaseIO.php
  84. 23 4
      src/Composer/IO/IOInterface.php
  85. 304 462
      src/Composer/Installer.php
  86. 119 9
      src/Composer/Installer/InstallationManager.php
  87. 18 68
      src/Composer/Installer/InstallerEvent.php
  88. 4 21
      src/Composer/Installer/InstallerEvents.php
  89. 49 7
      src/Composer/Installer/InstallerInterface.php
  90. 35 2
      src/Composer/Installer/LibraryInstaller.php
  91. 25 1
      src/Composer/Installer/MetapackageInstaller.php
  92. 21 0
      src/Composer/Installer/NoopInstaller.php
  93. 78 9
      src/Composer/Installer/PackageEvent.php
  94. 25 7
      src/Composer/Installer/PluginInstaller.php
  95. 27 2
      src/Composer/Installer/ProjectInstaller.php
  96. 85 14
      src/Composer/Installer/SuggestedPackagesReporter.php
  97. 10 10
      src/Composer/Json/JsonFile.php
  98. 5 0
      src/Composer/Package/AliasPackage.php
  99. 7 2
      src/Composer/Package/Archiver/ArchiveManager.php
  100. 21 9
      src/Composer/Package/BasePackage.php

+ 1 - 0
.gitattributes

@@ -15,3 +15,4 @@
 .travis.yml export-ignore
 appveyor.yml export-ignore
 phpunit.xml.dist export-ignore
+/phpstan/ export-ignore

+ 30 - 13
.travis.yml

@@ -1,6 +1,6 @@
 language: php
 
-dist: trusty
+dist: bionic
 
 git:
   depth: 5
@@ -9,27 +9,40 @@ cache:
   directories:
     - $HOME/.composer/cache
 
-addons:
-  apt:
-    packages:
-      - parallel
-
 matrix:
   include:
     - php: 5.3
       dist: precise
     - php: 5.4
+      dist: trusty
     - php: 5.5
+      dist: trusty
     - php: 5.6
+      dist: xenial
     - php: 7.0
+      dist: xenial
     - php: 7.1
+      dist: xenial
     - php: 7.2
+      dist: xenial
     - php: 7.3
-    - php: nightly
+      dist: xenial
+    # Regular 7.4 build with locked deps
+    - php: 7.4
+      env:
+        - SYMFONY_PHPUNIT_VERSION=7.5
+    # High deps check
     - php: 7.4
       env:
         - deps=high
         - SYMFONY_PHPUNIT_VERSION=7.5
+    # PHPStan checks
+    - php: 7.4
+      env:
+        - deps=high
+        - PHPSTAN=1
+        - SYMFONY_PHPUNIT_VERSION=7.5
+    - php: nightly
   fast_finish: true
   allow_failures:
     - php: nightly
@@ -44,9 +57,9 @@ before_install:
 
 install:
   # flags to pass to install
-  - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress"
+  - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-progress"
   # update deps to latest in case of high deps build
-  - if [ "$deps" == "high" ]; then composer config platform.php 7.2.4; composer update $flags; fi
+  - if [ "$deps" == "high" ]; then composer config platform.php 7.4.0; composer update $flags; fi
   # install dependencies using system provided composer binary
   - composer install $flags
   # install dependencies using composer from source
@@ -58,9 +71,13 @@ before_script:
   - git config --global user.email travis@example.com
 
 script:
-  - ./vendor/bin/simple-phpunit
-  # run test suite directories in parallel using GNU parallel
-#  - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);'
+  - if [[ $PHPSTAN == "1" ]]; then
+      bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --no-update &&
+      bin/composer update phpstan/* phpunit/* sebastian/* --with-dependencies &&
+      vendor/bin/phpstan analyse --configuration=phpstan/config.neon;
+    else
+      vendor/bin/simple-phpunit;
+    fi
 
 before_deploy:
   - php -d phar.readonly=0 bin/compile
@@ -73,4 +90,4 @@ deploy:
   on:
     tags: true
     repo: composer/composer
-    php:  '7.2'
+    php:  '7.3'

+ 10 - 1
appveyor.yml

@@ -3,7 +3,7 @@ clone_depth: 5
 
 environment:
   # This sets the PHP version (from Chocolatey)
-  PHPCI_CHOCO_VERSION: 7.3.1
+  PHPCI_CHOCO_VERSION: 7.3.14
   PHPCI_CACHE: C:\tools\phpci
   PHPCI_PHP: C:\tools\phpci\php
   PHPCI_COMPOSER: C:\tools\phpci\composer
@@ -25,6 +25,15 @@ install:
   - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%"
   - php -v
   - IF %PHP%==0 (composer --version) ELSE (composer self-update)
+  - IF %PHP%==0 cd %PHPCI_PHP%
+  - IF %PHP%==0 copy php.ini-production php.ini /Y
+  - IF %PHP%==0 echo date.timezone="UTC" >> php.ini
+  - IF %PHP%==0 echo extension_dir=ext >> php.ini
+  - IF %PHP%==0 echo extension=php_openssl.dll >> php.ini
+  - IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini
+  - IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini
+  - IF %PHP%==0 echo extension=php_intl.dll >> php.ini
+  - IF %PHP%==0 echo extension=php_curl.dll >> php.ini
   - cd %APPVEYOR_BUILD_FOLDER%
   - composer install --prefer-dist --no-progress
 

+ 11 - 5
composer.json

@@ -24,7 +24,7 @@
     "require": {
         "php": "^5.3.2 || ^7.0",
         "composer/ca-bundle": "^1.0",
-        "composer/semver": "^1.0",
+        "composer/semver": "^2.0@dev",
         "composer/spdx-licenses": "^1.2",
         "composer/xdebug-handler": "^1.1",
         "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
@@ -34,7 +34,8 @@
         "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
         "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0",
         "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0",
-        "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0"
+        "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0",
+        "react/promise": "^1.2 || ^2.7"
     },
     "conflict": {
         "symfony/console": "2.8.38"
@@ -55,7 +56,7 @@
     },
     "extra": {
         "branch-alias": {
-            "dev-master": "1.10-dev"
+            "dev-master": "2.0-dev"
         }
     },
     "autoload": {
@@ -65,8 +66,13 @@
     },
     "autoload-dev": {
         "psr-4": {
-            "Composer\\Test\\": "tests/Composer/Test"
-        }
+            "Composer\\Test\\": "tests/Composer/Test",
+            "Composer\\PHPStanRules\\": "phpstan/Rules/src",
+            "Composer\\PHPStanRulesTests\\": "phpstan/Rules/tests"
+        },
+        "classmap": [
+            "phpstan/Rules/tests/data"
+        ]
     },
     "bin": [
         "bin/composer"

+ 122 - 9
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "cc6f9640996dfad00a5b03a8be01a571",
+    "content-hash": "a0a9399315ac0b612d4296b8df745112",
     "packages": [
         {
             "name": "composer/ca-bundle",
@@ -60,20 +60,25 @@
                 "ssl",
                 "tls"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/ca-bundle/issues",
+                "source": "https://github.com/composer/ca-bundle/tree/master"
+            },
             "time": "2020-01-13T10:02:55+00:00"
         },
         {
             "name": "composer/semver",
-            "version": "1.5.1",
+            "version": "2.0.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/semver.git",
-                "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
+                "reference": "4df5ff3249f01018504939d66040d8d2b783d820"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
-                "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
+                "url": "https://api.github.com/repos/composer/semver/zipball/4df5ff3249f01018504939d66040d8d2b783d820",
+                "reference": "4df5ff3249f01018504939d66040d8d2b783d820",
                 "shasum": ""
             },
             "require": {
@@ -85,7 +90,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-master": "2.x-dev"
                 }
             },
             "autoload": {
@@ -121,7 +126,22 @@
                 "validation",
                 "versioning"
             ],
-            "time": "2020-01-13T12:06:48+00:00"
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/semver/issues",
+                "source": "https://github.com/composer/semver/tree/2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-11T13:41:23+00:00"
         },
         {
             "name": "composer/spdx-licenses",
@@ -181,6 +201,11 @@
                 "spdx",
                 "validator"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/spdx-licenses/issues",
+                "source": "https://github.com/composer/spdx-licenses/tree/1.5.3"
+            },
             "time": "2020-02-14T07:44:31+00:00"
         },
         {
@@ -225,6 +250,11 @@
                 "Xdebug",
                 "performance"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/xdebug-handler/issues",
+                "source": "https://github.com/composer/xdebug-handler/tree/master"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
@@ -353,6 +383,44 @@
             },
             "time": "2019-11-01T11:05:21+00:00"
         },
+        {
+            "name": "react/promise",
+            "version": "v1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/reactphp/promise.git",
+                "reference": "eefff597e67ff66b719f8171480add3c91474a1e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/reactphp/promise/zipball/eefff597e67ff66b719f8171480add3c91474a1e",
+                "reference": "eefff597e67ff66b719f8171480add3c91474a1e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "React\\Promise": "src/"
+                },
+                "files": [
+                    "src/React/Promise/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+            "time": "2016-03-07T13:46:50+00:00"
+        },
         {
             "name": "seld/jsonlint",
             "version": "1.7.2",
@@ -448,6 +516,10 @@
             "keywords": [
                 "phar"
             ],
+            "support": {
+                "issues": "https://github.com/Seldaek/phar-utils/issues",
+                "source": "https://github.com/Seldaek/phar-utils/tree/1.1.0"
+            },
             "time": "2020-02-14T15:25:33+00:00"
         },
         {
@@ -735,6 +807,9 @@
                 "polyfill",
                 "portable"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/master"
+            },
             "time": "2020-01-13T11:15:53+00:00"
         },
         {
@@ -794,6 +869,9 @@
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/master"
+            },
             "time": "2020-01-13T11:15:53+00:00"
         },
         {
@@ -949,6 +1027,16 @@
             "license": [
                 "MIT"
             ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "mike.vanriel@naenius.com"
+                }
+            ],
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/2.x"
+            },
             "time": "2016-01-25T08:17:30+00:00"
         },
         {
@@ -1012,6 +1100,10 @@
                 "spy",
                 "stub"
             ],
+            "support": {
+                "issues": "https://github.com/phpspec/prophecy/issues",
+                "source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
+            },
             "time": "2020-03-05T15:02:03+00:00"
         },
         {
@@ -1076,6 +1168,10 @@
                 "compare",
                 "equality"
             ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/comparator/issues",
+                "source": "https://github.com/sebastianbergmann/comparator/tree/1.2"
+            },
             "time": "2017-01-29T09:50:25+00:00"
         },
         {
@@ -1128,6 +1224,10 @@
             "keywords": [
                 "diff"
             ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues",
+                "source": "https://github.com/sebastianbergmann/diff/tree/1.4"
+            },
             "time": "2017-05-22T07:24:03+00:00"
         },
         {
@@ -1195,6 +1295,10 @@
                 "export",
                 "exporter"
             ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/exporter/issues",
+                "source": "https://github.com/sebastianbergmann/exporter/tree/master"
+            },
             "time": "2016-11-19T08:54:04+00:00"
         },
         {
@@ -1248,6 +1352,10 @@
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/master"
+            },
             "time": "2016-11-19T07:33:16+00:00"
         },
         {
@@ -1313,6 +1421,9 @@
             ],
             "description": "Symfony PHPUnit Bridge",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
@@ -1332,7 +1443,9 @@
     ],
     "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {
+        "composer/semver": 20
+    },
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": {
@@ -1342,5 +1455,5 @@
     "platform-overrides": {
         "php": "5.3.9"
     },
-    "plugin-api-version": "1.1.0"
+    "plugin-api-version": "2.0.0"
 }

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

+ 15 - 9
doc/03-cli.md

@@ -106,7 +106,6 @@ resolution.
 * **--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.
-* **--no-suggest:** Skips suggested packages in the output.
 * **--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.
@@ -156,9 +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.
-* **--no-suggest:** Skips suggested packages in the output.
-* **--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.
@@ -198,11 +196,11 @@ If you do not specify a package, composer will prompt you to search for a packag
 ### Options
 
 * **--dev:** Add packages to `require-dev`.
+* **--dry-run:** Simulate the command without actually doing anything.
 * **--prefer-source:** Install packages from `source` when available.
 * **--prefer-dist:** Install packages from `dist` when available.
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
-* **--no-suggest:** Skips suggested packages in the output.
 * **--no-update:** Disables the automatic update of the dependencies.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--update-no-dev:** Run the dependency update with the `--no-dev` option.
@@ -236,6 +234,7 @@ uninstalled.
 
 ### Options
 * **--dev:** Remove packages from `require-dev`.
+* **--dry-run:** Simulate the command without actually doing anything.
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
 * **--no-update:** Disables the automatic update of the dependencies.
@@ -408,16 +407,18 @@ Lists all packages suggested by currently installed set of packages. You can
 optionally pass one or multiple package names in the format of `vendor/package`
 to limit output to suggestions made by those packages only.
 
-Use the `--by-package` or `--by-suggestion` flags to group the output by
+Use the `--by-package` (default) or `--by-suggestion` flags to group the output by
 the package offering the suggestions or the suggested packages respectively.
 
-Use the `--verbose (-v)` flag to display the suggesting package and the suggestion reason.
-This implies `--by-package --by-suggestion`, showing both lists.
+If you only want a list of suggested package names, use `--list`.
 
 ### Options
 
-* **--by-package:** Groups output by suggesting package.
+* **--by-package:** Groups output by suggesting package (default).
 * **--by-suggestion:** Groups output by suggested package.
+* **--all:** Show suggestions from all dependencies, including transitive ones (by
+  default only direct dependencies' suggestions are shown).
+* **--list:** Show only list of suggested package names.
 * **--no-dev:** Excludes suggestions from `require-dev` packages.
 
 ## fund
@@ -952,4 +953,9 @@ The env var accepts domains, IP addresses, and IP address blocks in CIDR
 notation. You can restrict the filter to a particular port (e.g. `:80`). You
 can also set it to `*` to ignore the proxy for all HTTP requests.
 
+### COMPOSER_DISABLE_NETWORK
+
+If set to `1`, disables network access (best effort). This can be used for debugging or
+to run Composer on a plane or a starship with poor connectivity.
+
 ← [Libraries](02-libraries.md)  |  [Schema](04-schema.md) →

+ 2 - 2
doc/articles/plugins.md

@@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface
 
         if ($protocol === 's3') {
             $awsClient = new AwsClient($this->io, $this->composer->getConfig());
-            $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient);
-            $event->setRemoteFilesystem($s3RemoteFilesystem);
+            $s3Downloader = new S3Downloader($this->io, $event->getHttpDownloader()->getOptions(), $awsClient);
+            $event->setHttpdownloader($s3Downloader);
         }
     }
 }

+ 5 - 3
doc/articles/scripts.md

@@ -43,8 +43,8 @@ Composer fires the following named events during its execution process:
 
 ### Installer Events
 
-- **pre-dependencies-solving**: occurs before the dependencies are resolved.
-- **post-dependencies-solving**: occurs after the dependencies have been resolved.
+- **pre-operations-exec**: occurs before the install/upgrade/.. operations
+  are executed when installing a lock file.
 
 ### Package Events
 
@@ -61,11 +61,13 @@ Composer fires the following named events during its execution process:
 - **command**: occurs before any Composer Command is executed on the CLI. It
   provides you with access to the input and output objects of the program.
 - **pre-file-download**: occurs before files are downloaded and allows
-  you to manipulate the `RemoteFilesystem` object prior to downloading files
+  you to manipulate the `HttpDownloader` object prior to downloading files
   based on the URL to be downloaded.
 - **pre-command-run**: occurs before a command is executed and allows you to
   manipulate the `InputInterface` object's options and arguments to tweak
   a command's behavior.
+- **pre-pool-create**: occurs before the Pool of packages is created, and lets
+  you filter the list of packages which is going to enter the Solver.
 
 > **Note:** Composer makes no assumptions about the state of your dependencies
 > prior to `install` or `update`. Therefore, you should not specify scripts

+ 46 - 0
phpstan/Rules/src/AnonymousFunctionWithThisRule.php

@@ -0,0 +1,46 @@
+<?php declare(strict_types = 1);
+
+namespace Composer\PHPStanRules;
+
+use PhpParser\Node;
+use PHPStan\Analyser\Scope;
+use PHPStan\Rules\Rule;
+
+/**
+ * @phpstan-implements Rule<\PhpParser\Node\Expr\Variable>
+ */
+final class AnonymousFunctionWithThisRule implements Rule
+{
+    /**
+     * @inheritDoc
+     */
+    public function getNodeType(): string
+    {
+        return \PhpParser\Node\Expr\Variable::class;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function processNode(Node $node, Scope $scope): array
+    {
+        if (!\is_string($node->name) || $node->name !== 'this') {
+            return [];
+        }
+
+        if ($scope->isInClosureBind()) {
+            return [];
+        }
+
+        if (!$scope->isInClass()) {
+            // reported in other standard rule on level 0
+            return [];
+        }
+
+        if ($scope->isInAnonymousFunction()) {
+            return ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.'];
+        }
+
+        return [];
+    }
+}

+ 28 - 0
phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php

@@ -0,0 +1,28 @@
+<?php declare(strict_types = 1);
+
+namespace Composer\PHPStanRulesTests;
+
+use Composer\PHPStanRules\AnonymousFunctionWithThisRule;
+use PHPStan\Testing\RuleTestCase;
+
+/**
+ * @phpstan-extends RuleTestCase<AnonymousFunctionWithThisRule>
+ */
+final class AnonymousFunctionWithThisRuleTest extends RuleTestCase
+{
+    /**
+     * @inheritDoc
+     */
+    protected function getRule(): \PHPStan\Rules\Rule
+    {
+        return new AnonymousFunctionWithThisRule();
+    }
+
+    public function testWithThis(): void
+    {
+        $this->analyse([__DIR__ . '/data/method-with-this.php'], [
+            ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 13],
+            ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 17],
+        ]);
+    }
+}

+ 34 - 0
phpstan/Rules/tests/data/method-with-this.php

@@ -0,0 +1,34 @@
+<?php
+
+class FirstClass
+{
+    /**
+     * @var int
+     */
+    private $firstProp = 9;
+
+    public function funMethod()
+    {
+        function() {
+            $this->firstProp;
+        };
+
+        call_user_func(function() {
+            $this->funMethod();
+        }, $this);
+
+        $bind = 'bind';
+        function() use($bind) {
+
+        };
+    }
+}
+
+function global_ok() {
+    $_SERVER['REMOTE_ADDR'];
+}
+
+function global_this() {
+    // not checked by our rule, it is checked with standard phpstan rule on level 0
+    $this['REMOTE_ADDR'];
+}

+ 5 - 0
phpstan/autoload.php

@@ -0,0 +1,5 @@
+<?php
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+require_once __DIR__ . '/../src/bootstrap.php';

+ 39 - 0
phpstan/config.neon

@@ -0,0 +1,39 @@
+parameters:
+    autoload_files:
+        - autoload.php
+    level: 0
+    excludes_analyse:
+       - '../tests/Composer/Test/Fixtures/*'
+       - '../tests/Composer/Test/Autoload/Fixtures/*'
+       - '../tests/Composer/Test/Plugin/Fixtures/*'
+    ignoreErrors:
+        # ion cube is not installed
+        - '~^Function ioncube_loader_\w+ not found\.$~'
+
+        # variables from global scope
+        - '~^Undefined variable: \$vendorDir$~'
+        - '~^Undefined variable: \$baseDir$~'
+
+        # variable defined in eval
+        - '~^Undefined variable: \$res$~'
+
+        # erroneous detection of missing const, see https://github.com/phpstan/phpstan/issues/2960
+        - '~^Access to undefined constant ZipArchive::LIBZIP_VERSION.$~'
+
+        # we don't have different constructors for parent/child
+        - '~^Unsafe usage of new static\(\)\.$~'
+
+        # BC with older PHPUnit
+        - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~'
+
+        # hhvm should have support for $this in closures
+        -
+            count: 1
+            message: '~^Using \$this inside anonymous function is prohibited because of PHP 5\.3 support\.$~'
+            path: '../tests/Composer/Test/Repository/PlatformRepositoryTest.php'
+    paths:
+        - ../src
+        - ../tests
+
+rules:
+    - Composer\PHPStanRules\AnonymousFunctionWithThisRule

+ 8 - 13
src/Composer/Autoload/ClassMapGenerator.php

@@ -113,10 +113,10 @@ class ClassMapGenerator
 
             $classes = self::findClasses($filePath);
             if (null !== $autoloadType) {
-                list($classes, $validClasses) = self::filterByNamespace($classes, $filePath, $namespace, $autoloadType, $basePath, $io);
+                $classes = self::filterByNamespace($classes, $filePath, $namespace, $autoloadType, $basePath, $io);
 
                 // if no valid class was found in the file then we do not mark it as scanned as it might still be matched by another rule later
-                if ($validClasses) {
+                if ($classes) {
                     $scannedFiles[$realPath] = true;
                 }
             } else {
@@ -126,8 +126,7 @@ class ClassMapGenerator
 
             foreach ($classes as $class) {
                 // skip classes not within the given namespace prefix
-                // TODO enable in Composer v1.11 or 2.0 whichever comes first
-                if (/* null === $autoloadType && */ null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) {
+                if (null === $autoloadType && null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) {
                     continue;
                 }
 
@@ -196,19 +195,15 @@ class ClassMapGenerator
         // warn only if no valid classes, else silently skip invalid
         if (empty($validClasses)) {
             foreach ($rejectedClasses as $class) {
-                trigger_error(
-                    "Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. It will not autoload anymore in Composer v2.0.",
-                    E_USER_DEPRECATED
-                );
+                if ($io) {
+                    $io->writeError("<warning>Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. Skipping.</warning>");
+                }
             }
 
-            // TODO enable in Composer 2.0
-            //return array();
+            return array();
         }
 
-        // TODO enable in Composer 2.0 & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses
-        //return $validClasses;
-        return array($classes, $validClasses);
+        return $validClasses;
     }
 
     /**

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

+ 4 - 2
src/Composer/Command/ArchiveCommand.php

@@ -22,6 +22,7 @@ use Composer\Script\ScriptEvents;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -111,8 +112,9 @@ EOT
             $archiveManager = $composer->getArchiveManager();
         } else {
             $factory = new Factory;
-            $downloadManager = $factory->createDownloadManager($io, $config);
-            $archiveManager = $factory->createArchiveManager($config, $downloadManager);
+            $httpDownloader = $factory->createHttpDownloader($io, $config);
+            $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader);
+            $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader));
         }
 
         if ($packageName) {

+ 4 - 2
src/Composer/Command/BaseCommand.php

@@ -27,6 +27,8 @@ use Symfony\Component\Console\Command\Command;
 /**
  * Base class for Composer commands
  *
+ * @method Application getApplication()
+ *
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  */
@@ -46,7 +48,7 @@ abstract class BaseCommand extends Command
      * @param  bool              $required
      * @param  bool|null         $disablePlugins
      * @throws \RuntimeException
-     * @return Composer
+     * @return Composer|null
      */
     public function getComposer($required = true, $disablePlugins = null)
     {
@@ -173,7 +175,7 @@ abstract class BaseCommand extends Command
 
         if ($input->getOption('prefer-source') || $input->getOption('prefer-dist') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'))) {
             $preferSource = $input->getOption('prefer-source') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'));
-            $preferDist = $input->getOption('prefer-dist');
+            $preferDist = (bool) $input->getOption('prefer-dist');
         }
 
         return array($preferSource, $preferDist);

+ 9 - 11
src/Composer/Command/BaseDependencyCommand.php

@@ -12,11 +12,12 @@
 
 namespace Composer\Command;
 
-use Composer\DependencyResolver\Pool;
 use Composer\Package\Link;
 use Composer\Package\PackageInterface;
-use Composer\Repository\ArrayRepository;
+use Composer\Repository\InstalledArrayRepository;
 use Composer\Repository\CompositeRepository;
+use Composer\Repository\RootPackageRepository;
+use Composer\Repository\InstalledRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryFactory;
 use Composer\Plugin\CommandEvent;
@@ -71,15 +72,12 @@ class BaseDependencyCommand extends BaseCommand
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
 
-        // Prepare repositories and set up a pool
         $platformOverrides = $composer->getConfig()->get('platform') ?: array();
-        $repository = new CompositeRepository(array(
-            new ArrayRepository(array($composer->getPackage())),
+        $installedRepo = new InstalledRepository(array(
+            new RootPackageRepository($composer->getPackage()),
             $composer->getRepositoryManager()->getLocalRepository(),
             new PlatformRepository(array(), $platformOverrides),
         ));
-        $pool = new Pool();
-        $pool->addRepository($repository);
 
         // Parse package name and constraint
         list($needle, $textConstraint) = array_pad(
@@ -89,17 +87,17 @@ class BaseDependencyCommand extends BaseCommand
         );
 
         // Find packages that are or provide the requested package first
-        $packages = $pool->whatProvides(strtolower($needle));
+        $packages = $installedRepo->findPackagesWithReplacersAndProviders($needle);
         if (empty($packages)) {
             throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle));
         }
 
         // If the version we ask for is not installed then we need to locate it in remote repos and add it.
         // This is needed for why-not to resolve conflicts from an uninstalled version against installed packages.
-        if (!$repository->findPackage($needle, $textConstraint)) {
+        if (!$installedRepo->findPackage($needle, $textConstraint)) {
             $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO()));
             if ($match = $defaultRepos->findPackage($needle, $textConstraint)) {
-                $repository->addRepository(new ArrayRepository(array(clone $match)));
+                $installedRepo->addRepository(new InstalledArrayRepository(array(clone $match)));
             }
         }
 
@@ -126,7 +124,7 @@ class BaseDependencyCommand extends BaseCommand
         $recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE);
 
         // Resolve dependencies
-        $results = $repository->getDependents($needles, $constraint, $inverted, $recursive);
+        $results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive);
         if (empty($results)) {
             $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : '';
             $this->getIO()->writeError(sprintf(

+ 58 - 45
src/Composer/Command/CheckPlatformReqsCommand.php

@@ -20,6 +20,7 @@ use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Composer\Repository\PlatformRepository;
+use Composer\Repository\InstalledRepository;
 
 class CheckPlatformReqsCommand extends BaseCommand
 {
@@ -48,12 +49,13 @@ EOT
 
         $requires = $composer->getPackage()->getRequires();
         if ($input->getOption('no-dev')) {
-            $dependencies = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'))->getPackages();
+            $installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'));
+            $dependencies = $installedRepo->getPackages();
         } else {
-            $dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages();
+            $installedRepo = $composer->getRepositoryManager()->getLocalRepository();
             // fallback to lockfile if installed repo is empty
-            if (!$dependencies) {
-                $dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages();
+            if (!$installedRepo->getPackages()) {
+                $installedRepo = $composer->getLocker()->getLockedRepository(true);
             }
             $requires += $composer->getPackage()->getDevRequires();
         }
@@ -61,7 +63,8 @@ EOT
             $requires[$require] = array($link);
         }
 
-        foreach ($dependencies as $package) {
+        $installedRepo = new InstalledRepository(array($installedRepo));
+        foreach ($installedRepo->getPackages() as $package) {
             foreach ($package->getRequires() as $require => $link) {
                 $requires[$require][] = $link;
             }
@@ -69,19 +72,9 @@ EOT
 
         ksort($requires);
 
-        $platformRepo = new PlatformRepository(array(), array());
-        $currentPlatformPackages = $platformRepo->getPackages();
-        $currentPlatformPackageMap = array();
-
-        /**
-         * @var PackageInterface $currentPlatformPackage
-         */
-        foreach ($currentPlatformPackages as $currentPlatformPackage) {
-            $currentPlatformPackageMap[$currentPlatformPackage->getName()] = $currentPlatformPackage;
-        }
+        $installedRepo->addRepository(new PlatformRepository(array(), array()));
 
         $results = array();
-
         $exitCode = 0;
 
         /**
@@ -89,42 +82,62 @@ EOT
          */
         foreach ($requires as $require => $links) {
             if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) {
-                if (isset($currentPlatformPackageMap[$require])) {
-                    $pass = true;
-                    $version = $currentPlatformPackageMap[$require]->getVersion();
-
-                    foreach ($links as $link) {
-                        if (!$link->getConstraint()->matches(new Constraint('=', $version))) {
-                            $results[] = array(
-                                $currentPlatformPackageMap[$require]->getPrettyName(),
-                                $currentPlatformPackageMap[$require]->getPrettyVersion(),
-                                $link,
-                                '<error>failed</error>',
-                            );
-                            $pass = false;
-
-                            $exitCode = max($exitCode, 1);
+                $candidates = $installedRepo->findPackagesWithReplacersAndProviders($require);
+                if ($candidates) {
+                    $reqResults = array();
+                    foreach ($candidates as $candidate) {
+                        if ($candidate->getName() === $require) {
+                            $candidateConstraint = new Constraint('=', $candidate->getVersion());
+                            $candidateConstraint->setPrettyString($candidate->getPrettyVersion());
+                        } else {
+                            foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) {
+                                if ($link->getTarget() === $require) {
+                                    $candidateConstraint = $link->getConstraint();
+                                    break;
+                                }
+                            }
+                        }
+
+                        foreach ($links as $link) {
+                            if (!$link->getConstraint()->matches($candidateConstraint)) {
+                                $reqResults[] = array(
+                                    $candidate->getName() === $require ? $candidate->getPrettyName() : $require,
+                                    $candidateConstraint->getPrettyString(),
+                                    $link,
+                                    '<error>failed</error>'.($candidate->getName() === $require ? '' : ' <comment>provided by '.$candidate->getPrettyName().'</comment>'),
+                                );
+
+                                // skip to next candidate
+                                continue 2;
+                            }
                         }
-                    }
 
-                    if ($pass) {
                         $results[] = array(
-                            $currentPlatformPackageMap[$require]->getPrettyName(),
-                            $currentPlatformPackageMap[$require]->getPrettyVersion(),
+                            $candidate->getName() === $require ? $candidate->getPrettyName() : $require,
+                            $candidateConstraint->getPrettyString(),
                             null,
-                            '<info>success</info>',
+                            '<info>success</info>'.($candidate->getName() === $require ? '' : ' <comment>provided by '.$candidate->getPrettyName().'</comment>'),
                         );
+
+                        // candidate matched, skip to next requirement
+                        continue 2;
                     }
-                } else {
-                    $results[] = array(
-                        $require,
-                        'n/a',
-                        $links[0],
-                        '<error>missing</error>',
-                    );
-
-                    $exitCode = max($exitCode, 2);
+
+                    // show the first error from every failed candidate
+                    $results = array_merge($results, $reqResults);
+                    $exitCode = max($exitCode, 1);
+
+                    continue;
                 }
+
+                $results[] = array(
+                    $require,
+                    'n/a',
+                    $links[0],
+                    '<error>missing</error>',
+                );
+
+                $exitCode = max($exitCode, 2);
             }
         }
 

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

@@ -236,7 +236,7 @@ EOT
         }
 
         $settingKey = $input->getArgument('setting-key');
-        if (!$settingKey) {
+        if (!$settingKey || !is_string($settingKey)) {
             return 0;
         }
 

+ 12 - 23
src/Composer/Command/CreateProjectCommand.php

@@ -20,7 +20,6 @@ use Composer\Installer\InstallationManager;
 use Composer\Installer\SuggestedPackagesReporter;
 use Composer\IO\IOInterface;
 use Composer\Package\BasePackage;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Operation\InstallOperation;
 use Composer\Package\Version\VersionSelector;
 use Composer\Package\AliasPackage;
@@ -28,6 +27,7 @@ use Composer\Repository\RepositoryFactory;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\InstalledFilesystemRepository;
+use Composer\Repository\RepositorySet;
 use Composer\Script\ScriptEvents;
 use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputArgument;
@@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
 use Composer\Json\JsonFile;
 use Composer\Config\JsonConfigSource;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Composer\Package\Version\VersionParser;
 
 /**
@@ -182,8 +183,6 @@ EOT
             $composer = Factory::create($io, null, $disablePlugins);
         }
 
-        $composer->getDownloadManager()->setOutputProgress(!$noProgress);
-
         $fs = new Filesystem();
 
         if ($noScripts === false) {
@@ -334,8 +333,8 @@ EOT
             throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities)));
         }
 
-        $pool = new Pool($stability);
-        $pool->addRepository($sourceRepo);
+        $repositorySet = new RepositorySet($stability);
+        $repositorySet->addRepository($sourceRepo);
 
         $phpVersion = null;
         $prettyPhpVersion = null;
@@ -349,7 +348,7 @@ EOT
         }
 
         // find the latest version if there are multiple
-        $versionSelector = new VersionSelector($pool);
+        $versionSelector = new VersionSelector($repositorySet);
         $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability);
 
         if (!$package) {
@@ -384,15 +383,17 @@ EOT
             $package = $package->getAliasOf();
         }
 
-        $dm = $this->createDownloadManager($io, $config);
+        $factory = new Factory();
+
+        $httpDownloader = $factory->createHttpDownloader($io, $config);
+        $dm = $factory->createDownloadManager($io, $config, $httpDownloader);
         $dm->setPreferSource($preferSource)
-            ->setPreferDist($preferDist)
-            ->setOutputProgress(!$noProgress);
+            ->setPreferDist($preferDist);
 
         $projectInstaller = new ProjectInstaller($directory, $dm);
-        $im = $this->createInstallationManager();
+        $im = $factory->createInstallationManager(new Loop($httpDownloader), $io);
         $im->addInstaller($projectInstaller);
-        $im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package));
+        $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package)));
         $im->notifyInstalls($io);
 
         // collect suggestions
@@ -408,16 +409,4 @@ EOT
 
         return $installedFromVcs;
     }
-
-    protected function createDownloadManager(IOInterface $io, Config $config)
-    {
-        $factory = new Factory();
-
-        return $factory->createDownloadManager($io, $config);
-    }
-
-    protected function createInstallationManager()
-    {
-        return new InstallationManager();
-    }
 }

+ 16 - 31
src/Composer/Command/DiagnoseCommand.php

@@ -22,7 +22,7 @@ use Composer\Plugin\PluginEvents;
 use Composer\Util\ConfigValidator;
 use Composer\Util\IniHelper;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\Util\StreamContextFactory;
 use Composer\SelfUpdate\Keys;
 use Composer\SelfUpdate\Versions;
@@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface;
  */
 class DiagnoseCommand extends BaseCommand
 {
-    /** @var RemoteFilesystem */
-    protected $rfs;
+    /** @var HttpDownloader */
+    protected $httpDownloader;
 
     /** @var ProcessExecutor */
     protected $process;
@@ -86,7 +86,7 @@ EOT
         $config->merge(array('config' => array('secure-http' => false)));
         $config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO);
 
-        $this->rfs = Factory::createRemoteFilesystem($io, $config);
+        $this->httpDownloader = Factory::createHttpDownloader($io, $config);
         $this->process = new ProcessExecutor($io);
 
         $io->write('Checking platform settings: ', false);
@@ -156,7 +156,7 @@ EOT
             $this->outputResult($this->checkVersion($config));
         }
 
-        $io->write(sprintf('Composer version: <comment>%s</comment>', Composer::VERSION));
+        $io->write(sprintf('Composer version: <comment>%s</comment>', Composer::getVersion()));
 
         $platformOverrides = $config->get('platform') ?: array();
         $platformRepo = new PlatformRepository(array(), $platformOverrides);
@@ -229,7 +229,7 @@ EOT
         }
 
         try {
-            $this->rfs->getContents('packagist.org', $proto . '://repo.packagist.org/packages.json', false);
+            $this->httpDownloader->get($proto . '://repo.packagist.org/packages.json');
         } catch (TransportException $e) {
             if (false !== strpos($e->getMessage(), 'cafile')) {
                 $result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
@@ -256,11 +256,11 @@ EOT
 
         $protocol = extension_loaded('openssl') ? 'https' : 'http';
         try {
-            $json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/packages.json', false), true);
+            $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson();
             $hash = reset($json['provider-includes']);
             $hash = $hash['sha256'];
             $path = str_replace('%hash%', $hash, key($json['provider-includes']));
-            $provider = $this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/'.$path, false);
+            $provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody();
 
             if (hash('sha256', $provider) !== $hash) {
                 return 'It seems that your proxy is modifying http traffic on the fly';
@@ -288,10 +288,10 @@ EOT
 
         $url = 'http://repo.packagist.org/packages.json';
         try {
-            $this->rfs->getContents('packagist.org', $url, false);
+            $this->httpDownloader->get($url);
         } catch (TransportException $e) {
             try {
-                $this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false)));
+                $this->httpDownloader->get($url, array('http' => array('request_fulluri' => false)));
             } catch (TransportException $e) {
                 return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')';
             }
@@ -322,10 +322,10 @@ EOT
 
         $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0';
         try {
-            $this->rfs->getContents('github.com', $url, false);
+            $this->httpDownloader->get($url);
         } catch (TransportException $e) {
             try {
-                $this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false)));
+                $this->httpDownloader->get($url, array('http' => array('request_fulluri' => false)));
             } catch (TransportException $e) {
                 return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
             }
@@ -347,7 +347,7 @@ EOT
         try {
             $url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/';
 
-            return $this->rfs->getContents($domain, $url, false, array(
+            return $this->httpDownloader->get($url, array(
                 'retry-auth-failure' => false,
             )) ? true : 'Unexpected error';
         } catch (\Exception $e) {
@@ -377,8 +377,7 @@ EOT
         }
 
         $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit';
-        $json = $this->rfs->getContents($domain, $url, false, array('retry-auth-failure' => false));
-        $data = json_decode($json, true);
+        $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson();
 
         return $data['resources']['core'];
     }
@@ -431,7 +430,7 @@ EOT
             return $result;
         }
 
-        $versionsUtil = new Versions($config, $this->rfs);
+        $versionsUtil = new Versions($config, $this->httpDownloader);
         $latest = $versionsUtil->getLatest();
 
         if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') {
@@ -613,20 +612,6 @@ EOT
                         $text .= "Install either of them or recompile php without --disable-iconv";
                         break;
 
-                    case 'unicode':
-                        $text = PHP_EOL."The detect_unicode setting must be disabled.".PHP_EOL;
-                        $text .= "Add the following to the end of your `php.ini`:".PHP_EOL;
-                        $text .= "    detect_unicode = Off";
-                        $displayIniMessage = true;
-                        break;
-
-                    case 'suhosin':
-                        $text = PHP_EOL."The suhosin.executor.include.whitelist setting is incorrect.".PHP_EOL;
-                        $text .= "Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):".PHP_EOL;
-                        $text .= "    suhosin.executor.include.whitelist = phar ".$current;
-                        $displayIniMessage = true;
-                        break;
-
                     case 'php':
                         $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher.";
                         break;
@@ -729,7 +714,7 @@ EOT
     /**
      * Check if allow_url_fopen is ON
      *
-     * @return bool|string
+     * @return true|string
      */
     private function checkConnectivity()
     {

+ 2 - 2
src/Composer/Command/HomeCommand.php

@@ -14,7 +14,7 @@ namespace Composer\Command;
 
 use Composer\Package\CompletePackageInterface;
 use Composer\Repository\RepositoryInterface;
-use Composer\Repository\ArrayRepository;
+use Composer\Repository\RootPackageRepository;
 use Composer\Repository\RepositoryFactory;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
@@ -157,7 +157,7 @@ EOT
 
         if ($composer) {
             return array_merge(
-                array(new ArrayRepository(array($composer->getPackage()))), // root package
+                array(new RootPackageRepository($composer->getPackage())), // root package
                 array($composer->getRepositoryManager()->getLocalRepository()), // installed packages
                 $composer->getRepositoryManager()->getRepositories() // remotes
             );

+ 12 - 12
src/Composer/Command/InitCommand.php

@@ -12,7 +12,6 @@
 
 namespace Composer\Command;
 
-use Composer\DependencyResolver\Pool;
 use Composer\Factory;
 use Composer\Json\JsonFile;
 use Composer\Package\BasePackage;
@@ -22,6 +21,7 @@ use Composer\Package\Version\VersionSelector;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryFactory;
+use Composer\Repository\RepositorySet;
 use Composer\Util\ProcessExecutor;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Input\InputInterface;
@@ -42,8 +42,8 @@ class InitCommand extends BaseCommand
     /** @var array */
     private $gitConfig;
 
-    /** @var Pool[] */
-    private $pools;
+    /** @var RepositorySet[] */
+    private $repositorySets;
 
     /**
      * {@inheritdoc}
@@ -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']);
@@ -688,16 +688,16 @@ EOT
         return false !== filter_var($email, FILTER_VALIDATE_EMAIL);
     }
 
-    private function getPool(InputInterface $input, $minimumStability = null)
+    private function getRepositorySet(InputInterface $input, $minimumStability = null)
     {
         $key = $minimumStability ?: 'default';
 
-        if (!isset($this->pools[$key])) {
-            $this->pools[$key] = $pool = new Pool($minimumStability ?: $this->getMinimumStability($input));
-            $pool->addRepository($this->getRepos());
+        if (!isset($this->repositorySets[$key])) {
+            $this->repositorySets[$key] = $repositorySet = new RepositorySet($minimumStability ?: $this->getMinimumStability($input));
+            $repositorySet->addRepository($this->getRepos());
         }
 
-        return $this->pools[$key];
+        return $this->repositorySets[$key];
     }
 
     private function getMinimumStability(InputInterface $input)
@@ -733,8 +733,8 @@ EOT
      */
     private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null)
     {
-        // find the latest version allowed in this pool
-        $versionSelector = new VersionSelector($this->getPool($input, $minimumStability));
+        // find the latest version allowed in this repo set
+        $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability));
         $ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs');
 
         // ignore phpVersion if platform requirements are ignored

+ 0 - 3
src/Composer/Command/InstallCommand.php

@@ -44,7 +44,6 @@ class InstallCommand 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('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
                 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`.'),
@@ -86,7 +85,6 @@ EOT
         }
 
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
-        $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
 
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@@ -108,7 +106,6 @@ EOT
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)

+ 42 - 7
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;
@@ -38,6 +39,7 @@ class RemoveCommand extends BaseCommand
             ->setDefinition(array(
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'),
+                new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
@@ -92,26 +94,44 @@ EOT
             }
         }
 
+        $dryRun = $input->getOption('dry-run');
+        $toRemove = array();
         foreach ($packages as $package) {
             if (isset($composer[$type][$package])) {
-                $json->removeLink($type, $composer[$type][$package]);
+                if ($dryRun) {
+                    $toRemove[$type][] = $composer[$type][$package];
+                } else {
+                    $json->removeLink($type, $composer[$type][$package]);
+                }
             } elseif (isset($composer[$altType][$package])) {
                 $io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                 if ($io->isInteractive()) {
                     if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
-                        $json->removeLink($altType, $composer[$altType][$package]);
+                        if ($dryRun) {
+                            $toRemove[$altType][] = $composer[$altType][$package];
+                        } else {
+                            $json->removeLink($altType, $composer[$altType][$package]);
+                        }
                     }
                 }
             } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) {
                 foreach ($matches as $matchedPackage) {
-                    $json->removeLink($type, $matchedPackage);
+                    if ($dryRun) {
+                        $toRemove[$type][] = $matchedPackage;
+                    } else {
+                        $json->removeLink($type, $matchedPackage);
+                    }
                 }
             } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) {
                 foreach ($matches as $matchedPackage) {
                     $io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                     if ($io->isInteractive()) {
                         if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
-                            $json->removeLink($altType, $matchedPackage);
+                            if ($dryRun) {
+                                $toRemove[$altType][] = $matchedPackage;
+                            } else {
+                                $json->removeLink($altType, $matchedPackage);
+                            }
                         }
                     }
                 }
@@ -127,7 +147,21 @@ EOT
         // Update packages
         $this->resetComposer();
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
-        $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
+
+        if ($dryRun) {
+            $rootPackage = $composer->getPackage();
+            $links = array(
+                'require' => $rootPackage->getRequires(),
+                'require-dev' => $rootPackage->getDevRequires(),
+            );
+            foreach ($toRemove as $type => $packages) {
+                foreach ($packages as $package) {
+                    unset($links[$type][$package]);
+                }
+            }
+            $rootPackage->setRequires($links['require']);
+            $rootPackage->setDevRequires($links['require-dev']);
+        }
 
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@@ -146,10 +180,11 @@ 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)
         ;
 
         $status = $install->run();

+ 69 - 11
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;
@@ -21,6 +22,8 @@ use Composer\Installer;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonManipulator;
 use Composer\Package\Version\VersionParser;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\BasePackage;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Repository\CompositeRepository;
@@ -35,9 +38,14 @@ use Composer\Util\Silencer;
 class RequireCommand extends InitCommand
 {
     private $newlyCreated;
+    private $firstRequire;
     private $json;
     private $file;
     private $composerBackup;
+    /** @var string file name */
+    private $lock;
+    /** @var ?string contents before modification if the lock file exists */
+    private $lockBackup;
 
     protected function configure()
     {
@@ -47,16 +55,19 @@ class RequireCommand extends InitCommand
             ->setDefinition(array(
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
+                new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
                 new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
                 new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
+                new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
                 new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
-                new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
                 new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
                 new InputOption('update-with-dependencies', null, InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'),
                 new InputOption('update-with-all-dependencies', null, InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
+                new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-dependencies'),
+                new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'),
                 new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'),
                 new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'),
                 new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'),
@@ -113,7 +124,9 @@ EOT
         }
 
         $this->json = new JsonFile($this->file);
+        $this->lock = Factory::getLockFile($this->file);
         $this->composerBackup = file_get_contents($this->json->getPath());
+        $this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null;
 
         // check for writability by writing to the file as is_writable can not be trusted on network-mounts
         // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
@@ -186,7 +199,15 @@ EOT
 
         $sortPackages = $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages');
 
-        if (!$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) {
+        $this->firstRequire = $this->newlyCreated;
+        if (!$this->firstRequire) {
+            $composerDefinition = $this->json->read();
+            if (empty($composerDefinition['require']) && empty($composerDefinition['require-dev'])) {
+                $this->firstRequire = true;
+            }
+        }
+
+        if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) {
             $composerDefinition = $this->json->read();
             foreach ($requirements as $package => $version) {
                 $composerDefinition[$requireKey][$package] = $version;
@@ -202,51 +223,78 @@ EOT
         }
 
         try {
-            return $this->doUpdate($input, $output, $io, $requirements);
+            return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey);
         } catch (\Exception $e) {
             $this->revertComposerFile(false);
             throw $e;
         }
     }
 
-    private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements)
+    private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, $requireKey, $removeKey)
     {
         // Update packages
         $this->resetComposer();
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
-        $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
+
+        if ($input->getOption('dry-run')) {
+            $rootPackage = $composer->getPackage();
+            $links = array(
+                'require' => $rootPackage->getRequires(),
+                'require-dev' => $rootPackage->getDevRequires(),
+            );
+            $loader = new ArrayLoader();
+            $newLinks = $loader->parseLinks($rootPackage->getName(), $rootPackage->getPrettyVersion(), BasePackage::$supportedLinkTypes[$requireKey]['description'], $requirements);
+            $links[$requireKey] = array_merge($links[$requireKey], $newLinks);
+            foreach ($requirements as $package => $constraint) {
+                unset($links[$removeKey][$package]);
+            }
+            $rootPackage->setRequires($links['require']);
+            $rootPackage->setDevRequires($links['require-dev']);
+        }
 
         $updateDevMode = !$input->getOption('update-no-dev');
         $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader');
         $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') || $input->getOption('with-all-dependencies')) {
+            $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS;
+        } elseif ($input->getOption('update-with-dependencies') || $input->getOption('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);
 
         $install = Installer::create($io, $composer);
 
         $install
+            ->setDryRun($input->getOption('dry-run'))
             ->setVerbose($input->getOption('verbose'))
             ->setPreferSource($input->getOption('prefer-source'))
             ->setPreferDist($input->getOption('prefer-dist'))
             ->setDevMode($updateDevMode)
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setUpdate(true)
-            ->setUpdateWhitelist(array_keys($requirements))
-            ->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'))
+            ->setDryRun($input->getOption('dry-run'))
         ;
 
+        // 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->setUpdateAllowList(array_keys($requirements));
+        }
+
         $status = $install->run();
-        if ($status !== 0) {
+        if ($status !== 0 || $input->getOption('dry-run')) {
             $this->revertComposerFile(false);
         }
 
@@ -285,9 +333,19 @@ EOT
         if ($this->newlyCreated) {
             $io->writeError("\n".'<error>Installation failed, deleting '.$this->file.'.</error>');
             unlink($this->json->getPath());
+            if (file_exists($this->lock)) {
+                unlink($this->lock);
+            }
         } else {
-            $io->writeError("\n".'<error>Installation failed, reverting '.$this->file.' to its original content.</error>');
+            $msg = ' to its ';
+            if ($this->lockBackup) {
+                $msg = ' and '.$this->lock.' to their ';
+            }
+            $io->writeError("\n".'<error>Installation failed, reverting '.$this->file.$msg.'original content.</error>');
             file_put_contents($this->json->getPath(), $this->composerBackup);
+            if ($this->lockBackup) {
+                file_put_contents($this->lock, $this->lockBackup);
+            }
         }
 
         if ($hardExit) {

+ 5 - 5
src/Composer/Command/SelfUpdateCommand.php

@@ -77,9 +77,9 @@ EOT
         }
 
         $io = $this->getIO();
-        $remoteFilesystem = Factory::createRemoteFilesystem($io, $config);
+        $httpDownloader = Factory::createHttpDownloader($io, $config);
 
-        $versionsUtil = new Versions($config, $remoteFilesystem);
+        $versionsUtil = new Versions($config, $httpDownloader);
 
         // switch channel if requested
         foreach (array('stable', 'preview', 'snapshot') as $channel) {
@@ -154,11 +154,11 @@ EOT
 
         $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion);
 
-        $io->write(sprintf("Updating to version <info>%s</info> (%s channel).", $updateVersion, $versionsUtil->getChannel()));
+        $io->write(sprintf("Upgrading to version <info>%s</info> (%s channel).", $updateVersion, $versionsUtil->getChannel()));
         $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar');
-        $signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false);
+        $signature = $httpDownloader->get($remoteFilename.'.sig')->getBody();
         $io->writeError('   ', false);
-        $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress'));
+        $httpDownloader->copy($remoteFilename, $tempFilename);
         $io->writeError('');
 
         if (!file_exists($tempFilename) || !$signature) {

+ 66 - 68
src/Composer/Command/ShowCommand.php

@@ -14,7 +14,6 @@ namespace Composer\Command;
 
 use Composer\Composer;
 use Composer\DependencyResolver\DefaultPolicy;
-use Composer\DependencyResolver\Pool;
 use Composer\Json\JsonFile;
 use Composer\Package\BasePackage;
 use Composer\Package\CompletePackageInterface;
@@ -23,12 +22,14 @@ use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionSelector;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
-use Composer\Repository\ArrayRepository;
 use Composer\Repository\ComposerRepository;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryFactory;
+use Composer\Repository\InstalledRepository;
 use Composer\Repository\RepositoryInterface;
+use Composer\Repository\RepositorySet;
+use Composer\Repository\RootPackageRepository;
 use Composer\Semver\Constraint\ConstraintInterface;
 use Composer\Semver\Semver;
 use Composer\Spdx\SpdxLicenses;
@@ -52,8 +53,8 @@ class ShowCommand extends BaseCommand
     protected $versionParser;
     protected $colors;
 
-    /** @var Pool */
-    private $pool;
+    /** @var RepositorySet */
+    private $repositorySet;
 
     protected function configure()
     {
@@ -152,13 +153,14 @@ EOT
 
         if ($input->getOption('self')) {
             $package = $this->getComposer()->getPackage();
-            $repos = $installedRepo = new ArrayRepository(array($package));
+            $repos = $installedRepo = new InstalledRepository(array(new RootPackageRepository($package)));
         } elseif ($input->getOption('platform')) {
-            $repos = $installedRepo = $platformRepo;
+            $repos = $installedRepo = new InstalledRepository(array($platformRepo));
         } elseif ($input->getOption('available')) {
-            $installedRepo = $platformRepo;
+            $installedRepo = new InstalledRepository(array($platformRepo));
             if ($composer) {
                 $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories());
+                $installedRepo->addRepository($composer->getRepositoryManager()->getLocalRepository());
             } else {
                 $defaultRepos = RepositoryFactory::defaultRepos($io);
                 $repos = new CompositeRepository($defaultRepos);
@@ -166,15 +168,15 @@ EOT
             }
         } elseif ($input->getOption('all') && $composer) {
             $localRepo = $composer->getRepositoryManager()->getLocalRepository();
-            $installedRepo = new CompositeRepository(array($localRepo, $platformRepo));
+            $installedRepo = new InstalledRepository(array($localRepo, $platformRepo));
             $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories()));
         } elseif ($input->getOption('all')) {
             $defaultRepos = RepositoryFactory::defaultRepos($io);
             $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos)));
-            $installedRepo = $platformRepo;
+            $installedRepo = new InstalledRepository(array($platformRepo));
             $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
         } else {
-            $repos = $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
+            $repos = $installedRepo = new InstalledRepository(array($this->getComposer()->getRepositoryManager()->getLocalRepository()));
             $rootPkg = $this->getComposer()->getPackage();
             if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) {
                 $io->writeError('<warning>No dependencies installed. Try running composer install or update.</warning>');
@@ -313,16 +315,13 @@ EOT
         foreach ($repos as $repo) {
             if ($repo === $platformRepo) {
                 $type = 'platform';
-            } elseif (
-                $repo === $installedRepo
-                || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true))
-            ) {
+            } elseif ($repo === $installedRepo || in_array($repo, $installedRepo->getRepositories(), true)) {
                 $type = 'installed';
             } else {
                 $type = 'available';
             }
-            if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
-                foreach ($repo->getProviderNames() as $name) {
+            if ($repo instanceof ComposerRepository) {
+                foreach ($repo->getPackageNames() as $name) {
                     if (!$packageFilter || preg_match($packageFilter, $name)) {
                         $packages[$type][$name] = $name;
                     }
@@ -528,32 +527,27 @@ EOT
     /**
      * finds a package by name and version if provided
      *
-     * @param  RepositoryInterface        $installedRepo
+     * @param  InstalledRepository        $installedRepo
      * @param  RepositoryInterface        $repos
      * @param  string                     $name
      * @param  ConstraintInterface|string $version
      * @throws \InvalidArgumentException
      * @return array                      array(CompletePackageInterface, array of versions)
      */
-    protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null)
+    protected function getPackage(InstalledRepository $installedRepo, RepositoryInterface $repos, $name, $version = null)
     {
         $name = strtolower($name);
         $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version;
 
         $policy = new DefaultPolicy();
-        $pool = new Pool('dev');
-        $pool->addRepository($repos);
+        $repositorySet = new RepositorySet('dev');
+        $repositorySet->allowInstalledRepositories();
+        $repositorySet->addRepository($repos);
 
         $matchedPackage = null;
         $versions = array();
-        $matches = $pool->whatProvides($name, $constraint);
+        $matches = $repositorySet->findPackages($name, $constraint);
         foreach ($matches as $index => $package) {
-            // skip providers/replacers
-            if ($package->getName() !== $name) {
-                unset($matches[$index]);
-                continue;
-            }
-
             // select an exact match if it is in the installed repo and no specific version was required
             if (null === $version && $installedRepo->hasPackage($package)) {
                 $matchedPackage = $package;
@@ -563,8 +557,10 @@ EOT
             $matches[$index] = $package->getId();
         }
 
+        $pool = $repositorySet->createPoolForPackage($name);
+
         // select preferred package according to policy rules
-        if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) {
+        if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) {
             $matchedPackage = $pool->literalToPackage($preferred[0]);
         }
 
@@ -576,10 +572,10 @@ EOT
      *
      * @param CompletePackageInterface $package
      * @param array                    $versions
-     * @param RepositoryInterface      $installedRepo
+     * @param InstalledRepository      $installedRepo
      * @param PackageInterface|null    $latestPackage
      */
-    protected function printPackageInfo(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
+    protected function printPackageInfo(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null)
     {
         $io = $this->getIO();
 
@@ -604,10 +600,10 @@ EOT
      *
      * @param CompletePackageInterface $package
      * @param array                    $versions
-     * @param RepositoryInterface      $installedRepo
+     * @param InstalledRepository      $installedRepo
      * @param PackageInterface|null    $latestPackage
      */
-    protected function printMeta(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
+    protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null)
     {
         $io = $this->getIO();
         $io->write('<info>name</info>     : ' . $package->getPrettyName());
@@ -676,19 +672,21 @@ EOT
      *
      * @param CompletePackageInterface $package
      * @param array                    $versions
-     * @param RepositoryInterface      $installedRepo
+     * @param InstalledRepository      $installedRepo
      */
-    protected function printVersions(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo)
+    protected function printVersions(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo)
     {
-        uasort($versions, 'version_compare');
-        $versions = array_keys(array_reverse($versions));
+        $versions = array_keys($versions);
+        $versions = Semver::rsort($versions);
 
         // highlight installed version
-        if ($installedRepo->hasPackage($package)) {
-            $installedVersion = $package->getPrettyVersion();
-            $key = array_search($installedVersion, $versions);
-            if (false !== $key) {
-                $versions[$key] = '<info>* ' . $installedVersion . '</info>';
+        if ($installedPackages = $installedRepo->findPackages($package->getName())) {
+            foreach ($installedPackages as $installedPackage) {
+                $installedVersion = $installedPackage->getPrettyVersion();
+                $key = array_search($installedVersion, $versions);
+                if (false !== $key) {
+                    $versions[$key] = '<info>* ' . $installedVersion . '</info>';
+                }
             }
         }
 
@@ -752,10 +750,10 @@ EOT
      *
      * @param CompletePackageInterface $package
      * @param array                    $versions
-     * @param RepositoryInterface      $installedRepo
+     * @param InstalledRepository      $installedRepo
      * @param PackageInterface|null    $latestPackage
      */
-    protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
+    protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null)
     {
         $json = array(
             'name' => $package->getPrettyName(),
@@ -975,15 +973,15 @@ EOT
     /**
      * Generate the package tree
      *
-     * @param  PackageInterface $package
-     * @param  RepositoryInterface     $installedRepo
-     * @param  RepositoryInterface     $distantRepos
+     * @param  PackageInterface    $package
+     * @param  InstalledRepository $installedRepo
+     * @param  RepositoryInterface $remoteRepos
      * @return array
      */
     protected function generatePackageTree(
         PackageInterface $package,
-        RepositoryInterface $installedRepo,
-        RepositoryInterface $distantRepos
+        InstalledRepository $installedRepo,
+        RepositoryInterface $remoteRepos
     ) {
         $requires = $package->getRequires();
         ksort($requires);
@@ -996,7 +994,7 @@ EOT
                 'version' => $require->getPrettyConstraint(),
             );
 
-            $deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree);
+            $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $packagesInTree);
 
             if ($deepChildren) {
                 $treeChildDesc['requires'] = $deepChildren;
@@ -1020,10 +1018,10 @@ EOT
     /**
      * Display a package tree
      *
-     * @param PackageInterface|string $package
-     * @param array                   $packagesInTree
-     * @param string                  $previousTreeBar
-     * @param int                     $level
+     * @param array|string $package
+     * @param array        $packagesInTree
+     * @param string       $previousTreeBar
+     * @param int          $level
      */
     protected function displayTree(
         $package,
@@ -1032,7 +1030,7 @@ EOT
         $level = 1
     ) {
         $previousTreeBar = str_replace('├', '│', $previousTreeBar);
-        if (isset($package['requires'])) {
+        if (is_array($package) && isset($package['requires'])) {
             $requires = $package['requires'];
             $treeBar = $previousTreeBar . '  ├';
             $i = 0;
@@ -1075,22 +1073,22 @@ EOT
      *
      * @param  string                  $name
      * @param  PackageInterface|string $package
-     * @param  RepositoryInterface     $installedRepo
-     * @param  RepositoryInterface     $distantRepos
+     * @param  InstalledRepository     $installedRepo
+     * @param  RepositoryInterface     $remoteRepos
      * @param  array                   $packagesInTree
      * @return array
      */
     protected function addTree(
         $name,
         $package,
-        RepositoryInterface $installedRepo,
-        RepositoryInterface $distantRepos,
+        InstalledRepository $installedRepo,
+        RepositoryInterface $remoteRepos,
         array $packagesInTree
     ) {
         $children = array();
         list($package, $versions) = $this->getPackage(
             $installedRepo,
-            $distantRepos,
+            $remoteRepos,
             $name,
             $package->getPrettyConstraint() === 'self.version' ? $package->getConstraint() : $package->getPrettyConstraint()
         );
@@ -1107,7 +1105,7 @@ EOT
 
                 if (!in_array($requireName, $currentTree, true)) {
                     $currentTree[] = $requireName;
-                    $deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $currentTree);
+                    $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $currentTree);
                     if ($deepChildren) {
                         $treeChildDesc['requires'] = $deepChildren;
                     }
@@ -1165,13 +1163,13 @@ EOT
      * @param string           $phpVersion
      * @param bool             $minorOnly
      *
-     * @return PackageInterface|null
+     * @return PackageInterface|false
      */
     private function findLatestPackage(PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false)
     {
-        // find the latest version allowed in this pool
+        // find the latest version allowed in this repo set
         $name = $package->getName();
-        $versionSelector = new VersionSelector($this->getPool($composer));
+        $versionSelector = new VersionSelector($this->getRepositorySet($composer));
         $stability = $composer->getPackage()->getMinimumStability();
         $flags = $composer->getPackage()->getStabilityFlags();
         if (isset($flags[$name])) {
@@ -1195,13 +1193,13 @@ EOT
         return $versionSelector->findBestCandidate($name, $targetVersion, $phpVersion, $bestStability);
     }
 
-    private function getPool(Composer $composer)
+    private function getRepositorySet(Composer $composer)
     {
-        if (!$this->pool) {
-            $this->pool = new Pool($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags());
-            $this->pool->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories()));
+        if (!$this->repositorySet) {
+            $this->repositorySet = new RepositorySet($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags());
+            $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories()));
         }
 
-        return $this->pool;
+        return $this->repositorySet;
     }
 }

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

@@ -90,7 +90,7 @@ EOT
 
         // list packages
         foreach ($installedRepo->getCanonicalPackages() as $package) {
-            $downloader = $dm->getDownloaderForInstalledPackage($package);
+            $downloader = $dm->getDownloaderForPackage($package);
             $targetDir = $im->getInstallPath($package);
 
             if ($downloader instanceof ChangeReportInterface) {

+ 43 - 90
src/Composer/Command/SuggestsCommand.php

@@ -13,6 +13,9 @@
 namespace Composer\Command;
 
 use Composer\Repository\PlatformRepository;
+use Composer\Repository\RootPackageRepository;
+use Composer\Repository\CompositeRepository;
+use Composer\Installer\SuggestedPackagesReporter;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -26,8 +29,10 @@ class SuggestsCommand extends BaseCommand
             ->setName('suggests')
             ->setDescription('Shows package suggestions.')
             ->setDefinition(array(
-                new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package'),
+                new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'),
                 new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'),
+                new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'),
+                new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'),
                 new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'),
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'),
             ))
@@ -36,118 +41,66 @@ class SuggestsCommand extends BaseCommand
 
 The <info>%command.name%</info> command shows a sorted list of suggested packages.
 
-Enabling <info>-v</info> implies <info>--by-package --by-suggestion</info>, showing both lists.
-
 Read more at https://getcomposer.org/doc/03-cli.md#suggests
 EOT
             )
         ;
     }
 
+    /**
+     * {@inheritDoc}
+     */
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $lock = $this->getComposer()->getLocker()->getLockData();
-
-        if (empty($lock)) {
-            throw new \RuntimeException('Lockfile seems to be empty?');
+        $composer = $this->getComposer();
+
+        $installedRepos = array(
+            new RootPackageRepository(clone $composer->getPackage()),
+        );
+
+        $locker = $composer->getLocker();
+        if ($locker->isLocked()) {
+            $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides());
+            $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev'));
+        } else {
+            $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array());
+            $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository();
         }
 
-        $packages = $lock['packages'];
-
-        if (!$input->getOption('no-dev')) {
-            $packages += $lock['packages-dev'];
-        }
+        $installedRepo = new CompositeRepository($installedRepos);
+        $reporter = new SuggestedPackagesReporter($this->getIO());
 
         $filter = $input->getArgument('packages');
-
-        // First assemble lookup list of packages that are installed, replaced or provided
-        $installed = array();
-        foreach ($packages as $package) {
-            $installed[] = $package['name'];
-
-            if (!empty($package['provide'])) {
-                $installed = array_merge($installed, array_keys($package['provide']));
-            }
-
-            if (!empty($package['replace'])) {
-                $installed = array_merge($installed, array_keys($package['replace']));
-            }
+        if (empty($filter) && !$input->getOption('all')) {
+            $filter = array_map(function ($link) {
+                return $link->getTarget();
+            }, array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires()));
         }
-
-        // Undub and sort the install list into a sorted lookup array
-        $installed = array_flip($installed);
-        ksort($installed);
-
-        // Init platform repo
-        $platform = new PlatformRepository(array(), $this->getComposer()->getConfig()->get('platform') ?: array());
-
-        // Next gather all suggestions that are not in that list
-        $suggesters = array();
-        $suggested = array();
-        foreach ($packages as $package) {
-            $packageName = $package['name'];
-            if ((!empty($filter) && !in_array($packageName, $filter)) || empty($package['suggest'])) {
+        foreach ($installedRepo->getPackages() as $package) {
+            if (!empty($filter) && !in_array($package->getName(), $filter)) {
                 continue;
             }
-            foreach ($package['suggest'] as $suggestion => $reason) {
-                if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $suggestion) && null !== $platform->findPackage($suggestion, '*')) {
-                    continue;
-                }
-                if (!isset($installed[$suggestion])) {
-                    $suggesters[$packageName][$suggestion] = $reason;
-                    $suggested[$suggestion][$packageName] = $reason;
-                }
-            }
+
+            $reporter->addSuggestionsFromPackage($package);
         }
-        ksort($suggesters);
-        ksort($suggested);
 
-        // Determine output mode
-        $mode = 0;
+        // Determine output mode, default is by-package
+        $mode = SuggestedPackagesReporter::MODE_BY_PACKAGE;
         $io = $this->getIO();
-        if ($input->getOption('by-package') || $io->isVerbose()) {
-            $mode |= 1;
-        }
+        // if by-suggestion is given we override the default
         if ($input->getOption('by-suggestion')) {
-            $mode |= 2;
+            $mode = SuggestedPackagesReporter::MODE_BY_SUGGESTION;
         }
-
-        // Simple mode
-        if ($mode === 0) {
-            foreach (array_keys($suggested) as $suggestion) {
-                $io->write(sprintf('<info>%s</info>', $suggestion));
-            }
-
-            return 0;
+        // unless by-package is also present then we enable both
+        if ($input->getOption('by-package')) {
+            $mode |= SuggestedPackagesReporter::MODE_BY_PACKAGE;
         }
-
-        // Grouped by package
-        if ($mode & 1) {
-            foreach ($suggesters as $suggester => $suggestions) {
-                $io->write(sprintf('<comment>%s</comment> suggests:', $suggester));
-
-                foreach ($suggestions as $suggestion => $reason) {
-                    $io->write(sprintf(' - <info>%s</info>: %s', $suggestion, $reason ?: '*'));
-                }
-                $io->write('');
-            }
+        // list is exclusive and overrides everything else
+        if ($input->getOption('list')) {
+            $mode = SuggestedPackagesReporter::MODE_LIST;
         }
 
-        // Grouped by suggestion
-        if ($mode & 2) {
-            // Improve readability in full mode
-            if ($mode & 1) {
-                $io->write(str_repeat('-', 78));
-            }
-            foreach ($suggested as $suggestion => $suggesters) {
-                $io->write(sprintf('<comment>%s</comment> is suggested by:', $suggestion));
-
-                foreach ($suggesters as $suggester => $reason) {
-                    $io->write(sprintf(' - <info>%s</info>: %s', $suggester, $reason ?: '*'));
-                }
-                $io->write('');
-            }
-        }
+        $reporter->output($mode, $installedRepo);
 
         return 0;
     }

+ 25 - 8
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,9 +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('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
-                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`.'),
@@ -121,7 +121,18 @@ EOT
             }
         }
 
-        $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
+        // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead
+        // they are further mutually exclusive with listing actual package names
+        $filteredPackages = array_filter($packages, function ($package) {
+            return !in_array($package, array('lock', 'nothing', 'mirrors'), true);
+        });
+        $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages);
+        $packages = $filteredPackages;
+
+        if ($updateMirrors && !empty($packages)) {
+            $io->writeError('<error>You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.</error>');
+            return -1;
+        }
 
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@@ -135,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'))
@@ -143,14 +161,13 @@ EOT
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setUpdate(true)
-            ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages)
-            ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies'))
-            ->setWhitelistAllDependencies($input->getOption('with-all-dependencies'))
+            ->setUpdateMirrors($updateMirrors)
+            ->setUpdateAllowList($packages)
+            ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
             ->setPreferStable($input->getOption('prefer-stable'))
             ->setPreferLowest($input->getOption('prefer-lowest'))

+ 1 - 0
src/Composer/Compiler.php

@@ -124,6 +124,7 @@ class Compiler
             ->in(__DIR__.'/../../vendor/composer/ca-bundle/')
             ->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
             ->in(__DIR__.'/../../vendor/psr/')
+            ->in(__DIR__.'/../../vendor/react/')
             ->sort($finderSort)
         ;
 

+ 1 - 1
src/Composer/Composer.php

@@ -53,7 +53,7 @@ class Composer
     const VERSION = '@package_version@';
     const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@';
     const RELEASE_DATE = '@release_date@';
-    const SOURCE_VERSION = '1.10-dev+source';
+    const SOURCE_VERSION = '2.0-dev+source';
 
     public static function getVersion()
     {

+ 1 - 1
src/Composer/Config/JsonConfigSource.php

@@ -265,7 +265,7 @@ class JsonConfigSource implements ConfigSourceInterface
      *
      * @param  array $array
      * @param  mixed $value
-     * @return array
+     * @return int
      */
     private function arrayUnshiftRef(&$array, &$value)
     {

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

@@ -230,6 +230,12 @@ class Application extends BaseApplication
                 if (function_exists('posix_getuid') && posix_getuid() === 0) {
                     if ($commandName !== 'self-update' && $commandName !== 'selfupdate') {
                         $io->writeError('<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>');
+                        
+                        if ($io->isInteractive()) {
+                            if (!$io->askConfirmation('<info>Continue as root/super user</info> [<comment>yes</comment>]? ', true)) {
+                                return 1;
+                            }
+                        }
                     }
                     if ($uid = (int) getenv('SUDO_UID')) {
                         // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on
@@ -292,7 +298,7 @@ class Application extends BaseApplication
 
             return $result;
         } catch (ScriptExecutionException $e) {
-            return $e->getCode();
+            return (int) $e->getCode();
         } catch (\Exception $e) {
             $this->hintCommonErrors($e);
             restore_error_handler();

+ 43 - 112
src/Composer/DependencyResolver/DefaultPolicy.php

@@ -44,54 +44,33 @@ class DefaultPolicy implements PolicyInterface
         return $constraint->matchSpecific($version, true);
     }
 
-    public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false)
+    public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null)
     {
-        $packages = array();
-
-        foreach ($pool->whatProvides($package->getName(), null, $mustMatchName) as $candidate) {
-            if ($candidate !== $package) {
-                $packages[] = $candidate;
-            }
-        }
-
-        return $packages;
-    }
+        $packages = $this->groupLiteralsByName($pool, $literals);
 
-    public function getPriority(Pool $pool, PackageInterface $package)
-    {
-        return $pool->getPriority($package->getRepository());
-    }
-
-    public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null)
-    {
-        $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals);
-
-        foreach ($packages as &$literals) {
+        foreach ($packages as &$nameLiterals) {
             $policy = $this;
-            usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) {
-                return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
+            usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) {
+                return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
             });
         }
 
-        foreach ($packages as &$literals) {
-            $literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals);
-
-            $literals = $this->pruneToBestVersion($pool, $literals);
-
-            $literals = $this->pruneRemoteAliases($pool, $literals);
+        foreach ($packages as &$sortedLiterals) {
+            $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals);
+            $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals);
         }
 
         $selected = call_user_func_array('array_merge', $packages);
 
         // now sort the result across all packages to respect replaces across packages
-        usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) {
-            return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
+        usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) {
+            return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
         });
 
         return $selected;
     }
 
-    protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals)
+    protected function groupLiteralsByName(Pool $pool, $literals)
     {
         $packages = array();
         foreach ($literals as $literal) {
@@ -100,12 +79,7 @@ class DefaultPolicy implements PolicyInterface
             if (!isset($packages[$packageName])) {
                 $packages[$packageName] = array();
             }
-
-            if (isset($installedMap[abs($literal)])) {
-                array_unshift($packages[$packageName], $literal);
-            } else {
-                $packages[$packageName][] = $literal;
-            }
+            $packages[$packageName][] = $literal;
         }
 
         return $packages;
@@ -114,61 +88,49 @@ class DefaultPolicy implements PolicyInterface
     /**
      * @protected
      */
-    public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false)
+    public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false)
     {
-        if ($a->getRepository() === $b->getRepository()) {
-            // prefer aliases to the original package
-            if ($a->getName() === $b->getName()) {
-                $aAliased = $a instanceof AliasPackage;
-                $bAliased = $b instanceof AliasPackage;
-                if ($aAliased && !$bAliased) {
-                    return -1; // use a
-                }
-                if (!$aAliased && $bAliased) {
-                    return 1; // use b
-                }
+        // prefer aliases to the original package
+        if ($a->getName() === $b->getName()) {
+            $aAliased = $a instanceof AliasPackage;
+            $bAliased = $b instanceof AliasPackage;
+            if ($aAliased && !$bAliased) {
+                return -1; // use a
+            }
+            if (!$aAliased && $bAliased) {
+                return 1; // use b
             }
+        }
 
-            if (!$ignoreReplace) {
-                // return original, not replaced
-                if ($this->replaces($a, $b)) {
-                    return 1; // use b
-                }
-                if ($this->replaces($b, $a)) {
-                    return -1; // use a
-                }
+        if (!$ignoreReplace) {
+            // return original, not replaced
+            if ($this->replaces($a, $b)) {
+                return 1; // use b
+            }
+            if ($this->replaces($b, $a)) {
+                return -1; // use a
+            }
 
-                // for replacers not replacing each other, put a higher prio on replacing
-                // packages with the same vendor as the required package
-                if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) {
-                    $requiredVendor = substr($requiredPackage, 0, $pos);
+            // for replacers not replacing each other, put a higher prio on replacing
+            // packages with the same vendor as the required package
+            if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) {
+                $requiredVendor = substr($requiredPackage, 0, $pos);
 
-                    $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor;
-                    $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor;
+                $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor;
+                $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor;
 
-                    if ($bIsSameVendor !== $aIsSameVendor) {
-                        return $aIsSameVendor ? -1 : 1;
-                    }
+                if ($bIsSameVendor !== $aIsSameVendor) {
+                    return $aIsSameVendor ? -1 : 1;
                 }
             }
-
-            // priority equal, sort by package id to make reproducible
-            if ($a->id === $b->id) {
-                return 0;
-            }
-
-            return ($a->id < $b->id) ? -1 : 1;
-        }
-
-        if (isset($installedMap[$a->id])) {
-            return -1;
         }
 
-        if (isset($installedMap[$b->id])) {
-            return 1;
+        // priority equal, sort by package id to make reproducible
+        if ($a->id === $b->id) {
+            return 0;
         }
 
-        return ($this->getPriority($pool, $a) > $this->getPriority($pool, $b)) ? -1 : 1;
+        return ($a->id < $b->id) ? -1 : 1;
     }
 
     /**
@@ -218,37 +180,6 @@ class DefaultPolicy implements PolicyInterface
         return $bestLiterals;
     }
 
-    /**
-     * Assumes that installed packages come first and then all highest priority packages
-     */
-    protected function pruneToHighestPriorityOrInstalled(Pool $pool, array $installedMap, array $literals)
-    {
-        $selected = array();
-
-        $priority = null;
-
-        foreach ($literals as $literal) {
-            $package = $pool->literalToPackage($literal);
-
-            if (isset($installedMap[$package->id])) {
-                $selected[] = $literal;
-                continue;
-            }
-
-            if (null === $priority) {
-                $priority = $this->getPriority($pool, $package);
-            }
-
-            if ($this->getPriority($pool, $package) != $priority) {
-                break;
-            }
-
-            $selected[] = $literal;
-        }
-
-        return $selected;
-    }
-
     /**
      * Assumes that locally aliased (in root package requires) packages take priority over branch-alias ones
      *

+ 5 - 6
src/Composer/DependencyResolver/GenericRule.php

@@ -23,14 +23,13 @@ class GenericRule extends Rule
     protected $literals;
 
     /**
-     * @param array                 $literals
-     * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
-     * @param Link|PackageInterface $reasonData
-     * @param array                 $job        The job this rule was created from
+     * @param array                          $literals
+     * @param int|null                       $reason     A RULE_* constant describing the reason for generating this rule
+     * @param Link|PackageInterface|int|null $reasonData
      */
-    public function __construct(array $literals, $reason, $reasonData, $job = null)
+    public function __construct(array $literals, $reason, $reasonData)
     {
-        parent::__construct($reason, $reasonData, $job);
+        parent::__construct($reason, $reasonData);
 
         // sort all packages ascending by id
         sort($literals);

+ 36 - 0
src/Composer/DependencyResolver/LocalRepoTransaction.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\DependencyResolver;
+
+use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
+use Composer\DependencyResolver\Operation\UninstallOperation;
+use Composer\Package\AliasPackage;
+use Composer\Package\Link;
+use Composer\Package\PackageInterface;
+use Composer\Repository\PlatformRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Semver\Constraint\Constraint;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class LocalRepoTransaction extends Transaction
+{
+    public function __construct(RepositoryInterface $lockedRepository, $localRepository)
+    {
+        parent::__construct(
+            $localRepository->getPackages(),
+            $lockedRepository->getPackages()
+        );
+    }
+}

+ 133 - 0
src/Composer/DependencyResolver/LockTransaction.php

@@ -0,0 +1,133 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\DependencyResolver;
+
+use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\Package\AliasPackage;
+use Composer\Package\RootAliasPackage;
+use Composer\Package\RootPackageInterface;
+use Composer\Repository\ArrayRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Test\Repository\ArrayRepositoryTest;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class LockTransaction extends Transaction
+{
+    /**
+     * packages in current lock file, platform repo or otherwise present
+     * @var array
+     */
+    protected $presentMap;
+
+    /**
+     * Packages which cannot be mapped, platform repo, root package, other fixed repos
+     * @var array
+     */
+    protected $unlockableMap;
+
+    /**
+     * @var array
+     */
+    protected $resultPackages;
+
+    public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions)
+    {
+        $this->presentMap = $presentMap;
+        $this->unlockableMap = $unlockableMap;
+
+        $this->setResultPackages($pool, $decisions);
+        parent::__construct($this->presentMap, $this->resultPackages['all']);
+
+    }
+
+    // TODO make this a bit prettier instead of the two text indexes?
+    public function setResultPackages(Pool $pool, Decisions $decisions)
+    {
+        $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array());
+        foreach ($decisions as $i => $decision) {
+            $literal = $decision[Decisions::DECISION_LITERAL];
+
+            if ($literal > 0) {
+                $package = $pool->literalToPackage($literal);
+                $this->resultPackages['all'][] = $package;
+                if (!isset($this->unlockableMap[$package->id])) {
+                    $this->resultPackages['non-dev'][] = $package;
+                }
+            }
+        }
+    }
+
+    public function setNonDevPackages(LockTransaction $extractionResult)
+    {
+        $packages = $extractionResult->getNewLockPackages(false);
+
+        $this->resultPackages['dev'] = $this->resultPackages['non-dev'];
+        $this->resultPackages['non-dev'] = array();
+
+        foreach ($packages as $package) {
+            foreach ($this->resultPackages['dev'] as $i => $resultPackage) {
+                // TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible?
+                if ($package->getName() == $resultPackage->getName()) {
+                    $this->resultPackages['non-dev'][] = $resultPackage;
+                    unset($this->resultPackages['dev'][$i]);
+                }
+            }
+        }
+    }
+
+    // TODO additionalFixedRepository needs to be looked at here as well?
+    public function getNewLockPackages($devMode, $updateMirrors = false)
+    {
+        $packages = array();
+        foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) {
+            if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) {
+                // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is
+                // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed
+                if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) {
+                    foreach ($this->presentMap as $presentPackage) {
+                        if ($package->getName() == $presentPackage->getName() &&
+                            $package->getVersion() == $presentPackage->getVersion() &&
+                            $presentPackage->getSourceReference() &&
+                            $presentPackage->getSourceType() === $package->getSourceType()
+                        ) {
+                            $package->setSourceDistReferences($presentPackage->getSourceReference());
+                        }
+                    }
+                }
+                $packages[] = $package;
+            }
+        }
+
+        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;
+    }
+}

+ 105 - 0
src/Composer/DependencyResolver/MultiConflictRule.php

@@ -0,0 +1,105 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\DependencyResolver;
+
+use Composer\Package\PackageInterface;
+use Composer\Package\Link;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ *
+ * MultiConflictRule([A, B, C]) acts as Rule([-A, -B]), Rule([-A, -C]), Rule([-B, -C])
+ */
+class MultiConflictRule extends Rule
+{
+    protected $literals;
+
+    /**
+     * @param array                 $literals
+     * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
+     * @param Link|PackageInterface $reasonData
+     */
+    public function __construct(array $literals, $reason, $reasonData)
+    {
+        parent::__construct($reason, $reasonData);
+
+        if (count($literals) < 3) {
+            throw new \RuntimeException("multi conflict rule requires at least 3 literals");
+        }
+
+        // sort all packages ascending by id
+        sort($literals);
+
+        $this->literals = $literals;
+    }
+
+    public function getLiterals()
+    {
+        return $this->literals;
+    }
+
+    public function getHash()
+    {
+        $data = unpack('ihash', md5('c:'.implode(',', $this->literals), true));
+
+        return $data['hash'];
+    }
+
+    /**
+     * Checks if this rule is equal to another one
+     *
+     * Ignores whether either of the rules is disabled.
+     *
+     * @param  Rule $rule The rule to check against
+     * @return bool Whether the rules are equal
+     */
+    public function equals(Rule $rule)
+    {
+        if ($rule instanceof MultiConflictRule) {
+            return $this->literals === $rule->getLiterals();
+        }
+        return false;
+    }
+
+    public function isAssertion()
+    {
+        return false;
+    }
+
+    public function disable()
+    {
+        throw new \RuntimeException("Disabling multi conflict rules is not possible. Please contact composer at https://github.com/composer/composer to let us debug what lead to this situation.");
+    }
+
+    /**
+     * Formats a rule as a string of the format (Literal1|Literal2|...)
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        // TODO multi conflict?
+        $result = $this->isDisabled() ? 'disabled(multi(' : '(multi(';
+
+        foreach ($this->literals as $i => $literal) {
+            if ($i != 0) {
+                $result .= '|';
+            }
+            $result .= $literal;
+        }
+
+        $result .= '))';
+
+        return $result;
+    }
+}

+ 11 - 3
src/Composer/DependencyResolver/Operation/InstallOperation.php

@@ -47,20 +47,28 @@ class InstallOperation extends SolverOperation
     }
 
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      * @return string
      */
-    public function getJobType()
+    public function getOperationType()
     {
         return 'install';
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
+    }
+
     /**
      * {@inheritDoc}
      */
     public function __toString()
     {
-        return 'Installing '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
+        return $this->show(false);
     }
 }

+ 11 - 3
src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php

@@ -48,20 +48,28 @@ class MarkAliasInstalledOperation extends SolverOperation
     }
 
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      * @return string
      */
-    public function getJobType()
+    public function getOperationType()
     {
         return 'markAliasInstalled';
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')';
+    }
+
     /**
      * {@inheritDoc}
      */
     public function __toString()
     {
-        return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')';
+        return $this->show(false);
     }
 }

+ 11 - 3
src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php

@@ -48,20 +48,28 @@ class MarkAliasUninstalledOperation extends SolverOperation
     }
 
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      * @return string
      */
-    public function getJobType()
+    public function getOperationType()
     {
         return 'markAliasUninstalled';
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')';
+    }
+
     /**
      * {@inheritDoc}
      */
     public function __toString()
     {
-        return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')';
+        return $this->show(false);
     }
 }

+ 10 - 2
src/Composer/DependencyResolver/Operation/OperationInterface.php

@@ -20,11 +20,11 @@ namespace Composer\DependencyResolver\Operation;
 interface OperationInterface
 {
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      * @return string
      */
-    public function getJobType();
+    public function getOperationType();
 
     /**
      * Returns operation reason.
@@ -33,6 +33,14 @@ interface OperationInterface
      */
     public function getReason();
 
+    /**
+     * Serializes the operation in a human readable format
+     *
+     * @param $lock bool Whether this is an operation on the lock file
+     * @return string
+     */
+    public function show($lock);
+
     /**
      * Serializes the operation in a human readable format
      *

+ 5 - 4
src/Composer/DependencyResolver/Operation/SolverOperation.php

@@ -43,8 +43,9 @@ abstract class SolverOperation implements OperationInterface
         return $this->reason;
     }
 
-    protected function formatVersion(PackageInterface $package)
-    {
-        return $package->getFullPrettyVersion();
-    }
+    /**
+     * @param $lock bool Whether this is an operation on the lock file
+    * @return string
+    */
+    abstract public function show($lock);
 }

+ 11 - 3
src/Composer/DependencyResolver/Operation/UninstallOperation.php

@@ -47,20 +47,28 @@ class UninstallOperation extends SolverOperation
     }
 
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      * @return string
      */
-    public function getJobType()
+    public function getOperationType()
     {
         return 'uninstall';
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
+    }
+
     /**
      * {@inheritDoc}
      */
     public function __toString()
     {
-        return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
+        return $this->show(false);
     }
 }

+ 24 - 6
src/Composer/DependencyResolver/Operation/UpdateOperation.php

@@ -61,11 +61,11 @@ class UpdateOperation extends SolverOperation
     }
 
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      * @return string
      */
-    public function getJobType()
+    public function getOperationType()
     {
         return 'update';
     }
@@ -73,11 +73,29 @@ class UpdateOperation extends SolverOperation
     /**
      * {@inheritDoc}
      */
-    public function __toString()
+    public function show($lock)
     {
-        $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Updating' : 'Downgrading';
+        $fromVersion = $this->initialPackage->getFullPrettyVersion();
+        $toVersion = $this->targetPackage->getFullPrettyVersion();
+
+        if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) {
+            $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
+            $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
+        } elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) {
+            $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
+            $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
+        }
+
+        $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading';
+
+        return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')';
+    }
 
-        return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '.
-            $this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')';
+    /**
+     * {@inheritDoc}
+     */
+    public function __toString()
+    {
+        return $this->show(false);
     }
 }

+ 1 - 4
src/Composer/DependencyResolver/PolicyInterface.php

@@ -20,8 +20,5 @@ use Composer\Package\PackageInterface;
 interface PolicyInterface
 {
     public function versionCompare(PackageInterface $a, PackageInterface $b, $operator);
-
-    public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package);
-
-    public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null);
+    public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null);
 }

+ 34 - 193
src/Composer/DependencyResolver/Pool.php

@@ -12,141 +12,54 @@
 
 namespace Composer\DependencyResolver;
 
-use Composer\Package\BasePackage;
 use Composer\Package\AliasPackage;
 use Composer\Package\Version\VersionParser;
 use Composer\Semver\Constraint\ConstraintInterface;
 use Composer\Semver\Constraint\Constraint;
 use Composer\Semver\Constraint\EmptyConstraint;
-use Composer\Repository\RepositoryInterface;
-use Composer\Repository\CompositeRepository;
-use Composer\Repository\ComposerRepository;
-use Composer\Repository\InstalledRepositoryInterface;
-use Composer\Repository\PlatformRepository;
 use Composer\Package\PackageInterface;
 
 /**
- * A package pool contains repositories that provide packages.
+ * A package pool contains all packages for dependency resolution
  *
  * @author Nils Adermann <naderman@naderman.de>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
 class Pool implements \Countable
 {
-    const MATCH_NAME = -1;
     const MATCH_NONE = 0;
     const MATCH = 1;
     const MATCH_PROVIDE = 2;
     const MATCH_REPLACE = 3;
-    const MATCH_FILTERED = 4;
 
-    protected $repositories = array();
-    protected $providerRepos = array();
     protected $packages = array();
     protected $packageByName = array();
     protected $packageByExactName = array();
-    protected $acceptableStabilities;
-    protected $stabilityFlags;
     protected $versionParser;
     protected $providerCache = array();
-    protected $filterRequires;
-    protected $whitelist = null;
-    protected $id = 1;
+    protected $unacceptableFixedPackages;
 
-    public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array())
+    public function __construct(array $packages = array(), array $unacceptableFixedPackages = array())
     {
         $this->versionParser = new VersionParser;
-        $this->acceptableStabilities = array();
-        foreach (BasePackage::$stabilities as $stability => $value) {
-            if ($value <= BasePackage::$stabilities[$minimumStability]) {
-                $this->acceptableStabilities[$stability] = $value;
-            }
-        }
-        $this->stabilityFlags = $stabilityFlags;
-        $this->filterRequires = $filterRequires;
-        foreach ($filterRequires as $name => $constraint) {
-            if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) {
-                unset($this->filterRequires[$name]);
-            }
-        }
-    }
-
-    public function setWhitelist($whitelist)
-    {
-        $this->whitelist = $whitelist;
-        $this->providerCache = array();
+        $this->setPackages($packages);
+        $this->unacceptableFixedPackages = $unacceptableFixedPackages;
     }
 
-    /**
-     * Adds a repository and its packages to this package pool
-     *
-     * @param RepositoryInterface $repo        A package repository
-     * @param array               $rootAliases
-     */
-    public function addRepository(RepositoryInterface $repo, $rootAliases = array())
+    private function setPackages(array $packages)
     {
-        if ($repo instanceof CompositeRepository) {
-            $repos = $repo->getRepositories();
-        } else {
-            $repos = array($repo);
-        }
+        $id = 1;
 
-        foreach ($repos as $repo) {
-            $this->repositories[] = $repo;
-
-            $exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface;
-
-            if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
-                $this->providerRepos[] = $repo;
-                $repo->setRootAliases($rootAliases);
-                $repo->resetPackageIds();
-            } else {
-                foreach ($repo->getPackages() as $package) {
-                    $names = $package->getNames();
-                    $stability = $package->getStability();
-                    if ($exempt || $this->isPackageAcceptable($names, $stability)) {
-                        $package->setId($this->id++);
-                        $this->packages[] = $package;
-                        $this->packageByExactName[$package->getName()][$package->id] = $package;
-
-                        foreach ($names as $provided) {
-                            $this->packageByName[$provided][] = $package;
-                        }
-
-                        // handle root package aliases
-                        $name = $package->getName();
-                        if (isset($rootAliases[$name][$package->getVersion()])) {
-                            $alias = $rootAliases[$name][$package->getVersion()];
-                            if ($package instanceof AliasPackage) {
-                                $package = $package->getAliasOf();
-                            }
-                            $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']);
-                            $aliasPackage->setRootPackageAlias(true);
-                            $aliasPackage->setId($this->id++);
-
-                            $package->getRepository()->addPackage($aliasPackage);
-                            $this->packages[] = $aliasPackage;
-                            $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage;
-
-                            foreach ($aliasPackage->getNames() as $name) {
-                                $this->packageByName[$name][] = $aliasPackage;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
+        foreach ($packages as $package) {
+            $this->packages[] = $package;
 
-    public function getPriority(RepositoryInterface $repo)
-    {
-        $priority = array_search($repo, $this->repositories, true);
+            $package->id = $id++;
+            $this->packageByExactName[$package->getName()][$package->id] = $package;
 
-        if (false === $priority) {
-            throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool.");
+            foreach ($package->getNames() as $provided) {
+                $this->packageByName[$provided][] = $package;
+            }
         }
-
-        return -$priority;
     }
 
     /**
@@ -176,104 +89,52 @@ class Pool implements \Countable
      *                                            packages must match or null to return all
      * @param  bool                $mustMatchName Whether the name of returned packages
      *                                            must match the given name
-     * @param  bool                $bypassFilters If enabled, filterRequires and stability matching is ignored
      * @return PackageInterface[]  A set of packages
      */
-    public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false, $bypassFilters = false)
+    public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false)
     {
-        if ($bypassFilters) {
-            return $this->computeWhatProvides($name, $constraint, $mustMatchName, true);
-        }
-
         $key = ((int) $mustMatchName).$constraint;
         if (isset($this->providerCache[$name][$key])) {
             return $this->providerCache[$name][$key];
         }
 
-        return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName, $bypassFilters);
+        return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName);
     }
 
     /**
      * @see whatProvides
      */
-    private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false)
+    private function computeWhatProvides($name, $constraint, $mustMatchName = false)
     {
         $candidates = array();
 
-        foreach ($this->providerRepos as $repo) {
-            foreach ($repo->whatProvides($this, $name, $bypassFilters) as $candidate) {
-                $candidates[] = $candidate;
-                if ($candidate->id < 1) {
-                    $candidate->setId($this->id++);
-                    $this->packages[$this->id - 2] = $candidate;
-                }
-            }
-        }
-
         if ($mustMatchName) {
-            $candidates = array_filter($candidates, function ($candidate) use ($name) {
-                return $candidate->getName() == $name;
-            });
             if (isset($this->packageByExactName[$name])) {
-                $candidates = array_merge($candidates, $this->packageByExactName[$name]);
+                $candidates = $this->packageByExactName[$name];
             }
         } elseif (isset($this->packageByName[$name])) {
-            $candidates = array_merge($candidates, $this->packageByName[$name]);
+            $candidates = $this->packageByName[$name];
         }
 
-        $matches = $provideMatches = array();
-        $nameMatch = false;
+        $matches = array();
 
         foreach ($candidates as $candidate) {
-            $aliasOfCandidate = null;
-
-            // alias packages are not white listed, make sure that the package
-            // being aliased is white listed
-            if ($candidate instanceof AliasPackage) {
-                $aliasOfCandidate = $candidate->getAliasOf();
-            }
-
-            if ($this->whitelist !== null && !$bypassFilters && (
-                (!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) ||
-                ($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id]))
-            )) {
-                continue;
-            }
-            switch ($this->match($candidate, $name, $constraint, $bypassFilters)) {
+            switch ($this->match($candidate, $name, $constraint)) {
                 case self::MATCH_NONE:
                     break;
 
-                case self::MATCH_NAME:
-                    $nameMatch = true;
-                    break;
-
                 case self::MATCH:
-                    $nameMatch = true;
-                    $matches[] = $candidate;
-                    break;
-
                 case self::MATCH_PROVIDE:
-                    $provideMatches[] = $candidate;
-                    break;
-
                 case self::MATCH_REPLACE:
                     $matches[] = $candidate;
                     break;
 
-                case self::MATCH_FILTERED:
-                    break;
-
                 default:
                     throw new \UnexpectedValueException('Unexpected match type');
             }
         }
 
-        // if a package with the required name exists, we ignore providers
-        if ($nameMatch) {
-            return $matches;
-        }
-
-        return array_merge($matches, $provideMatches);
+        return $matches;
     }
 
     public function literalToPackage($literal)
@@ -296,23 +157,6 @@ class Pool implements \Countable
         return $prefix.' '.$package->getPrettyString();
     }
 
-    public function isPackageAcceptable($name, $stability)
-    {
-        foreach ((array) $name as $n) {
-            // allow if package matches the global stability requirement and has no exception
-            if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) {
-                return true;
-            }
-
-            // allow if package matches the package-specific stability flag
-            if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     /**
      * Checks if the package matches the given constraint directly or through
      * provided or replaced packages
@@ -322,27 +166,19 @@ class Pool implements \Countable
      * @param  ConstraintInterface    $constraint The constraint to verify
      * @return int                    One of the MATCH* constants of this class or 0 if there is no match
      */
-    public function match($candidate, $name, ConstraintInterface $constraint = null, $bypassFilters)
+    public function match($candidate, $name, ConstraintInterface $constraint = null)
     {
         $candidateName = $candidate->getName();
         $candidateVersion = $candidate->getVersion();
-        $isDev = $candidate->getStability() === 'dev';
-        $isAlias = $candidate instanceof AliasPackage;
-
-        if (!$bypassFilters && !$isDev && !$isAlias && isset($this->filterRequires[$name])) {
-            $requireFilter = $this->filterRequires[$name];
-        } else {
-            $requireFilter = new EmptyConstraint;
-        }
 
         if ($candidateName === $name) {
             $pkgConstraint = new Constraint('==', $candidateVersion);
 
             if ($constraint === null || $constraint->matches($pkgConstraint)) {
-                return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED;
+                return self::MATCH;
             }
 
-            return self::MATCH_NAME;
+            return self::MATCH_NONE;
         }
 
         $provides = $candidate->getProvides();
@@ -352,13 +188,13 @@ class Pool implements \Countable
         if (isset($replaces[0]) || isset($provides[0])) {
             foreach ($provides as $link) {
                 if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
-                    return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED;
+                    return self::MATCH_PROVIDE;
                 }
             }
 
             foreach ($replaces as $link) {
                 if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
-                    return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED;
+                    return self::MATCH_REPLACE;
                 }
             }
 
@@ -366,13 +202,18 @@ class Pool implements \Countable
         }
 
         if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) {
-            return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED;
+            return self::MATCH_PROVIDE;
         }
 
         if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) {
-            return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED;
+            return self::MATCH_REPLACE;
         }
 
         return self::MATCH_NONE;
     }
+
+    public function isUnacceptableFixedPackage(PackageInterface $package)
+    {
+        return in_array($package, $this->unacceptableFixedPackages, true);
+    }
 }

+ 376 - 0
src/Composer/DependencyResolver/PoolBuilder.php

@@ -0,0 +1,376 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\DependencyResolver;
+
+use Composer\IO\IOInterface;
+use Composer\Package\AliasPackage;
+use Composer\Package\BasePackage;
+use Composer\Package\Package;
+use Composer\Package\PackageInterface;
+use Composer\Package\Version\StabilityFilter;
+use Composer\Repository\PlatformRepository;
+use Composer\Repository\RootPackageRepository;
+use Composer\Semver\Constraint\Constraint;
+use Composer\Semver\Constraint\EmptyConstraint;
+use Composer\Semver\Constraint\MultiConstraint;
+use Composer\EventDispatcher\EventDispatcher;
+use Composer\Plugin\PrePoolCreateEvent;
+use Composer\Plugin\PluginEvents;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class PoolBuilder
+{
+    private $acceptableStabilities;
+    private $stabilityFlags;
+    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, 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;
+            $this->loadedNames[$package->getName()] = true;
+
+            // replace means conflict, so if a fixed package replaces a name, no need to load that one, packages would conflict anyways
+            foreach ($package->getReplaces() as $link) {
+                $this->nameConstraints[$package->getName()] = null;
+                $this->loadedNames[$link->getTarget()] = true;
+            }
+
+            // TODO in how far can we do the above for conflicts? It's more tricky cause conflicts can be limited to
+            // specific versions while replace is a conflict with all versions of the name
+
+            if (
+                $package->getRepository() instanceof RootPackageRepository
+                || $package->getRepository() instanceof PlatformRepository
+                || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability())
+            ) {
+                $loadNames += $this->loadPackage($request, $package, false);
+            } else {
+                $this->unacceptableFixedPackages[] = $package;
+            }
+        }
+
+        foreach ($request->getRequires() as $packageName => $constraint) {
+            // fixed packages have already been added, so if a root require needs one of them, no need to do anything
+            if (isset($this->loadedNames[$packageName])) {
+                continue;
+            }
+
+            $loadNames[$packageName] = $constraint;
+            $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null;
+        }
+
+        // clean up loadNames for anything we manually marked loaded above
+        foreach ($loadNames as $name => $void) {
+            if (isset($this->loadedNames[$name])) {
+                unset($loadNames[$name]);
+            }
+        }
+
+        while (!empty($loadNames)) {
+            foreach ($loadNames as $name => $void) {
+                $this->loadedNames[$name] = true;
+            }
+
+            $newLoadNames = array();
+            foreach ($repositories as $repository) {
+                // these repos have their packages fixed if they need to be loaded so we
+                // never need to load anything else from them
+                if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) {
+                    continue;
+                }
+                $result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags);
+
+                foreach ($result['namesFound'] as $name) {
+                    // avoid loading the same package again from other repositories once it has been found
+                    unset($loadNames[$name]);
+                }
+                foreach ($result['packages'] as $package) {
+                    $newLoadNames += $this->loadPackage($request, $package);
+                }
+            }
+
+            $loadNames = $newLoadNames;
+        }
+
+        // filter packages according to all the require statements collected for each package
+        foreach ($this->packages as $i => $package) {
+            // we check all alias related packages at once, so no need to check individual aliases
+            // isset also checks non-null value
+            if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) {
+                $constraint = $this->nameConstraints[$package->getName()];
+
+                $aliasedPackages = array($i => $package);
+                if (isset($this->aliasMap[spl_object_hash($package)])) {
+                    $aliasedPackages += $this->aliasMap[spl_object_hash($package)];
+                }
+
+                $found = false;
+                foreach ($aliasedPackages as $packageOrAlias) {
+                    if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) {
+                        $found = true;
+                    }
+                }
+                if (!$found) {
+                    foreach ($aliasedPackages as $index => $packageOrAlias) {
+                        unset($this->packages[$index]);
+                    }
+                }
+            }
+        }
+
+        if ($this->eventDispatcher) {
+            $prePoolCreateEvent = new PrePoolCreateEvent(
+                PluginEvents::PRE_POOL_CREATE,
+                $repositories,
+                $request,
+                $this->acceptableStabilities,
+                $this->stabilityFlags,
+                $this->rootAliases,
+                $this->rootReferences,
+                $this->packages,
+                $this->unacceptableFixedPackages
+            );
+            $this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent);
+            $this->packages = $prePoolCreateEvent->getPackages();
+            $this->unacceptableFixedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages();
+        }
+
+        $pool = new Pool($this->packages, $this->unacceptableFixedPackages);
+
+        $this->aliasMap = array();
+        $this->nameConstraints = array();
+        $this->loadedNames = array();
+        $this->packages = array();
+        $this->unacceptableFixedPackages = array();
+
+        return $pool;
+    }
+
+    private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true)
+    {
+        end($this->packages);
+        $index = key($this->packages) + 1;
+        $this->packages[] = $package;
+
+        if ($package instanceof AliasPackage) {
+            $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package;
+        }
+
+        $name = $package->getName();
+
+        // we're simply setting the root references on all versions for a name here and rely on the solver to pick the
+        // right version. It'd be more work to figure out which versions and which aliases of those versions this may
+        // apply to
+        if (isset($this->rootReferences[$name])) {
+            // do not modify the references on already locked packages
+            if (!$request->isFixedPackage($package)) {
+                $package->setSourceDistReferences($this->rootReferences[$name]);
+            }
+        }
+
+        // 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();
+            } else {
+                $basePackage = $package;
+            }
+            $aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']);
+            $aliasPackage->setRootPackageAlias(true);
+
+            $this->packages[] = $aliasPackage;
+            $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage;
+        }
+
+        $loadNames = array();
+        foreach ($package->getRequires() as $link) {
+            $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();
+            if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) {
+                if (!array_key_exists($require, $this->nameConstraints)) {
+                    $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false);
+                } elseif ($this->nameConstraints[$require]) {
+                    // TODO addConstraint function?
+                    $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false);
+                }
+                // else it is null and should stay null
+            } else {
+                $this->nameConstraints[$require] = null;
+            }
+        }
+
+        // if we're doing a partial update with deps we also need to unfix packages which are being replaced in case they
+        // are currently locked and thus prevent this updateable package from being installable/updateable
+        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]);
+    }
+}
+

+ 187 - 123
src/Composer/DependencyResolver/Problem.php

@@ -13,6 +13,8 @@
 namespace Composer\DependencyResolver;
 
 use Composer\Package\CompletePackageInterface;
+use Composer\Repository\RepositorySet;
+use Composer\Semver\Constraint\Constraint;
 
 /**
  * Represents a problem detected while solving dependencies
@@ -28,20 +30,13 @@ class Problem
     protected $reasonSeen;
 
     /**
-     * A set of reasons for the problem, each is a rule or a job and a rule
+     * A set of reasons for the problem, each is a rule or a root require and a rule
      * @var array
      */
     protected $reasons = array();
 
     protected $section = 0;
 
-    protected $pool;
-
-    public function __construct(Pool $pool)
-    {
-        $this->pool = $pool;
-    }
-
     /**
      * Add a rule as a reason
      *
@@ -49,10 +44,7 @@ class Problem
      */
     public function addRule(Rule $rule)
     {
-        $this->addReason(spl_object_hash($rule), array(
-            'rule' => $rule,
-            'job' => $rule->getJob(),
-        ));
+        $this->addReason(spl_object_hash($rule), $rule);
     }
 
     /**
@@ -68,124 +60,67 @@ class Problem
     /**
      * A human readable textual representation of the problem's reasons
      *
-     * @param  array  $installedMap A map of all installed packages
+     * @param  array  $installedMap A map of all present packages
      * @return string
      */
-    public function getPrettyString(array $installedMap = array())
+    public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
     {
+        // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections?
         $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
 
         if (count($reasons) === 1) {
             reset($reasons);
-            $reason = current($reasons);
+            $rule = current($reasons);
 
-            $job = $reason['job'];
+            if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) {
+                throw new \LogicException("Single reason problems must contain a request rule.");
+            }
 
-            $packageName = $job['packageName'];
-            $constraint = $job['constraint'];
+            $reasonData = $rule->getReasonData();
+            $packageName = $reasonData['packageName'];
+            $constraint = $reasonData['constraint'];
 
             if (isset($constraint)) {
-                $packages = $this->pool->whatProvides($packageName, $constraint);
+                $packages = $pool->whatProvides($packageName, $constraint);
             } else {
                 $packages = array();
             }
 
-            if ($job && $job['cmd'] === 'install' && empty($packages)) {
-
-                // handle php/hhvm
-                if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
-                    $version = phpversion();
-                    $available = $this->pool->whatProvides($packageName);
-
-                    if (count($available)) {
-                        $firstAvailable = reset($available);
-                        $version = $firstAvailable->getPrettyVersion();
-                        $extra = $firstAvailable->getExtra();
-                        if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
-                            $version .= '; ' . $firstAvailable->getDescription();
-                        }
-                    }
-
-                    $msg = "\n    - This package requires ".$packageName.$this->constraintToText($constraint).' but ';
-
-                    if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
-                        return $msg . 'your HHVM version does not satisfy that requirement.';
-                    }
-
-                    if ($packageName === 'hhvm') {
-                        return $msg . 'you are running this with PHP and not HHVM.';
-                    }
-
-                    return $msg . 'your PHP version ('. $version .') does not satisfy that requirement.';
-                }
-
-                // handle php extensions
-                if (0 === stripos($packageName, 'ext-')) {
-                    if (false !== strpos($packageName, ' ')) {
-                        return "\n    - The requested PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.';
-                    }
-
-                    $ext = substr($packageName, 4);
-                    $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
-
-                    return "\n    - The requested PHP extension ".$packageName.$this->constraintToText($constraint).' '.$error.'. Install or enable PHP\'s '.$ext.' extension.';
-                }
-
-                // handle linked libs
-                if (0 === stripos($packageName, 'lib-')) {
-                    if (strtolower($packageName) === 'lib-icu') {
-                        $error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.';
-
-                        return "\n    - The requested linked library ".$packageName.$this->constraintToText($constraint).' '.$error;
-                    }
-
-                    return "\n    - The requested linked library ".$packageName.$this->constraintToText($constraint).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.';
-                }
-
-                if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
-                    $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
-
-                    return "\n    - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.';
-                }
-
-                if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) {
-                    return "\n    - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.';
-                }
-
-                if ($providers = $this->pool->whatProvides($packageName, null, true, true)) {
-                    return "\n    - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.';
-                }
-
-                return "\n    - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.';
+            if (empty($packages)) {
+                return "\n    ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint));
             }
         }
 
         $messages = array();
+        foreach ($reasons as $rule) {
+            $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
+        }
 
-        foreach ($reasons as $reason) {
-            $rule = $reason['rule'];
-            $job = $reason['job'];
+        return "\n    - ".implode("\n    - ", $messages);
+    }
 
-            if ($job) {
-                $messages[] = $this->jobToText($job);
-            } elseif ($rule) {
-                if ($rule instanceof Rule) {
-                    $messages[] = $rule->getPrettyString($this->pool, $installedMap);
+    public function isCausedByLock()
+    {
+        foreach ($this->reasons as $sectionRules) {
+            foreach ($sectionRules as $rule) {
+                if ($rule->isCausedByLock()) {
+                    return true;
                 }
             }
         }
-
-        return "\n    - ".implode("\n    - ", $messages);
     }
 
     /**
      * Store a reason descriptor but ignore duplicates
      *
      * @param string $id     A canonical identifier for the reason
-     * @param string $reason The reason descriptor
+     * @param Rule $reason The reason descriptor
      */
-    protected function addReason($id, $reason)
+    protected function addReason($id, Rule $reason)
     {
+        // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message
+        // that is important to understand the issue?
+
         if (!isset($this->reasonSeen[$id])) {
             $this->reasonSeen[$id] = true;
             $this->reasons[$this->section][] = $reason;
@@ -198,39 +133,150 @@ class Problem
     }
 
     /**
-     * Turns a job into a human readable description
-     *
-     * @param  array  $job
-     * @return string
+     * @internal
      */
-    protected function jobToText($job)
+    public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null)
     {
-        $packageName = $job['packageName'];
-        $constraint = $job['constraint'];
-        switch ($job['cmd']) {
-            case 'install':
-                $packages = $this->pool->whatProvides($packageName, $constraint);
-                if (!$packages) {
-                    return 'No package found to satisfy install request for '.$packageName.$this->constraintToText($constraint);
+        // handle php/hhvm
+        if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
+            $version = phpversion();
+            $available = $pool->whatProvides($packageName);
+
+            if (count($available)) {
+                $firstAvailable = reset($available);
+                $version = $firstAvailable->getPrettyVersion();
+                $extra = $firstAvailable->getExtra();
+                if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
+                    $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription());
+                }
+            }
+
+            $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but ';
+
+            if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
+                return array($msg, 'your HHVM version does not satisfy that requirement.');
+            }
+
+            if ($packageName === 'hhvm') {
+                return array($msg, 'you are running this with PHP and not HHVM.');
+            }
+
+            return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.');
+        }
+
+        // handle php extensions
+        if (0 === stripos($packageName, 'ext-')) {
+            if (false !== strpos($packageName, ' ')) {
+                return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.');
+            }
+
+            $ext = substr($packageName, 4);
+            $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system';
+
+            return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.');
+        }
+
+        // handle linked libs
+        if (0 === stripos($packageName, 'lib-')) {
+            if (strtolower($packageName) === 'lib-icu') {
+                $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.';
+
+                return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error);
+            }
+
+            return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.');
+        }
+
+        $fixedPackage = null;
+        foreach ($request->getFixedPackages() as $package) {
+            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 list it as an argument for the update command.');
+                }
+                break;
+            }
+        }
+
+        // first check if the actual requested package is found in normal conditions
+        // if so it must mean it is rejected by another constraint than the one given here
+        if ($packages = $repositorySet->findPackages($packageName, $constraint)) {
+            $rootReqs = $repositorySet->getRootRequires();
+            if (isset($rootReqs[$packageName])) {
+                $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) {
+                    return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
+                });
+                if (0 === count($filtered)) {
+                    return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').');
+                }
+            }
+
+            if ($fixedPackage) {
+                $fixedConstraint = new Constraint('==', $fixedPackage->getVersion());
+                $filtered = array_filter($packages, function ($p) use ($fixedConstraint) {
+                    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 list it as an argument for the update command.');
                 }
+            }
 
-                return 'Installation request for '.$packageName.$this->constraintToText($constraint).' -> satisfiable by '.$this->getPackageList($packages).'.';
-            case 'update':
-                return 'Update request for '.$packageName.$this->constraintToText($constraint).'.';
-            case 'remove':
-                return 'Removal request for '.$packageName.$this->constraintToText($constraint).'';
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.');
         }
 
-        if (isset($constraint)) {
-            $packages = $this->pool->whatProvides($packageName, $constraint);
-        } else {
-            $packages = array();
+        // check if the package is found when bypassing stability checks
+        if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.');
         }
 
-        return 'Job(cmd='.$job['cmd'].', target='.$packageName.', packages=['.$this->getPackageList($packages).'])';
+        // check if the package is found when bypassing the constraint check
+        if ($packages = $repositorySet->findPackages($packageName, null)) {
+            // we must first verify if a valid package would be found in a lower priority repository
+            if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
+                $higherRepoPackages = $repositorySet->findPackages($packageName, null);
+                $nextRepoPackages = array();
+                $nextRepo = null;
+
+                foreach ($allReposPackages as $package) {
+                    if ($nextRepo === null || $nextRepo === $package->getRepository()) {
+                        $nextRepoPackages[] = $package;
+                        $nextRepo = $package->getRepository();
+                    } else {
+                        break;
+                    }
+                }
+
+                return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.');
+            }
+
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.');
+        }
+
+        if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
+            $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
+
+            return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.');
+        }
+
+        if ($providers = $repositorySet->getProviders($packageName)) {
+            $maxProviders = 20;
+            $providersStr = implode(array_map(function ($p) {
+                $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : '';
+                return "      - ${p['name']}".$description."\n";
+            }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers));
+            if (count($providers) > $maxProviders+1) {
+                $providersStr .= '      ... and '.(count($providers)-$maxProviders).' more.'."\n";
+            }
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr."      Consider requiring one of these to satisfy the $packageName requirement.");
+        }
+
+        return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name.");
     }
 
-    protected function getPackageList($packages)
+    /**
+     * @internal
+     */
+    public static function getPackageList(array $packages)
     {
         $prepared = array();
         foreach ($packages as $package) {
@@ -238,19 +284,37 @@ class Problem
             $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
         }
         foreach ($prepared as $name => $package) {
+            // remove the implicit dev-master alias to avoid cruft in the display
+            if (isset($package['versions']['9999999-dev']) && isset($package['versions']['dev-master'])) {
+                unset($package['versions']['9999999-dev']);
+            }
             $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
         }
 
         return implode(', ', $prepared);
     }
 
+    private static function hasMultipleNames(array $packages)
+    {
+        $name = null;
+        foreach ($packages as $package) {
+            if ($name === null || $name === $package->getName()) {
+                $name = $package->getName();
+            } else {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
-     * Turns a constraint into text usable in a sentence describing a job
+     * Turns a constraint into text usable in a sentence describing a request
      *
      * @param  \Composer\Semver\Constraint\ConstraintInterface $constraint
      * @return string
      */
-    protected function constraintToText($constraint)
+    protected static function constraintToText($constraint)
     {
         return $constraint ? ' '.$constraint->getPrettyString() : '';
     }

+ 104 - 31
src/Composer/DependencyResolver/Request.php

@@ -12,6 +12,10 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\Package\Package;
+use Composer\Package\PackageInterface;
+use Composer\Package\RootAliasPackage;
+use Composer\Repository\LockArrayRepository;
 use Composer\Semver\Constraint\ConstraintInterface;
 
 /**
@@ -19,60 +23,129 @@ use Composer\Semver\Constraint\ConstraintInterface;
  */
 class Request
 {
-    protected $jobs;
+    /**
+     * Identifies a partial update for listed packages only, all dependencies will remain at locked versions
+     */
+    const UPDATE_ONLY_LISTED = 0;
 
-    public function __construct()
-    {
-        $this->jobs = array();
-    }
+    /**
+     * 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;
 
-    public function install($packageName, ConstraintInterface $constraint = null)
-    {
-        $this->addJob($packageName, 'install', $constraint);
-    }
+    /**
+     * 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 update($packageName, ConstraintInterface $constraint = null)
+    public function __construct(LockArrayRepository $lockedRepository = null)
     {
-        $this->addJob($packageName, 'update', $constraint);
+        $this->lockedRepository = $lockedRepository;
     }
 
-    public function remove($packageName, ConstraintInterface $constraint = null)
+    public function requireName($packageName, ConstraintInterface $constraint = null)
     {
-        $this->addJob($packageName, 'remove', $constraint);
+        $packageName = strtolower($packageName);
+        $this->requires[$packageName] = $constraint;
     }
 
     /**
      * Mark an existing package as being installed and having to remain installed
      *
-     * These jobs will not be tempered with by the solver
-     *
-     * @param string                   $packageName
-     * @param ConstraintInterface|null $constraint
+     * @param bool $lockable if set to false, the package will not be written to the lock file
      */
-    public function fix($packageName, ConstraintInterface $constraint = null)
+    public function fixPackage(PackageInterface $package, $lockable = true)
     {
-        $this->addJob($packageName, 'install', $constraint, true);
+        $this->fixedPackages[spl_object_hash($package)] = $package;
+
+        if (!$lockable) {
+            $this->unlockables[spl_object_hash($package)] = $package;
+        }
     }
 
-    protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null, $fixed = false)
+    public function unfixPackage(PackageInterface $package)
     {
-        $packageName = strtolower($packageName);
+        unset($this->fixedPackages[spl_object_hash($package)]);
+        unset($this->unlockables[spl_object_hash($package)]);
+    }
 
-        $this->jobs[] = array(
-            'cmd' => $cmd,
-            'packageName' => $packageName,
-            'constraint' => $constraint,
-            'fixed' => $fixed,
-        );
+    public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies)
+    {
+        $this->updateAllowList = $updateAllowList;
+        $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies;
     }
 
-    public function updateAll()
+    public function getUpdateAllowList()
     {
-        $this->jobs[] = array('cmd' => 'update-all');
+        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;
+    }
+
+    public function getFixedPackages()
+    {
+        return $this->fixedPackages;
+    }
+
+    public function isFixedPackage(PackageInterface $package)
+    {
+        return isset($this->fixedPackages[spl_object_hash($package)]);
+    }
+
+    // TODO look into removing the packageIds option, the only place true is used is for the installed map in the solver problems
+    // some locked packages may not be in the pool so they have a package->id of -1
+    public function getPresentMap($packageIds = false)
+    {
+        $presentMap = array();
+
+        if ($this->lockedRepository) {
+            foreach ($this->lockedRepository->getPackages() as $package) {
+                $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package;
+            }
+        }
+
+        foreach ($this->fixedPackages as $package) {
+            $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package;
+        }
+
+        return $presentMap;
+    }
+
+    public function getUnlockableMap()
+    {
+        $unlockableMap = array();
+
+        foreach ($this->unlockables as $package) {
+            $unlockableMap[$package->id] = $package;
+        }
+
+        return $unlockableMap;
     }
 
-    public function getJobs()
+    public function getLockedRepository()
     {
-        return $this->jobs;
+        return $this->lockedRepository;
     }
 }

+ 127 - 77
src/Composer/DependencyResolver/Rule.php

@@ -15,6 +15,7 @@ namespace Composer\DependencyResolver;
 use Composer\Package\CompletePackage;
 use Composer\Package\Link;
 use Composer\Package\PackageInterface;
+use Composer\Repository\RepositorySet;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
@@ -24,8 +25,8 @@ abstract class Rule
 {
     // reason constants
     const RULE_INTERNAL_ALLOW_UPDATE = 1;
-    const RULE_JOB_INSTALL = 2;
-    const RULE_JOB_REMOVE = 3;
+    const RULE_ROOT_REQUIRE = 2;
+    const RULE_FIXED = 3;
     const RULE_PACKAGE_CONFLICT = 6;
     const RULE_PACKAGE_REQUIRES = 7;
     const RULE_PACKAGE_OBSOLETES = 8;
@@ -41,22 +42,17 @@ abstract class Rule
     const BITFIELD_DISABLED = 16;
 
     protected $bitfield;
-    protected $job;
+    protected $request;
     protected $reasonData;
 
     /**
      * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
      * @param Link|PackageInterface $reasonData
-     * @param array                 $job        The job this rule was created from
      */
-    public function __construct($reason, $reasonData, $job = null)
+    public function __construct($reason, $reasonData)
     {
         $this->reasonData = $reasonData;
 
-        if ($job) {
-            $this->job = $job;
-        }
-
         $this->bitfield = (0 << self::BITFIELD_DISABLED) |
             ($reason << self::BITFIELD_REASON) |
             (255 << self::BITFIELD_TYPE);
@@ -66,11 +62,6 @@ abstract class Rule
 
     abstract public function getHash();
 
-    public function getJob()
-    {
-        return $this->job;
-    }
-
     abstract public function equals(Rule $rule);
 
     public function getReason()
@@ -85,11 +76,17 @@ abstract class Rule
 
     public function getRequiredPackage()
     {
-        if ($this->getReason() === self::RULE_JOB_INSTALL) {
-            return $this->reasonData;
+        $reason = $this->getReason();
+
+        if ($reason === self::RULE_ROOT_REQUIRE) {
+            return $this->reasonData['packageName'];
+        }
+
+        if ($reason === self::RULE_FIXED) {
+            return $this->reasonData['package']->getName();
         }
 
-        if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) {
+        if ($reason === self::RULE_PACKAGE_REQUIRES) {
             return $this->reasonData->getTarget();
         }
     }
@@ -126,7 +123,12 @@ abstract class Rule
 
     abstract public function isAssertion();
 
-    public function getPrettyString(Pool $pool, array $installedMap = array())
+    public function isCausedByLock()
+    {
+        return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable'];
+    }
+
+    public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
     {
         $literals = $this->getLiterals();
 
@@ -142,17 +144,30 @@ abstract class Rule
             case self::RULE_INTERNAL_ALLOW_UPDATE:
                 return $ruleText;
 
-            case self::RULE_JOB_INSTALL:
-                return "Install command rule ($ruleText)";
+            case self::RULE_ROOT_REQUIRE:
+                $packageName = $this->reasonData['packageName'];
+                $constraint = $this->reasonData['constraint'];
 
-            case self::RULE_JOB_REMOVE:
-                return "Remove command rule ($ruleText)";
+                $packages = $pool->whatProvides($packageName, $constraint);
+                if (!$packages) {
+                    return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '');
+                }
+
+                return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages).'.';
+
+            case self::RULE_FIXED:
+                $package = $this->reasonData['package'];
+                if ($this->reasonData['lockable']) {
+                    return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.';
+                }
+
+                return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer';
 
             case self::RULE_PACKAGE_CONFLICT:
                 $package1 = $pool->literalToPackage($literals[0]);
                 $package2 = $pool->literalToPackage($literals[1]);
 
-                return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.';
+                return $package2->getPrettyString().' conflicts with '.$package1->getPrettyString().'.';
 
             case self::RULE_PACKAGE_REQUIRES:
                 $sourceLiteral = array_shift($literals);
@@ -169,73 +184,103 @@ abstract class Rule
                 } else {
                     $targetName = $this->reasonData->getTarget();
 
-                    if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') {
-                        // handle php/hhvm
-                        if (defined('HHVM_VERSION')) {
-                            return $text . ' -> your HHVM version does not satisfy that requirement.';
-                        }
+                    $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint());
 
-                        $packages = $pool->whatProvides($targetName);
-                        $package = count($packages) ? current($packages) : phpversion();
+                    return $text . ' -> ' . $reason[1];
+                }
 
-                        if ($targetName === 'hhvm') {
-                            if ($package instanceof CompletePackage) {
-                                return $text . ' -> your HHVM version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
-                            } else {
-                                return $text . ' -> you are running this with PHP and not HHVM.';
-                            }
-                        }
+                return $text;
 
+            case self::RULE_PACKAGE_OBSOLETES:
+                if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) {
+                    $package1 = $pool->literalToPackage($literals[0]);
+                    $package2 = $pool->literalToPackage($literals[1]);
+
+                    $replaces1 = $this->getReplacedNames($package1);
+                    $replaces2 = $this->getReplacedNames($package2);
+
+                    $reason = null;
+                    if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) {
+                        $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and thus cannot coexist.';
+                    } elseif (in_array($package1->getName(), $replaces2, true)) {
+                        $reason = $package2->getName().' replaces '.$package1->getName().' and thus cannot coexist with it.';
+                    } elseif (in_array($package2->getName(), $replaces1, true)) {
+                        $reason = $package1->getName().' replaces '.$package2->getName().' and thus cannot coexist with it.';
+                    }
 
-                        if (!($package instanceof CompletePackage)) {
-                            return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.';
+                    if ($reason) {
+                        if (isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) {
+                            // swap vars so the if below passes
+                            $tmp = $package2;
+                            $package2 = $package1;
+                            $package1 = $tmp;
                         }
-
-                        $extra = $package->getExtra();
-
-                        if (!empty($extra['config.platform'])) {
-                            $text .= ' -> your PHP version ('.phpversion().') overridden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
-                        } else {
-                            $text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
+                        if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) {
+                            return $package1->getPrettyString().' cannot be installed as that would require removing '.$package2->getPrettyString().'. '.$reason;
                         }
 
-                        return $text;
+                        if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) {
+                            return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. '.$reason;
+                        }
                     }
 
-                    if (0 === strpos($targetName, 'ext-')) {
-                        // handle php extensions
-                        $ext = substr($targetName, 4);
-                        $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
+                    return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.';
+                }
 
-                        return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.';
+                return $ruleText;
+            case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
+                return $ruleText;
+            case self::RULE_PACKAGE_SAME_NAME:
+                $replacedNames = null;
+                $packageNames = array();
+                foreach ($literals as $literal) {
+                    $package = $pool->literalToPackage($literal);
+                    $pkgReplaces = $this->getReplacedNames($package);
+                    if ($pkgReplaces) {
+                        if ($replacedNames === null) {
+                            $replacedNames = $this->getReplacedNames($package);
+                        } else {
+                            $replacedNames = array_intersect($replacedNames, $this->getReplacedNames($package));
+                        }
                     }
+                    $packageNames[$package->getName()] = true;
+                }
 
-                    if (0 === strpos($targetName, 'lib-')) {
-                        // handle linked libs
-                        $lib = substr($targetName, 4);
-
-                        return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.';
+                if ($replacedNames) {
+                    $replacedNames = array_values(array_intersect(array_keys($packageNames), $replacedNames));
+                }
+                if ($replacedNames && count($packageNames) > 1) {
+                    $replacer = null;
+                    foreach ($literals as $literal) {
+                        $package = $pool->literalToPackage($literal);
+                        if (array_intersect($replacedNames, $this->getReplacedNames($package))) {
+                            $replacer = $package;
+                            break;
+                        }
                     }
+                    $replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0];
 
-                    if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) {
-                        return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.';
+                    if ($replacer) {
+                        return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and thus cannot coexist with it.';
                     }
-
-                    return $text . ' -> no matching package found.';
                 }
 
-                return $text;
-
-            case self::RULE_PACKAGE_OBSOLETES:
-                return $ruleText;
-            case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
-                return $ruleText;
-            case self::RULE_PACKAGE_SAME_NAME:
-                return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.';
+                return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.';
             case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
                 return $ruleText;
             case self::RULE_LEARNED:
-                return 'Conclusion: '.$ruleText;
+                if (isset($learnedPool[$this->reasonData])) {
+                    $learnedString = ', learned rules:'."\n        - ";
+                    $reasons = array();
+                    foreach ($learnedPool[$this->reasonData] as $learnedRule) {
+                        $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
+                    }
+                    $learnedString .= implode("\n        - ", array_unique($reasons));
+                } else {
+                    $learnedString = ' (reasoning unavailable)';
+                }
+
+                return 'Conclusion: '.$ruleText.$learnedString;
             case self::RULE_PACKAGE_ALIAS:
                 return $ruleText;
             default:
@@ -252,17 +297,22 @@ abstract class Rule
     protected function formatPackagesUnique($pool, array $packages)
     {
         $prepared = array();
-        foreach ($packages as $package) {
+        foreach ($packages as $index => $package) {
             if (!is_object($package)) {
-                $package = $pool->literalToPackage($package);
+                $packages[$index] = $pool->literalToPackage($package);
             }
-            $prepared[$package->getName()]['name'] = $package->getPrettyName();
-            $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
         }
-        foreach ($prepared as $name => $package) {
-            $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
+
+        return Problem::getPackageList($packages);
+    }
+
+    private function getReplacedNames(PackageInterface $package)
+    {
+        $names = array();
+        foreach ($package->getReplaces() as $link) {
+            $names[] = $link->getTarget();
         }
 
-        return implode(', ', $prepared);
+        return $names;
     }
 }

+ 2 - 3
src/Composer/DependencyResolver/Rule2Literals.php

@@ -28,11 +28,10 @@ class Rule2Literals extends Rule
      * @param int                   $literal2
      * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
      * @param Link|PackageInterface $reasonData
-     * @param array                 $job        The job this rule was created from
      */
-    public function __construct($literal1, $literal2, $reason, $reasonData, $job = null)
+    public function __construct($literal1, $literal2, $reason, $reasonData)
     {
-        parent::__construct($reason, $reasonData, $job);
+        parent::__construct($reason, $reasonData);
 
         if ($literal1 < $literal2) {
             $this->literal1 = $literal1;

+ 7 - 5
src/Composer/DependencyResolver/RuleSet.php

@@ -12,6 +12,8 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\Repository\RepositorySet;
+
 /**
  * @author Nils Adermann <naderman@naderman.de>
  */
@@ -19,7 +21,7 @@ class RuleSet implements \IteratorAggregate, \Countable
 {
     // highest priority => lowest number
     const TYPE_PACKAGE = 0;
-    const TYPE_JOB = 1;
+    const TYPE_REQUEST = 1;
     const TYPE_LEARNED = 4;
 
     /**
@@ -32,7 +34,7 @@ class RuleSet implements \IteratorAggregate, \Countable
     protected static $types = array(
         255 => 'UNKNOWN',
         self::TYPE_PACKAGE => 'PACKAGE',
-        self::TYPE_JOB => 'JOB',
+        self::TYPE_REQUEST => 'REQUEST',
         self::TYPE_LEARNED => 'LEARNED',
     );
 
@@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable
         return array_keys($types);
     }
 
-    public function getPrettyString(Pool $pool = null)
+    public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null)
     {
         $string = "\n";
         foreach ($this->rules as $type => $rules) {
             $string .= str_pad(self::$types[$type], 8, ' ') . ": ";
             foreach ($rules as $rule) {
-                $string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n";
+                $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n";
             }
             $string .= "\n\n";
         }
@@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable
 
     public function __toString()
     {
-        return $this->getPrettyString(null);
+        return $this->getPrettyString(null, null, null);
     }
 }

+ 76 - 117
src/Composer/DependencyResolver/RuleSetGenerator.php

@@ -12,9 +12,11 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\Package\LinkConstraint\VersionConstraint;
 use Composer\Package\PackageInterface;
 use Composer\Package\AliasPackage;
 use Composer\Repository\PlatformRepository;
+use Composer\Semver\Constraint\Constraint;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
@@ -24,13 +26,11 @@ class RuleSetGenerator
     protected $policy;
     protected $pool;
     protected $rules;
-    protected $jobs;
-    protected $installedMap;
-    protected $whitelistedMap;
     protected $addedMap;
     protected $conflictAddedMap;
     protected $addedPackages;
     protected $addedPackagesByNames;
+    protected $conflictsForName;
 
     public function __construct(PolicyInterface $policy, Pool $pool)
     {
@@ -76,33 +76,17 @@ class RuleSetGenerator
      * @param  array $packages The set of packages to choose from
      * @param  int   $reason   A RULE_* constant describing the reason for
      *                         generating this rule
-     * @param  array $job      The job this rule was created from
+     * @param  array $reasonData Additional data like the root require or fix request info
      * @return Rule  The generated rule
      */
-    protected function createInstallOneOfRule(array $packages, $reason, $job)
+    protected function createInstallOneOfRule(array $packages, $reason, $reasonData)
     {
         $literals = array();
         foreach ($packages as $package) {
             $literals[] = $package->id;
         }
 
-        return new GenericRule($literals, $reason, $job['packageName'], $job);
-    }
-
-    /**
-     * Creates a rule to remove a package
-     *
-     * The rule for a package A is (-A).
-     *
-     * @param  PackageInterface $package The package to be removed
-     * @param  int              $reason  A RULE_* constant describing the
-     *                                   reason for generating this rule
-     * @param  array            $job     The job this rule was created from
-     * @return Rule             The generated rule
-     */
-    protected function createRemoveRule(PackageInterface $package, $reason, $job)
-    {
-        return new GenericRule(array(-$package->id), $reason, $job['packageName'], $job);
+        return new GenericRule($literals, $reason, $reasonData);
     }
 
     /**
@@ -129,6 +113,20 @@ class RuleSetGenerator
         return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData);
     }
 
+    protected function createMultiConflictRule(array $packages, $reason, $reasonData = null)
+    {
+        $literals = array();
+        foreach ($packages as $package) {
+            $literals[] = -$package->id;
+        }
+
+        if (count($literals) == 2) {
+            return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData);
+        }
+
+        return new MultiConflictRule($literals, $reason, $reasonData);
+    }
+
     /**
      * Adds a rule unless it duplicates an existing one of any type
      *
@@ -147,41 +145,6 @@ class RuleSetGenerator
         $this->rules->add($newRule, $type);
     }
 
-    protected function whitelistFromPackage(PackageInterface $package)
-    {
-        $workQueue = new \SplQueue;
-        $workQueue->enqueue($package);
-
-        while (!$workQueue->isEmpty()) {
-            $package = $workQueue->dequeue();
-            if (isset($this->whitelistedMap[$package->id])) {
-                continue;
-            }
-
-            $this->whitelistedMap[$package->id] = true;
-
-            foreach ($package->getRequires() as $link) {
-                $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true);
-
-                foreach ($possibleRequires as $require) {
-                    $workQueue->enqueue($require);
-                }
-            }
-
-            $obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true);
-
-            foreach ($obsoleteProviders as $provider) {
-                if ($provider === $package) {
-                    continue;
-                }
-
-                if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
-                    $workQueue->enqueue($provider);
-                }
-            }
-        }
-    }
-
     protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs)
     {
         $workQueue = new \SplQueue;
@@ -225,9 +188,16 @@ class RuleSetGenerator
 
                 if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
                     $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
-                } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
-                    $reason = ($packageName == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
-                    $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $package));
+                } else {
+                    if (!isset($this->conflictsForName[$packageName])) {
+                        $this->conflictsForName[$packageName] = array();
+                    }
+                    if (!$package instanceof AliasPackage) {
+                        $this->conflictsForName[$packageName][$package->id] = $package;
+                    }
+                    if (!$provider instanceof AliasPackage) {
+                        $this->conflictsForName[$packageName][$provider->id] = $provider;
+                    }
                 }
             }
         }
@@ -248,7 +218,7 @@ class RuleSetGenerator
 
                 /** @var PackageInterface $possibleConflict */
                 foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) {
-                    $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true);
+                    $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint());
 
                     if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) {
                         $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link));
@@ -258,8 +228,6 @@ class RuleSetGenerator
             }
 
             // check obsoletes and implicit obsoletes of a package
-            $isInstalled = isset($this->installedMap[$package->id]);
-
             foreach ($package->getReplaces() as $link) {
                 if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
                     continue;
@@ -272,12 +240,19 @@ class RuleSetGenerator
                     }
 
                     if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
-                        $reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
+                        $reason = Rule::RULE_PACKAGE_OBSOLETES;
                         $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link));
                     }
                 }
             }
         }
+
+        foreach ($this->conflictsForName as $name => $packages) {
+            if (count($packages) > 1) {
+                $reason = Rule::RULE_PACKAGE_SAME_NAME;
+                $this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, null));
+            }
+        }
     }
 
     protected function obsoleteImpossibleForAlias($package, $provider)
@@ -294,77 +269,61 @@ class RuleSetGenerator
         return $impossible;
     }
 
-    protected function whitelistFromJobs()
+    protected function addRulesForRequest(Request $request, $ignorePlatformReqs)
     {
-        foreach ($this->jobs as $job) {
-            switch ($job['cmd']) {
-                case 'install':
-                    $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true);
-                    foreach ($packages as $package) {
-                        $this->whitelistFromPackage($package);
-                    }
-                    break;
+        $unlockableMap = $request->getUnlockableMap();
+
+        foreach ($request->getFixedPackages() as $package) {
+            if ($package->id == -1) {
+                // fixed package was not added to the pool as it did not pass the stability requirements, this is fine
+                if ($this->pool->isUnacceptableFixedPackage($package)) {
+                    continue;
+                }
+
+                // otherwise, looks like a bug
+                throw new \LogicException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool.");
             }
+
+            $this->addRulesForPackage($package, $ignorePlatformReqs);
+
+            $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array(
+                'package' => $package,
+                'lockable' => !isset($unlockableMap[$package->id]),
+            ));
+            $this->addRule(RuleSet::TYPE_REQUEST, $rule);
         }
-    }
 
-    protected function addRulesForJobs($ignorePlatformReqs)
-    {
-        foreach ($this->jobs as $job) {
-            switch ($job['cmd']) {
-                case 'install':
-                    if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
-                        break;
-                    }
+        foreach ($request->getRequires() as $packageName => $constraint) {
+            if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) {
+                continue;
+            }
 
-                    $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
-                    if ($packages) {
-                        foreach ($packages as $package) {
-                            if (!isset($this->installedMap[$package->id])) {
-                                $this->addRulesForPackage($package, $ignorePlatformReqs);
-                            }
-                        }
+            $packages = $this->pool->whatProvides($packageName, $constraint);
+            if ($packages) {
+                foreach ($packages as $package) {
+                    $this->addRulesForPackage($package, $ignorePlatformReqs);
+                }
 
-                        $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job);
-                        $this->addRule(RuleSet::TYPE_JOB, $rule);
-                    }
-                    break;
-                case 'remove':
-                    // remove all packages with this name including uninstalled
-                    // ones to make sure none of them are picked as replacements
-                    $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
-                    foreach ($packages as $package) {
-                        $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job);
-                        $this->addRule(RuleSet::TYPE_JOB, $rule);
-                    }
-                    break;
+                $rule = $this->createInstallOneOfRule($packages, Rule::RULE_ROOT_REQUIRE, array(
+                    'packageName' => $packageName,
+                    'constraint' => $constraint,
+                ));
+                $this->addRule(RuleSet::TYPE_REQUEST, $rule);
             }
         }
     }
 
-    public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false)
+    public function getRulesFor(Request $request, $ignorePlatformReqs = false)
     {
-        $this->jobs = $jobs;
         $this->rules = new RuleSet;
-        $this->installedMap = $installedMap;
-
-        $this->whitelistedMap = array();
-        foreach ($this->installedMap as $package) {
-            $this->whitelistFromPackage($package);
-        }
-        $this->whitelistFromJobs();
-
-        $this->pool->setWhitelist($this->whitelistedMap);
 
         $this->addedMap = array();
         $this->conflictAddedMap = array();
         $this->addedPackages = array();
         $this->addedPackagesByNames = array();
-        foreach ($this->installedMap as $package) {
-            $this->addRulesForPackage($package, $ignorePlatformReqs);
-        }
+        $this->conflictsForName = array();
 
-        $this->addRulesForJobs($ignorePlatformReqs);
+        $this->addRulesForRequest($request, $ignorePlatformReqs);
 
         $this->addConflictRules($ignorePlatformReqs);
 

+ 44 - 21
src/Composer/DependencyResolver/RuleWatchGraph.php

@@ -44,13 +44,24 @@ class RuleWatchGraph
             return;
         }
 
-        foreach (array($node->watch1, $node->watch2) as $literal) {
-            if (!isset($this->watchChains[$literal])) {
-                $this->watchChains[$literal] = new RuleWatchChain;
+        if (!$node->getRule() instanceof MultiConflictRule) {
+            foreach (array($node->watch1, $node->watch2) as $literal) {
+                if (!isset($this->watchChains[$literal])) {
+                    $this->watchChains[$literal] = new RuleWatchChain;
+                }
+
+                $this->watchChains[$literal]->unshift($node);
             }
+        } else {
+            foreach ($node->getRule()->getLiterals() as $literal) {
+                if (!isset($this->watchChains[$literal])) {
+                    $this->watchChains[$literal] = new RuleWatchChain;
+                }
 
-            $this->watchChains[$literal]->unshift($node);
+                $this->watchChains[$literal]->unshift($node);
+            }
         }
+
     }
 
     /**
@@ -92,28 +103,40 @@ class RuleWatchGraph
         $chain->rewind();
         while ($chain->valid()) {
             $node = $chain->current();
-            $otherWatch = $node->getOtherWatch($literal);
+            if (!$node->getRule() instanceof MultiConflictRule) {
+                $otherWatch = $node->getOtherWatch($literal);
 
-            if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) {
-                $ruleLiterals = $node->getRule()->getLiterals();
+                if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) {
+                    $ruleLiterals = $node->getRule()->getLiterals();
 
-                $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) {
-                    return $literal !== $ruleLiteral &&
-                        $otherWatch !== $ruleLiteral &&
-                        !$decisions->conflict($ruleLiteral);
-                });
+                    $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) {
+                        return $literal !== $ruleLiteral &&
+                            $otherWatch !== $ruleLiteral &&
+                            !$decisions->conflict($ruleLiteral);
+                    });
 
-                if ($alternativeLiterals) {
-                    reset($alternativeLiterals);
-                    $this->moveWatch($literal, current($alternativeLiterals), $node);
-                    continue;
-                }
+                    if ($alternativeLiterals) {
+                        reset($alternativeLiterals);
+                        $this->moveWatch($literal, current($alternativeLiterals), $node);
+                        continue;
+                    }
 
-                if ($decisions->conflict($otherWatch)) {
-                    return $node->getRule();
-                }
+                    if ($decisions->conflict($otherWatch)) {
+                        return $node->getRule();
+                    }
 
-                $decisions->decide($otherWatch, $level, $node->getRule());
+                    $decisions->decide($otherWatch, $level, $node->getRule());
+                }
+            } else {
+                foreach ($node->getRule()->getLiterals() as $otherLiteral) {
+                    if ($literal !== $otherLiteral && !$decisions->satisfy($otherLiteral)) {
+                        if ($decisions->conflict($otherLiteral)) {
+                            return $node->getRule();
+                        }
+
+                        $decisions->decide($otherLiteral, $level, $node->getRule());
+                    }
+                }
             }
 
             $chain->next();

+ 1 - 1
src/Composer/DependencyResolver/RuleWatchNode.php

@@ -55,7 +55,7 @@ class RuleWatchNode
         $literals = $this->rule->getLiterals();
 
         // if there are only 2 elements, both are being watched anyway
-        if (count($literals) < 3) {
+        if (count($literals) < 3 || $this->rule instanceof MultiConflictRule) {
             return;
         }
 

+ 65 - 141
src/Composer/DependencyResolver/Solver.php

@@ -13,7 +13,7 @@
 namespace Composer\DependencyResolver;
 
 use Composer\IO\IOInterface;
-use Composer\Repository\RepositoryInterface;
+use Composer\Package\PackageInterface;
 use Composer\Repository\PlatformRepository;
 
 /**
@@ -28,23 +28,18 @@ class Solver
     protected $policy;
     /** @var Pool */
     protected $pool;
-    /** @var RepositoryInterface */
-    protected $installed;
+
     /** @var RuleSet */
     protected $rules;
     /** @var RuleSetGenerator */
     protected $ruleSetGenerator;
-    /** @var array */
-    protected $jobs;
 
-    /** @var int[] */
-    protected $updateMap = array();
     /** @var RuleWatchGraph */
     protected $watchGraph;
     /** @var Decisions */
     protected $decisions;
-    /** @var int[] */
-    protected $installedMap;
+    /** @var PackageInterface[] */
+    protected $fixedMap;
 
     /** @var int */
     protected $propagateIndex;
@@ -66,16 +61,13 @@ class Solver
     /**
      * @param PolicyInterface     $policy
      * @param Pool                $pool
-     * @param RepositoryInterface $installed
      * @param IOInterface         $io
      */
-    public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io)
+    public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io)
     {
         $this->io = $io;
         $this->policy = $policy;
         $this->pool = $pool;
-        $this->installed = $installed;
-        $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
     }
 
     /**
@@ -86,6 +78,11 @@ class Solver
         return count($this->rules);
     }
 
+    public function getPool()
+    {
+        return $this->pool;
+    }
+
     // aka solver_makeruledecisions
 
     private function makeAssertionRuleDecisions()
@@ -121,23 +118,23 @@ class Solver
             $conflict = $this->decisions->decisionRule($literal);
 
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
-                $problem = new Problem($this->pool);
+                $problem = new Problem();
 
                 $problem->addRule($rule);
                 $problem->addRule($conflict);
-                $this->disableProblem($rule);
+                $rule->disable();
                 $this->problems[] = $problem;
                 continue;
             }
 
-            // conflict with another job
-            $problem = new Problem($this->pool);
+            // conflict with another root require/fixed package
+            $problem = new Problem();
             $problem->addRule($rule);
             $problem->addRule($conflict);
 
-            // push all of our rules (can only be job rules)
+            // push all of our rules (can only be root require/fixed package rules)
             // asserting this literal on the problem stack
-            foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) {
+            foreach ($this->rules->getIteratorFor(RuleSet::TYPE_REQUEST) as $assertRule) {
                 if ($assertRule->isDisabled() || !$assertRule->isAssertion()) {
                     continue;
                 }
@@ -148,9 +145,8 @@ class Solver
                 if (abs($literal) !== abs($assertRuleLiteral)) {
                     continue;
                 }
-
                 $problem->addRule($assertRule);
-                $this->disableProblem($assertRule);
+                $assertRule->disable();
             }
             $this->problems[] = $problem;
 
@@ -159,47 +155,29 @@ class Solver
         }
     }
 
-    protected function setupInstalledMap()
+    protected function setupFixedMap(Request $request)
     {
-        $this->installedMap = array();
-        foreach ($this->installed->getPackages() as $package) {
-            $this->installedMap[$package->id] = $package;
+        $this->fixedMap = array();
+        foreach ($request->getFixedPackages() as $package) {
+            $this->fixedMap[$package->id] = $package;
         }
     }
 
     /**
+     * @param  Request $request
      * @param bool $ignorePlatformReqs
      */
-    protected function checkForRootRequireProblems($ignorePlatformReqs)
+    protected function checkForRootRequireProblems($request, $ignorePlatformReqs)
     {
-        foreach ($this->jobs as $job) {
-            switch ($job['cmd']) {
-                case 'update':
-                    $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
-                    foreach ($packages as $package) {
-                        if (isset($this->installedMap[$package->id])) {
-                            $this->updateMap[$package->id] = true;
-                        }
-                    }
-                    break;
-
-                case 'update-all':
-                    foreach ($this->installedMap as $package) {
-                        $this->updateMap[$package->id] = true;
-                    }
-                    break;
-
-                case 'install':
-                    if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
-                        break;
-                    }
+        foreach ($request->getRequires() as $packageName => $constraint) {
+            if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) {
+                continue;
+            }
 
-                    if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) {
-                        $problem = new Problem($this->pool);
-                        $problem->addRule(new GenericRule(array(), null, null, $job));
-                        $this->problems[] = $problem;
-                    }
-                    break;
+            if (!$this->pool->whatProvides($packageName, $constraint)) {
+                $problem = new Problem();
+                $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint)));
+                $this->problems[] = $problem;
             }
         }
     }
@@ -207,15 +185,16 @@ class Solver
     /**
      * @param  Request $request
      * @param  bool    $ignorePlatformReqs
-     * @return array
+     * @return LockTransaction
      */
     public function solve(Request $request, $ignorePlatformReqs = false)
     {
-        $this->jobs = $request->getJobs();
+        $this->setupFixedMap($request);
 
-        $this->setupInstalledMap();
-        $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs);
-        $this->checkForRootRequireProblems($ignorePlatformReqs);
+        $this->io->writeError('Generating rules', true, IOInterface::DEBUG);
+        $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool);
+        $this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs);
+        $this->checkForRootRequireProblems($request, $ignorePlatformReqs);
         $this->decisions = new Decisions($this->pool);
         $this->watchGraph = new RuleWatchGraph;
 
@@ -223,29 +202,20 @@ class Solver
             $this->watchGraph->insert(new RuleWatchNode($rule));
         }
 
-        /* make decisions based on job/update assertions */
+        /* make decisions based on root require/fix assertions */
         $this->makeAssertionRuleDecisions();
 
         $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG);
         $before = microtime(true);
-        $this->runSat(true);
+        $this->runSat();
         $this->io->writeError('', true, IOInterface::DEBUG);
         $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE);
 
-        // decide to remove everything that's installed and undecided
-        foreach ($this->installedMap as $packageId => $void) {
-            if ($this->decisions->undecided($packageId)) {
-                $this->decisions->decide(-$packageId, 1, null);
-            }
-        }
-
         if ($this->problems) {
-            throw new SolverProblemsException($this->problems, $this->installedMap);
+            throw new SolverProblemsException($this->problems, $this->learnedPool);
         }
 
-        $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
-
-        return $transaction->getOperations();
+        return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
     }
 
     /**
@@ -322,11 +292,10 @@ class Solver
      *
      * @param  int        $level
      * @param  string|int $literal
-     * @param  bool       $disableRules
      * @param  Rule       $rule
      * @return int
      */
-    private function setPropagateLearn($level, $literal, $disableRules, Rule $rule)
+    private function setPropagateLearn($level, $literal, Rule $rule)
     {
         $level++;
 
@@ -340,7 +309,7 @@ class Solver
             }
 
             if ($level == 1) {
-                return $this->analyzeUnsolvable($rule, $disableRules);
+                return $this->analyzeUnsolvable($rule);
             }
 
             // conflict
@@ -377,14 +346,13 @@ class Solver
     /**
      * @param  int   $level
      * @param  array $decisionQueue
-     * @param  bool  $disableRules
      * @param  Rule  $rule
      * @return int
      */
-    private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule)
+    private function selectAndInstall($level, array $decisionQueue, Rule $rule)
     {
         // choose best package to install from decisionQueue
-        $literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage());
+        $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage());
 
         $selectedLiteral = array_shift($literals);
 
@@ -393,7 +361,7 @@ class Solver
             $this->branches[] = array($literals, $level);
         }
 
-        return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule);
+        return $this->setPropagateLearn($level, $selectedLiteral, $rule);
     }
 
     /**
@@ -539,12 +507,11 @@ class Solver
 
     /**
      * @param  Rule $conflictRule
-     * @param  bool $disableRules
      * @return int
      */
-    private function analyzeUnsolvable(Rule $conflictRule, $disableRules)
+    private function analyzeUnsolvable(Rule $conflictRule)
     {
-        $problem = new Problem($this->pool);
+        $problem = new Problem();
         $problem->addRule($conflictRule);
 
         $this->analyzeUnsolvableRule($problem, $conflictRule);
@@ -586,41 +553,9 @@ class Solver
             }
         }
 
-        if ($disableRules) {
-            foreach ($this->problems[count($this->problems) - 1] as $reason) {
-                $this->disableProblem($reason['rule']);
-            }
-
-            $this->resetSolver();
-
-            return 1;
-        }
-
         return 0;
     }
 
-    /**
-     * @param Rule $why
-     */
-    private function disableProblem(Rule $why)
-    {
-        $job = $why->getJob();
-
-        if (!$job) {
-            $why->disable();
-
-            return;
-        }
-
-        // disable all rules of this job
-        foreach ($this->rules as $rule) {
-            /** @var Rule $rule */
-            if ($job === $rule->getJob()) {
-                $rule->disable();
-            }
-        }
-    }
-
     private function resetSolver()
     {
         $this->decisions->reset();
@@ -661,17 +596,14 @@ class Solver
         }
     }
 
-    /**
-     * @param bool $disableRules
-     */
-    private function runSat($disableRules = true)
+    private function runSat()
     {
         $this->propagateIndex = 0;
 
         /*
          * here's the main loop:
          * 1) propagate new decisions (only needed once)
-         * 2) fulfill jobs
+         * 2) fulfill root requires/fixed packages
          * 3) fulfill all unresolved rules
          * 4) minimalize solution if we had choices
          * if we encounter a problem, we rewind to a safe level and restart
@@ -679,10 +611,7 @@ class Solver
          */
 
         $decisionQueue = array();
-        /**
-         * @todo this makes $disableRules always false; determine the rationale and possibly remove dead code?
-         */
-        $disableRules = array();
+        $decisionSupplementQueue = array();
 
         $level = 1;
         $systemLevel = $level + 1;
@@ -691,7 +620,7 @@ class Solver
             if (1 === $level) {
                 $conflictRule = $this->propagate($level);
                 if (null !== $conflictRule) {
-                    if ($this->analyzeUnsolvable($conflictRule, $disableRules)) {
+                    if ($this->analyzeUnsolvable($conflictRule)) {
                         continue;
                     }
 
@@ -699,9 +628,9 @@ class Solver
                 }
             }
 
-            // handle job rules
+            // handle root require/fixed package rules
             if ($level < $systemLevel) {
-                $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB);
+                $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_REQUEST);
                 foreach ($iterator as $rule) {
                     if ($rule->isEnabled()) {
                         $decisionQueue = array();
@@ -718,26 +647,21 @@ class Solver
                         }
 
                         if ($noneSatisfied && count($decisionQueue)) {
-                            // prune all update packages until installed version
-                            // except for requested updates
-                            if (count($this->installed) != count($this->updateMap)) {
-                                $prunedQueue = array();
-                                foreach ($decisionQueue as $literal) {
-                                    if (isset($this->installedMap[abs($literal)])) {
-                                        $prunedQueue[] = $literal;
-                                        if (isset($this->updateMap[abs($literal)])) {
-                                            $prunedQueue = $decisionQueue;
-                                            break;
-                                        }
-                                    }
+                            // if any of the options in the decision queue are fixed, only use those
+                            $prunedQueue = array();
+                            foreach ($decisionQueue as $literal) {
+                                if (isset($this->fixedMap[abs($literal)])) {
+                                    $prunedQueue[] = $literal;
                                 }
+                            }
+                            if (!empty($prunedQueue)) {
                                 $decisionQueue = $prunedQueue;
                             }
                         }
 
                         if ($noneSatisfied && count($decisionQueue)) {
                             $oLevel = $level;
-                            $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
+                            $level = $this->selectAndInstall($level, $decisionQueue, $rule);
 
                             if (0 === $level) {
                                 return;
@@ -751,7 +675,7 @@ class Solver
 
                 $systemLevel = $level + 1;
 
-                // jobs left
+                // root requires/fixed packages left
                 $iterator->next();
                 if ($iterator->valid()) {
                     continue;
@@ -813,7 +737,7 @@ class Solver
                     continue;
                 }
 
-                $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
+                $level = $this->selectAndInstall($level, $decisionQueue, $rule);
 
                 if (0 === $level) {
                     return;
@@ -856,7 +780,7 @@ class Solver
 
                     $why = $this->decisions->lastReason();
 
-                    $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
+                    $level = $this->setPropagateLearn($level, $lastLiteral, $why);
 
                     if ($level == 0) {
                         return;

+ 18 - 9
src/Composer/DependencyResolver/SolverProblemsException.php

@@ -13,6 +13,7 @@
 namespace Composer\DependencyResolver;
 
 use Composer\Util\IniHelper;
+use Composer\Repository\RepositorySet;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
@@ -20,29 +21,33 @@ use Composer\Util\IniHelper;
 class SolverProblemsException extends \RuntimeException
 {
     protected $problems;
-    protected $installedMap;
+    protected $learnedPool;
 
-    public function __construct(array $problems, array $installedMap)
+    public function __construct(array $problems, array $learnedPool)
     {
         $this->problems = $problems;
-        $this->installedMap = $installedMap;
+        $this->learnedPool = $learnedPool;
 
-        parent::__construct($this->createMessage(), 2);
+        parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2);
     }
 
-    protected function createMessage()
+    public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isDevExtraction = false)
     {
+        $installedMap = $request->getPresentMap(true);
         $text = "\n";
         $hasExtensionProblems = false;
+        $isCausedByLock = false;
         foreach ($this->problems as $i => $problem) {
-            $text .= "  Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n";
+            $text .= "  Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n";
 
             if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
                 $hasExtensionProblems = true;
             }
+
+            $isCausedByLock |= $problem->isCausedByLock();
         }
 
-        if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) {
+        if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) {
             $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
         }
 
@@ -50,6 +55,10 @@ class SolverProblemsException extends \RuntimeException
             $text .= $this->createExtensionHint();
         }
 
+        if ($isCausedByLock && !$isDevExtraction) {
+            $text .= "\nUse the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions.";
+        }
+
         return $text;
     }
 
@@ -76,8 +85,8 @@ class SolverProblemsException extends \RuntimeException
     private function hasExtensionProblems(array $reasonSets)
     {
         foreach ($reasonSets as $reasonSet) {
-            foreach ($reasonSet as $reason) {
-                if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) {
+            foreach ($reasonSet as $rule) {
+                if (0 === strpos($rule->getRequiredPackage(), 'ext-')) {
                     return true;
                 }
             }

+ 214 - 152
src/Composer/DependencyResolver/Transaction.php

@@ -13,161 +13,205 @@
 namespace Composer\DependencyResolver;
 
 use Composer\Package\AliasPackage;
+use Composer\Package\Link;
+use Composer\Package\PackageInterface;
+use Composer\Repository\PlatformRepository;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
  */
 class Transaction
 {
-    protected $policy;
-    protected $pool;
-    protected $installedMap;
-    protected $decisions;
-    protected $transaction;
-
-    public function __construct($policy, $pool, $installedMap, $decisions)
+    /**
+     * @var array
+     */
+    protected $operations;
+
+    /**
+     * Packages present at the beginning of the transaction
+     * @var array
+     */
+    protected $presentPackages;
+
+    /**
+     * Package set resulting from this transaction
+     * @var array
+     */
+    protected $resultPackageMap;
+
+    /**
+     * @var array
+     */
+    protected $resultPackagesByName = array();
+
+    public function __construct($presentPackages, $resultPackages)
     {
-        $this->policy = $policy;
-        $this->pool = $pool;
-        $this->installedMap = $installedMap;
-        $this->decisions = $decisions;
-        $this->transaction = array();
+        $this->presentPackages = $presentPackages;
+        $this->setResultPackageMaps($resultPackages);
+        $this->operations = $this->calculateOperations();
     }
 
     public function getOperations()
     {
-        $installMeansUpdateMap = $this->findUpdates();
-
-        $updateMap = array();
-        $installMap = array();
-        $uninstallMap = array();
-
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $reason = $decision[Decisions::DECISION_REASON];
-
-            $package = $this->pool->literalToPackage($literal);
+        return $this->operations;
+    }
 
-            // wanted & installed || !wanted & !installed
-            if (($literal > 0) == isset($this->installedMap[$package->id])) {
-                continue;
+    private function setResultPackageMaps($resultPackages)
+    {
+        $packageSort = function (PackageInterface $a, PackageInterface $b) {
+            // sort alias packages by the same name behind their non alias version
+            if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) {
+                return $a instanceof AliasPackage ? -1 : 1;
             }
-
-            if ($literal > 0) {
-                if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) {
-                    $source = $installMeansUpdateMap[abs($literal)];
-
-                    $updateMap[$package->id] = array(
-                        'package' => $package,
-                        'source' => $source,
-                        'reason' => $reason,
-                    );
-
-                    // avoid updates to one package from multiple origins
-                    unset($installMeansUpdateMap[abs($literal)]);
-                    $ignoreRemove[$source->id] = true;
-                } else {
-                    $installMap[$package->id] = array(
-                        'package' => $package,
-                        'reason' => $reason,
-                    );
-                }
+            return strcmp($b->getName(), $a->getName());
+        };
+
+        $this->resultPackageMap = array();
+        foreach ($resultPackages as $package) {
+            $this->resultPackageMap[spl_object_hash($package)] = $package;
+            foreach ($package->getNames() as $name) {
+                $this->resultPackagesByName[$name][] = $package;
             }
         }
 
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $reason = $decision[Decisions::DECISION_REASON];
-            $package = $this->pool->literalToPackage($literal);
-
-            if ($literal <= 0 &&
-                isset($this->installedMap[$package->id]) &&
-                !isset($ignoreRemove[$package->id])) {
-                $uninstallMap[$package->id] = array(
-                    'package' => $package,
-                    'reason' => $reason,
-                );
-            }
+        uasort($this->resultPackageMap, $packageSort);
+        foreach ($this->resultPackagesByName as $name => $packages) {
+            uasort($this->resultPackagesByName[$name], $packageSort);
         }
-
-        $this->transactionFromMaps($installMap, $updateMap, $uninstallMap);
-
-        return $this->transaction;
     }
 
-    protected function transactionFromMaps($installMap, $updateMap, $uninstallMap)
+    protected function calculateOperations()
     {
-        $queue = array_map(
-            function ($operation) {
-                return $operation['package'];
-            },
-            $this->findRootPackages($installMap, $updateMap)
-        );
+        $operations = array();
+
+        $presentPackageMap = array();
+        $removeMap = array();
+        $presentAliasMap = array();
+        $removeAliasMap = array();
+        foreach ($this->presentPackages as $package) {
+            if ($package instanceof AliasPackage) {
+                $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
+                $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
+            } else {
+                $presentPackageMap[$package->getName()] = $package;
+                $removeMap[$package->getName()] = $package;
+            }
+        }
+
+        $stack = $this->getRootPackages();
 
         $visited = array();
+        $processed = array();
+
+        while (!empty($stack)) {
+            $package = array_pop($stack);
 
-        while (!empty($queue)) {
-            $package = array_pop($queue);
-            $packageId = $package->id;
+            if (isset($processed[spl_object_hash($package)])) {
+                continue;
+            }
 
-            if (!isset($visited[$packageId])) {
-                $queue[] = $package;
+            if (!isset($visited[spl_object_hash($package)])) {
+                $visited[spl_object_hash($package)] = true;
 
+                $stack[] = $package;
                 if ($package instanceof AliasPackage) {
-                    $queue[] = $package->getAliasOf();
+                    $stack[] = $package->getAliasOf();
                 } else {
                     foreach ($package->getRequires() as $link) {
-                        $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+                        $possibleRequires = $this->getProvidersInResult($link);
 
                         foreach ($possibleRequires as $require) {
-                            $queue[] = $require;
+                            $stack[] = $require;
                         }
                     }
                 }
+            } elseif (!isset($processed[spl_object_hash($package)])) {
+                $processed[spl_object_hash($package)] = true;
 
-                $visited[$package->id] = true;
-            } else {
-                if (isset($installMap[$packageId])) {
-                    $this->install(
-                        $installMap[$packageId]['package'],
-                        $installMap[$packageId]['reason']
-                    );
-                    unset($installMap[$packageId]);
-                }
-                if (isset($updateMap[$packageId])) {
-                    $this->update(
-                        $updateMap[$packageId]['source'],
-                        $updateMap[$packageId]['package'],
-                        $updateMap[$packageId]['reason']
-                    );
-                    unset($updateMap[$packageId]);
+                if ($package instanceof AliasPackage) {
+                    $aliasKey = $package->getName().'::'.$package->getVersion();
+                    if (isset($presentAliasMap[$aliasKey])) {
+                        unset($removeAliasMap[$aliasKey]);
+                    } else {
+                        $operations[] = new Operation\MarkAliasInstalledOperation($package);
+                    }
+                } else {
+                    if (isset($presentPackageMap[$package->getName()])) {
+                        $source = $presentPackageMap[$package->getName()];
+
+                        // do we need to update?
+                        // TODO different for lock?
+                        if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() ||
+                            $package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() ||
+                            $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()
+                        ) {
+                            $operations[] = new Operation\UpdateOperation($source, $package);
+                        }
+                        unset($removeMap[$package->getName()]);
+                    } else {
+                        $operations[] = new Operation\InstallOperation($package);
+                        unset($removeMap[$package->getName()]);
+                    }
                 }
             }
         }
 
-        foreach ($uninstallMap as $uninstall) {
-            $this->uninstall($uninstall['package'], $uninstall['reason']);
+        foreach ($removeMap as $name => $package) {
+            array_unshift($operations, new Operation\UninstallOperation($package, null));
         }
+        foreach ($removeAliasMap as $nameVersion => $package) {
+            $operations[] = new Operation\MarkAliasUninstalledOperation($package, null);
+        }
+
+        $operations = $this->movePluginsToFront($operations);
+        // TODO fix this:
+        // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls
+        $operations = $this->moveUninstallsToFront($operations);
+
+        // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place?
+        /*
+        if ('update' === $opType) {
+            $targetPackage = $operation->getTargetPackage();
+            if ($targetPackage->isDev()) {
+                $initialPackage = $operation->getInitialPackage();
+                if ($targetPackage->getVersion() === $initialPackage->getVersion()
+                    && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference())
+                    && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference())
+                ) {
+                    $this->io->writeError('  - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG);
+                    $this->io->writeError('', true, IOInterface::DEBUG);
+
+                    continue;
+                }
+            }
+        }*/
+
+        return $this->operations = $operations;
     }
 
-    protected function findRootPackages($installMap, $updateMap)
+    /**
+     * Determine which packages in the result are not required by any other packages in it.
+     *
+     * These serve as a starting point to enumerate packages in a topological order despite potential cycles.
+     * If there are packages with a cycle on the top level the package with the lowest name gets picked
+     *
+     * @return array
+     */
+    protected function getRootPackages()
     {
-        $packages = $installMap + $updateMap;
-        $roots = $packages;
-
-        foreach ($packages as $packageId => $operation) {
-            $package = $operation['package'];
+        $roots = $this->resultPackageMap;
 
-            if (!isset($roots[$packageId])) {
+        foreach ($this->resultPackageMap as $packageHash => $package) {
+            if (!isset($roots[$packageHash])) {
                 continue;
             }
 
             foreach ($package->getRequires() as $link) {
-                $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+                $possibleRequires = $this->getProvidersInResult($link);
 
                 foreach ($possibleRequires as $require) {
                     if ($require !== $package) {
-                        unset($roots[$require->id]);
+                        unset($roots[spl_object_hash($require)]);
                     }
                 }
             }
@@ -176,69 +220,87 @@ class Transaction
         return $roots;
     }
 
-    protected function findUpdates()
+    protected function getProvidersInResult(Link $link)
     {
-        $installMeansUpdateMap = array();
-
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $package = $this->pool->literalToPackage($literal);
+        if (!isset($this->resultPackagesByName[$link->getTarget()])) {
+            return array();
+        }
+        return $this->resultPackagesByName[$link->getTarget()];
+    }
 
-            if ($package instanceof AliasPackage) {
+    /**
+     * Workaround: if your packages depend on plugins, we must be sure
+     * that those are installed / updated first; else it would lead to packages
+     * being installed multiple times in different folders, when running Composer
+     * twice.
+     *
+     * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147,
+     * it at least fixes the symptoms and makes usage of composer possible (again)
+     * in such scenarios.
+     *
+     * @param  Operation\OperationInterface[] $operations
+     * @return Operation\OperationInterface[] reordered operation list
+     */
+    private function movePluginsToFront(array $operations)
+    {
+        $pluginsNoDeps = array();
+        $pluginsWithDeps = array();
+        $pluginRequires = array();
+
+        foreach (array_reverse($operations, true) as $idx => $op) {
+            if ($op instanceof Operation\InstallOperation) {
+                $package = $op->getPackage();
+            } elseif ($op instanceof Operation\UpdateOperation) {
+                $package = $op->getTargetPackage();
+            } else {
                 continue;
             }
 
-            // !wanted & installed
-            if ($literal <= 0 && isset($this->installedMap[$package->id])) {
-                $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
+            // is this package a plugin?
+            $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
 
-                $literals = array($package->id);
+            // is this a plugin or a dependency of a plugin?
+            if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) {
+                // get the package's requires, but filter out any platform requirements or 'composer-plugin-api'
+                $requires = array_filter(array_keys($package->getRequires()), function ($req) {
+                    return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req);
+                });
 
-                foreach ($updates as $update) {
-                    $literals[] = $update->id;
+                // is this a plugin with no meaningful dependencies?
+                if ($isPlugin && !count($requires)) {
+                    // plugins with no dependencies go to the very front
+                    array_unshift($pluginsNoDeps, $op);
+                } else {
+                    // capture the requirements for this package so those packages will be moved up as well
+                    $pluginRequires = array_merge($pluginRequires, $requires);
+                    // move the operation to the front
+                    array_unshift($pluginsWithDeps, $op);
                 }
 
-                foreach ($literals as $updateLiteral) {
-                    if ($updateLiteral !== $literal) {
-                        $installMeansUpdateMap[abs($updateLiteral)] = $package;
-                    }
-                }
+                unset($operations[$idx]);
             }
         }
 
-        return $installMeansUpdateMap;
+        return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations);
     }
 
-    protected function install($package, $reason)
+    /**
+     * Removals of packages should be executed before installations in
+     * case two packages resolve to the same path (due to custom installers)
+     *
+     * @param  Operation\OperationInterface[] $operations
+     * @return Operation\OperationInterface[] reordered operation list
+     */
+    private function moveUninstallsToFront(array $operations)
     {
-        if ($package instanceof AliasPackage) {
-            return $this->markAliasInstalled($package, $reason);
-        }
-
-        $this->transaction[] = new Operation\InstallOperation($package, $reason);
-    }
-
-    protected function update($from, $to, $reason)
-    {
-        $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason);
-    }
-
-    protected function uninstall($package, $reason)
-    {
-        if ($package instanceof AliasPackage) {
-            return $this->markAliasUninstalled($package, $reason);
+        $uninstOps = array();
+        foreach ($operations as $idx => $op) {
+            if ($op instanceof Operation\UninstallOperation) {
+                $uninstOps[] = $op;
+                unset($operations[$idx]);
+            }
         }
 
-        $this->transaction[] = new Operation\UninstallOperation($package, $reason);
-    }
-
-    protected function markAliasInstalled($package, $reason)
-    {
-        $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason);
-    }
-
-    protected function markAliasUninstalled($package, $reason)
-    {
-        $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason);
+        return array_merge($uninstOps, $operations);
     }
 }

+ 50 - 55
src/Composer/Downloader/ArchiveDownloader.php

@@ -30,33 +30,56 @@ abstract class ArchiveDownloader extends FileDownloader
      * @throws \RuntimeException
      * @throws \UnexpectedValueException
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function install(PackageInterface $package, $path, $output = true)
     {
-        $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
-        $retries = 3;
-        while ($retries--) {
-            $fileName = parent::download($package, $path, $output);
+        if ($output) {
+            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): Extracting archive");
+        } else {
+            $this->io->writeError('Extracting archive', false);
+        }
 
-            if ($output) {
-                $this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE);
-            }
+        $this->filesystem->ensureDirectoryExists($path);
+        if (!$this->filesystem->isDirEmpty($path)) {
+            throw new \RuntimeException('Expected empty path to extract '.$package.' into but directory exists: '.$path);
+        }
+
+        do {
+            $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
+        } while (is_dir($temporaryDir));
 
+        $fileName = $this->getFileName($package, $path);
+
+        try {
+            $this->filesystem->ensureDirectoryExists($temporaryDir);
             try {
-                $this->filesystem->ensureDirectoryExists($temporaryDir);
-                try {
-                    $this->extract($fileName, $temporaryDir);
-                } catch (\Exception $e) {
-                    // remove cache if the file was corrupted
-                    parent::clearLastCacheWrite($package);
-                    throw $e;
-                }
+                $this->extract($package, $fileName, $temporaryDir);
+            } catch (\Exception $e) {
+                // remove cache if the file was corrupted
+                parent::clearLastCacheWrite($package);
+                throw $e;
+            }
+
+            $this->filesystem->unlink($fileName);
 
-                $this->filesystem->unlink($fileName);
+            $renameAsOne = false;
+            if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) {
+                $renameAsOne = true;
+            }
 
-                $contentDir = $this->getFolderContent($temporaryDir);
+            $contentDir = $this->getFolderContent($temporaryDir);
+            $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir));
 
+            if ($renameAsOne) {
+                // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents
+                if ($singleDirAtTopLevel) {
+                    $extractedDir = (string) reset($contentDir);
+                } else {
+                    $extractedDir = $temporaryDir;
+                }
+                $this->filesystem->rename($extractedDir, $path);
+            } else {
                 // only one dir in the archive, extract its contents out of it
-                if (1 === count($contentDir) && is_dir(reset($contentDir))) {
+                if ($singleDirAtTopLevel) {
                     $contentDir = $this->getFolderContent((string) reset($contentDir));
                 }
 
@@ -65,44 +88,16 @@ abstract class ArchiveDownloader extends FileDownloader
                     $file = (string) $file;
                     $this->filesystem->rename($file, $path . '/' . basename($file));
                 }
-
-                $this->filesystem->removeDirectory($temporaryDir);
-                if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
-                    $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
-                }
-                if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
-                    $this->filesystem->removeDirectory($this->config->get('vendor-dir'));
-                }
-            } catch (\Exception $e) {
-                // clean up
-                $this->filesystem->removeDirectory($path);
-                $this->filesystem->removeDirectory($temporaryDir);
-
-                // retry downloading if we have an invalid zip file
-                if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) {
-                    $this->io->writeError('');
-                    if ($this->io->isDebug()) {
-                        $this->io->writeError('    Invalid zip file ('.$e->getMessage().'), retrying...');
-                    } else {
-                        $this->io->writeError('    Invalid zip file, retrying...');
-                    }
-                    usleep(500000);
-                    continue;
-                }
-
-                throw $e;
             }
 
-            break;
-        }
-    }
+            $this->filesystem->removeDirectory($temporaryDir);
+        } catch (\Exception $e) {
+            // clean up
+            $this->filesystem->removeDirectory($path);
+            $this->filesystem->removeDirectory($temporaryDir);
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function getFileName(PackageInterface $package, $path)
-    {
-        return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
+            throw $e;
+        }
     }
 
     /**
@@ -113,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader
      *
      * @throws \UnexpectedValueException If can not extract downloaded file to path
      */
-    abstract protected function extract($file, $path);
+    abstract protected function extract(PackageInterface $package, $file, $path);
 
     /**
      * Returns the folder content, excluding dotfiles

+ 210 - 88
src/Composer/Downloader/DownloadManager.php

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Package\PackageInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 /**
  * Downloaders manager.
@@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
 class DownloadManager
 {
     private $io;
+    private $httpDownloader;
     private $preferDist = false;
     private $preferSource = false;
     private $packagePreferences = array();
@@ -33,9 +35,9 @@ class DownloadManager
     /**
      * Initializes download manager.
      *
-     * @param IOInterface     $io           The Input Output Interface
-     * @param bool            $preferSource prefer downloading from source
-     * @param Filesystem|null $filesystem   custom Filesystem object
+     * @param IOInterface     $io             The Input Output Interface
+     * @param bool            $preferSource   prefer downloading from source
+     * @param Filesystem|null $filesystem     custom Filesystem object
      */
     public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
     {
@@ -83,22 +85,6 @@ class DownloadManager
         return $this;
     }
 
-    /**
-     * Sets whether to output download progress information for all registered
-     * downloaders
-     *
-     * @param  bool            $outputProgress
-     * @return DownloadManager
-     */
-    public function setOutputProgress($outputProgress)
-    {
-        foreach ($this->downloaders as $downloader) {
-            $downloader->setOutputProgress($outputProgress);
-        }
-
-        return $this;
-    }
-
     /**
      * Sets installer downloader for a specific installation type.
      *
@@ -140,7 +126,7 @@ class DownloadManager
      *                                           wrong type
      * @return DownloaderInterface|null
      */
-    public function getDownloaderForInstalledPackage(PackageInterface $package)
+    public function getDownloaderForPackage(PackageInterface $package)
     {
         $installationSource = $package->getInstallationSource();
 
@@ -154,7 +140,7 @@ class DownloadManager
             $downloader = $this->getDownloader($package->getSourceType());
         } else {
             throw new \InvalidArgumentException(
-                'Package '.$package.' seems not been installed properly'
+                'Package '.$package.' does not have an installation source set'
             );
         }
 
@@ -171,63 +157,117 @@ class DownloadManager
         return $downloader;
     }
 
+    public function getDownloaderType(DownloaderInterface $downloader)
+    {
+        return array_search($downloader, $this->downloaders);
+    }
+
     /**
      * Downloads package into target dir.
      *
-     * @param PackageInterface $package      package instance
-     * @param string           $targetDir    target dir
-     * @param bool             $preferSource prefer installation from source
+     * @param PackageInterface      $package      package instance
+     * @param string                $targetDir    target dir
+     * @param PackageInterface|null $prevPackage  previous package instance in case of updates
      *
+     * @return PromiseInterface
      * @throws \InvalidArgumentException if package have no urls to download from
      * @throws \RuntimeException
      */
-    public function download(PackageInterface $package, $targetDir, $preferSource = null)
+    public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
     {
-        $preferSource = null !== $preferSource ? $preferSource : $this->preferSource;
-        $sourceType = $package->getSourceType();
-        $distType = $package->getDistType();
-
-        $sources = array();
-        if ($sourceType) {
-            $sources[] = 'source';
-        }
-        if ($distType) {
-            $sources[] = 'dist';
-        }
-
-        if (empty($sources)) {
-            throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
-        }
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $this->filesystem->ensureDirectoryExists(dirname($targetDir));
 
-        if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
-            $sources = array_reverse($sources);
-        }
+        $sources = $this->getAvailableSources($package, $prevPackage);
 
-        $this->filesystem->ensureDirectoryExists($targetDir);
+        $io = $this->io;
+        $self = $this;
 
-        foreach ($sources as $i => $source) {
-            if (isset($e)) {
-                $this->io->writeError('    <warning>Now trying to download from ' . $source . '</warning>');
+        $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) {
+            $source = array_shift($sources);
+            if ($retry) {
+                $io->writeError('    <warning>Now trying to download from ' . $source . '</warning>');
             }
             $package->setInstallationSource($source);
-            try {
-                $downloader = $this->getDownloaderForInstalledPackage($package);
-                if ($downloader) {
-                    $downloader->download($package, $targetDir);
-                }
-                break;
-            } catch (\RuntimeException $e) {
-                if ($i === count($sources) - 1) {
-                    throw $e;
+
+            $downloader = $self->getDownloaderForPackage($package);
+            if (!$downloader) {
+                return \React\Promise\resolve();
+            }
+
+            $handleError = function ($e) use ($sources, $source, $package, $io, $download) {
+                if ($e instanceof \RuntimeException) {
+                    if (!$sources) {
+                        throw $e;
+                    }
+
+                    $io->writeError(
+                        '    <warning>Failed to download '.
+                        $package->getPrettyName().
+                        ' from ' . $source . ': '.
+                        $e->getMessage().'</warning>'
+                    );
+
+                    return $download(true);
                 }
 
-                $this->io->writeError(
-                    '    <warning>Failed to download '.
-                    $package->getPrettyName().
-                    ' from ' . $source . ': '.
-                    $e->getMessage().'</warning>'
-                );
+                throw $e;
+            };
+
+            try {
+                $result = $downloader->download($package, $targetDir, $prevPackage);
+            } catch (\Exception $e) {
+                return $handleError($e);
             }
+            if (!$result instanceof PromiseInterface) {
+                return \React\Promise\resolve($result);
+            }
+
+            $res = $result->then(function ($res) {
+                return $res;
+            }, $handleError);
+
+            return $res;
+        };
+
+        return $download();
+    }
+
+    /**
+     * Prepares an operation execution
+     *
+     * @param string                $type         one of install/update/uninstall
+     * @param PackageInterface      $package      package instance
+     * @param string                $targetDir    target dir
+     * @param PackageInterface|null $prevPackage  previous package instance in case of updates
+     *
+     * @return PromiseInterface|null
+     */
+    public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
+    {
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $downloader = $this->getDownloaderForPackage($package);
+        if ($downloader) {
+            return $downloader->prepare($type, $package, $targetDir, $prevPackage);
+        }
+    }
+
+    /**
+     * Installs package into target dir.
+     *
+     * @param PackageInterface $package      package instance
+     * @param string           $targetDir    target dir
+     *
+     * @return PromiseInterface|null
+     * @throws \InvalidArgumentException if package have no urls to download from
+     * @throws \RuntimeException
+     */
+    public function install(PackageInterface $package, $targetDir)
+    {
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $downloader = $this->getDownloaderForPackage($package);
+        if ($downloader) {
+            return $downloader->install($package, $targetDir);
         }
     }
 
@@ -238,39 +278,30 @@ class DownloadManager
      * @param PackageInterface $target    target package version
      * @param string           $targetDir target dir
      *
+     * @return PromiseInterface|null
      * @throws \InvalidArgumentException if initial package is not installed
      */
     public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
     {
-        $downloader = $this->getDownloaderForInstalledPackage($initial);
-        if (!$downloader) {
-            return;
-        }
-
-        $installationSource = $initial->getInstallationSource();
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $downloader = $this->getDownloaderForPackage($target);
+        $initialDownloader = $this->getDownloaderForPackage($initial);
 
-        if ('dist' === $installationSource) {
-            $initialType = $initial->getDistType();
-            $targetType = $target->getDistType();
-        } else {
-            $initialType = $initial->getSourceType();
-            $targetType = $target->getSourceType();
+        // no downloaders present means update from metapackage to metapackage, nothing to do
+        if (!$initialDownloader && !$downloader) {
+            return;
         }
 
-        // upgrading from a dist stable package to a dev package, force source reinstall
-        if ($target->isDev() && 'dist' === $installationSource) {
-            $downloader->remove($initial, $targetDir);
-            $this->download($target, $targetDir);
-
-            return;
+        // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
+        if (!$downloader) {
+            return $initialDownloader->remove($initial, $targetDir);
         }
 
+        $initialType = $this->getDownloaderType($initialDownloader);
+        $targetType = $this->getDownloaderType($downloader);
         if ($initialType === $targetType) {
-            $target->setInstallationSource($installationSource);
             try {
-                $downloader->update($initial, $target, $targetDir);
-
-                return;
+                return $downloader->update($initial, $target, $targetDir);
             } catch (\RuntimeException $e) {
                 if (!$this->io->isInteractive()) {
                     throw $e;
@@ -282,8 +313,17 @@ class DownloadManager
             }
         }
 
-        $downloader->remove($initial, $targetDir);
-        $this->download($target, $targetDir, 'source' === $installationSource);
+        // if downloader type changed, or update failed and user asks for reinstall,
+        // we wipe the dir and do a new install instead of updating it
+        $promise = $initialDownloader->remove($initial, $targetDir);
+        if ($promise) {
+            $self = $this;
+            return $promise->then(function ($res) use ($self, $target, $targetDir) {
+                return $self->install($target, $targetDir);
+            });
+        }
+
+        return $this->install($target, $targetDir);
     }
 
     /**
@@ -291,12 +331,34 @@ class DownloadManager
      *
      * @param PackageInterface $package   package instance
      * @param string           $targetDir target dir
+     *
+     * @return PromiseInterface|null
      */
     public function remove(PackageInterface $package, $targetDir)
     {
-        $downloader = $this->getDownloaderForInstalledPackage($package);
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $downloader = $this->getDownloaderForPackage($package);
         if ($downloader) {
-            $downloader->remove($package, $targetDir);
+            return $downloader->remove($package, $targetDir);
+        }
+    }
+
+    /**
+     * Cleans up a failed operation
+     *
+     * @param string                $type         one of install/update/uninstall
+     * @param PackageInterface      $package      package instance
+     * @param string                $targetDir    target dir
+     * @param PackageInterface|null $prevPackage  previous package instance in case of updates
+     *
+     * @return PromiseInterface|null
+     */
+    public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
+    {
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $downloader = $this->getDownloaderForPackage($package);
+        if ($downloader) {
+            return $downloader->cleanup($type, $package, $targetDir, $prevPackage);
         }
     }
 
@@ -322,4 +384,64 @@ class DownloadManager
 
         return $package->isDev() ? 'source' : 'dist';
     }
+
+    /**
+     * @return string[]
+     */
+    private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $sourceType = $package->getSourceType();
+        $distType = $package->getDistType();
+
+        // add source before dist by default
+        $sources = array();
+        if ($sourceType) {
+            $sources[] = 'source';
+        }
+        if ($distType) {
+            $sources[] = 'dist';
+        }
+
+        if (empty($sources)) {
+            throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
+        }
+
+        if (
+            $prevPackage
+            // if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
+            && in_array($prevPackage->getInstallationSource(), $sources, true)
+            // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
+            && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
+        ) {
+            $prevSource = $prevPackage->getInstallationSource();
+            usort($sources, function ($a, $b) use ($prevSource) {
+                return $a === $prevSource ? -1 : 1;
+            });
+
+            return $sources;
+        }
+
+        // reverse sources in case dist is the preferred source for this package
+        if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
+            $sources = array_reverse($sources);
+        }
+
+        return $sources;
+    }
+
+    /**
+     * Downloaders expect a /path/to/dir without trailing slash
+     *
+     * If any Installer provides a path with a trailing slash, this can cause bugs so make sure we remove them
+     *
+     * @return string
+     */
+    private function normalizeTargetDir($dir)
+    {
+        if ($dir === '\\' || $dir === '/') {
+            return $dir;
+        }
+
+        return rtrim($dir, '\\/');
+    }
 }

+ 37 - 6
src/Composer/Downloader/DownloaderInterface.php

@@ -13,6 +13,7 @@
 namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
+use React\Promise\PromiseInterface;
 
 /**
  * Downloader interface.
@@ -30,12 +31,35 @@ interface DownloaderInterface
     public function getInstallationSource();
 
     /**
-     * Downloads specific package into specific folder.
+     * This should do any network-related tasks to prepare for an upcoming install/update
+     *
+     * @return PromiseInterface|null
+     */
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null);
+
+    /**
+     * Do anything that needs to be done between all downloads have been completed and the actual operation is executed
+     *
+     * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore
+     * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or
+     * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can
+     * be undone as much as possible.
+     *
+     * @param  string                $type        one of install/update/uninstall
+     * @param  PackageInterface      $package     package instance
+     * @param  string                $path        download path
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
+     */
+    public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null);
+
+    /**
+     * Installs specific package into specific folder.
      *
      * @param PackageInterface $package package instance
      * @param string           $path    download path
      */
-    public function download(PackageInterface $package, $path);
+    public function install(PackageInterface $package, $path);
 
     /**
      * Updates specific package in specific folder from initial to target version.
@@ -55,10 +79,17 @@ interface DownloaderInterface
     public function remove(PackageInterface $package, $path);
 
     /**
-     * Sets whether to output download progress information or not
+     * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps
+     *
+     * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give
+     * all installers a change to cleanup things they did previously, so you need to keep track of changes
+     * applied in the installer/downloader themselves.
      *
-     * @param  bool                $outputProgress
-     * @return DownloaderInterface
+     * @param  string                $type        one of install/update/uninstall
+     * @param  PackageInterface      $package     package instance
+     * @param  string                $path        download path
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
      */
-    public function setOutputProgress($outputProgress);
+    public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null);
 }

+ 164 - 98
src/Composer/Downloader/FileDownloader.php

@@ -24,8 +24,9 @@ use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PreFileDownloadEvent;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\Util\Url as UrlUtil;
+use Composer\Downloader\TransportException;
 
 /**
  * Base downloader for files
@@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
 {
     protected $io;
     protected $config;
-    protected $rfs;
+    protected $httpDownloader;
     protected $filesystem;
     protected $cache;
-    protected $outputProgress = true;
-    private $lastCacheWrites = array();
+    /**
+     * @private this is only public for php 5.3 support in closures
+     */
+    public $lastCacheWrites = array();
     private $eventDispatcher;
 
     /**
@@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      *
      * @param IOInterface      $io              The IO instance
      * @param Config           $config          The config
+     * @param HttpDownloader   $httpDownloader  The remote filesystem
      * @param EventDispatcher  $eventDispatcher The event dispatcher
-     * @param Cache            $cache           Optional cache instance
-     * @param RemoteFilesystem $rfs             The remote filesystem
+     * @param Cache            $cache           Cache instance
      * @param Filesystem       $filesystem      The filesystem
      */
-    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
+    public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null)
     {
         $this->io = $io;
         $this->config = $config;
         $this->eventDispatcher = $eventDispatcher;
-        $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
+        $this->httpDownloader = $httpDownloader;
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->cache = $cache;
 
@@ -81,127 +84,191 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
     /**
      * {@inheritDoc}
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         if (!$package->getDistUrl()) {
             throw new \InvalidArgumentException('The given package is missing url information');
         }
 
-        if ($output) {
-            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
+        $retries = 3;
+        $urls = $package->getDistUrls();
+        foreach ($urls as $index => $url) {
+            $processedUrl = $this->processUrl($package, $url);
+            $urls[$index] = array(
+                'base' => $url,
+                'processed' => $processedUrl,
+                'cacheKey' => $this->getCacheKey($package, $processedUrl)
+            );
         }
 
-        $urls = $package->getDistUrls();
-        while ($url = array_shift($urls)) {
-            try {
-                $fileName = $this->doDownload($package, $path, $url);
-                break;
-            } catch (\Exception $e) {
-                if ($this->io->isDebug()) {
-                    $this->io->writeError('');
-                    $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
-                } elseif (count($urls)) {
-                    $this->io->writeError('');
-                    $this->io->writeError(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')', false);
-                }
+        $fileName = $this->getFileName($package, $path);
+        $this->filesystem->ensureDirectoryExists($path);
+        $this->filesystem->ensureDirectoryExists(dirname($fileName));
+
+        $io = $this->io;
+        $cache = $this->cache;
+        $httpDownloader = $this->httpDownloader;
+        $eventDispatcher = $this->eventDispatcher;
+        $filesystem = $this->filesystem;
+        $self = $this;
+
+        $accept = null;
+        $reject = null;
+        $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) {
+            $url = reset($urls);
+
+            if ($eventDispatcher) {
+                $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']);
+                $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
+            }
 
-                if (!count($urls)) {
-                    throw $e;
+            $checksum = $package->getDistSha1Checksum();
+            $cacheKey = $url['cacheKey'];
+
+            // use from cache if it is present and has a valid checksum or we have no checksum to check against
+            if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) {
+                if ($output) {
+                    $io->writeError("  - Loading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) from cache", true, IOInterface::VERY_VERBOSE);
+                }
+                $result = \React\Promise\resolve($fileName);
+            } else {
+                if ($output) {
+                    $io->writeError("  - Downloading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
                 }
+
+                $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions())
+                    ->then($accept, $reject);
             }
-        }
 
-        if ($output) {
-            $this->io->writeError('');
-        }
+            return $result->then(function ($result) use ($fileName, $checksum, $url) {
+                // in case of retry, the first call's Promise chain finally calls this twice at the end,
+                // once with $result being the returned $fileName from $accept, and then once for every
+                // failed request with a null result, which can be skipped.
+                if (null === $result) {
+                    return $fileName;
+                }
 
-        return $fileName;
-    }
+                if (!file_exists($fileName)) {
+                    throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the'
+                        .' directory is writable and you have internet connectivity');
+                }
 
-    protected function doDownload(PackageInterface $package, $path, $url)
-    {
-        $this->filesystem->emptyDirectory($path);
+                if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
+                    throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')');
+                }
 
-        $fileName = $this->getFileName($package, $path);
+                return $fileName;
+            });
+        };
 
-        $processedUrl = $this->processUrl($package, $url);
-        $origin = RemoteFilesystem::getOrigin($processedUrl);
+        $accept = function ($response) use ($cache, $package, $fileName, $self, &$urls) {
+            $url = reset($urls);
+            $cacheKey = $url['cacheKey'];
 
-        $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl);
-        if ($this->eventDispatcher) {
-            $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
-        }
-        $rfs = $preFileDownloadEvent->getRemoteFilesystem();
+            if ($cache) {
+                $self->lastCacheWrites[$package->getName()] = $cacheKey;
+                $cache->copyFrom($cacheKey, $fileName);
+            }
 
-        try {
-            $checksum = $package->getDistSha1Checksum();
-            $cacheKey = $this->getCacheKey($package, $processedUrl);
+            $response->collect();
 
-            // use from cache if it is present and has a valid checksum or we have no checksum to check against
-            if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) {
-                $this->io->writeError('Loading from cache', false);
-            } else {
-                // download if cache restore failed
-                if (!$this->outputProgress) {
-                    $this->io->writeError('Downloading', false);
-                }
+            return $fileName;
+        };
 
-                // try to download 3 times then fail hard
-                $retries = 3;
-                while ($retries--) {
-                    try {
-                        $rfs->copy($origin, $processedUrl, $fileName, $this->outputProgress, $package->getTransportOptions());
-                        break;
-                    } catch (TransportException $e) {
-                        // if we got an http response with a proper code, then requesting again will probably not help, abort
-                        if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
-                            throw $e;
-                        }
-                        $this->io->writeError('');
-                        $this->io->writeError('    Download failed, retrying...', true, IOInterface::VERBOSE);
-                        usleep(500000);
-                    }
-                }
+        $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) {
+            // clean up
+            if (file_exists($fileName)) {
+                $filesystem->unlink($fileName);
+            }
+            $self->clearLastCacheWrite($package);
 
-                if (!$this->outputProgress) {
-                    $this->io->writeError(' (<comment>100%</comment>)', false);
+            if ($e instanceof TransportException) {
+                // if we got an http response with a proper code, then requesting again will probably not help, abort
+                if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
+                    $retries = 0;
                 }
+            }
 
-                if ($this->cache) {
-                    $this->lastCacheWrites[$package->getName()] = $cacheKey;
-                    $this->cache->copyFrom($cacheKey, $fileName);
-                }
+            // special error code returned when network is being artificially disabled
+            if ($e instanceof TransportException && $e->getStatusCode() === 499) {
+                $retries = 0;
+                $urls = array();
             }
 
-            if (!file_exists($fileName)) {
-                throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
-                    .' directory is writable and you have internet connectivity');
+            if ($retries) {
+                usleep(500000);
+                $retries--;
+
+                return $download();
             }
 
-            if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
-                throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
+            array_shift($urls);
+            if ($urls) {
+                if ($io->isDebug()) {
+                    $io->writeError('    Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
+                    $io->writeError('    Trying the next URL for '.$package->getName());
+                } elseif (count($urls)) {
+                    $io->writeError('    Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')');
+                }
+
+                $retries = 3;
+                usleep(100000);
+
+                return $download();
             }
-        } catch (\Exception $e) {
-            // clean up
-            $this->filesystem->removeDirectory($path);
-            $this->clearLastCacheWrite($package);
+
             throw $e;
-        }
+        };
+
+        return $download();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
+    {
+    }
 
-        return $fileName;
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
+    {
+        $fileName = $this->getFileName($package, $path);
+        if (file_exists($fileName)) {
+            $this->filesystem->unlink($fileName);
+        }
+        if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
+            $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
+        }
+        if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
+            $this->filesystem->removeDirectory($this->config->get('vendor-dir'));
+        }
+        if (is_dir($path) && $this->filesystem->isDirEmpty($path)) {
+            $this->filesystem->removeDirectory($path);
+        }
     }
 
     /**
      * {@inheritDoc}
      */
-    public function setOutputProgress($outputProgress)
+    public function install(PackageInterface $package, $path, $output = true)
     {
-        $this->outputProgress = $outputProgress;
+        if ($output) {
+            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
+        }
 
-        return $this;
+        $this->filesystem->emptyDirectory($path);
+        $this->filesystem->ensureDirectoryExists($path);
+        $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME));
     }
 
-    protected function clearLastCacheWrite(PackageInterface $package)
+    /**
+     * TODO mark private in v3
+     * @protected This is public due to PHP 5.3
+     */
+    public function clearLastCacheWrite(PackageInterface $package)
     {
         if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
             $this->cache->remove($this->lastCacheWrites[$package->getName()]);
@@ -218,11 +285,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         $from = $initial->getFullPrettyVersion();
         $to = $target->getFullPrettyVersion();
 
-        $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
+        $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
 
         $this->remove($initial, $path, false);
-        $this->download($target, $path, false);
+        $this->install($target, $path, false);
 
         $this->io->writeError('');
     }
@@ -249,7 +316,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      */
     protected function getFileName(PackageInterface $package, $path)
     {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
+        return rtrim($this->config->get('vendor-dir').'/composer/'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
     }
 
     /**
@@ -291,15 +358,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
     public function getLocalChanges(PackageInterface $package, $targetDir)
     {
         $prevIO = $this->io;
-        $prevProgress = $this->outputProgress;
 
         $this->io = new NullIO;
         $this->io->loadConfiguration($this->config);
-        $this->outputProgress = false;
         $e = null;
 
         try {
-            $this->download($package, $targetDir.'_compare', false);
+            $res = $this->download($package, $targetDir.'_compare', null, false);
+            $this->httpDownloader->wait();
+            $res = $this->install($package, $targetDir.'_compare', false);
 
             $comparer = new Comparer();
             $comparer->setSource($targetDir.'_compare');
@@ -311,7 +378,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         }
 
         $this->io = $prevIO;
-        $this->outputProgress = $prevProgress;
 
         if ($e) {
             throw $e;

+ 10 - 2
src/Composer/Downloader/FossilDownloader.php

@@ -23,7 +23,15 @@ class FossilDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
+    {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function doInstall(PackageInterface $package, $path, $url)
     {
         // Ensure we are allowed to use this URL by config
         $this->config->prohibitUrlByConfig($url, $this->io);
@@ -49,7 +57,7 @@ class FossilDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         // Ensure we are allowed to use this URL by config
         $this->config->prohibitUrlByConfig($url, $this->io);

+ 77 - 48
src/Composer/Downloader/GitDownloader.php

@@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Git as GitUtil;
+use Composer\Util\Url;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Cache;
@@ -29,6 +30,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     private $hasStashedChanges = false;
     private $hasDiscardedChanges = false;
     private $gitUtil;
+    private $cachedPackages = array();
 
     public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null)
     {
@@ -39,39 +41,49 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
     {
         GitUtil::cleanEnv();
-        $path = $this->normalizePath($path);
-        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
-        $ref = $package->getSourceReference();
-        $flag = Platform::isWindows() ? '/D ' : '';
 
-        // --dissociate option is only available since git 2.3.0-rc0
+        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
         $gitVersion = $this->gitUtil->getVersion();
-        $msg = "Cloning ".$this->getShortHash($ref);
 
-        $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer && git remote set-url origin %sanitizedUrl% && git remote set-url composer %sanitizedUrl%';
+        // --dissociate option is only available since git 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("  - Syncing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) into cache");
             $this->io->writeError(sprintf('    Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG);
-            try {
-                if (!$this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref)) {
-                    $this->io->writeError('<error>Failed to update '.$url.' in cache, package installation for '.$package->getPrettyName().' might fail.</error>');
-                }
-                if (is_dir($cachePath)) {
-                    $command =
-                        'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% '
-                        . '&& cd '.$flag.'%path% '
-                        . '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%';
-                    $msg = "Cloning ".$this->getShortHash($ref).' from cache';
-                }
-            } catch (\RuntimeException $e) {
-                if (0 === strpos(get_class($e), 'PHPUnit')) {
-                    throw $e;
-                }
+            $ref = $package->getSourceReference();
+            if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref) && is_dir($cachePath)) {
+                $this->cachedPackages[$package->getId()][$ref] = true;
             }
         }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function doInstall(PackageInterface $package, $path, $url)
+    {
+        GitUtil::cleanEnv();
+        $path = $this->normalizePath($path);
+        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
+        $ref = $package->getSourceReference();
+        $flag = Platform::isWindows() ? '/D ' : '';
+
+        if (!empty($this->cachedPackages[$package->getId()][$ref])) {
+            $msg = "Cloning ".$this->getShortHash($ref).' from cache';
+            $command =
+                'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% '
+                . '&& cd '.$flag.'%path% '
+                . '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%';
+        } else {
+            $msg = "Cloning ".$this->getShortHash($ref);
+            $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer && git remote set-url origin %sanitizedUrl% && git remote set-url composer %sanitizedUrl%';
+            if (getenv('COMPOSER_DISABLE_NETWORK')) {
+                throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting');
+            }
+        }
+
         $this->io->writeError($msg);
 
         $commandCallable = function ($url) use ($path, $command, $cachePath) {
@@ -105,34 +117,41 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         GitUtil::cleanEnv();
+        $path = $this->normalizePath($path);
         if (!$this->hasMetadataRepository($path)) {
             throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
         }
 
-        $updateOriginUrl = false;
-        if (
-            0 === $this->process->execute('git remote -v', $output, $path)
-            && preg_match('{^origin\s+(?P<url>\S+)}m', $output, $originMatch)
-            && preg_match('{^composer\s+(?P<url>\S+)}m', $output, $composerMatch)
-        ) {
-            if ($originMatch['url'] === $composerMatch['url'] && $composerMatch['url'] !== $target->getSourceUrl()) {
-                $updateOriginUrl = true;
+        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
+        $ref = $target->getSourceReference();
+        $flag = Platform::isWindows() ? '/D ' : '';
+
+        if (!empty($this->cachedPackages[$target->getId()][$ref])) {
+            $msg = "Checking out ".$this->getShortHash($ref).' from cache';
+            $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%';
+        } else {
+            $msg = "Checking out ".$this->getShortHash($ref);
+            $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%';
+            if (getenv('COMPOSER_DISABLE_NETWORK')) {
+                throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting');
             }
         }
 
-        $ref = $target->getSourceReference();
-        $this->io->writeError(" Checking out ".$this->getShortHash($ref));
-        $command = '(git remote set-url composer %s && git rev-parse --quiet --verify %s || (git fetch composer && git fetch --tags composer)) && git remote set-url composer %s';
-
-        $commandCallable = function ($url) use ($command, $ref) {
-            return sprintf(
-                $command,
-                ProcessExecutor::escape($url),
-                ProcessExecutor::escape($ref.'^{commit}'),
-                ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url))
+        $this->io->writeError($msg);
+
+        $commandCallable = function ($url) use ($ref, $command, $cachePath) {
+            return str_replace(
+                array('%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'),
+                array(
+                    ProcessExecutor::escape($url),
+                    ProcessExecutor::escape($ref.'^{commit}'),
+                    ProcessExecutor::escape($cachePath),
+                    ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url)),
+                ),
+                $command
             );
         };
 
@@ -144,6 +163,16 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
             $target->setSourceReference($newRef);
         }
 
+        $updateOriginUrl = false;
+        if (
+            0 === $this->process->execute('git remote -v', $output, $path)
+            && preg_match('{^origin\s+(?P<url>\S+)}m', $output, $originMatch)
+            && preg_match('{^composer\s+(?P<url>\S+)}m', $output, $composerMatch)
+        ) {
+            if ($originMatch['url'] === $composerMatch['url'] && $composerMatch['url'] !== $target->getSourceUrl()) {
+                $updateOriginUrl = true;
+            }
+        }
         if ($updateOriginUrl) {
             $this->updateOriginUrl($path, $target->getSourceUrl());
         }
@@ -272,7 +301,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
         $changes = array_map(function ($elem) {
             return '    '.$elem;
         }, preg_split('{\s*\r?\n\s*}', $changes));
-        $this->io->writeError('    <error>The package has modified files:</error>');
+        $this->io->writeError('    <error>'.$package->getPrettyName().' has modified files:</error>');
         $this->io->writeError(array_slice($changes, 0, 10));
         if (count($changes) > 10) {
             $this->io->writeError('    <info>' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list</info>');
@@ -373,7 +402,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
         ) {
             $command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference));
             if (0 === $this->process->execute($command, $output, $path)) {
-                return;
+                return null;
             }
         }
 
@@ -391,14 +420,14 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
             ) {
                 $command = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference));
                 if (0 === $this->process->execute($command, $output, $path)) {
-                    return;
+                    return null;
                 }
             }
         }
 
         $command = sprintf($template, ProcessExecutor::escape($gitRef));
         if (0 === $this->process->execute($command, $output, $path)) {
-            return;
+            return null;
         }
 
         // reference was not found (prints "fatal: reference is not a tree: $ref")
@@ -406,7 +435,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
             $this->io->writeError('    <warning>'.$reference.' is gone (history was rewritten?)</warning>');
         }
 
-        throw new \RuntimeException(GitUtil::sanitizeUrl('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()));
+        throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()));
     }
 
     protected function updateOriginUrl($path, $url)

+ 7 - 13
src/Composer/Downloader/GzipDownloader.php

@@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher;
 use Composer\Package\PackageInterface;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 
 /**
@@ -28,17 +28,19 @@ use Composer\IO\IOInterface;
  */
 class GzipDownloader extends ArchiveDownloader
 {
+    /** @var ProcessExecutor */
     protected $process;
 
-    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
+    public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
     {
         $this->process = $process ?: new ProcessExecutor($io);
-        parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
+        parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
-        $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
+        $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME);
+        $targetFilepath = $path . DIRECTORY_SEPARATOR . $filename;
 
         // Try to use gunzip on *nix
         if (!Platform::isWindows()) {
@@ -63,14 +65,6 @@ class GzipDownloader extends ArchiveDownloader
         $this->extractUsingExt($file, $targetFilepath);
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function getFileName(PackageInterface $package, $path)
-    {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
-    }
-
     private function extractUsingExt($file, $targetFilepath)
     {
         $archiveFile = gzopen($file, 'rb');

+ 10 - 2
src/Composer/Downloader/HgDownloader.php

@@ -24,7 +24,15 @@ class HgDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
+    {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function doInstall(PackageInterface $package, $path, $url)
     {
         $hgUtils = new HgUtils($this->io, $this->config, $this->process);
 
@@ -44,7 +52,7 @@ class HgDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         $hgUtils = new HgUtils($this->io, $this->config, $this->process);
 

+ 28 - 11
src/Composer/Downloader/PathDownloader.php

@@ -37,7 +37,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
     /**
      * {@inheritdoc}
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         $url = $package->getDistUrl();
         $realUrl = realpath($url);
@@ -50,14 +50,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
         }
 
         if (realpath($path) === $realUrl) {
-            if ($output) {
-                $this->io->writeError(sprintf(
-                    '  - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
-                    $package->getName(),
-                    $package->getFullPrettyVersion()
-                ));
-            }
-
             return;
         }
 
@@ -73,6 +65,29 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
                 $realUrl
             ));
         }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function install(PackageInterface $package, $path, $output = true)
+    {
+        $url = $package->getDistUrl();
+        $realUrl = realpath($url);
+
+        if (realpath($path) === $realUrl) {
+            if ($output) {
+                $this->io->writeError(sprintf(
+                    '  - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
+                    $package->getName(),
+                    $package->getFullPrettyVersion()
+                ));
+            } else {
+                $this->io->writeError('Source already present', false);
+            }
+
+            return;
+        }
 
         // Get the transport options with default values
         $transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true);
@@ -154,7 +169,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
             $fileSystem->mirror($realUrl, $path, $iterator);
         }
 
-        $this->io->writeError('');
+        if ($output) {
+            $this->io->writeError('');
+        }
     }
 
     /**
@@ -164,7 +181,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
     {
         $realUrl = realpath($package->getDistUrl());
 
-        if (realpath($path) === $realUrl) {
+        if ($path === $realUrl) {
             if ($output) {
                 $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>), source is still present in $path");
             }

+ 11 - 3
src/Composer/Downloader/PerforceDownloader.php

@@ -27,7 +27,15 @@ class PerforceDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
+    {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function doInstall(PackageInterface $package, $path, $url)
     {
         $ref = $package->getSourceReference();
         $label = $this->getLabelFromSourceReference($ref);
@@ -76,9 +84,9 @@ class PerforceDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
-        $this->doDownload($target, $path, $url);
+        $this->doInstall($target, $path, $url);
     }
 
     /**

+ 3 - 1
src/Composer/Downloader/PharDownloader.php

@@ -12,6 +12,8 @@
 
 namespace Composer\Downloader;
 
+use Composer\Package\PackageInterface;
+
 /**
  * Downloader for phar files
  *
@@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader
     /**
      * {@inheritDoc}
      */
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         // Can throw an UnexpectedValueException
         $archive = new \Phar($file);

+ 6 - 4
src/Composer/Downloader/RarDownloader.php

@@ -18,8 +18,9 @@ use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\IniHelper;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
+use Composer\Package\PackageInterface;
 use RarArchive;
 
 /**
@@ -31,15 +32,16 @@ use RarArchive;
  */
 class RarDownloader extends ArchiveDownloader
 {
+    /** @var ProcessExecutor */
     protected $process;
 
-    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
+    public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
     {
         $this->process = $process ?: new ProcessExecutor($io);
-        parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
+        parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         $processError = null;
 

+ 15 - 7
src/Composer/Downloader/SvnDownloader.php

@@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
+    {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function doInstall(PackageInterface $package, $path, $url)
     {
         SvnUtil::cleanEnv();
         $ref = $package->getSourceReference();
@@ -42,13 +50,13 @@ class SvnDownloader extends VcsDownloader
         }
 
         $this->io->writeError(" Checking out ".$package->getSourceReference());
-        $this->execute($url, "svn co", sprintf("%s/%s", $url, $ref), null, $path);
+        $this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         SvnUtil::cleanEnv();
         $ref = $target->getSourceReference();
@@ -64,7 +72,7 @@ class SvnDownloader extends VcsDownloader
         }
 
         $this->io->writeError(" Checking out " . $ref);
-        $this->execute($url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path);
+        $this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path);
     }
 
     /**
@@ -93,7 +101,7 @@ class SvnDownloader extends VcsDownloader
      * @throws \RuntimeException
      * @return string
      */
-    protected function execute($baseUrl, $command, $url, $cwd = null, $path = null)
+    protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null)
     {
         $util = new SvnUtil($baseUrl, $this->io, $this->config);
         $util->setCacheCredentials($this->cacheCredentials);
@@ -101,7 +109,7 @@ class SvnDownloader extends VcsDownloader
             return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose());
         } catch (\RuntimeException $e) {
             throw new \RuntimeException(
-                'Package could not be downloaded, '.$e->getMessage()
+                $package->getPrettyName().' could not be downloaded, '.$e->getMessage()
             );
         }
     }
@@ -127,7 +135,7 @@ class SvnDownloader extends VcsDownloader
             return '    '.$elem;
         }, preg_split('{\s*\r?\n\s*}', $changes));
         $countChanges = count($changes);
-        $this->io->writeError(sprintf('    <error>The package has modified file%s:</error>', $countChanges === 1 ? '' : 's'));
+        $this->io->writeError(sprintf('    <error>'.$package->getPrettyName().' has modified file%s:</error>', $countChanges === 1 ? '' : 's'));
         $this->io->writeError(array_slice($changes, 0, 10));
         if ($countChanges > 10) {
             $remainingChanges = $countChanges - 10;

+ 3 - 1
src/Composer/Downloader/TarDownloader.php

@@ -12,6 +12,8 @@
 
 namespace Composer\Downloader;
 
+use Composer\Package\PackageInterface;
+
 /**
  * Downloader for tar files: tar, tar.gz or tar.bz2
  *
@@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader
     /**
      * {@inheritDoc}
      */
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         // Can throw an UnexpectedValueException
         $archive = new \PharData($file);

+ 111 - 47
src/Composer/Downloader/VcsDownloader.php

@@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -54,44 +55,78 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     /**
      * {@inheritDoc}
      */
-    public function download(PackageInterface $package, $path)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null)
     {
         if (!$package->getSourceReference()) {
             throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
         }
 
-        $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
-        $this->filesystem->emptyDirectory($path);
+        $urls = $this->prepareUrls($package->getSourceUrls());
 
-        $urls = $package->getSourceUrls();
         while ($url = array_shift($urls)) {
             try {
-                if (Filesystem::isLocalPath($url)) {
-                    // realpath() below will not understand
-                    // url that starts with "file://"
-                    $needle = 'file://';
-                    $isFileProtocol = false;
-                    if (0 === strpos($url, $needle)) {
-                        $url = substr($url, strlen($needle));
-                        $isFileProtocol = true;
-                    }
-
-                    // realpath() below will not understand %20 spaces etc.
-                    if (false !== strpos($url, '%')) {
-                        $url = rawurldecode($url);
-                    }
-
-                    $url = realpath($url);
-
-                    if ($isFileProtocol) {
-                        $url = $needle . $url;
-                    }
+                return $this->doDownload($package, $path, $url, $prevPackage);
+            } catch (\Exception $e) {
+                // rethrow phpunit exceptions to avoid hard to debug bug failures
+                if ($e instanceof \PHPUnit\Framework\Exception) {
+                    throw $e;
+                }
+                if ($this->io->isDebug()) {
+                    $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage());
+                } elseif (count($urls)) {
+                    $this->io->writeError('    Failed, trying the next URL');
                 }
-                $this->doDownload($package, $path, $url);
+                if (!count($urls)) {
+                    throw $e;
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
+    {
+        if ($type === 'update') {
+            $this->cleanChanges($prevPackage, $path, true);
+        } elseif ($type === 'install') {
+            $this->filesystem->emptyDirectory($path);
+        } elseif ($type === 'uninstall') {
+            $this->cleanChanges($package, $path, false);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
+    {
+        if ($type === 'update') {
+            // TODO keep track of whether prepare was called for this package
+            $this->reapplyChanges($path);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(PackageInterface $package, $path)
+    {
+        if (!$package->getSourceReference()) {
+            throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
+        }
+
+        $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
+
+        $urls = $this->prepareUrls($package->getSourceUrls());
+        while ($url = array_shift($urls)) {
+            try {
+                $this->doInstall($package, $path, $url);
                 break;
             } catch (\Exception $e) {
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
-                if ($e instanceof \PHPUnit_Framework_Exception) {
+                if ($e instanceof \PHPUnit\Framework\Exception) {
                     throw $e;
                 }
                 if ($this->io->isDebug()) {
@@ -130,25 +165,21 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
             $to = $target->getFullPrettyVersion();
         }
 
-        $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
+        $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
 
-        $this->cleanChanges($initial, $path, true);
-        $urls = $target->getSourceUrls();
+        $urls = $this->prepareUrls($target->getSourceUrls());
 
         $exception = null;
         while ($url = array_shift($urls)) {
             try {
-                if (Filesystem::isLocalPath($url)) {
-                    $url = realpath($url);
-                }
                 $this->doUpdate($initial, $target, $path, $url);
 
                 $exception = null;
                 break;
             } catch (\Exception $exception) {
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
-                if ($exception instanceof \PHPUnit_Framework_Exception) {
+                if ($exception instanceof \PHPUnit\Framework\Exception) {
                     throw $exception;
                 }
                 if ($this->io->isDebug()) {
@@ -159,8 +190,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
             }
         }
 
-        $this->reapplyChanges($path);
-
         // print the commit logs if in verbose mode and VCS metadata is present
         // because in case of missing metadata code would trigger another exception
         if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) {
@@ -196,21 +225,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     public function remove(PackageInterface $package, $path)
     {
         $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
-        $this->cleanChanges($package, $path, false);
         if (!$this->filesystem->removeDirectory($path)) {
             throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
         }
     }
 
-    /**
-     * Download progress information is not available for all VCS downloaders.
-     * {@inheritDoc}
-     */
-    public function setOutputProgress($outputProgress)
-    {
-        return $this;
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -244,7 +263,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     }
 
     /**
-     * Guarantee that no changes have been made to the local copy
+     * Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not)
      *
      * @param  string            $path
      * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly
@@ -253,14 +272,28 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     {
     }
 
+    /**
+     * Downloads data needed to run an install/update later
+     *
+     * @param PackageInterface      $package     package instance
+     * @param string                $path        download path
+     * @param string                $url         package url
+     * @param PackageInterface|null $prevPackage previous package (in case of an update)
+     *
+     * @return PromiseInterface|null
+     */
+    abstract protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null);
+
     /**
      * Downloads specific package into specific folder.
      *
      * @param PackageInterface $package package instance
      * @param string           $path    download path
      * @param string           $url     package url
+     *
+     * @return PromiseInterface|null
      */
-    abstract protected function doDownload(PackageInterface $package, $path, $url);
+    abstract protected function doInstall(PackageInterface $package, $path, $url);
 
     /**
      * Updates specific package in specific folder from initial to target version.
@@ -269,6 +302,8 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * @param PackageInterface $target  updated package
      * @param string           $path    download path
      * @param string           $url     package url
+     *
+     * @return PromiseInterface|null
      */
     abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url);
 
@@ -290,4 +325,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * @return bool
      */
     abstract protected function hasMetadataRepository($path);
+
+    private function prepareUrls(array $urls)
+    {
+        foreach ($urls as $index => $url) {
+            if (Filesystem::isLocalPath($url)) {
+                // realpath() below will not understand
+                // url that starts with "file://"
+                $fileProtocol = 'file://';
+                $isFileProtocol = false;
+                if (0 === strpos($url, $fileProtocol)) {
+                    $url = substr($url, strlen($fileProtocol));
+                    $isFileProtocol = true;
+                }
+
+                // realpath() below will not understand %20 spaces etc.
+                if (false !== strpos($url, '%')) {
+                    $url = rawurldecode($url);
+                }
+
+                $urls[$index] = realpath($url);
+
+                if ($isFileProtocol) {
+                    $urls[$index] = $fileProtocol . $urls[$index];
+                }
+            }
+        }
+
+        return $urls;
+    }
 }

+ 5 - 12
src/Composer/Downloader/XzDownloader.php

@@ -17,7 +17,7 @@ use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Package\PackageInterface;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 
 /**
@@ -28,16 +28,17 @@ use Composer\IO\IOInterface;
  */
 class XzDownloader extends ArchiveDownloader
 {
+    /** @var ProcessExecutor */
     protected $process;
 
-    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
+    public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
     {
         $this->process = $process ?: new ProcessExecutor($io);
 
-        parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
+        parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
 
@@ -49,12 +50,4 @@ class XzDownloader extends ArchiveDownloader
 
         throw new \RuntimeException($processError);
     }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function getFileName(PackageInterface $package, $path)
-    {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
-    }
 }

+ 8 - 6
src/Composer/Downloader/ZipDownloader.php

@@ -19,7 +19,7 @@ use Composer\Package\PackageInterface;
 use Composer\Util\IniHelper;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Symfony\Component\Process\ExecutableFinder;
 use ZipArchive;
@@ -33,19 +33,21 @@ class ZipDownloader extends ArchiveDownloader
     private static $hasZipArchive;
     private static $isWindows;
 
+    /** @var ProcessExecutor */
     protected $process;
+    /** @var ZipArchive|null */
     private $zipArchiveObject;
 
-    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
+    public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
     {
         $this->process = $process ?: new ProcessExecutor($io);
-        parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
+        parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         if (null === self::$hasSystemUnzip) {
             $finder = new ExecutableFinder;
@@ -74,7 +76,7 @@ class ZipDownloader extends ArchiveDownloader
             }
         }
 
-        return parent::download($package, $path, $output);
+        return parent::download($package, $path, $prevPackage, $output);
     }
 
     /**
@@ -185,7 +187,7 @@ class ZipDownloader extends ArchiveDownloader
      * @param string $file File to extract
      * @param string $path Path where to extract file
      */
-    public function extract($file, $path)
+    public function extract(PackageInterface $package, $file, $path)
     {
         // Each extract calls its alternative if not available or fails
         if (self::$isWindows) {

+ 51 - 63
src/Composer/EventDispatcher/EventDispatcher.php

@@ -13,13 +13,16 @@
 namespace Composer\EventDispatcher;
 
 use Composer\DependencyResolver\PolicyInterface;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
+use Composer\DependencyResolver\Pool;
+use Composer\DependencyResolver\Transaction;
 use Composer\Installer\InstallerEvent;
 use Composer\IO\IOInterface;
 use Composer\Composer;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\Repository\CompositeRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Repository\RepositorySet;
 use Composer\Script;
 use Composer\Installer\PackageEvent;
 use Composer\Installer\BinaryInstaller;
@@ -46,7 +49,7 @@ class EventDispatcher
     protected $io;
     protected $loader;
     protected $process;
-    protected $listeners;
+    protected $listeners = array();
     private $eventStack;
 
     /**
@@ -99,40 +102,34 @@ class EventDispatcher
     /**
      * Dispatch a package event.
      *
-     * @param string              $eventName     The constant in PackageEvents
-     * @param bool                $devMode       Whether or not we are in dev mode
-     * @param PolicyInterface     $policy        The policy
-     * @param Pool                $pool          The pool
-     * @param CompositeRepository $installedRepo The installed repository
-     * @param Request             $request       The request
-     * @param array               $operations    The list of operations
-     * @param OperationInterface  $operation     The package being installed/updated/removed
+     * @param string              $eventName  The constant in PackageEvents
+     * @param bool                $devMode    Whether or not we are in dev mode
+     * @param RepositoryInterface $localRepo  The installed repository
+     * @param array               $operations The list of operations
+     * @param OperationInterface  $operation  The package being installed/updated/removed
      *
      * @return int return code of the executed script if any, for php scripts a false return
      *             value is changed to 1, anything else to 0
      */
-    public function dispatchPackageEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation)
+    public function dispatchPackageEvent($eventName, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation)
     {
-        return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations, $operation));
+        return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation));
     }
 
     /**
      * Dispatch a installer event.
      *
-     * @param string              $eventName     The constant in InstallerEvents
-     * @param bool                $devMode       Whether or not we are in dev mode
-     * @param PolicyInterface     $policy        The policy
-     * @param Pool                $pool          The pool
-     * @param CompositeRepository $installedRepo The installed repository
-     * @param Request             $request       The request
-     * @param array               $operations    The list of operations
+     * @param string              $eventName         The constant in InstallerEvents
+     * @param bool                $devMode           Whether or not we are in dev mode
+     * @param bool                $executeOperations True if operations will be executed, false in --dry-run
+     * @param Transaction         $transaction       The transaction contains the list of operations
      *
      * @return int return code of the executed script if any, for php scripts a false return
      *             value is changed to 1, anything else to 0
      */
-    public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array())
+    public function dispatchInstallerEvent($eventName, $devMode, $executeOperations, Transaction $transaction)
     {
-        return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations));
+        return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction));
     }
 
     /**
@@ -160,6 +157,9 @@ class EventDispatcher
 
                     throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public');
                 }
+                if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) {
+                    $this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1] ), true, IOInterface::VERBOSE);
+                }
                 $event = $this->checkListenerExpectedEvent($callable, $event);
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
             } elseif ($this->isComposerScript($callable)) {
@@ -172,8 +172,8 @@ class EventDispatcher
                 $args = array_merge($script, $event->getArguments());
                 $flags = $event->getFlags();
                 if (substr($callable, 0, 10) === '@composer ') {
-                    $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($callable, 9);
-                    if (0 !== ($exitCode = $this->process->execute($exec))) {
+                    $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args);
+                    if (0 !== ($exitCode = $this->executeTty($exec))) {
                         $this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
 
                         throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@@ -184,6 +184,7 @@ class EventDispatcher
                     }
 
                     try {
+                        /** @var InstallerEvent $event */
                         $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
                         $scriptEvent->setOriginatingEvent($event);
                         $return = $this->dispatch($scriptName, $scriptEvent);
@@ -247,7 +248,7 @@ class EventDispatcher
                     }
                 }
 
-                if (0 !== ($exitCode = $this->process->execute($exec))) {
+                if (0 !== ($exitCode = $this->executeTty($exec))) {
                     $this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
 
                     throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@@ -264,6 +265,15 @@ class EventDispatcher
         return $return;
     }
 
+    protected function executeTty($exec)
+    {
+        if ($this->io->isInteractive()) {
+            return $this->process->executeTty($exec);
+        }
+
+        return $this->process->execute($exec);
+    }
+
     protected function getPhpExecCommand()
     {
         $finder = new PhpExecutableFinder();
@@ -327,44 +337,6 @@ class EventDispatcher
 
         $expected = $typehint->getName();
 
-        // BC support
-        if (!$event instanceof $expected && $expected === 'Composer\Script\CommandEvent') {
-            trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED);
-            $event = new \Composer\Script\CommandEvent(
-                $event->getName(),
-                $event->getComposer(),
-                $event->getIO(),
-                $event->isDevMode(),
-                $event->getArguments()
-            );
-        }
-        if (!$event instanceof $expected && $expected === 'Composer\Script\PackageEvent') {
-            trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED);
-            $event = new \Composer\Script\PackageEvent(
-                $event->getName(),
-                $event->getComposer(),
-                $event->getIO(),
-                $event->isDevMode(),
-                $event->getPolicy(),
-                $event->getPool(),
-                $event->getInstalledRepo(),
-                $event->getRequest(),
-                $event->getOperations(),
-                $event->getOperation()
-            );
-        }
-        if (!$event instanceof $expected && $expected === 'Composer\Script\Event') {
-            trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED);
-            $event = new \Composer\Script\Event(
-                $event->getName(),
-                $event->getComposer(),
-                $event->getIO(),
-                $event->isDevMode(),
-                $event->getArguments(),
-                $event->getFlags()
-            );
-        }
-
         return $event;
     }
 
@@ -397,6 +369,22 @@ class EventDispatcher
         $this->listeners[$eventName][$priority][] = $listener;
     }
 
+    /**
+     * @param callable|object $listener A callable or an object instance for which all listeners should be removed
+     */
+    public function removeListener($listener)
+    {
+        foreach ($this->listeners as $eventName => $priorities) {
+            foreach ($priorities as $priority => $listeners) {
+                foreach ($listeners as $index => $candidate) {
+                    if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) {
+                        unset($this->listeners[$eventName][$priority][$index]);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Adds object methods as listeners for the events in getSubscribedEvents
      *
@@ -513,7 +501,7 @@ class EventDispatcher
      *
      * @param  Event             $event
      * @throws \RuntimeException
-     * @return number
+     * @return int
      */
     protected function pushEvent(Event $event)
     {

+ 40 - 39
src/Composer/Factory.php

@@ -23,7 +23,8 @@ use Composer\Repository\WritableRepositoryInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
+use Composer\Util\Loop;
 use Composer\Util\Silencer;
 use Composer\Plugin\PluginEvents;
 use Composer\EventDispatcher\Event;
@@ -222,6 +223,13 @@ class Factory
         return trim(getenv('COMPOSER')) ?: './composer.json';
     }
 
+    public static function getLockFile($composerFile)
+    {
+        return "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
+                ? substr($composerFile, 0, -4).'lock'
+                : $composerFile . '.lock';
+    }
+
     public static function createAdditionalStyles()
     {
         return array(
@@ -325,14 +333,15 @@ class Factory
             $io->loadConfiguration($config);
         }
 
-        $rfs = self::createRemoteFilesystem($io, $config);
+        $httpDownloader = self::createHttpDownloader($io, $config);
+        $loop = new Loop($httpDownloader);
 
         // initialize event dispatcher
         $dispatcher = new EventDispatcher($composer, $io);
         $composer->setEventDispatcher($dispatcher);
 
         // initialize repository manager
-        $rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs);
+        $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
         $composer->setRepositoryManager($rm);
 
         // load local repository
@@ -352,12 +361,12 @@ class Factory
         $composer->setPackage($package);
 
         // initialize installation manager
-        $im = $this->createInstallationManager();
+        $im = $this->createInstallationManager($loop, $io, $dispatcher);
         $composer->setInstallationManager($im);
 
         if ($fullLoad) {
             // initialize download manager
-            $dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs);
+            $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher);
             $composer->setDownloadManager($dm);
 
             // initialize autoload generator
@@ -365,7 +374,7 @@ class Factory
             $composer->setAutoloadGenerator($generator);
 
             // initialize archive manager
-            $am = $this->createArchiveManager($config, $dm);
+            $am = $this->createArchiveManager($config, $dm, $loop);
             $composer->setArchiveManager($am);
         }
 
@@ -386,11 +395,9 @@ class Factory
 
         // init locker if possible
         if ($fullLoad && isset($composerFile)) {
-            $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
-                ? substr($composerFile, 0, -4).'lock'
-                : $composerFile . '.lock';
+            $lockFile = self::getLockFile($composerFile);
 
-            $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $rm, $im, file_get_contents($composerFile));
+            $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile));
             $composer->setLocker($locker);
         }
 
@@ -411,7 +418,7 @@ class Factory
     /**
      * @param  IOInterface $io             IO instance
      * @param  bool        $disablePlugins Whether plugins should not be loaded
-     * @return Composer
+     * @return Composer|null
      */
     public static function createGlobal(IOInterface $io, $disablePlugins = false)
     {
@@ -451,7 +458,7 @@ class Factory
      * @param  EventDispatcher            $eventDispatcher
      * @return Downloader\DownloadManager
      */
-    public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
+    public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null)
     {
         $cache = null;
         if ($config->get('cache-files-ttl') > 0) {
@@ -484,14 +491,14 @@ class Factory
         $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
-        $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
-        $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
-        $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs));
-        $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
-        $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
-        $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs));
-        $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs));
-        $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs));
+        $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
+        $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
+        $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
+        $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
+        $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
+        $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
+        $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
+        $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
 
         return $dm;
     }
@@ -501,15 +508,9 @@ class Factory
      * @param  Downloader\DownloadManager $dm     Manager use to download sources
      * @return Archiver\ArchiveManager
      */
-    public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null)
+    public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop)
     {
-        if (null === $dm) {
-            $io = new IO\NullIO();
-            $io->loadConfiguration($config);
-            $dm = $this->createDownloadManager($io, $config);
-        }
-
-        $am = new Archiver\ArchiveManager($dm);
+        $am = new Archiver\ArchiveManager($dm, $loop);
         $am->addArchiver(new Archiver\ZipArchiver);
         $am->addArchiver(new Archiver\PharArchiver);
 
@@ -531,9 +532,9 @@ class Factory
     /**
      * @return Installer\InstallationManager
      */
-    protected function createInstallationManager()
+    public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null)
     {
-        return new Installer\InstallationManager();
+        return new Installer\InstallationManager($loop, $io, $eventDispatcher);
     }
 
     /**
@@ -579,10 +580,10 @@ class Factory
     /**
      * @param  IOInterface      $io      IO instance
      * @param  Config           $config  Config instance
-     * @param  array            $options Array of options passed directly to RemoteFilesystem constructor
-     * @return RemoteFilesystem
+     * @param  array            $options Array of options passed directly to HttpDownloader constructor
+     * @return HttpDownloader
      */
-    public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array())
+    public static function createHttpDownloader(IOInterface $io, Config $config = null, $options = array())
     {
         static $warned = false;
         $disableTls = false;
@@ -596,18 +597,18 @@ class Factory
             throw new Exception\NoSslException('The openssl extension is required for SSL/TLS protection but is not available. '
                 . 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.');
         }
-        $remoteFilesystemOptions = array();
+        $httpDownloaderOptions = array();
         if ($disableTls === false) {
             if ($config && $config->get('cafile')) {
-                $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
+                $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile');
             }
             if ($config && $config->get('capath')) {
-                $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath');
+                $httpDownloaderOptions['ssl']['capath'] = $config->get('capath');
             }
-            $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options);
+            $httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options);
         }
         try {
-            $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);
+            $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls);
         } catch (TransportException $e) {
             if (false !== strpos($e->getMessage(), 'cafile')) {
                 $io->write('<error>Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.</error>');
@@ -620,7 +621,7 @@ class Factory
             throw $e;
         }
 
-        return $remoteFilesystem;
+        return $httpDownloader;
     }
 
     /**

+ 1 - 2
src/Composer/IO/BaseIO.php

@@ -14,10 +14,9 @@ namespace Composer\IO;
 
 use Composer\Config;
 use Composer\Util\ProcessExecutor;
-use Psr\Log\LoggerInterface;
 use Psr\Log\LogLevel;
 
-abstract class BaseIO implements IOInterface, LoggerInterface
+abstract class BaseIO implements IOInterface
 {
     protected $authentications = array();
 

+ 23 - 4
src/Composer/IO/IOInterface.php

@@ -13,13 +13,14 @@
 namespace Composer\IO;
 
 use Composer\Config;
+use Psr\Log\LoggerInterface;
 
 /**
  * The Input/Output helper interface.
  *
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  */
-interface IOInterface
+interface IOInterface extends LoggerInterface
 {
     const QUIET = 1;
     const NORMAL = 2;
@@ -80,6 +81,24 @@ interface IOInterface
      */
     public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
 
+    /**
+     * Writes a message to the output, without formatting it.
+     *
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
+     */
+    public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL);
+
+    /**
+     * Writes a message to the error output, without formatting it.
+     *
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
+     */
+    public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL);
+
     /**
      * Overwrites a previous message to the output.
      *
@@ -107,7 +126,7 @@ interface IOInterface
      * @param string $default  The default answer if none is given by the user
      *
      * @throws \RuntimeException If there is no data to read in the input stream
-     * @return string            The user answer
+     * @return string|null       The user answer
      */
     public function ask($question, $default = null);
 
@@ -145,7 +164,7 @@ interface IOInterface
      *
      * @param string $question The question to ask
      *
-     * @return string The answer
+     * @return string|null The answer
      */
     public function askAndHideAnswer($question);
 
@@ -160,7 +179,7 @@ interface IOInterface
      * @param bool        $multiselect  Select more than one value separated by comma
      *
      * @throws \InvalidArgumentException
-     * @return int|string|array          The selected value or values (the key of the choices array)
+     * @return int|string|array|bool    The selected value or values (the key of the choices array)
      */
     public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false);
 

File diff suppressed because it is too large
+ 304 - 462
src/Composer/Installer.php


+ 119 - 9
src/Composer/Installer/InstallationManager.php

@@ -23,7 +23,9 @@ use Composer\DependencyResolver\Operation\UpdateOperation;
 use Composer\DependencyResolver\Operation\UninstallOperation;
 use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
 use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\StreamContextFactory;
+use Composer\Util\Loop;
 
 /**
  * Package operation manager.
@@ -37,6 +39,16 @@ class InstallationManager
     private $installers = array();
     private $cache = array();
     private $notifiablePackages = array();
+    private $loop;
+    private $io;
+    private $eventDispatcher;
+
+    public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null)
+    {
+        $this->loop = $loop;
+        $this->io = $io;
+        $this->eventDispatcher = $eventDispatcher;
+    }
 
     public function reset()
     {
@@ -151,13 +163,105 @@ class InstallationManager
     /**
      * Executes solver operation.
      *
-     * @param RepositoryInterface $repo      repository in which to check
-     * @param OperationInterface  $operation operation instance
+     * @param RepositoryInterface  $repo       repository in which to add/remove/update packages
+     * @param OperationInterface[] $operations operations to execute
+     * @param bool                 $devMode    whether the install is being run in dev mode
+     * @param bool                 $operation  whether to dispatch script events
      */
-    public function execute(RepositoryInterface $repo, OperationInterface $operation)
+    public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true)
     {
-        $method = $operation->getJobType();
-        $this->$method($repo, $operation);
+        $promises = array();
+
+        foreach ($operations as $operation) {
+            $opType = $operation->getOperationType();
+            $promise = null;
+
+            if ($opType === 'install') {
+                $package = $operation->getPackage();
+                $installer = $this->getInstaller($package->getType());
+                $promise = $installer->download($package);
+            } elseif ($opType === 'update') {
+                $target = $operation->getTargetPackage();
+                $targetType = $target->getType();
+                $installer = $this->getInstaller($targetType);
+                $promise = $installer->download($target, $operation->getInitialPackage());
+            }
+
+            if ($promise) {
+                $promises[] = $promise;
+            }
+        }
+
+        if (!empty($promises)) {
+            $this->loop->wait($promises);
+        }
+
+        foreach ($operations as $operation) {
+            $opType = $operation->getOperationType();
+
+            // ignoring alias ops as they don't need to execute anything
+            if (!in_array($opType, array('update', 'install', 'uninstall'))) {
+                // output alias ops in debug verbosity as they have no output otherwise
+                if ($this->io->isDebug()) {
+                    $this->io->writeError('  - ' . $operation->show(false));
+                }
+                $this->$opType($repo, $operation);
+
+                continue;
+            }
+
+            if ($opType === 'install' || $opType === 'uninstall') {
+                $package = $operation->getPackage();
+                $initialPackage = null;
+            } elseif ($opType === 'update') {
+                $package = $operation->getTargetPackage();
+                $initialPackage = $operation->getInitialPackage();
+            }
+            $installer = $this->getInstaller($package->getType());
+
+            $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType);
+            if (defined($event) && $runScripts && $this->eventDispatcher) {
+                $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
+            }
+
+            $dispatcher = $this->eventDispatcher;
+            $installManager = $this;
+            $loop = $this->loop;
+            $io = $this->io;
+
+            $promise = $installer->prepare($opType, $package, $initialPackage);
+            if (null === $promise) {
+                $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); });
+            }
+
+            $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) {
+                return $installManager->$opType($repo, $operation);
+            })->then(function () use ($opType, $installer, $package, $initialPackage) {
+                return $installer->cleanup($opType, $package, $initialPackage);
+            })->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) {
+                $repo->write($devMode, $installManager);
+
+                $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType);
+                if (defined($event) && $runScripts && $dispatcher) {
+                    $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
+                }
+            }, function ($e) use ($opType, $installer, $package, $initialPackage, $loop, $io) {
+                $io->writeError('    <error>' . ucfirst($opType) .' of '.$package->getPrettyName().' failed</error>');
+
+                $promise = $installer->cleanup($opType, $package, $initialPackage);
+                if ($promise) {
+                    $loop->wait(array($promise));
+                }
+
+                throw $e;
+            });
+
+            $promises[] = $promise;
+        }
+
+        if (!empty($promises)) {
+            $this->loop->wait($promises);
+        }
     }
 
     /**
@@ -170,8 +274,10 @@ class InstallationManager
     {
         $package = $operation->getPackage();
         $installer = $this->getInstaller($package->getType());
-        $installer->install($repo, $package);
+        $promise = $installer->install($repo, $package);
         $this->markForNotification($package);
+
+        return $promise;
     }
 
     /**
@@ -190,12 +296,15 @@ class InstallationManager
 
         if ($initialType === $targetType) {
             $installer = $this->getInstaller($initialType);
-            $installer->update($repo, $initial, $target);
+            $promise = $installer->update($repo, $initial, $target);
             $this->markForNotification($target);
         } else {
             $this->getInstaller($initialType)->uninstall($repo, $initial);
-            $this->getInstaller($targetType)->install($repo, $target);
+            $installer = $this->getInstaller($targetType);
+            $promise = $installer->install($repo, $target);
         }
+
+        return $promise;
     }
 
     /**
@@ -208,7 +317,8 @@ class InstallationManager
     {
         $package = $operation->getPackage();
         $installer = $this->getInstaller($package->getType());
-        $installer->uninstall($repo, $package);
+
+        return $installer->uninstall($repo, $package);
     }
 
     /**

+ 18 - 68
src/Composer/Installer/InstallerEvent.php

@@ -14,18 +14,13 @@ namespace Composer\Installer;
 
 use Composer\Composer;
 use Composer\DependencyResolver\PolicyInterface;
-use Composer\DependencyResolver\Operation\OperationInterface;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
+use Composer\DependencyResolver\Pool;
+use Composer\DependencyResolver\Transaction;
 use Composer\EventDispatcher\Event;
 use Composer\IO\IOInterface;
-use Composer\Repository\CompositeRepository;
+use Composer\Repository\RepositorySet;
 
-/**
- * An event for all installer.
- *
- * @author François Pluchino <francois.pluchino@gmail.com>
- */
 class InstallerEvent extends Event
 {
     /**
@@ -44,29 +39,14 @@ class InstallerEvent extends Event
     private $devMode;
 
     /**
-     * @var PolicyInterface
-     */
-    private $policy;
-
-    /**
-     * @var Pool
-     */
-    private $pool;
-
-    /**
-     * @var CompositeRepository
-     */
-    private $installedRepo;
-
-    /**
-     * @var Request
+     * @var bool
      */
-    private $request;
+    private $executeOperations;
 
     /**
-     * @var OperationInterface[]
+     * @var Transaction
      */
-    private $operations;
+    private $transaction;
 
     /**
      * Constructor.
@@ -75,24 +55,18 @@ class InstallerEvent extends Event
      * @param Composer             $composer
      * @param IOInterface          $io
      * @param bool                 $devMode
-     * @param PolicyInterface      $policy
-     * @param Pool                 $pool
-     * @param CompositeRepository  $installedRepo
-     * @param Request              $request
-     * @param OperationInterface[] $operations
+     * @param bool                 $executeOperations
+     * @param Transaction          $transaction
      */
-    public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array())
+    public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, $executeOperations, Transaction $transaction)
     {
         parent::__construct($eventName);
 
         $this->composer = $composer;
         $this->io = $io;
         $this->devMode = $devMode;
-        $this->policy = $policy;
-        $this->pool = $pool;
-        $this->installedRepo = $installedRepo;
-        $this->request = $request;
-        $this->operations = $operations;
+        $this->executeOperations = $executeOperations;
+        $this->transaction = $transaction;
     }
 
     /**
@@ -120,42 +94,18 @@ class InstallerEvent extends Event
     }
 
     /**
-     * @return PolicyInterface
-     */
-    public function getPolicy()
-    {
-        return $this->policy;
-    }
-
-    /**
-     * @return Pool
-     */
-    public function getPool()
-    {
-        return $this->pool;
-    }
-
-    /**
-     * @return CompositeRepository
-     */
-    public function getInstalledRepo()
-    {
-        return $this->installedRepo;
-    }
-
-    /**
-     * @return Request
+     * @return bool
      */
-    public function getRequest()
+    public function isExecutingOperations()
     {
-        return $this->request;
+        return $this->executeOperations;
     }
 
     /**
-     * @return OperationInterface[]
+     * @return Transaction|null
      */
-    public function getOperations()
+    public function getTransaction()
     {
-        return $this->operations;
+        return $this->transaction;
     }
 }

+ 4 - 21
src/Composer/Installer/InstallerEvents.php

@@ -12,32 +12,15 @@
 
 namespace Composer\Installer;
 
-/**
- * The Installer Events.
- *
- * @author François Pluchino <francois.pluchino@gmail.com>
- */
 class InstallerEvents
 {
     /**
-     * The PRE_DEPENDENCIES_SOLVING event occurs as a installer begins
-     * resolve operations.
-     *
-     * The event listener method receives a
-     * Composer\Installer\InstallerEvent instance.
-     *
-     * @var string
-     */
-    const PRE_DEPENDENCIES_SOLVING = 'pre-dependencies-solving';
-
-    /**
-     * The POST_DEPENDENCIES_SOLVING event occurs as a installer after
-     * resolve operations.
+     * The PRE_OPERATIONS_EXEC event occurs before the lock file gets
+     * installed and operations are executed.
      *
-     * The event listener method receives a
-     * Composer\Installer\InstallerEvent instance.
+     * The event listener method receives an Composer\Installer\InstallerEvent instance.
      *
      * @var string
      */
-    const POST_DEPENDENCIES_SOLVING = 'post-dependencies-solving';
+    const PRE_OPERATIONS_EXEC = 'pre-operations-exec';
 }

+ 49 - 7
src/Composer/Installer/InstallerInterface.php

@@ -15,6 +15,7 @@ namespace Composer\Installer;
 use Composer\Package\PackageInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use InvalidArgumentException;
+use React\Promise\PromiseInterface;
 
 /**
  * Interface for the package installation manager.
@@ -42,20 +43,46 @@ interface InstallerInterface
      */
     public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package);
 
+    /**
+     * Downloads the files needed to later install the given package.
+     *
+     * @param  PackageInterface      $package     package instance
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null);
+
+    /**
+     * Do anything that needs to be done between all downloads have been completed and the actual operation is executed
+     *
+     * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore
+     * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or
+     * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can
+     * be undone as much as possible.
+     *
+     * @param  string                $type        one of install/update/uninstall
+     * @param  PackageInterface      $package     package instance
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null);
+
     /**
      * Installs specific package.
      *
-     * @param InstalledRepositoryInterface $repo    repository in which to check
-     * @param PackageInterface             $package package instance
+     * @param  InstalledRepositoryInterface $repo    repository in which to check
+     * @param  PackageInterface             $package package instance
+     * @return PromiseInterface|null
      */
     public function install(InstalledRepositoryInterface $repo, PackageInterface $package);
 
     /**
      * Updates specific package.
      *
-     * @param InstalledRepositoryInterface $repo    repository in which to check
-     * @param PackageInterface             $initial already installed package version
-     * @param PackageInterface             $target  updated version
+     * @param  InstalledRepositoryInterface $repo    repository in which to check
+     * @param  PackageInterface             $initial already installed package version
+     * @param  PackageInterface             $target  updated version
+     * @return PromiseInterface|null
      *
      * @throws InvalidArgumentException if $initial package is not installed
      */
@@ -64,11 +91,26 @@ interface InstallerInterface
     /**
      * Uninstalls specific package.
      *
-     * @param InstalledRepositoryInterface $repo    repository in which to check
-     * @param PackageInterface             $package package instance
+     * @param  InstalledRepositoryInterface $repo    repository in which to check
+     * @param  PackageInterface             $package package instance
+     * @return PromiseInterface|null
      */
     public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package);
 
+    /**
+     * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps
+     *
+     * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give
+     * all installers a change to cleanup things they did previously, so you need to keep track of changes
+     * applied in the installer/downloader themselves.
+     *
+     * @param  string                $type        one of install/update/uninstall
+     * @param  PackageInterface      $package     package instance
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null);
+
     /**
      * Returns the installation path of a package
      *

+ 35 - 2
src/Composer/Installer/LibraryInstaller.php

@@ -43,7 +43,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
      *
      * @param IOInterface     $io
      * @param Composer        $composer
-     * @param string          $type
+     * @param string|null     $type
      * @param Filesystem      $filesystem
      * @param BinaryInstaller $binaryInstaller
      */
@@ -85,6 +85,39 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
         return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $this->initializeVendorDir();
+        $downloadPath = $this->getInstallPath($package);
+
+        return $this->downloadManager->download($package, $downloadPath, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $this->initializeVendorDir();
+        $downloadPath = $this->getInstallPath($package);
+
+        return $this->downloadManager->prepare($type, $package, $downloadPath, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $this->initializeVendorDir();
+        $downloadPath = $this->getInstallPath($package);
+
+        return $this->downloadManager->cleanup($type, $package, $downloadPath, $prevPackage);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -194,7 +227,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
     protected function installCode(PackageInterface $package)
     {
         $downloadPath = $this->getInstallPath($package);
-        $this->downloadManager->download($package, $downloadPath);
+        $this->downloadManager->install($package, $downloadPath);
     }
 
     protected function updateCode(PackageInterface $initial, PackageInterface $target)

+ 25 - 1
src/Composer/Installer/MetapackageInstaller.php

@@ -47,6 +47,30 @@ class MetapackageInstaller implements InstallerInterface
         return $repo->hasPackage($package);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        // noop
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        // noop
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        // noop
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -69,7 +93,7 @@ class MetapackageInstaller implements InstallerInterface
         $name = $target->getName();
         $from = $initial->getFullPrettyVersion();
         $to = $target->getFullPrettyVersion();
-        $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
+        $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>)");
 
         $repo->removePackage($initial);

+ 21 - 0
src/Composer/Installer/NoopInstaller.php

@@ -40,6 +40,27 @@ class NoopInstaller implements InstallerInterface
         return $repo->hasPackage($package);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+    }
+
     /**
      * {@inheritDoc}
      */

+ 78 - 9
src/Composer/Installer/PackageEvent.php

@@ -16,19 +16,45 @@ use Composer\Composer;
 use Composer\IO\IOInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\PolicyInterface;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
-use Composer\Repository\CompositeRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Repository\RepositorySet;
+use Composer\EventDispatcher\Event;
 
 /**
  * The Package Event.
  *
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
-class PackageEvent extends InstallerEvent
+class PackageEvent extends Event
 {
     /**
-     * @var OperationInterface The package instance
+     * @var Composer
+     */
+    private $composer;
+
+    /**
+     * @var IOInterface
+     */
+    private $io;
+
+    /**
+     * @var bool
+     */
+    private $devMode;
+
+    /**
+     * @var RepositoryInterface
+     */
+    private $localRepo;
+
+    /**
+     * @var OperationInterface[]
+     */
+    private $operations;
+
+    /**
+     * @var OperationInterface The operation instance which is being executed
      */
     private $operation;
 
@@ -39,20 +65,63 @@ class PackageEvent extends InstallerEvent
      * @param Composer             $composer
      * @param IOInterface          $io
      * @param bool                 $devMode
-     * @param PolicyInterface      $policy
-     * @param Pool                 $pool
-     * @param CompositeRepository  $installedRepo
+     * @param RepositoryInterface  $localRepo
      * @param Request              $request
      * @param OperationInterface[] $operations
      * @param OperationInterface   $operation
      */
-    public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation)
+    public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositoryInterface $localRepo, array $operations = array(), OperationInterface $operation)
     {
-        parent::__construct($eventName, $composer, $io, $devMode, $policy, $pool, $installedRepo, $request, $operations);
+        parent::__construct($eventName);
 
+        $this->composer = $composer;
+        $this->io = $io;
+        $this->devMode = $devMode;
+        $this->localRepo = $localRepo;
+        $this->operations = $operations;
         $this->operation = $operation;
     }
 
+    /**
+     * @return Composer
+     */
+    public function getComposer()
+    {
+        return $this->composer;
+    }
+
+    /**
+     * @return IOInterface
+     */
+    public function getIO()
+    {
+        return $this->io;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isDevMode()
+    {
+        return $this->devMode;
+    }
+
+    /**
+     * @return RepositoryInterface
+     */
+    public function getLocalRepo()
+    {
+        return $this->localRepo;
+    }
+
+    /**
+     * @return OperationInterface[]
+     */
+    public function getOperations()
+    {
+        return $this->operations;
+    }
+
     /**
      * Returns the package instance.
      *

+ 25 - 7
src/Composer/Installer/PluginInstaller.php

@@ -50,19 +50,27 @@ class PluginInstaller extends LibraryInstaller
     /**
      * {@inheritDoc}
      */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
         $extra = $package->getExtra();
         if (empty($extra['class'])) {
             throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
         }
 
+        return parent::download($package, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    {
         parent::install($repo, $package);
         try {
             $this->composer->getPluginManager()->registerPackage($package, true);
         } catch (\Exception $e) {
             // Rollback installation
-            $this->io->writeError('Plugin installation failed, rolling back');
+            $this->io->writeError('Plugin initialization failed, uninstalling plugin');
             parent::uninstall($repo, $package);
             throw $e;
         }
@@ -73,12 +81,22 @@ class PluginInstaller extends LibraryInstaller
      */
     public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
     {
-        $extra = $target->getExtra();
-        if (empty($extra['class'])) {
-            throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
+        parent::update($repo, $initial, $target);
+
+        try {
+            $this->composer->getPluginManager()->deactivatePackage($initial, true);
+            $this->composer->getPluginManager()->registerPackage($target, true);
+        } catch (\Exception $e) {
+            // Rollback installation
+            $this->io->writeError('Plugin initialization failed, uninstalling plugin');
+            parent::uninstall($repo, $target);
+            throw $e;
         }
+    }
 
-        parent::update($repo, $initial, $target);
-        $this->composer->getPluginManager()->registerPackage($target, true);
+    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
+    {
+        $this->composer->getPluginManager()->uninstallPackage($package, true);
+        parent::uninstall($repo, $package);
     }
 }

+ 27 - 2
src/Composer/Installer/ProjectInstaller.php

@@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface
     /**
      * {@inheritDoc}
      */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
         $installPath = $this->installPath;
         if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@@ -67,7 +67,32 @@ class ProjectInstaller implements InstallerInterface
         if (!is_dir($installPath)) {
             mkdir($installPath, 0777, true);
         }
-        $this->downloadManager->download($package, $installPath);
+
+        return $this->downloadManager->download($package, $installPath, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    {
+        $this->downloadManager->install($package, $this->installPath);
     }
 
     /**

+ 85 - 14
src/Composer/Installer/SuggestedPackagesReporter.php

@@ -24,6 +24,10 @@ use Symfony\Component\Console\Formatter\OutputFormatter;
  */
 class SuggestedPackagesReporter
 {
+    const MODE_LIST = 1;
+    const MODE_BY_PACKAGE = 2;
+    const MODE_BY_SUGGESTION = 4;
+
     /**
      * @var array
      */
@@ -91,38 +95,105 @@ class SuggestedPackagesReporter
 
     /**
      * Output suggested packages.
+     *
      * Do not list the ones already installed if installed repository provided.
      *
-     * @param  RepositoryInterface       $installedRepo Installed packages
+     * @param  int                       $mode One of the MODE_* constants from this class
      * @return SuggestedPackagesReporter
      */
-    public function output(RepositoryInterface $installedRepo = null)
+    public function output($mode, RepositoryInterface $installedRepo = null)
+    {
+        $suggestedPackages = $this->getFilteredSuggestions($installedRepo);
+
+        $suggesters = array();
+        $suggested = array();
+        foreach ($suggestedPackages as $suggestion) {
+            $suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason'];
+            $suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason'];
+        }
+        ksort($suggesters);
+        ksort($suggested);
+
+        // Simple mode
+        if ($mode & self::MODE_LIST) {
+            foreach (array_keys($suggested) as $name) {
+                $this->io->write(sprintf('<info>%s</info>', $name));
+            }
+
+            return 0;
+        }
+
+        // Grouped by package
+        if ($mode & self::MODE_BY_PACKAGE) {
+            foreach ($suggesters as $suggester => $suggestions) {
+                $this->io->write(sprintf('<comment>%s</comment> suggests:', $suggester));
+
+                foreach ($suggestions as $suggestion => $reason) {
+                    $this->io->write(sprintf(' - <info>%s</info>' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason)));
+                }
+                $this->io->write('');
+            }
+        }
+
+        // Grouped by suggestion
+        if ($mode & self::MODE_BY_SUGGESTION) {
+            // Improve readability in full mode
+            if ($mode & self::MODE_BY_PACKAGE) {
+                $this->io->write(str_repeat('-', 78));
+            }
+            foreach ($suggested as $suggestion => $suggesters) {
+                $this->io->write(sprintf('<comment>%s</comment> is suggested by:', $suggestion));
+
+                foreach ($suggesters as $suggester => $reason) {
+                    $this->io->write(sprintf(' - <info>%s</info>' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason)));
+                }
+                $this->io->write('');
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Output number of new suggested packages and a hint to use suggest command.
+     **
+     * Do not list the ones already installed if installed repository provided.
+     *
+     * @return SuggestedPackagesReporter
+     */
+    public function outputMinimalistic(RepositoryInterface $installedRepo = null)
+    {
+        $suggestedPackages = $this->getFilteredSuggestions($installedRepo);
+        if ($suggestedPackages) {
+            $this->io->writeError('<info>'.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.</info>');
+        }
+
+        return $this;
+    }
+
+    private function getFilteredSuggestions(RepositoryInterface $installedRepo = null)
     {
         $suggestedPackages = $this->getPackages();
-        $installedPackages = array();
-        if (null !== $installedRepo && ! empty($suggestedPackages)) {
+        $installedNames = array();
+        if (null !== $installedRepo && !empty($suggestedPackages)) {
             foreach ($installedRepo->getPackages() as $package) {
-                $installedPackages = array_merge(
-                    $installedPackages,
+                $installedNames = array_merge(
+                    $installedNames,
                     $package->getNames()
                 );
             }
         }
 
+        $suggestions = array();
         foreach ($suggestedPackages as $suggestion) {
-            if (in_array($suggestion['target'], $installedPackages)) {
+            if (in_array($suggestion['target'], $installedNames)) {
                 continue;
             }
 
-            $this->io->writeError(sprintf(
-                '%s suggests installing %s%s',
-                $suggestion['source'],
-                $this->escapeOutput($suggestion['target']),
-                $this->escapeOutput('' !== $suggestion['reason'] ? ' ('.$suggestion['reason'].')' : '')
-            ));
+            $suggestions[] = $suggestion;
         }
 
-        return $this;
+        return $suggestions;
     }
 
     /**

+ 10 - 10
src/Composer/Json/JsonFile.php

@@ -15,7 +15,7 @@ namespace Composer\Json;
 use JsonSchema\Validator;
 use Seld\JsonLint\JsonParser;
 use Seld\JsonLint\ParsingException;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Composer\Downloader\TransportException;
 
@@ -37,25 +37,25 @@ class JsonFile
     const COMPOSER_SCHEMA_PATH = '/../../../res/composer-schema.json';
 
     private $path;
-    private $rfs;
+    private $httpDownloader;
     private $io;
 
     /**
      * Initializes json file reader/parser.
      *
-     * @param  string                    $path path to a lockfile
-     * @param  RemoteFilesystem          $rfs  required for loading http/https json files
+     * @param  string                    $path           path to a lockfile
+     * @param  HttpDownloader            $httpDownloader required for loading http/https json files
      * @param  IOInterface               $io
      * @throws \InvalidArgumentException
      */
-    public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null)
+    public function __construct($path, HttpDownloader $httpDownloader = null, IOInterface $io = null)
     {
         $this->path = $path;
 
-        if (null === $rfs && preg_match('{^https?://}i', $path)) {
-            throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
+        if (null === $httpDownloader && preg_match('{^https?://}i', $path)) {
+            throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed');
         }
-        $this->rfs = $rfs;
+        $this->httpDownloader = $httpDownloader;
         $this->io = $io;
     }
 
@@ -86,8 +86,8 @@ class JsonFile
     public function read()
     {
         try {
-            if ($this->rfs) {
-                $json = $this->rfs->getContents($this->path, $this->path, false);
+            if ($this->httpDownloader) {
+                $json = $this->httpDownloader->get($this->path)->getBody();
             } else {
                 if ($this->io && $this->io->isDebug()) {
                     $this->io->writeError('Reading ' . $this->path);

+ 5 - 0
src/Composer/Package/AliasPackage.php

@@ -416,4 +416,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
     {
         return $this->aliasOf->setDistType($type);
     }
+
+    public function setSourceDistReferences($reference)
+    {
+        return $this->aliasOf->setSourceDistReferences($reference);
+    }
 }

+ 7 - 2
src/Composer/Package/Archiver/ArchiveManager.php

@@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager;
 use Composer\Package\PackageInterface;
 use Composer\Package\RootPackageInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Composer\Json\JsonFile;
 
 /**
@@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
 class ArchiveManager
 {
     protected $downloadManager;
+    protected $loop;
 
     protected $archivers = array();
 
@@ -36,9 +38,10 @@ class ArchiveManager
     /**
      * @param DownloadManager $downloadManager A manager used to download package sources
      */
-    public function __construct(DownloadManager $downloadManager)
+    public function __construct(DownloadManager $downloadManager, Loop $loop)
     {
         $this->downloadManager = $downloadManager;
+        $this->loop = $loop;
     }
 
     /**
@@ -149,7 +152,9 @@ class ArchiveManager
 
             try {
                 // Download sources
-                $this->downloadManager->download($package, $sourcePath);
+                $promise = $this->downloadManager->download($package, $sourcePath);
+                $this->loop->wait(array($promise));
+                $this->downloadManager->install($package, $sourcePath);
             } catch (\Exception $e) {
                 $filesystem->removeDirectory($sourcePath);
                 throw  $e;

+ 21 - 9
src/Composer/Package/BasePackage.php

@@ -210,18 +210,30 @@ abstract class BasePackage implements PackageInterface
     /**
      * {@inheritDoc}
      */
-    public function getFullPrettyVersion($truncate = true)
+    public function getFullPrettyVersion($truncate = true, $displayMode = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV)
     {
-        if (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) {
+        if ($displayMode === PackageInterface::DISPLAY_SOURCE_REF_IF_DEV &&
+            (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git')))
+        ) {
             return $this->getPrettyVersion();
         }
 
+        switch ($displayMode) {
+            case PackageInterface::DISPLAY_SOURCE_REF_IF_DEV:
+            case PackageInterface::DISPLAY_SOURCE_REF:
+                $reference = $this->getSourceReference();
+                break;
+            case PackageInterface::DISPLAY_DIST_REF:
+                $reference = $this->getDistReference();
+                break;
+        }
+
         // if source reference is a sha1 hash -- truncate
-        if ($truncate && strlen($this->getSourceReference()) === 40) {
-            return $this->getPrettyVersion() . ' ' . substr($this->getSourceReference(), 0, 7);
+        if ($truncate && strlen($reference) === 40) {
+            return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7);
         }
 
-        return $this->getPrettyVersion() . ' ' . $this->getSourceReference();
+        return $this->getPrettyVersion() . ' ' . $reference;
     }
 
     public function getStabilityPriority()
@@ -238,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);
     }
 }

Some files were not shown because too many files changed in this diff