Browse Source

Merge branch '2.0'

Jordi Boggiano 5 years ago
parent
commit
87757de6bc
100 changed files with 4111 additions and 2546 deletions
  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
 .travis.yml export-ignore
 appveyor.yml export-ignore
 appveyor.yml export-ignore
 phpunit.xml.dist export-ignore
 phpunit.xml.dist export-ignore
+/phpstan/ export-ignore

+ 30 - 13
.travis.yml

@@ -1,6 +1,6 @@
 language: php
 language: php
 
 
-dist: trusty
+dist: bionic
 
 
 git:
 git:
   depth: 5
   depth: 5
@@ -9,27 +9,40 @@ cache:
   directories:
   directories:
     - $HOME/.composer/cache
     - $HOME/.composer/cache
 
 
-addons:
-  apt:
-    packages:
-      - parallel
-
 matrix:
 matrix:
   include:
   include:
     - php: 5.3
     - php: 5.3
       dist: precise
       dist: precise
     - php: 5.4
     - php: 5.4
+      dist: trusty
     - php: 5.5
     - php: 5.5
+      dist: trusty
     - php: 5.6
     - php: 5.6
+      dist: xenial
     - php: 7.0
     - php: 7.0
+      dist: xenial
     - php: 7.1
     - php: 7.1
+      dist: xenial
     - php: 7.2
     - php: 7.2
+      dist: xenial
     - php: 7.3
     - 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
     - php: 7.4
       env:
       env:
         - deps=high
         - deps=high
         - SYMFONY_PHPUNIT_VERSION=7.5
         - 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
   fast_finish: true
   allow_failures:
   allow_failures:
     - php: nightly
     - php: nightly
@@ -44,9 +57,9 @@ before_install:
 
 
 install:
 install:
   # flags to pass to 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
   # 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
   # install dependencies using system provided composer binary
   - composer install $flags
   - composer install $flags
   # install dependencies using composer from source
   # install dependencies using composer from source
@@ -58,9 +71,13 @@ before_script:
   - git config --global user.email travis@example.com
   - git config --global user.email travis@example.com
 
 
 script:
 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:
 before_deploy:
   - php -d phar.readonly=0 bin/compile
   - php -d phar.readonly=0 bin/compile
@@ -73,4 +90,4 @@ deploy:
   on:
   on:
     tags: true
     tags: true
     repo: composer/composer
     repo: composer/composer
-    php:  '7.2'
+    php:  '7.3'

+ 10 - 1
appveyor.yml

@@ -3,7 +3,7 @@ clone_depth: 5
 
 
 environment:
 environment:
   # This sets the PHP version (from Chocolatey)
   # 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_CACHE: C:\tools\phpci
   PHPCI_PHP: C:\tools\phpci\php
   PHPCI_PHP: C:\tools\phpci\php
   PHPCI_COMPOSER: C:\tools\phpci\composer
   PHPCI_COMPOSER: C:\tools\phpci\composer
@@ -25,6 +25,15 @@ install:
   - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%"
   - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%"
   - php -v
   - php -v
   - IF %PHP%==0 (composer --version) ELSE (composer self-update)
   - 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%
   - cd %APPVEYOR_BUILD_FOLDER%
   - composer install --prefer-dist --no-progress
   - composer install --prefer-dist --no-progress
 
 

+ 11 - 5
composer.json

@@ -24,7 +24,7 @@
     "require": {
     "require": {
         "php": "^5.3.2 || ^7.0",
         "php": "^5.3.2 || ^7.0",
         "composer/ca-bundle": "^1.0",
         "composer/ca-bundle": "^1.0",
-        "composer/semver": "^1.0",
+        "composer/semver": "^2.0@dev",
         "composer/spdx-licenses": "^1.2",
         "composer/spdx-licenses": "^1.2",
         "composer/xdebug-handler": "^1.1",
         "composer/xdebug-handler": "^1.1",
         "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
         "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
@@ -34,7 +34,8 @@
         "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
         "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
         "symfony/filesystem": "^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/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": {
     "conflict": {
         "symfony/console": "2.8.38"
         "symfony/console": "2.8.38"
@@ -55,7 +56,7 @@
     },
     },
     "extra": {
     "extra": {
         "branch-alias": {
         "branch-alias": {
-            "dev-master": "1.10-dev"
+            "dev-master": "2.0-dev"
         }
         }
     },
     },
     "autoload": {
     "autoload": {
@@ -65,8 +66,13 @@
     },
     },
     "autoload-dev": {
     "autoload-dev": {
         "psr-4": {
         "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": [
         "bin/composer"
         "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",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "cc6f9640996dfad00a5b03a8be01a571",
+    "content-hash": "a0a9399315ac0b612d4296b8df745112",
     "packages": [
     "packages": [
         {
         {
             "name": "composer/ca-bundle",
             "name": "composer/ca-bundle",
@@ -60,20 +60,25 @@
                 "ssl",
                 "ssl",
                 "tls"
                 "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"
             "time": "2020-01-13T10:02:55+00:00"
         },
         },
         {
         {
             "name": "composer/semver",
             "name": "composer/semver",
-            "version": "1.5.1",
+            "version": "2.0.x-dev",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/composer/semver.git",
                 "url": "https://github.com/composer/semver.git",
-                "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
+                "reference": "4df5ff3249f01018504939d66040d8d2b783d820"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "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": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -85,7 +90,7 @@
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "branch-alias": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-master": "2.x-dev"
                 }
                 }
             },
             },
             "autoload": {
             "autoload": {
@@ -121,7 +126,22 @@
                 "validation",
                 "validation",
                 "versioning"
                 "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",
             "name": "composer/spdx-licenses",
@@ -181,6 +201,11 @@
                 "spdx",
                 "spdx",
                 "validator"
                 "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"
             "time": "2020-02-14T07:44:31+00:00"
         },
         },
         {
         {
@@ -225,6 +250,11 @@
                 "Xdebug",
                 "Xdebug",
                 "performance"
                 "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": [
             "funding": [
                 {
                 {
                     "url": "https://packagist.com",
                     "url": "https://packagist.com",
@@ -353,6 +383,44 @@
             },
             },
             "time": "2019-11-01T11:05:21+00:00"
             "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",
             "name": "seld/jsonlint",
             "version": "1.7.2",
             "version": "1.7.2",
@@ -448,6 +516,10 @@
             "keywords": [
             "keywords": [
                 "phar"
                 "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"
             "time": "2020-02-14T15:25:33+00:00"
         },
         },
         {
         {
@@ -735,6 +807,9 @@
                 "polyfill",
                 "polyfill",
                 "portable"
                 "portable"
             ],
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/master"
+            },
             "time": "2020-01-13T11:15:53+00:00"
             "time": "2020-01-13T11:15:53+00:00"
         },
         },
         {
         {
@@ -794,6 +869,9 @@
                 "portable",
                 "portable",
                 "shim"
                 "shim"
             ],
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/master"
+            },
             "time": "2020-01-13T11:15:53+00:00"
             "time": "2020-01-13T11:15:53+00:00"
         },
         },
         {
         {
@@ -949,6 +1027,16 @@
             "license": [
             "license": [
                 "MIT"
                 "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"
             "time": "2016-01-25T08:17:30+00:00"
         },
         },
         {
         {
@@ -1012,6 +1100,10 @@
                 "spy",
                 "spy",
                 "stub"
                 "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"
             "time": "2020-03-05T15:02:03+00:00"
         },
         },
         {
         {
@@ -1076,6 +1168,10 @@
                 "compare",
                 "compare",
                 "equality"
                 "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"
             "time": "2017-01-29T09:50:25+00:00"
         },
         },
         {
         {
@@ -1128,6 +1224,10 @@
             "keywords": [
             "keywords": [
                 "diff"
                 "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"
             "time": "2017-05-22T07:24:03+00:00"
         },
         },
         {
         {
@@ -1195,6 +1295,10 @@
                 "export",
                 "export",
                 "exporter"
                 "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"
             "time": "2016-11-19T08:54:04+00:00"
         },
         },
         {
         {
@@ -1248,6 +1352,10 @@
             ],
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "description": "Provides functionality to recursively process PHP variables",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
             "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"
             "time": "2016-11-19T07:33:16+00:00"
         },
         },
         {
         {
@@ -1313,6 +1421,9 @@
             ],
             ],
             "description": "Symfony PHPUnit Bridge",
             "description": "Symfony PHPUnit Bridge",
             "homepage": "https://symfony.com",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38"
+            },
             "funding": [
             "funding": [
                 {
                 {
                     "url": "https://symfony.com/sponsor",
                     "url": "https://symfony.com/sponsor",
@@ -1332,7 +1443,9 @@
     ],
     ],
     "aliases": [],
     "aliases": [],
     "minimum-stability": "stable",
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {
+        "composer/semver": 20
+    },
     "prefer-stable": false,
     "prefer-stable": false,
     "prefer-lowest": false,
     "prefer-lowest": false,
     "platform": {
     "platform": {
@@ -1342,5 +1455,5 @@
     "platform-overrides": {
     "platform-overrides": {
         "php": "5.3.9"
         "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
 > if the `composer.lock` has not been updated since changes were made to the
 > `composer.json` that might affect dependency resolution.
 > `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
 ```sh
 php composer.phar update monolog/monolog [...]
 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-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-progress:** Removes the progress display that can mess with some
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   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
 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
   autoloader. This is recommended especially for production, but can take
   autoloader. This is recommended especially for production, but can take
   a bit of time to run so it is currently not done by default.
   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-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-progress:** Removes the progress display that can mess with some
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   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
 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
   autoloader. This is recommended especially for production, but can take
   autoloader. This is recommended especially for production, but can take
   a bit of time to run so it is currently not done by default.
   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
 ### Options
 
 
 * **--dev:** Add packages to `require-dev`.
 * **--dev:** Add packages to `require-dev`.
+* **--dry-run:** Simulate the command without actually doing anything.
 * **--prefer-source:** Install packages from `source` when available.
 * **--prefer-source:** Install packages from `source` when available.
 * **--prefer-dist:** Install packages from `dist` when available.
 * **--prefer-dist:** Install packages from `dist` when available.
 * **--no-progress:** Removes the progress display that can mess with some
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   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-update:** Disables the automatic update of the dependencies.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--update-no-dev:** Run the dependency update with the `--no-dev` option.
 * **--update-no-dev:** Run the dependency update with the `--no-dev` option.
@@ -236,6 +234,7 @@ uninstalled.
 
 
 ### Options
 ### Options
 * **--dev:** Remove packages from `require-dev`.
 * **--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
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   terminals or scripts which don't handle backspace characters.
 * **--no-update:** Disables the automatic update of the dependencies.
 * **--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`
 optionally pass one or multiple package names in the format of `vendor/package`
 to limit output to suggestions made by those packages only.
 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.
 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
 ### Options
 
 
-* **--by-package:** Groups output by suggesting package.
+* **--by-package:** Groups output by suggesting package (default).
 * **--by-suggestion:** Groups output by suggested package.
 * **--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.
 * **--no-dev:** Excludes suggestions from `require-dev` packages.
 
 
 ## fund
 ## 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
 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.
 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) →
 ← [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') {
         if ($protocol === 's3') {
             $awsClient = new AwsClient($this->io, $this->composer->getConfig());
             $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
 ### 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
 ### 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
 - **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.
   provides you with access to the input and output objects of the program.
 - **pre-file-download**: occurs before files are downloaded and allows
 - **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.
   based on the URL to be downloaded.
 - **pre-command-run**: occurs before a command is executed and allows you to
 - **pre-command-run**: occurs before a command is executed and allows you to
   manipulate the `InputInterface` object's options and arguments to tweak
   manipulate the `InputInterface` object's options and arguments to tweak
   a command's behavior.
   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
 > **Note:** Composer makes no assumptions about the state of your dependencies
 > prior to `install` or `update`. Therefore, you should not specify scripts
 > 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);
             $classes = self::findClasses($filePath);
             if (null !== $autoloadType) {
             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 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;
                     $scannedFiles[$realPath] = true;
                 }
                 }
             } else {
             } else {
@@ -126,8 +126,7 @@ class ClassMapGenerator
 
 
             foreach ($classes as $class) {
             foreach ($classes as $class) {
                 // skip classes not within the given namespace prefix
                 // 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;
                     continue;
                 }
                 }
 
 
@@ -196,19 +195,15 @@ class ClassMapGenerator
         // warn only if no valid classes, else silently skip invalid
         // warn only if no valid classes, else silently skip invalid
         if (empty($validClasses)) {
         if (empty($validClasses)) {
             foreach ($rejectedClasses as $class) {
             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 $io;
     private $root;
     private $root;
     private $enabled = true;
     private $enabled = true;
-    private $whitelist;
+    private $allowlist;
     private $filesystem;
     private $filesystem;
 
 
     /**
     /**
      * @param IOInterface $io
      * @param IOInterface $io
      * @param string      $cacheDir   location of the cache
      * @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
      * @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->io = $io;
         $this->root = rtrim($cacheDir, '/\\') . '/';
         $this->root = rtrim($cacheDir, '/\\') . '/';
-        $this->whitelist = $whitelist;
+        $this->allowlist = $allowlist;
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->filesystem = $filesystem ?: new Filesystem();
 
 
         if (!self::isUsable($cacheDir)) {
         if (!self::isUsable($cacheDir)) {
@@ -77,7 +77,7 @@ class Cache
     public function read($file)
     public function read($file)
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
-            $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
+            $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
             if (file_exists($this->root . $file)) {
             if (file_exists($this->root . $file)) {
                 $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
                 $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
 
 
@@ -91,7 +91,7 @@ class Cache
     public function write($file, $contents)
     public function write($file, $contents)
     {
     {
         if ($this->enabled) {
         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);
             $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG);
 
 
@@ -129,7 +129,7 @@ class Cache
     public function copyFrom($file, $source)
     public function copyFrom($file, $source)
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
-            $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
+            $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
             $this->filesystem->ensureDirectoryExists(dirname($this->root . $file));
             $this->filesystem->ensureDirectoryExists(dirname($this->root . $file));
 
 
             if (!file_exists($source)) {
             if (!file_exists($source)) {
@@ -150,7 +150,7 @@ class Cache
     public function copyTo($file, $target)
     public function copyTo($file, $target)
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
-            $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
+            $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
             if (file_exists($this->root . $file)) {
             if (file_exists($this->root . $file)) {
                 try {
                 try {
                     touch($this->root . $file, filemtime($this->root . $file), time());
                     touch($this->root . $file, filemtime($this->root . $file), time());
@@ -177,7 +177,7 @@ class Cache
     public function remove($file)
     public function remove($file)
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
-            $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
+            $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
             if (file_exists($this->root . $file)) {
             if (file_exists($this->root . $file)) {
                 return $this->filesystem->unlink($this->root . $file);
                 return $this->filesystem->unlink($this->root . $file);
             }
             }
@@ -229,7 +229,7 @@ class Cache
     public function sha1($file)
     public function sha1($file)
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
-            $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
+            $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
             if (file_exists($this->root . $file)) {
             if (file_exists($this->root . $file)) {
                 return sha1_file($this->root . $file);
                 return sha1_file($this->root . $file);
             }
             }
@@ -241,7 +241,7 @@ class Cache
     public function sha256($file)
     public function sha256($file)
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
-            $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
+            $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
             if (file_exists($this->root . $file)) {
             if (file_exists($this->root . $file)) {
                 return hash_file('sha256', $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\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PluginEvents;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
@@ -111,8 +112,9 @@ EOT
             $archiveManager = $composer->getArchiveManager();
             $archiveManager = $composer->getArchiveManager();
         } else {
         } else {
             $factory = new Factory;
             $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) {
         if ($packageName) {

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

@@ -27,6 +27,8 @@ use Symfony\Component\Console\Command\Command;
 /**
 /**
  * Base class for Composer commands
  * Base class for Composer commands
  *
  *
+ * @method Application getApplication()
+ *
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  */
  */
@@ -46,7 +48,7 @@ abstract class BaseCommand extends Command
      * @param  bool              $required
      * @param  bool              $required
      * @param  bool|null         $disablePlugins
      * @param  bool|null         $disablePlugins
      * @throws \RuntimeException
      * @throws \RuntimeException
-     * @return Composer
+     * @return Composer|null
      */
      */
     public function getComposer($required = true, $disablePlugins = 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'))) {
         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'));
             $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);
         return array($preferSource, $preferDist);

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

@@ -12,11 +12,12 @@
 
 
 namespace Composer\Command;
 namespace Composer\Command;
 
 
-use Composer\DependencyResolver\Pool;
 use Composer\Package\Link;
 use Composer\Package\Link;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
-use Composer\Repository\ArrayRepository;
+use Composer\Repository\InstalledArrayRepository;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\CompositeRepository;
+use Composer\Repository\RootPackageRepository;
+use Composer\Repository\InstalledRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryFactory;
 use Composer\Repository\RepositoryFactory;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\CommandEvent;
@@ -71,15 +72,12 @@ class BaseDependencyCommand extends BaseCommand
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output);
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
 
 
-        // Prepare repositories and set up a pool
         $platformOverrides = $composer->getConfig()->get('platform') ?: array();
         $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(),
             $composer->getRepositoryManager()->getLocalRepository(),
             new PlatformRepository(array(), $platformOverrides),
             new PlatformRepository(array(), $platformOverrides),
         ));
         ));
-        $pool = new Pool();
-        $pool->addRepository($repository);
 
 
         // Parse package name and constraint
         // Parse package name and constraint
         list($needle, $textConstraint) = array_pad(
         list($needle, $textConstraint) = array_pad(
@@ -89,17 +87,17 @@ class BaseDependencyCommand extends BaseCommand
         );
         );
 
 
         // Find packages that are or provide the requested package first
         // Find packages that are or provide the requested package first
-        $packages = $pool->whatProvides(strtolower($needle));
+        $packages = $installedRepo->findPackagesWithReplacersAndProviders($needle);
         if (empty($packages)) {
         if (empty($packages)) {
             throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle));
             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.
         // 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.
         // 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()));
             $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO()));
             if ($match = $defaultRepos->findPackage($needle, $textConstraint)) {
             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);
         $recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE);
 
 
         // Resolve dependencies
         // Resolve dependencies
-        $results = $repository->getDependents($needles, $constraint, $inverted, $recursive);
+        $results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive);
         if (empty($results)) {
         if (empty($results)) {
             $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : '';
             $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : '';
             $this->getIO()->writeError(sprintf(
             $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\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
+use Composer\Repository\InstalledRepository;
 
 
 class CheckPlatformReqsCommand extends BaseCommand
 class CheckPlatformReqsCommand extends BaseCommand
 {
 {
@@ -48,12 +49,13 @@ EOT
 
 
         $requires = $composer->getPackage()->getRequires();
         $requires = $composer->getPackage()->getRequires();
         if ($input->getOption('no-dev')) {
         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 {
         } else {
-            $dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages();
+            $installedRepo = $composer->getRepositoryManager()->getLocalRepository();
             // fallback to lockfile if installed repo is empty
             // 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();
             $requires += $composer->getPackage()->getDevRequires();
         }
         }
@@ -61,7 +63,8 @@ EOT
             $requires[$require] = array($link);
             $requires[$require] = array($link);
         }
         }
 
 
-        foreach ($dependencies as $package) {
+        $installedRepo = new InstalledRepository(array($installedRepo));
+        foreach ($installedRepo->getPackages() as $package) {
             foreach ($package->getRequires() as $require => $link) {
             foreach ($package->getRequires() as $require => $link) {
                 $requires[$require][] = $link;
                 $requires[$require][] = $link;
             }
             }
@@ -69,19 +72,9 @@ EOT
 
 
         ksort($requires);
         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();
         $results = array();
-
         $exitCode = 0;
         $exitCode = 0;
 
 
         /**
         /**
@@ -89,42 +82,62 @@ EOT
          */
          */
         foreach ($requires as $require => $links) {
         foreach ($requires as $require => $links) {
             if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) {
             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(
                         $results[] = array(
-                            $currentPlatformPackageMap[$require]->getPrettyName(),
-                            $currentPlatformPackageMap[$require]->getPrettyVersion(),
+                            $candidate->getName() === $require ? $candidate->getPrettyName() : $require,
+                            $candidateConstraint->getPrettyString(),
                             null,
                             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');
         $settingKey = $input->getArgument('setting-key');
-        if (!$settingKey) {
+        if (!$settingKey || !is_string($settingKey)) {
             return 0;
             return 0;
         }
         }
 
 

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

@@ -20,7 +20,6 @@ use Composer\Installer\InstallationManager;
 use Composer\Installer\SuggestedPackagesReporter;
 use Composer\Installer\SuggestedPackagesReporter;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Package\BasePackage;
 use Composer\Package\BasePackage;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Operation\InstallOperation;
 use Composer\DependencyResolver\Operation\InstallOperation;
 use Composer\Package\Version\VersionSelector;
 use Composer\Package\Version\VersionSelector;
 use Composer\Package\AliasPackage;
 use Composer\Package\AliasPackage;
@@ -28,6 +27,7 @@ use Composer\Repository\RepositoryFactory;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\InstalledFilesystemRepository;
 use Composer\Repository\InstalledFilesystemRepository;
+use Composer\Repository\RepositorySet;
 use Composer\Script\ScriptEvents;
 use Composer\Script\ScriptEvents;
 use Composer\Util\Silencer;
 use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
@@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonFile;
 use Composer\Config\JsonConfigSource;
 use Composer\Config\JsonConfigSource;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
 
 
 /**
 /**
@@ -182,8 +183,6 @@ EOT
             $composer = Factory::create($io, null, $disablePlugins);
             $composer = Factory::create($io, null, $disablePlugins);
         }
         }
 
 
-        $composer->getDownloadManager()->setOutputProgress(!$noProgress);
-
         $fs = new Filesystem();
         $fs = new Filesystem();
 
 
         if ($noScripts === false) {
         if ($noScripts === false) {
@@ -334,8 +333,8 @@ EOT
             throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities)));
             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;
         $phpVersion = null;
         $prettyPhpVersion = null;
         $prettyPhpVersion = null;
@@ -349,7 +348,7 @@ EOT
         }
         }
 
 
         // find the latest version if there are multiple
         // find the latest version if there are multiple
-        $versionSelector = new VersionSelector($pool);
+        $versionSelector = new VersionSelector($repositorySet);
         $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability);
         $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability);
 
 
         if (!$package) {
         if (!$package) {
@@ -384,15 +383,17 @@ EOT
             $package = $package->getAliasOf();
             $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)
         $dm->setPreferSource($preferSource)
-            ->setPreferDist($preferDist)
-            ->setOutputProgress(!$noProgress);
+            ->setPreferDist($preferDist);
 
 
         $projectInstaller = new ProjectInstaller($directory, $dm);
         $projectInstaller = new ProjectInstaller($directory, $dm);
-        $im = $this->createInstallationManager();
+        $im = $factory->createInstallationManager(new Loop($httpDownloader), $io);
         $im->addInstaller($projectInstaller);
         $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);
         $im->notifyInstalls($io);
 
 
         // collect suggestions
         // collect suggestions
@@ -408,16 +409,4 @@ EOT
 
 
         return $installedFromVcs;
         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\ConfigValidator;
 use Composer\Util\IniHelper;
 use Composer\Util\IniHelper;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\Util\StreamContextFactory;
 use Composer\Util\StreamContextFactory;
 use Composer\SelfUpdate\Keys;
 use Composer\SelfUpdate\Keys;
 use Composer\SelfUpdate\Versions;
 use Composer\SelfUpdate\Versions;
@@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface;
  */
  */
 class DiagnoseCommand extends BaseCommand
 class DiagnoseCommand extends BaseCommand
 {
 {
-    /** @var RemoteFilesystem */
-    protected $rfs;
+    /** @var HttpDownloader */
+    protected $httpDownloader;
 
 
     /** @var ProcessExecutor */
     /** @var ProcessExecutor */
     protected $process;
     protected $process;
@@ -86,7 +86,7 @@ EOT
         $config->merge(array('config' => array('secure-http' => false)));
         $config->merge(array('config' => array('secure-http' => false)));
         $config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO);
         $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);
         $this->process = new ProcessExecutor($io);
 
 
         $io->write('Checking platform settings: ', false);
         $io->write('Checking platform settings: ', false);
@@ -156,7 +156,7 @@ EOT
             $this->outputResult($this->checkVersion($config));
             $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();
         $platformOverrides = $config->get('platform') ?: array();
         $platformRepo = new PlatformRepository(array(), $platformOverrides);
         $platformRepo = new PlatformRepository(array(), $platformOverrides);
@@ -229,7 +229,7 @@ EOT
         }
         }
 
 
         try {
         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) {
         } catch (TransportException $e) {
             if (false !== strpos($e->getMessage(), 'cafile')) {
             if (false !== strpos($e->getMessage(), 'cafile')) {
                 $result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
                 $result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
@@ -256,11 +256,11 @@ EOT
 
 
         $protocol = extension_loaded('openssl') ? 'https' : 'http';
         $protocol = extension_loaded('openssl') ? 'https' : 'http';
         try {
         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 = reset($json['provider-includes']);
             $hash = $hash['sha256'];
             $hash = $hash['sha256'];
             $path = str_replace('%hash%', $hash, key($json['provider-includes']));
             $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) {
             if (hash('sha256', $provider) !== $hash) {
                 return 'It seems that your proxy is modifying http traffic on the fly';
                 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';
         $url = 'http://repo.packagist.org/packages.json';
         try {
         try {
-            $this->rfs->getContents('packagist.org', $url, false);
+            $this->httpDownloader->get($url);
         } catch (TransportException $e) {
         } catch (TransportException $e) {
             try {
             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) {
             } catch (TransportException $e) {
                 return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')';
                 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';
         $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0';
         try {
         try {
-            $this->rfs->getContents('github.com', $url, false);
+            $this->httpDownloader->get($url);
         } catch (TransportException $e) {
         } catch (TransportException $e) {
             try {
             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) {
             } catch (TransportException $e) {
                 return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
                 return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
             }
             }
@@ -347,7 +347,7 @@ EOT
         try {
         try {
             $url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/';
             $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,
                 'retry-auth-failure' => false,
             )) ? true : 'Unexpected error';
             )) ? true : 'Unexpected error';
         } catch (\Exception $e) {
         } catch (\Exception $e) {
@@ -377,8 +377,7 @@ EOT
         }
         }
 
 
         $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit';
         $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'];
         return $data['resources']['core'];
     }
     }
@@ -431,7 +430,7 @@ EOT
             return $result;
             return $result;
         }
         }
 
 
-        $versionsUtil = new Versions($config, $this->rfs);
+        $versionsUtil = new Versions($config, $this->httpDownloader);
         $latest = $versionsUtil->getLatest();
         $latest = $versionsUtil->getLatest();
 
 
         if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') {
         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";
                         $text .= "Install either of them or recompile php without --disable-iconv";
                         break;
                         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':
                     case 'php':
                         $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher.";
                         $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher.";
                         break;
                         break;
@@ -729,7 +714,7 @@ EOT
     /**
     /**
      * Check if allow_url_fopen is ON
      * Check if allow_url_fopen is ON
      *
      *
-     * @return bool|string
+     * @return true|string
      */
      */
     private function checkConnectivity()
     private function checkConnectivity()
     {
     {

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

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

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

@@ -12,7 +12,6 @@
 
 
 namespace Composer\Command;
 namespace Composer\Command;
 
 
-use Composer\DependencyResolver\Pool;
 use Composer\Factory;
 use Composer\Factory;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonFile;
 use Composer\Package\BasePackage;
 use Composer\Package\BasePackage;
@@ -22,6 +21,7 @@ use Composer\Package\Version\VersionSelector;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryFactory;
 use Composer\Repository\RepositoryFactory;
+use Composer\Repository\RepositorySet;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
@@ -42,8 +42,8 @@ class InitCommand extends BaseCommand
     /** @var array */
     /** @var array */
     private $gitConfig;
     private $gitConfig;
 
 
-    /** @var Pool[] */
-    private $pools;
+    /** @var RepositorySet[] */
+    private $repositorySets;
 
 
     /**
     /**
      * {@inheritdoc}
      * {@inheritdoc}
@@ -86,8 +86,8 @@ EOT
     {
     {
         $io = $this->getIO();
         $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'])) {
         if (isset($options['author'])) {
             $options['authors'] = $this->formatAuthors($options['author']);
             $options['authors'] = $this->formatAuthors($options['author']);
@@ -688,16 +688,16 @@ EOT
         return false !== filter_var($email, FILTER_VALIDATE_EMAIL);
         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';
         $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)
     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)
     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');
         $ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs');
 
 
         // ignore phpVersion if platform requirements are ignored
         // 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-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-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-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('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('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`.'),
                 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 = $this->getComposer(true, $input->getOption('no-plugins'));
-        $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
 
 
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@@ -108,7 +106,6 @@ EOT
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)

+ 42 - 7
src/Composer/Command/RemoveCommand.php

@@ -13,6 +13,7 @@
 namespace Composer\Command;
 namespace Composer\Command;
 
 
 use Composer\Config\JsonConfigSource;
 use Composer\Config\JsonConfigSource;
+use Composer\DependencyResolver\Request;
 use Composer\Installer;
 use Composer\Installer;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PluginEvents;
@@ -38,6 +39,7 @@ class RemoveCommand extends BaseCommand
             ->setDefinition(array(
             ->setDefinition(array(
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'),
                 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('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-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-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('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) {
         foreach ($packages as $package) {
             if (isset($composer[$type][$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])) {
             } elseif (isset($composer[$altType][$package])) {
                 $io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                 $io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                 if ($io->isInteractive()) {
                 if ($io->isInteractive()) {
                     if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
                     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]))) {
             } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) {
                 foreach ($matches as $matchedPackage) {
                 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]))) {
             } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) {
                 foreach ($matches as $matchedPackage) {
                 foreach ($matches as $matchedPackage) {
                     $io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                     $io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                     if ($io->isInteractive()) {
                     if ($io->isInteractive()) {
                         if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
                         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
         // Update packages
         $this->resetComposer();
         $this->resetComposer();
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $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);
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@@ -146,10 +180,11 @@ EOT
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)
             ->setUpdate(true)
             ->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'))
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
+            ->setDryRun($dryRun)
         ;
         ;
 
 
         $status = $install->run();
         $status = $install->run();

+ 69 - 11
src/Composer/Command/RequireCommand.php

@@ -12,6 +12,7 @@
 
 
 namespace Composer\Command;
 namespace Composer\Command;
 
 
+use Composer\DependencyResolver\Request;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
@@ -21,6 +22,8 @@ use Composer\Installer;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonManipulator;
 use Composer\Json\JsonManipulator;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\BasePackage;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PluginEvents;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\CompositeRepository;
@@ -35,9 +38,14 @@ use Composer\Util\Silencer;
 class RequireCommand extends InitCommand
 class RequireCommand extends InitCommand
 {
 {
     private $newlyCreated;
     private $newlyCreated;
+    private $firstRequire;
     private $json;
     private $json;
     private $file;
     private $file;
     private $composerBackup;
     private $composerBackup;
+    /** @var string file name */
+    private $lock;
+    /** @var ?string contents before modification if the lock file exists */
+    private $lockBackup;
 
 
     protected function configure()
     protected function configure()
     {
     {
@@ -47,16 +55,19 @@ class RequireCommand extends InitCommand
             ->setDefinition(array(
             ->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 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('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-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('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('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-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-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('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-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-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('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('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-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'),
                 new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest 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->json = new JsonFile($this->file);
+        $this->lock = Factory::getLockFile($this->file);
         $this->composerBackup = file_get_contents($this->json->getPath());
         $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
         // 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
         // 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');
         $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();
             $composerDefinition = $this->json->read();
             foreach ($requirements as $package => $version) {
             foreach ($requirements as $package => $version) {
                 $composerDefinition[$requireKey][$package] = $version;
                 $composerDefinition[$requireKey][$package] = $version;
@@ -202,51 +223,78 @@ EOT
         }
         }
 
 
         try {
         try {
-            return $this->doUpdate($input, $output, $io, $requirements);
+            return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey);
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             $this->revertComposerFile(false);
             $this->revertComposerFile(false);
             throw $e;
             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
         // Update packages
         $this->resetComposer();
         $this->resetComposer();
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $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');
         $updateDevMode = !$input->getOption('update-no-dev');
         $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader');
         $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader');
         $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative');
         $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative');
         $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader');
         $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);
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
 
 
         $install = Installer::create($io, $composer);
         $install = Installer::create($io, $composer);
 
 
         $install
         $install
+            ->setDryRun($input->getOption('dry-run'))
             ->setVerbose($input->getOption('verbose'))
             ->setVerbose($input->getOption('verbose'))
             ->setPreferSource($input->getOption('prefer-source'))
             ->setPreferSource($input->getOption('prefer-source'))
             ->setPreferDist($input->getOption('prefer-dist'))
             ->setPreferDist($input->getOption('prefer-dist'))
             ->setDevMode($updateDevMode)
             ->setDevMode($updateDevMode)
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)
             ->setUpdate(true)
             ->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'))
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
             ->setPreferStable($input->getOption('prefer-stable'))
             ->setPreferStable($input->getOption('prefer-stable'))
             ->setPreferLowest($input->getOption('prefer-lowest'))
             ->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();
         $status = $install->run();
-        if ($status !== 0) {
+        if ($status !== 0 || $input->getOption('dry-run')) {
             $this->revertComposerFile(false);
             $this->revertComposerFile(false);
         }
         }
 
 
@@ -285,9 +333,19 @@ EOT
         if ($this->newlyCreated) {
         if ($this->newlyCreated) {
             $io->writeError("\n".'<error>Installation failed, deleting '.$this->file.'.</error>');
             $io->writeError("\n".'<error>Installation failed, deleting '.$this->file.'.</error>');
             unlink($this->json->getPath());
             unlink($this->json->getPath());
+            if (file_exists($this->lock)) {
+                unlink($this->lock);
+            }
         } else {
         } 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);
             file_put_contents($this->json->getPath(), $this->composerBackup);
+            if ($this->lockBackup) {
+                file_put_contents($this->lock, $this->lockBackup);
+            }
         }
         }
 
 
         if ($hardExit) {
         if ($hardExit) {

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

@@ -77,9 +77,9 @@ EOT
         }
         }
 
 
         $io = $this->getIO();
         $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
         // switch channel if requested
         foreach (array('stable', 'preview', 'snapshot') as $channel) {
         foreach (array('stable', 'preview', 'snapshot') as $channel) {
@@ -154,11 +154,11 @@ EOT
 
 
         $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion);
         $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');
         $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);
         $io->writeError('   ', false);
-        $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress'));
+        $httpDownloader->copy($remoteFilename, $tempFilename);
         $io->writeError('');
         $io->writeError('');
 
 
         if (!file_exists($tempFilename) || !$signature) {
         if (!file_exists($tempFilename) || !$signature) {

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

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

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

@@ -13,6 +13,9 @@
 namespace Composer\Command;
 namespace Composer\Command;
 
 
 use Composer\Repository\PlatformRepository;
 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\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
@@ -26,8 +29,10 @@ class SuggestsCommand extends BaseCommand
             ->setName('suggests')
             ->setName('suggests')
             ->setDescription('Shows package suggestions.')
             ->setDescription('Shows package suggestions.')
             ->setDefinition(array(
             ->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('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 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.'),
                 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.
 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
 Read more at https://getcomposer.org/doc/03-cli.md#suggests
 EOT
 EOT
             )
             )
         ;
         ;
     }
     }
 
 
+    /**
+     * {@inheritDoc}
+     */
     protected function execute(InputInterface $input, OutputInterface $output)
     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');
         $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;
                 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();
         $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')) {
         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;
         return 0;
     }
     }

+ 25 - 8
src/Composer/Command/UpdateCommand.php

@@ -13,6 +13,7 @@
 namespace Composer\Command;
 namespace Composer\Command;
 
 
 use Composer\Composer;
 use Composer\Composer;
+use Composer\DependencyResolver\Request;
 use Composer\Installer;
 use Composer\Installer;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Plugin\CommandEvent;
 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-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-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-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('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('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`.'),
                 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);
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@@ -135,6 +146,13 @@ EOT
         $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative');
         $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative');
         $apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader');
         $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
         $install
             ->setDryRun($input->getOption('dry-run'))
             ->setDryRun($input->getOption('dry-run'))
             ->setVerbose($input->getOption('verbose'))
             ->setVerbose($input->getOption('verbose'))
@@ -143,14 +161,13 @@ EOT
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)
             ->setUpdate(true)
             ->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'))
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
             ->setPreferStable($input->getOption('prefer-stable'))
             ->setPreferStable($input->getOption('prefer-stable'))
             ->setPreferLowest($input->getOption('prefer-lowest'))
             ->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/ca-bundle/')
             ->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
             ->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
             ->in(__DIR__.'/../../vendor/psr/')
             ->in(__DIR__.'/../../vendor/psr/')
+            ->in(__DIR__.'/../../vendor/react/')
             ->sort($finderSort)
             ->sort($finderSort)
         ;
         ;
 
 

+ 1 - 1
src/Composer/Composer.php

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

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

@@ -265,7 +265,7 @@ class JsonConfigSource implements ConfigSourceInterface
      *
      *
      * @param  array $array
      * @param  array $array
      * @param  mixed $value
      * @param  mixed $value
-     * @return array
+     * @return int
      */
      */
     private function arrayUnshiftRef(&$array, &$value)
     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 (function_exists('posix_getuid') && posix_getuid() === 0) {
                     if ($commandName !== 'self-update' && $commandName !== 'selfupdate') {
                     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>');
                         $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')) {
                     if ($uid = (int) getenv('SUDO_UID')) {
                         // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on
                         // 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;
             return $result;
         } catch (ScriptExecutionException $e) {
         } catch (ScriptExecutionException $e) {
-            return $e->getCode();
+            return (int) $e->getCode();
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             $this->hintCommonErrors($e);
             $this->hintCommonErrors($e);
             restore_error_handler();
             restore_error_handler();

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

@@ -44,54 +44,33 @@ class DefaultPolicy implements PolicyInterface
         return $constraint->matchSpecific($version, true);
         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;
             $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);
         $selected = call_user_func_array('array_merge', $packages);
 
 
         // now sort the result across all packages to respect replaces across 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;
         return $selected;
     }
     }
 
 
-    protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals)
+    protected function groupLiteralsByName(Pool $pool, $literals)
     {
     {
         $packages = array();
         $packages = array();
         foreach ($literals as $literal) {
         foreach ($literals as $literal) {
@@ -100,12 +79,7 @@ class DefaultPolicy implements PolicyInterface
             if (!isset($packages[$packageName])) {
             if (!isset($packages[$packageName])) {
                 $packages[$packageName] = array();
                 $packages[$packageName] = array();
             }
             }
-
-            if (isset($installedMap[abs($literal)])) {
-                array_unshift($packages[$packageName], $literal);
-            } else {
-                $packages[$packageName][] = $literal;
-            }
+            $packages[$packageName][] = $literal;
         }
         }
 
 
         return $packages;
         return $packages;
@@ -114,61 +88,49 @@ class DefaultPolicy implements PolicyInterface
     /**
     /**
      * @protected
      * @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;
         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
      * 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;
     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 all packages ascending by id
         sort($literals);
         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
      * @return string
      */
      */
-    public function getJobType()
+    public function getOperationType()
     {
     {
         return 'install';
         return 'install';
     }
     }
 
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
+    }
+
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
     public function __toString()
     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
      * @return string
      */
      */
-    public function getJobType()
+    public function getOperationType()
     {
     {
         return 'markAliasInstalled';
         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}
      * {@inheritDoc}
      */
      */
     public function __toString()
     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
      * @return string
      */
      */
-    public function getJobType()
+    public function getOperationType()
     {
     {
         return 'markAliasUninstalled';
         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}
      * {@inheritDoc}
      */
      */
     public function __toString()
     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
 interface OperationInterface
 {
 {
     /**
     /**
-     * Returns job type.
+     * Returns operation type.
      *
      *
      * @return string
      * @return string
      */
      */
-    public function getJobType();
+    public function getOperationType();
 
 
     /**
     /**
      * Returns operation reason.
      * Returns operation reason.
@@ -33,6 +33,14 @@ interface OperationInterface
      */
      */
     public function getReason();
     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
      * 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;
         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
      * @return string
      */
      */
-    public function getJobType()
+    public function getOperationType()
     {
     {
         return 'uninstall';
         return 'uninstall';
     }
     }
 
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
+    }
+
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
     public function __toString()
     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
      * @return string
      */
      */
-    public function getJobType()
+    public function getOperationType()
     {
     {
         return 'update';
         return 'update';
     }
     }
@@ -73,11 +73,29 @@ class UpdateOperation extends SolverOperation
     /**
     /**
      * {@inheritDoc}
      * {@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
 interface PolicyInterface
 {
 {
     public function versionCompare(PackageInterface $a, PackageInterface $b, $operator);
     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;
 namespace Composer\DependencyResolver;
 
 
-use Composer\Package\BasePackage;
 use Composer\Package\AliasPackage;
 use Composer\Package\AliasPackage;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
 use Composer\Semver\Constraint\ConstraintInterface;
 use Composer\Semver\Constraint\ConstraintInterface;
 use Composer\Semver\Constraint\Constraint;
 use Composer\Semver\Constraint\Constraint;
 use Composer\Semver\Constraint\EmptyConstraint;
 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;
 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 Nils Adermann <naderman@naderman.de>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
  */
 class Pool implements \Countable
 class Pool implements \Countable
 {
 {
-    const MATCH_NAME = -1;
     const MATCH_NONE = 0;
     const MATCH_NONE = 0;
     const MATCH = 1;
     const MATCH = 1;
     const MATCH_PROVIDE = 2;
     const MATCH_PROVIDE = 2;
     const MATCH_REPLACE = 3;
     const MATCH_REPLACE = 3;
-    const MATCH_FILTERED = 4;
 
 
-    protected $repositories = array();
-    protected $providerRepos = array();
     protected $packages = array();
     protected $packages = array();
     protected $packageByName = array();
     protected $packageByName = array();
     protected $packageByExactName = array();
     protected $packageByExactName = array();
-    protected $acceptableStabilities;
-    protected $stabilityFlags;
     protected $versionParser;
     protected $versionParser;
     protected $providerCache = array();
     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->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
      *                                            packages must match or null to return all
      * @param  bool                $mustMatchName Whether the name of returned packages
      * @param  bool                $mustMatchName Whether the name of returned packages
      *                                            must match the given name
      *                                            must match the given name
-     * @param  bool                $bypassFilters If enabled, filterRequires and stability matching is ignored
      * @return PackageInterface[]  A set of packages
      * @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;
         $key = ((int) $mustMatchName).$constraint;
         if (isset($this->providerCache[$name][$key])) {
         if (isset($this->providerCache[$name][$key])) {
             return $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
      * @see whatProvides
      */
      */
-    private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false)
+    private function computeWhatProvides($name, $constraint, $mustMatchName = false)
     {
     {
         $candidates = array();
         $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) {
         if ($mustMatchName) {
-            $candidates = array_filter($candidates, function ($candidate) use ($name) {
-                return $candidate->getName() == $name;
-            });
             if (isset($this->packageByExactName[$name])) {
             if (isset($this->packageByExactName[$name])) {
-                $candidates = array_merge($candidates, $this->packageByExactName[$name]);
+                $candidates = $this->packageByExactName[$name];
             }
             }
         } elseif (isset($this->packageByName[$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) {
         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:
                 case self::MATCH_NONE:
                     break;
                     break;
 
 
-                case self::MATCH_NAME:
-                    $nameMatch = true;
-                    break;
-
                 case self::MATCH:
                 case self::MATCH:
-                    $nameMatch = true;
-                    $matches[] = $candidate;
-                    break;
-
                 case self::MATCH_PROVIDE:
                 case self::MATCH_PROVIDE:
-                    $provideMatches[] = $candidate;
-                    break;
-
                 case self::MATCH_REPLACE:
                 case self::MATCH_REPLACE:
                     $matches[] = $candidate;
                     $matches[] = $candidate;
                     break;
                     break;
 
 
-                case self::MATCH_FILTERED:
-                    break;
-
                 default:
                 default:
                     throw new \UnexpectedValueException('Unexpected match type');
                     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)
     public function literalToPackage($literal)
@@ -296,23 +157,6 @@ class Pool implements \Countable
         return $prefix.' '.$package->getPrettyString();
         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
      * Checks if the package matches the given constraint directly or through
      * provided or replaced packages
      * provided or replaced packages
@@ -322,27 +166,19 @@ class Pool implements \Countable
      * @param  ConstraintInterface    $constraint The constraint to verify
      * @param  ConstraintInterface    $constraint The constraint to verify
      * @return int                    One of the MATCH* constants of this class or 0 if there is no match
      * @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();
         $candidateName = $candidate->getName();
         $candidateVersion = $candidate->getVersion();
         $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) {
         if ($candidateName === $name) {
             $pkgConstraint = new Constraint('==', $candidateVersion);
             $pkgConstraint = new Constraint('==', $candidateVersion);
 
 
             if ($constraint === null || $constraint->matches($pkgConstraint)) {
             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();
         $provides = $candidate->getProvides();
@@ -352,13 +188,13 @@ class Pool implements \Countable
         if (isset($replaces[0]) || isset($provides[0])) {
         if (isset($replaces[0]) || isset($provides[0])) {
             foreach ($provides as $link) {
             foreach ($provides as $link) {
                 if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
                 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) {
             foreach ($replaces as $link) {
                 if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
                 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()))) {
         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()))) {
         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;
         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;
 namespace Composer\DependencyResolver;
 
 
 use Composer\Package\CompletePackageInterface;
 use Composer\Package\CompletePackageInterface;
+use Composer\Repository\RepositorySet;
+use Composer\Semver\Constraint\Constraint;
 
 
 /**
 /**
  * Represents a problem detected while solving dependencies
  * Represents a problem detected while solving dependencies
@@ -28,20 +30,13 @@ class Problem
     protected $reasonSeen;
     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
      * @var array
      */
      */
     protected $reasons = array();
     protected $reasons = array();
 
 
     protected $section = 0;
     protected $section = 0;
 
 
-    protected $pool;
-
-    public function __construct(Pool $pool)
-    {
-        $this->pool = $pool;
-    }
-
     /**
     /**
      * Add a rule as a reason
      * Add a rule as a reason
      *
      *
@@ -49,10 +44,7 @@ class Problem
      */
      */
     public function addRule(Rule $rule)
     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
      * 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
      * @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));
         $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
 
 
         if (count($reasons) === 1) {
         if (count($reasons) === 1) {
             reset($reasons);
             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)) {
             if (isset($constraint)) {
-                $packages = $this->pool->whatProvides($packageName, $constraint);
+                $packages = $pool->whatProvides($packageName, $constraint);
             } else {
             } else {
                 $packages = array();
                 $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();
         $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
      * Store a reason descriptor but ignore duplicates
      *
      *
      * @param string $id     A canonical identifier for the reason
      * @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])) {
         if (!isset($this->reasonSeen[$id])) {
             $this->reasonSeen[$id] = true;
             $this->reasonSeen[$id] = true;
             $this->reasons[$this->section][] = $reason;
             $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();
         $prepared = array();
         foreach ($packages as $package) {
         foreach ($packages as $package) {
@@ -238,19 +284,37 @@ class Problem
             $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
             $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
         }
         }
         foreach ($prepared as $name => $package) {
         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']).']';
             $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
         }
         }
 
 
         return implode(', ', $prepared);
         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
      * @param  \Composer\Semver\Constraint\ConstraintInterface $constraint
      * @return string
      * @return string
      */
      */
-    protected function constraintToText($constraint)
+    protected static function constraintToText($constraint)
     {
     {
         return $constraint ? ' '.$constraint->getPrettyString() : '';
         return $constraint ? ' '.$constraint->getPrettyString() : '';
     }
     }

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

@@ -12,6 +12,10 @@
 
 
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
+use Composer\Package\Package;
+use Composer\Package\PackageInterface;
+use Composer\Package\RootAliasPackage;
+use Composer\Repository\LockArrayRepository;
 use Composer\Semver\Constraint\ConstraintInterface;
 use Composer\Semver\Constraint\ConstraintInterface;
 
 
 /**
 /**
@@ -19,60 +23,129 @@ use Composer\Semver\Constraint\ConstraintInterface;
  */
  */
 class Request
 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
      * 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\CompletePackage;
 use Composer\Package\Link;
 use Composer\Package\Link;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
+use Composer\Repository\RepositorySet;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
@@ -24,8 +25,8 @@ abstract class Rule
 {
 {
     // reason constants
     // reason constants
     const RULE_INTERNAL_ALLOW_UPDATE = 1;
     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_CONFLICT = 6;
     const RULE_PACKAGE_REQUIRES = 7;
     const RULE_PACKAGE_REQUIRES = 7;
     const RULE_PACKAGE_OBSOLETES = 8;
     const RULE_PACKAGE_OBSOLETES = 8;
@@ -41,22 +42,17 @@ abstract class Rule
     const BITFIELD_DISABLED = 16;
     const BITFIELD_DISABLED = 16;
 
 
     protected $bitfield;
     protected $bitfield;
-    protected $job;
+    protected $request;
     protected $reasonData;
     protected $reasonData;
 
 
     /**
     /**
      * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
      * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
      * @param Link|PackageInterface $reasonData
      * @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;
         $this->reasonData = $reasonData;
 
 
-        if ($job) {
-            $this->job = $job;
-        }
-
         $this->bitfield = (0 << self::BITFIELD_DISABLED) |
         $this->bitfield = (0 << self::BITFIELD_DISABLED) |
             ($reason << self::BITFIELD_REASON) |
             ($reason << self::BITFIELD_REASON) |
             (255 << self::BITFIELD_TYPE);
             (255 << self::BITFIELD_TYPE);
@@ -66,11 +62,6 @@ abstract class Rule
 
 
     abstract public function getHash();
     abstract public function getHash();
 
 
-    public function getJob()
-    {
-        return $this->job;
-    }
-
     abstract public function equals(Rule $rule);
     abstract public function equals(Rule $rule);
 
 
     public function getReason()
     public function getReason()
@@ -85,11 +76,17 @@ abstract class Rule
 
 
     public function getRequiredPackage()
     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();
             return $this->reasonData->getTarget();
         }
         }
     }
     }
@@ -126,7 +123,12 @@ abstract class Rule
 
 
     abstract public function isAssertion();
     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();
         $literals = $this->getLiterals();
 
 
@@ -142,17 +144,30 @@ abstract class Rule
             case self::RULE_INTERNAL_ALLOW_UPDATE:
             case self::RULE_INTERNAL_ALLOW_UPDATE:
                 return $ruleText;
                 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:
             case self::RULE_PACKAGE_CONFLICT:
                 $package1 = $pool->literalToPackage($literals[0]);
                 $package1 = $pool->literalToPackage($literals[0]);
                 $package2 = $pool->literalToPackage($literals[1]);
                 $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:
             case self::RULE_PACKAGE_REQUIRES:
                 $sourceLiteral = array_shift($literals);
                 $sourceLiteral = array_shift($literals);
@@ -169,73 +184,103 @@ abstract class Rule
                 } else {
                 } else {
                     $targetName = $this->reasonData->getTarget();
                     $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:
             case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
                 return $ruleText;
                 return $ruleText;
             case self::RULE_LEARNED:
             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:
             case self::RULE_PACKAGE_ALIAS:
                 return $ruleText;
                 return $ruleText;
             default:
             default:
@@ -252,17 +297,22 @@ abstract class Rule
     protected function formatPackagesUnique($pool, array $packages)
     protected function formatPackagesUnique($pool, array $packages)
     {
     {
         $prepared = array();
         $prepared = array();
-        foreach ($packages as $package) {
+        foreach ($packages as $index => $package) {
             if (!is_object($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                   $literal2
      * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
      * @param int                   $reason     A RULE_* constant describing the reason for generating this rule
      * @param Link|PackageInterface $reasonData
      * @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) {
         if ($literal1 < $literal2) {
             $this->literal1 = $literal1;
             $this->literal1 = $literal1;

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

@@ -12,6 +12,8 @@
 
 
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
+use Composer\Repository\RepositorySet;
+
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
  */
  */
@@ -19,7 +21,7 @@ class RuleSet implements \IteratorAggregate, \Countable
 {
 {
     // highest priority => lowest number
     // highest priority => lowest number
     const TYPE_PACKAGE = 0;
     const TYPE_PACKAGE = 0;
-    const TYPE_JOB = 1;
+    const TYPE_REQUEST = 1;
     const TYPE_LEARNED = 4;
     const TYPE_LEARNED = 4;
 
 
     /**
     /**
@@ -32,7 +34,7 @@ class RuleSet implements \IteratorAggregate, \Countable
     protected static $types = array(
     protected static $types = array(
         255 => 'UNKNOWN',
         255 => 'UNKNOWN',
         self::TYPE_PACKAGE => 'PACKAGE',
         self::TYPE_PACKAGE => 'PACKAGE',
-        self::TYPE_JOB => 'JOB',
+        self::TYPE_REQUEST => 'REQUEST',
         self::TYPE_LEARNED => 'LEARNED',
         self::TYPE_LEARNED => 'LEARNED',
     );
     );
 
 
@@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable
         return array_keys($types);
         return array_keys($types);
     }
     }
 
 
-    public function getPrettyString(Pool $pool = null)
+    public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null)
     {
     {
         $string = "\n";
         $string = "\n";
         foreach ($this->rules as $type => $rules) {
         foreach ($this->rules as $type => $rules) {
             $string .= str_pad(self::$types[$type], 8, ' ') . ": ";
             $string .= str_pad(self::$types[$type], 8, ' ') . ": ";
             foreach ($rules as $rule) {
             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";
             $string .= "\n\n";
         }
         }
@@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable
 
 
     public function __toString()
     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;
 namespace Composer\DependencyResolver;
 
 
+use Composer\Package\LinkConstraint\VersionConstraint;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\AliasPackage;
 use Composer\Package\AliasPackage;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
+use Composer\Semver\Constraint\Constraint;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
@@ -24,13 +26,11 @@ class RuleSetGenerator
     protected $policy;
     protected $policy;
     protected $pool;
     protected $pool;
     protected $rules;
     protected $rules;
-    protected $jobs;
-    protected $installedMap;
-    protected $whitelistedMap;
     protected $addedMap;
     protected $addedMap;
     protected $conflictAddedMap;
     protected $conflictAddedMap;
     protected $addedPackages;
     protected $addedPackages;
     protected $addedPackagesByNames;
     protected $addedPackagesByNames;
+    protected $conflictsForName;
 
 
     public function __construct(PolicyInterface $policy, Pool $pool)
     public function __construct(PolicyInterface $policy, Pool $pool)
     {
     {
@@ -76,33 +76,17 @@ class RuleSetGenerator
      * @param  array $packages The set of packages to choose from
      * @param  array $packages The set of packages to choose from
      * @param  int   $reason   A RULE_* constant describing the reason for
      * @param  int   $reason   A RULE_* constant describing the reason for
      *                         generating this rule
      *                         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
      * @return Rule  The generated rule
      */
      */
-    protected function createInstallOneOfRule(array $packages, $reason, $job)
+    protected function createInstallOneOfRule(array $packages, $reason, $reasonData)
     {
     {
         $literals = array();
         $literals = array();
         foreach ($packages as $package) {
         foreach ($packages as $package) {
             $literals[] = $package->id;
             $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);
         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
      * Adds a rule unless it duplicates an existing one of any type
      *
      *
@@ -147,41 +145,6 @@ class RuleSetGenerator
         $this->rules->add($newRule, $type);
         $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)
     protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs)
     {
     {
         $workQueue = new \SplQueue;
         $workQueue = new \SplQueue;
@@ -225,9 +188,16 @@ class RuleSetGenerator
 
 
                 if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
                 if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
                     $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
                     $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 */
                 /** @var PackageInterface $possibleConflict */
                 foreach ($this->addedPackagesByNames[$link->getTarget()] as $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) {
                     if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) {
                         $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link));
                         $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
             // check obsoletes and implicit obsoletes of a package
-            $isInstalled = isset($this->installedMap[$package->id]);
-
             foreach ($package->getReplaces() as $link) {
             foreach ($package->getReplaces() as $link) {
                 if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
                 if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
                     continue;
                     continue;
@@ -272,12 +240,19 @@ class RuleSetGenerator
                     }
                     }
 
 
                     if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
                     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));
                         $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)
     protected function obsoleteImpossibleForAlias($package, $provider)
@@ -294,77 +269,61 @@ class RuleSetGenerator
         return $impossible;
         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->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->addedMap = array();
         $this->conflictAddedMap = array();
         $this->conflictAddedMap = array();
         $this->addedPackages = array();
         $this->addedPackages = array();
         $this->addedPackagesByNames = 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);
         $this->addConflictRules($ignorePlatformReqs);
 
 

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

@@ -44,13 +44,24 @@ class RuleWatchGraph
             return;
             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();
         $chain->rewind();
         while ($chain->valid()) {
         while ($chain->valid()) {
             $node = $chain->current();
             $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();
             $chain->next();

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

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

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

@@ -13,7 +13,7 @@
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
-use Composer\Repository\RepositoryInterface;
+use Composer\Package\PackageInterface;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
 
 
 /**
 /**
@@ -28,23 +28,18 @@ class Solver
     protected $policy;
     protected $policy;
     /** @var Pool */
     /** @var Pool */
     protected $pool;
     protected $pool;
-    /** @var RepositoryInterface */
-    protected $installed;
+
     /** @var RuleSet */
     /** @var RuleSet */
     protected $rules;
     protected $rules;
     /** @var RuleSetGenerator */
     /** @var RuleSetGenerator */
     protected $ruleSetGenerator;
     protected $ruleSetGenerator;
-    /** @var array */
-    protected $jobs;
 
 
-    /** @var int[] */
-    protected $updateMap = array();
     /** @var RuleWatchGraph */
     /** @var RuleWatchGraph */
     protected $watchGraph;
     protected $watchGraph;
     /** @var Decisions */
     /** @var Decisions */
     protected $decisions;
     protected $decisions;
-    /** @var int[] */
-    protected $installedMap;
+    /** @var PackageInterface[] */
+    protected $fixedMap;
 
 
     /** @var int */
     /** @var int */
     protected $propagateIndex;
     protected $propagateIndex;
@@ -66,16 +61,13 @@ class Solver
     /**
     /**
      * @param PolicyInterface     $policy
      * @param PolicyInterface     $policy
      * @param Pool                $pool
      * @param Pool                $pool
-     * @param RepositoryInterface $installed
      * @param IOInterface         $io
      * @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->io = $io;
         $this->policy = $policy;
         $this->policy = $policy;
         $this->pool = $pool;
         $this->pool = $pool;
-        $this->installed = $installed;
-        $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
     }
     }
 
 
     /**
     /**
@@ -86,6 +78,11 @@ class Solver
         return count($this->rules);
         return count($this->rules);
     }
     }
 
 
+    public function getPool()
+    {
+        return $this->pool;
+    }
+
     // aka solver_makeruledecisions
     // aka solver_makeruledecisions
 
 
     private function makeAssertionRuleDecisions()
     private function makeAssertionRuleDecisions()
@@ -121,23 +118,23 @@ class Solver
             $conflict = $this->decisions->decisionRule($literal);
             $conflict = $this->decisions->decisionRule($literal);
 
 
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
-                $problem = new Problem($this->pool);
+                $problem = new Problem();
 
 
                 $problem->addRule($rule);
                 $problem->addRule($rule);
                 $problem->addRule($conflict);
                 $problem->addRule($conflict);
-                $this->disableProblem($rule);
+                $rule->disable();
                 $this->problems[] = $problem;
                 $this->problems[] = $problem;
                 continue;
                 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($rule);
             $problem->addRule($conflict);
             $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
             // 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()) {
                 if ($assertRule->isDisabled() || !$assertRule->isAssertion()) {
                     continue;
                     continue;
                 }
                 }
@@ -148,9 +145,8 @@ class Solver
                 if (abs($literal) !== abs($assertRuleLiteral)) {
                 if (abs($literal) !== abs($assertRuleLiteral)) {
                     continue;
                     continue;
                 }
                 }
-
                 $problem->addRule($assertRule);
                 $problem->addRule($assertRule);
-                $this->disableProblem($assertRule);
+                $assertRule->disable();
             }
             }
             $this->problems[] = $problem;
             $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
      * @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  Request $request
      * @param  bool    $ignorePlatformReqs
      * @param  bool    $ignorePlatformReqs
-     * @return array
+     * @return LockTransaction
      */
      */
     public function solve(Request $request, $ignorePlatformReqs = false)
     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->decisions = new Decisions($this->pool);
         $this->watchGraph = new RuleWatchGraph;
         $this->watchGraph = new RuleWatchGraph;
 
 
@@ -223,29 +202,20 @@ class Solver
             $this->watchGraph->insert(new RuleWatchNode($rule));
             $this->watchGraph->insert(new RuleWatchNode($rule));
         }
         }
 
 
-        /* make decisions based on job/update assertions */
+        /* make decisions based on root require/fix assertions */
         $this->makeAssertionRuleDecisions();
         $this->makeAssertionRuleDecisions();
 
 
         $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG);
         $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG);
         $before = microtime(true);
         $before = microtime(true);
-        $this->runSat(true);
+        $this->runSat();
         $this->io->writeError('', true, IOInterface::DEBUG);
         $this->io->writeError('', true, IOInterface::DEBUG);
         $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE);
         $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) {
         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  int        $level
      * @param  string|int $literal
      * @param  string|int $literal
-     * @param  bool       $disableRules
      * @param  Rule       $rule
      * @param  Rule       $rule
      * @return int
      * @return int
      */
      */
-    private function setPropagateLearn($level, $literal, $disableRules, Rule $rule)
+    private function setPropagateLearn($level, $literal, Rule $rule)
     {
     {
         $level++;
         $level++;
 
 
@@ -340,7 +309,7 @@ class Solver
             }
             }
 
 
             if ($level == 1) {
             if ($level == 1) {
-                return $this->analyzeUnsolvable($rule, $disableRules);
+                return $this->analyzeUnsolvable($rule);
             }
             }
 
 
             // conflict
             // conflict
@@ -377,14 +346,13 @@ class Solver
     /**
     /**
      * @param  int   $level
      * @param  int   $level
      * @param  array $decisionQueue
      * @param  array $decisionQueue
-     * @param  bool  $disableRules
      * @param  Rule  $rule
      * @param  Rule  $rule
      * @return int
      * @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
         // 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);
         $selectedLiteral = array_shift($literals);
 
 
@@ -393,7 +361,7 @@ class Solver
             $this->branches[] = array($literals, $level);
             $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  Rule $conflictRule
-     * @param  bool $disableRules
      * @return int
      * @return int
      */
      */
-    private function analyzeUnsolvable(Rule $conflictRule, $disableRules)
+    private function analyzeUnsolvable(Rule $conflictRule)
     {
     {
-        $problem = new Problem($this->pool);
+        $problem = new Problem();
         $problem->addRule($conflictRule);
         $problem->addRule($conflictRule);
 
 
         $this->analyzeUnsolvableRule($problem, $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;
         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()
     private function resetSolver()
     {
     {
         $this->decisions->reset();
         $this->decisions->reset();
@@ -661,17 +596,14 @@ class Solver
         }
         }
     }
     }
 
 
-    /**
-     * @param bool $disableRules
-     */
-    private function runSat($disableRules = true)
+    private function runSat()
     {
     {
         $this->propagateIndex = 0;
         $this->propagateIndex = 0;
 
 
         /*
         /*
          * here's the main loop:
          * here's the main loop:
          * 1) propagate new decisions (only needed once)
          * 1) propagate new decisions (only needed once)
-         * 2) fulfill jobs
+         * 2) fulfill root requires/fixed packages
          * 3) fulfill all unresolved rules
          * 3) fulfill all unresolved rules
          * 4) minimalize solution if we had choices
          * 4) minimalize solution if we had choices
          * if we encounter a problem, we rewind to a safe level and restart
          * if we encounter a problem, we rewind to a safe level and restart
@@ -679,10 +611,7 @@ class Solver
          */
          */
 
 
         $decisionQueue = array();
         $decisionQueue = array();
-        /**
-         * @todo this makes $disableRules always false; determine the rationale and possibly remove dead code?
-         */
-        $disableRules = array();
+        $decisionSupplementQueue = array();
 
 
         $level = 1;
         $level = 1;
         $systemLevel = $level + 1;
         $systemLevel = $level + 1;
@@ -691,7 +620,7 @@ class Solver
             if (1 === $level) {
             if (1 === $level) {
                 $conflictRule = $this->propagate($level);
                 $conflictRule = $this->propagate($level);
                 if (null !== $conflictRule) {
                 if (null !== $conflictRule) {
-                    if ($this->analyzeUnsolvable($conflictRule, $disableRules)) {
+                    if ($this->analyzeUnsolvable($conflictRule)) {
                         continue;
                         continue;
                     }
                     }
 
 
@@ -699,9 +628,9 @@ class Solver
                 }
                 }
             }
             }
 
 
-            // handle job rules
+            // handle root require/fixed package rules
             if ($level < $systemLevel) {
             if ($level < $systemLevel) {
-                $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB);
+                $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_REQUEST);
                 foreach ($iterator as $rule) {
                 foreach ($iterator as $rule) {
                     if ($rule->isEnabled()) {
                     if ($rule->isEnabled()) {
                         $decisionQueue = array();
                         $decisionQueue = array();
@@ -718,26 +647,21 @@ class Solver
                         }
                         }
 
 
                         if ($noneSatisfied && count($decisionQueue)) {
                         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;
                                 $decisionQueue = $prunedQueue;
                             }
                             }
                         }
                         }
 
 
                         if ($noneSatisfied && count($decisionQueue)) {
                         if ($noneSatisfied && count($decisionQueue)) {
                             $oLevel = $level;
                             $oLevel = $level;
-                            $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
+                            $level = $this->selectAndInstall($level, $decisionQueue, $rule);
 
 
                             if (0 === $level) {
                             if (0 === $level) {
                                 return;
                                 return;
@@ -751,7 +675,7 @@ class Solver
 
 
                 $systemLevel = $level + 1;
                 $systemLevel = $level + 1;
 
 
-                // jobs left
+                // root requires/fixed packages left
                 $iterator->next();
                 $iterator->next();
                 if ($iterator->valid()) {
                 if ($iterator->valid()) {
                     continue;
                     continue;
@@ -813,7 +737,7 @@ class Solver
                     continue;
                     continue;
                 }
                 }
 
 
-                $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
+                $level = $this->selectAndInstall($level, $decisionQueue, $rule);
 
 
                 if (0 === $level) {
                 if (0 === $level) {
                     return;
                     return;
@@ -856,7 +780,7 @@ class Solver
 
 
                     $why = $this->decisions->lastReason();
                     $why = $this->decisions->lastReason();
 
 
-                    $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
+                    $level = $this->setPropagateLearn($level, $lastLiteral, $why);
 
 
                     if ($level == 0) {
                     if ($level == 0) {
                         return;
                         return;

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

@@ -13,6 +13,7 @@
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
 use Composer\Util\IniHelper;
 use Composer\Util\IniHelper;
+use Composer\Repository\RepositorySet;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
@@ -20,29 +21,33 @@ use Composer\Util\IniHelper;
 class SolverProblemsException extends \RuntimeException
 class SolverProblemsException extends \RuntimeException
 {
 {
     protected $problems;
     protected $problems;
-    protected $installedMap;
+    protected $learnedPool;
 
 
-    public function __construct(array $problems, array $installedMap)
+    public function __construct(array $problems, array $learnedPool)
     {
     {
         $this->problems = $problems;
         $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";
         $text = "\n";
         $hasExtensionProblems = false;
         $hasExtensionProblems = false;
+        $isCausedByLock = false;
         foreach ($this->problems as $i => $problem) {
         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())) {
             if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
                 $hasExtensionProblems = true;
                 $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.";
             $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();
             $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;
         return $text;
     }
     }
 
 
@@ -76,8 +85,8 @@ class SolverProblemsException extends \RuntimeException
     private function hasExtensionProblems(array $reasonSets)
     private function hasExtensionProblems(array $reasonSets)
     {
     {
         foreach ($reasonSets as $reasonSet) {
         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;
                     return true;
                 }
                 }
             }
             }

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

@@ -13,161 +13,205 @@
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
 use Composer\Package\AliasPackage;
 use Composer\Package\AliasPackage;
+use Composer\Package\Link;
+use Composer\Package\PackageInterface;
+use Composer\Repository\PlatformRepository;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class Transaction
 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()
     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();
         $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) {
                 if ($package instanceof AliasPackage) {
-                    $queue[] = $package->getAliasOf();
+                    $stack[] = $package->getAliasOf();
                 } else {
                 } else {
                     foreach ($package->getRequires() as $link) {
                     foreach ($package->getRequires() as $link) {
-                        $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+                        $possibleRequires = $this->getProvidersInResult($link);
 
 
                         foreach ($possibleRequires as $require) {
                         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;
                 continue;
             }
             }
 
 
             foreach ($package->getRequires() as $link) {
             foreach ($package->getRequires() as $link) {
-                $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+                $possibleRequires = $this->getProvidersInResult($link);
 
 
                 foreach ($possibleRequires as $require) {
                 foreach ($possibleRequires as $require) {
                     if ($require !== $package) {
                     if ($require !== $package) {
-                        unset($roots[$require->id]);
+                        unset($roots[spl_object_hash($require)]);
                     }
                     }
                 }
                 }
             }
             }
@@ -176,69 +220,87 @@ class Transaction
         return $roots;
         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;
                 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 \RuntimeException
      * @throws \UnexpectedValueException
      * @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 {
             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
                 // 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));
                     $contentDir = $this->getFolderContent((string) reset($contentDir));
                 }
                 }
 
 
@@ -65,44 +88,16 @@ abstract class ArchiveDownloader extends FileDownloader
                     $file = (string) $file;
                     $file = (string) $file;
                     $this->filesystem->rename($file, $path . '/' . basename($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
      * @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
      * 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\Package\PackageInterface;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 
 /**
 /**
  * Downloaders manager.
  * Downloaders manager.
@@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
 class DownloadManager
 class DownloadManager
 {
 {
     private $io;
     private $io;
+    private $httpDownloader;
     private $preferDist = false;
     private $preferDist = false;
     private $preferSource = false;
     private $preferSource = false;
     private $packagePreferences = array();
     private $packagePreferences = array();
@@ -33,9 +35,9 @@ class DownloadManager
     /**
     /**
      * Initializes download manager.
      * 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)
     public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
     {
     {
@@ -83,22 +85,6 @@ class DownloadManager
         return $this;
         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.
      * Sets installer downloader for a specific installation type.
      *
      *
@@ -140,7 +126,7 @@ class DownloadManager
      *                                           wrong type
      *                                           wrong type
      * @return DownloaderInterface|null
      * @return DownloaderInterface|null
      */
      */
-    public function getDownloaderForInstalledPackage(PackageInterface $package)
+    public function getDownloaderForPackage(PackageInterface $package)
     {
     {
         $installationSource = $package->getInstallationSource();
         $installationSource = $package->getInstallationSource();
 
 
@@ -154,7 +140,7 @@ class DownloadManager
             $downloader = $this->getDownloader($package->getSourceType());
             $downloader = $this->getDownloader($package->getSourceType());
         } else {
         } else {
             throw new \InvalidArgumentException(
             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;
         return $downloader;
     }
     }
 
 
+    public function getDownloaderType(DownloaderInterface $downloader)
+    {
+        return array_search($downloader, $this->downloaders);
+    }
+
     /**
     /**
      * Downloads package into target dir.
      * 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 \InvalidArgumentException if package have no urls to download from
      * @throws \RuntimeException
      * @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);
             $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 PackageInterface $target    target package version
      * @param string           $targetDir target dir
      * @param string           $targetDir target dir
      *
      *
+     * @return PromiseInterface|null
      * @throws \InvalidArgumentException if initial package is not installed
      * @throws \InvalidArgumentException if initial package is not installed
      */
      */
     public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
     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) {
         if ($initialType === $targetType) {
-            $target->setInstallationSource($installationSource);
             try {
             try {
-                $downloader->update($initial, $target, $targetDir);
-
-                return;
+                return $downloader->update($initial, $target, $targetDir);
             } catch (\RuntimeException $e) {
             } catch (\RuntimeException $e) {
                 if (!$this->io->isInteractive()) {
                 if (!$this->io->isInteractive()) {
                     throw $e;
                     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 PackageInterface $package   package instance
      * @param string           $targetDir target dir
      * @param string           $targetDir target dir
+     *
+     * @return PromiseInterface|null
      */
      */
     public function remove(PackageInterface $package, $targetDir)
     public function remove(PackageInterface $package, $targetDir)
     {
     {
-        $downloader = $this->getDownloaderForInstalledPackage($package);
+        $targetDir = $this->normalizeTargetDir($targetDir);
+        $downloader = $this->getDownloaderForPackage($package);
         if ($downloader) {
         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 $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;
 namespace Composer\Downloader;
 
 
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
+use React\Promise\PromiseInterface;
 
 
 /**
 /**
  * Downloader interface.
  * Downloader interface.
@@ -30,12 +31,35 @@ interface DownloaderInterface
     public function getInstallationSource();
     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 PackageInterface $package package instance
      * @param string           $path    download path
      * @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.
      * Updates specific package in specific folder from initial to target version.
@@ -55,10 +79,17 @@ interface DownloaderInterface
     public function remove(PackageInterface $package, $path);
     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\Plugin\PreFileDownloadEvent;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\Util\Url as UrlUtil;
 use Composer\Util\Url as UrlUtil;
+use Composer\Downloader\TransportException;
 
 
 /**
 /**
  * Base downloader for files
  * Base downloader for files
@@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
 {
 {
     protected $io;
     protected $io;
     protected $config;
     protected $config;
-    protected $rfs;
+    protected $httpDownloader;
     protected $filesystem;
     protected $filesystem;
     protected $cache;
     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;
     private $eventDispatcher;
 
 
     /**
     /**
@@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      *
      *
      * @param IOInterface      $io              The IO instance
      * @param IOInterface      $io              The IO instance
      * @param Config           $config          The config
      * @param Config           $config          The config
+     * @param HttpDownloader   $httpDownloader  The remote filesystem
      * @param EventDispatcher  $eventDispatcher The event dispatcher
      * @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
      * @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->io = $io;
         $this->config = $config;
         $this->config = $config;
         $this->eventDispatcher = $eventDispatcher;
         $this->eventDispatcher = $eventDispatcher;
-        $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
+        $this->httpDownloader = $httpDownloader;
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->cache = $cache;
         $this->cache = $cache;
 
 
@@ -81,127 +84,191 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
     {
         if (!$package->getDistUrl()) {
         if (!$package->getDistUrl()) {
             throw new \InvalidArgumentException('The given package is missing url information');
             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;
             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}
      * {@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()])) {
         if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
             $this->cache->remove($this->lastCacheWrites[$package->getName()]);
             $this->cache->remove($this->lastCacheWrites[$package->getName()]);
@@ -218,11 +285,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         $from = $initial->getFullPrettyVersion();
         $from = $initial->getFullPrettyVersion();
         $to = $target->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->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
 
 
         $this->remove($initial, $path, false);
         $this->remove($initial, $path, false);
-        $this->download($target, $path, false);
+        $this->install($target, $path, false);
 
 
         $this->io->writeError('');
         $this->io->writeError('');
     }
     }
@@ -249,7 +316,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      */
      */
     protected function getFileName(PackageInterface $package, $path)
     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)
     public function getLocalChanges(PackageInterface $package, $targetDir)
     {
     {
         $prevIO = $this->io;
         $prevIO = $this->io;
-        $prevProgress = $this->outputProgress;
 
 
         $this->io = new NullIO;
         $this->io = new NullIO;
         $this->io->loadConfiguration($this->config);
         $this->io->loadConfiguration($this->config);
-        $this->outputProgress = false;
         $e = null;
         $e = null;
 
 
         try {
         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 = new Comparer();
             $comparer->setSource($targetDir.'_compare');
             $comparer->setSource($targetDir.'_compare');
@@ -311,7 +378,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         }
         }
 
 
         $this->io = $prevIO;
         $this->io = $prevIO;
-        $this->outputProgress = $prevProgress;
 
 
         if ($e) {
         if ($e) {
             throw $e;
             throw $e;

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

@@ -23,7 +23,15 @@ class FossilDownloader extends VcsDownloader
     /**
     /**
      * {@inheritDoc}
      * {@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
         // Ensure we are allowed to use this URL by config
         $this->config->prohibitUrlByConfig($url, $this->io);
         $this->config->prohibitUrlByConfig($url, $this->io);
@@ -49,7 +57,7 @@ class FossilDownloader extends VcsDownloader
     /**
     /**
      * {@inheritDoc}
      * {@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
         // Ensure we are allowed to use this URL by config
         $this->config->prohibitUrlByConfig($url, $this->io);
         $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\Package\PackageInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
 use Composer\Util\Git as GitUtil;
 use Composer\Util\Git as GitUtil;
+use Composer\Util\Url;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 use Composer\Cache;
 use Composer\Cache;
@@ -29,6 +30,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     private $hasStashedChanges = false;
     private $hasStashedChanges = false;
     private $hasDiscardedChanges = false;
     private $hasDiscardedChanges = false;
     private $gitUtil;
     private $gitUtil;
+    private $cachedPackages = array();
 
 
     public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null)
     public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null)
     {
     {
@@ -39,39 +41,49 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
     {
     {
         GitUtil::cleanEnv();
         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();
         $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)) {
         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);
             $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);
         $this->io->writeError($msg);
 
 
         $commandCallable = function ($url) use ($path, $command, $cachePath) {
         $commandCallable = function ($url) use ($path, $command, $cachePath) {
@@ -105,34 +117,41 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
     {
         GitUtil::cleanEnv();
         GitUtil::cleanEnv();
+        $path = $this->normalizePath($path);
         if (!$this->hasMetadataRepository($path)) {
         if (!$this->hasMetadataRepository($path)) {
             throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
             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);
             $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) {
         if ($updateOriginUrl) {
             $this->updateOriginUrl($path, $target->getSourceUrl());
             $this->updateOriginUrl($path, $target->getSourceUrl());
         }
         }
@@ -272,7 +301,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
         $changes = array_map(function ($elem) {
         $changes = array_map(function ($elem) {
             return '    '.$elem;
             return '    '.$elem;
         }, preg_split('{\s*\r?\n\s*}', $changes));
         }, 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));
         $this->io->writeError(array_slice($changes, 0, 10));
         if (count($changes) > 10) {
         if (count($changes) > 10) {
             $this->io->writeError('    <info>' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list</info>');
             $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));
             $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)) {
             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));
                 $command = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference));
                 if (0 === $this->process->execute($command, $output, $path)) {
                 if (0 === $this->process->execute($command, $output, $path)) {
-                    return;
+                    return null;
                 }
                 }
             }
             }
         }
         }
 
 
         $command = sprintf($template, ProcessExecutor::escape($gitRef));
         $command = sprintf($template, ProcessExecutor::escape($gitRef));
         if (0 === $this->process->execute($command, $output, $path)) {
         if (0 === $this->process->execute($command, $output, $path)) {
-            return;
+            return null;
         }
         }
 
 
         // reference was not found (prints "fatal: reference is not a tree: $ref")
         // 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>');
             $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)
     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\Package\PackageInterface;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 
 
 /**
 /**
@@ -28,17 +28,19 @@ use Composer\IO\IOInterface;
  */
  */
 class GzipDownloader extends ArchiveDownloader
 class GzipDownloader extends ArchiveDownloader
 {
 {
+    /** @var ProcessExecutor */
     protected $process;
     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);
         $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
         // Try to use gunzip on *nix
         if (!Platform::isWindows()) {
         if (!Platform::isWindows()) {
@@ -63,14 +65,6 @@ class GzipDownloader extends ArchiveDownloader
         $this->extractUsingExt($file, $targetFilepath);
         $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)
     private function extractUsingExt($file, $targetFilepath)
     {
     {
         $archiveFile = gzopen($file, 'rb');
         $archiveFile = gzopen($file, 'rb');

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

@@ -24,7 +24,15 @@ class HgDownloader extends VcsDownloader
     /**
     /**
      * {@inheritDoc}
      * {@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);
         $hgUtils = new HgUtils($this->io, $this->config, $this->process);
 
 
@@ -44,7 +52,7 @@ class HgDownloader extends VcsDownloader
     /**
     /**
      * {@inheritDoc}
      * {@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);
         $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}
      * {@inheritdoc}
      */
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
     {
         $url = $package->getDistUrl();
         $url = $package->getDistUrl();
         $realUrl = realpath($url);
         $realUrl = realpath($url);
@@ -50,14 +50,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
         }
         }
 
 
         if (realpath($path) === $realUrl) {
         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;
             return;
         }
         }
 
 
@@ -73,6 +65,29 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
                 $realUrl
                 $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
         // Get the transport options with default values
         $transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true);
         $transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true);
@@ -154,7 +169,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
             $fileSystem->mirror($realUrl, $path, $iterator);
             $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());
         $realUrl = realpath($package->getDistUrl());
 
 
-        if (realpath($path) === $realUrl) {
+        if ($path === $realUrl) {
             if ($output) {
             if ($output) {
                 $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>), source is still present in $path");
                 $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}
      * {@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();
         $ref = $package->getSourceReference();
         $label = $this->getLabelFromSourceReference($ref);
         $label = $this->getLabelFromSourceReference($ref);
@@ -76,9 +84,9 @@ class PerforceDownloader extends VcsDownloader
     /**
     /**
      * {@inheritDoc}
      * {@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;
 namespace Composer\Downloader;
 
 
+use Composer\Package\PackageInterface;
+
 /**
 /**
  * Downloader for phar files
  * Downloader for phar files
  *
  *
@@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
     {
         // Can throw an UnexpectedValueException
         // Can throw an UnexpectedValueException
         $archive = new \Phar($file);
         $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\IniHelper;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
+use Composer\Package\PackageInterface;
 use RarArchive;
 use RarArchive;
 
 
 /**
 /**
@@ -31,15 +32,16 @@ use RarArchive;
  */
  */
 class RarDownloader extends ArchiveDownloader
 class RarDownloader extends ArchiveDownloader
 {
 {
+    /** @var ProcessExecutor */
     protected $process;
     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);
         $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;
         $processError = null;
 
 

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

@@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader
     /**
     /**
      * {@inheritDoc}
      * {@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();
         SvnUtil::cleanEnv();
         $ref = $package->getSourceReference();
         $ref = $package->getSourceReference();
@@ -42,13 +50,13 @@ class SvnDownloader extends VcsDownloader
         }
         }
 
 
         $this->io->writeError(" Checking out ".$package->getSourceReference());
         $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}
      * {@inheritDoc}
      */
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
     {
         SvnUtil::cleanEnv();
         SvnUtil::cleanEnv();
         $ref = $target->getSourceReference();
         $ref = $target->getSourceReference();
@@ -64,7 +72,7 @@ class SvnDownloader extends VcsDownloader
         }
         }
 
 
         $this->io->writeError(" Checking out " . $ref);
         $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
      * @throws \RuntimeException
      * @return string
      * @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 = new SvnUtil($baseUrl, $this->io, $this->config);
         $util->setCacheCredentials($this->cacheCredentials);
         $util->setCacheCredentials($this->cacheCredentials);
@@ -101,7 +109,7 @@ class SvnDownloader extends VcsDownloader
             return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose());
             return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose());
         } catch (\RuntimeException $e) {
         } catch (\RuntimeException $e) {
             throw new \RuntimeException(
             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;
             return '    '.$elem;
         }, preg_split('{\s*\r?\n\s*}', $changes));
         }, preg_split('{\s*\r?\n\s*}', $changes));
         $countChanges = count($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));
         $this->io->writeError(array_slice($changes, 0, 10));
         if ($countChanges > 10) {
         if ($countChanges > 10) {
             $remainingChanges = $countChanges - 10;
             $remainingChanges = $countChanges - 10;

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

@@ -12,6 +12,8 @@
 
 
 namespace Composer\Downloader;
 namespace Composer\Downloader;
 
 
+use Composer\Package\PackageInterface;
+
 /**
 /**
  * Downloader for tar files: tar, tar.gz or tar.bz2
  * Downloader for tar files: tar, tar.gz or tar.bz2
  *
  *
@@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
     {
         // Can throw an UnexpectedValueException
         // Can throw an UnexpectedValueException
         $archive = new \PharData($file);
         $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\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -54,44 +55,78 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function download(PackageInterface $package, $path)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null)
     {
     {
         if (!$package->getSourceReference()) {
         if (!$package->getSourceReference()) {
             throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
             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)) {
         while ($url = array_shift($urls)) {
             try {
             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;
                 break;
             } catch (\Exception $e) {
             } catch (\Exception $e) {
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
-                if ($e instanceof \PHPUnit_Framework_Exception) {
+                if ($e instanceof \PHPUnit\Framework\Exception) {
                     throw $e;
                     throw $e;
                 }
                 }
                 if ($this->io->isDebug()) {
                 if ($this->io->isDebug()) {
@@ -130,25 +165,21 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
             $to = $target->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->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;
         $exception = null;
         while ($url = array_shift($urls)) {
         while ($url = array_shift($urls)) {
             try {
             try {
-                if (Filesystem::isLocalPath($url)) {
-                    $url = realpath($url);
-                }
                 $this->doUpdate($initial, $target, $path, $url);
                 $this->doUpdate($initial, $target, $path, $url);
 
 
                 $exception = null;
                 $exception = null;
                 break;
                 break;
             } catch (\Exception $exception) {
             } catch (\Exception $exception) {
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
-                if ($exception instanceof \PHPUnit_Framework_Exception) {
+                if ($exception instanceof \PHPUnit\Framework\Exception) {
                     throw $exception;
                     throw $exception;
                 }
                 }
                 if ($this->io->isDebug()) {
                 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
         // print the commit logs if in verbose mode and VCS metadata is present
         // because in case of missing metadata code would trigger another exception
         // because in case of missing metadata code would trigger another exception
         if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) {
         if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) {
@@ -196,21 +225,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     public function remove(PackageInterface $package, $path)
     public function remove(PackageInterface $package, $path)
     {
     {
         $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
         $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
-        $this->cleanChanges($package, $path, false);
         if (!$this->filesystem->removeDirectory($path)) {
         if (!$this->filesystem->removeDirectory($path)) {
             throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
             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}
      * {@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
      * @param  string            $path
      * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly
      * @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.
      * Downloads specific package into specific folder.
      *
      *
      * @param PackageInterface $package package instance
      * @param PackageInterface $package package instance
      * @param string           $path    download path
      * @param string           $path    download path
      * @param string           $url     package url
      * @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.
      * 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 PackageInterface $target  updated package
      * @param string           $path    download path
      * @param string           $path    download path
      * @param string           $url     package url
      * @param string           $url     package url
+     *
+     * @return PromiseInterface|null
      */
      */
     abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url);
     abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url);
 
 
@@ -290,4 +325,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * @return bool
      * @return bool
      */
      */
     abstract protected function hasMetadataRepository($path);
     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\EventDispatcher\EventDispatcher;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 
 
 /**
 /**
@@ -28,16 +28,17 @@ use Composer\IO\IOInterface;
  */
  */
 class XzDownloader extends ArchiveDownloader
 class XzDownloader extends ArchiveDownloader
 {
 {
+    /** @var ProcessExecutor */
     protected $process;
     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);
         $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);
         $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
 
 
@@ -49,12 +50,4 @@ class XzDownloader extends ArchiveDownloader
 
 
         throw new \RuntimeException($processError);
         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\IniHelper;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Symfony\Component\Process\ExecutableFinder;
 use Symfony\Component\Process\ExecutableFinder;
 use ZipArchive;
 use ZipArchive;
@@ -33,19 +33,21 @@ class ZipDownloader extends ArchiveDownloader
     private static $hasZipArchive;
     private static $hasZipArchive;
     private static $isWindows;
     private static $isWindows;
 
 
+    /** @var ProcessExecutor */
     protected $process;
     protected $process;
+    /** @var ZipArchive|null */
     private $zipArchiveObject;
     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);
         $this->process = $process ?: new ProcessExecutor($io);
-        parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
+        parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
     }
 
 
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
     {
         if (null === self::$hasSystemUnzip) {
         if (null === self::$hasSystemUnzip) {
             $finder = new ExecutableFinder;
             $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 $file File to extract
      * @param string $path Path where to extract file
      * @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
         // Each extract calls its alternative if not available or fails
         if (self::$isWindows) {
         if (self::$isWindows) {

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

@@ -13,13 +13,16 @@
 namespace Composer\EventDispatcher;
 namespace Composer\EventDispatcher;
 
 
 use Composer\DependencyResolver\PolicyInterface;
 use Composer\DependencyResolver\PolicyInterface;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
 use Composer\DependencyResolver\Request;
+use Composer\DependencyResolver\Pool;
+use Composer\DependencyResolver\Transaction;
 use Composer\Installer\InstallerEvent;
 use Composer\Installer\InstallerEvent;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Composer;
 use Composer\Composer;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\CompositeRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Repository\RepositorySet;
 use Composer\Script;
 use Composer\Script;
 use Composer\Installer\PackageEvent;
 use Composer\Installer\PackageEvent;
 use Composer\Installer\BinaryInstaller;
 use Composer\Installer\BinaryInstaller;
@@ -46,7 +49,7 @@ class EventDispatcher
     protected $io;
     protected $io;
     protected $loader;
     protected $loader;
     protected $process;
     protected $process;
-    protected $listeners;
+    protected $listeners = array();
     private $eventStack;
     private $eventStack;
 
 
     /**
     /**
@@ -99,40 +102,34 @@ class EventDispatcher
     /**
     /**
      * Dispatch a package event.
      * 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
      * @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
      *             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.
      * 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
      * @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
      *             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');
                     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);
                 $event = $this->checkListenerExpectedEvent($callable, $event);
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
             } elseif ($this->isComposerScript($callable)) {
             } elseif ($this->isComposerScript($callable)) {
@@ -172,8 +172,8 @@ class EventDispatcher
                 $args = array_merge($script, $event->getArguments());
                 $args = array_merge($script, $event->getArguments());
                 $flags = $event->getFlags();
                 $flags = $event->getFlags();
                 if (substr($callable, 0, 10) === '@composer ') {
                 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);
                         $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);
                         throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@@ -184,6 +184,7 @@ class EventDispatcher
                     }
                     }
 
 
                     try {
                     try {
+                        /** @var InstallerEvent $event */
                         $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
                         $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
                         $scriptEvent->setOriginatingEvent($event);
                         $scriptEvent->setOriginatingEvent($event);
                         $return = $this->dispatch($scriptName, $scriptEvent);
                         $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);
                     $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);
                     throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@@ -264,6 +265,15 @@ class EventDispatcher
         return $return;
         return $return;
     }
     }
 
 
+    protected function executeTty($exec)
+    {
+        if ($this->io->isInteractive()) {
+            return $this->process->executeTty($exec);
+        }
+
+        return $this->process->execute($exec);
+    }
+
     protected function getPhpExecCommand()
     protected function getPhpExecCommand()
     {
     {
         $finder = new PhpExecutableFinder();
         $finder = new PhpExecutableFinder();
@@ -327,44 +337,6 @@ class EventDispatcher
 
 
         $expected = $typehint->getName();
         $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;
         return $event;
     }
     }
 
 
@@ -397,6 +369,22 @@ class EventDispatcher
         $this->listeners[$eventName][$priority][] = $listener;
         $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
      * Adds object methods as listeners for the events in getSubscribedEvents
      *
      *
@@ -513,7 +501,7 @@ class EventDispatcher
      *
      *
      * @param  Event             $event
      * @param  Event             $event
      * @throws \RuntimeException
      * @throws \RuntimeException
-     * @return number
+     * @return int
      */
      */
     protected function pushEvent(Event $event)
     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\Filesystem;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
+use Composer\Util\Loop;
 use Composer\Util\Silencer;
 use Composer\Util\Silencer;
 use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PluginEvents;
 use Composer\EventDispatcher\Event;
 use Composer\EventDispatcher\Event;
@@ -222,6 +223,13 @@ class Factory
         return trim(getenv('COMPOSER')) ?: './composer.json';
         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()
     public static function createAdditionalStyles()
     {
     {
         return array(
         return array(
@@ -325,14 +333,15 @@ class Factory
             $io->loadConfiguration($config);
             $io->loadConfiguration($config);
         }
         }
 
 
-        $rfs = self::createRemoteFilesystem($io, $config);
+        $httpDownloader = self::createHttpDownloader($io, $config);
+        $loop = new Loop($httpDownloader);
 
 
         // initialize event dispatcher
         // initialize event dispatcher
         $dispatcher = new EventDispatcher($composer, $io);
         $dispatcher = new EventDispatcher($composer, $io);
         $composer->setEventDispatcher($dispatcher);
         $composer->setEventDispatcher($dispatcher);
 
 
         // initialize repository manager
         // initialize repository manager
-        $rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs);
+        $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
         $composer->setRepositoryManager($rm);
         $composer->setRepositoryManager($rm);
 
 
         // load local repository
         // load local repository
@@ -352,12 +361,12 @@ class Factory
         $composer->setPackage($package);
         $composer->setPackage($package);
 
 
         // initialize installation manager
         // initialize installation manager
-        $im = $this->createInstallationManager();
+        $im = $this->createInstallationManager($loop, $io, $dispatcher);
         $composer->setInstallationManager($im);
         $composer->setInstallationManager($im);
 
 
         if ($fullLoad) {
         if ($fullLoad) {
             // initialize download manager
             // initialize download manager
-            $dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs);
+            $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher);
             $composer->setDownloadManager($dm);
             $composer->setDownloadManager($dm);
 
 
             // initialize autoload generator
             // initialize autoload generator
@@ -365,7 +374,7 @@ class Factory
             $composer->setAutoloadGenerator($generator);
             $composer->setAutoloadGenerator($generator);
 
 
             // initialize archive manager
             // initialize archive manager
-            $am = $this->createArchiveManager($config, $dm);
+            $am = $this->createArchiveManager($config, $dm, $loop);
             $composer->setArchiveManager($am);
             $composer->setArchiveManager($am);
         }
         }
 
 
@@ -386,11 +395,9 @@ class Factory
 
 
         // init locker if possible
         // init locker if possible
         if ($fullLoad && isset($composerFile)) {
         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);
             $composer->setLocker($locker);
         }
         }
 
 
@@ -411,7 +418,7 @@ class Factory
     /**
     /**
      * @param  IOInterface $io             IO instance
      * @param  IOInterface $io             IO instance
      * @param  bool        $disablePlugins Whether plugins should not be loaded
      * @param  bool        $disablePlugins Whether plugins should not be loaded
-     * @return Composer
+     * @return Composer|null
      */
      */
     public static function createGlobal(IOInterface $io, $disablePlugins = false)
     public static function createGlobal(IOInterface $io, $disablePlugins = false)
     {
     {
@@ -451,7 +458,7 @@ class Factory
      * @param  EventDispatcher            $eventDispatcher
      * @param  EventDispatcher            $eventDispatcher
      * @return Downloader\DownloadManager
      * @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;
         $cache = null;
         if ($config->get('cache-files-ttl') > 0) {
         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('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
         $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;
         return $dm;
     }
     }
@@ -501,15 +508,9 @@ class Factory
      * @param  Downloader\DownloadManager $dm     Manager use to download sources
      * @param  Downloader\DownloadManager $dm     Manager use to download sources
      * @return Archiver\ArchiveManager
      * @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\ZipArchiver);
         $am->addArchiver(new Archiver\PharArchiver);
         $am->addArchiver(new Archiver\PharArchiver);
 
 
@@ -531,9 +532,9 @@ class Factory
     /**
     /**
      * @return Installer\InstallationManager
      * @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  IOInterface      $io      IO instance
      * @param  Config           $config  Config 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;
         static $warned = false;
         $disableTls = 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. '
             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.');
                 . '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 ($disableTls === false) {
             if ($config && $config->get('cafile')) {
             if ($config && $config->get('cafile')) {
-                $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
+                $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile');
             }
             }
             if ($config && $config->get('capath')) {
             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 {
         try {
-            $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);
+            $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls);
         } catch (TransportException $e) {
         } catch (TransportException $e) {
             if (false !== strpos($e->getMessage(), 'cafile')) {
             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>');
                 $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;
             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\Config;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
-use Psr\Log\LoggerInterface;
 use Psr\Log\LogLevel;
 use Psr\Log\LogLevel;
 
 
-abstract class BaseIO implements IOInterface, LoggerInterface
+abstract class BaseIO implements IOInterface
 {
 {
     protected $authentications = array();
     protected $authentications = array();
 
 

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

@@ -13,13 +13,14 @@
 namespace Composer\IO;
 namespace Composer\IO;
 
 
 use Composer\Config;
 use Composer\Config;
+use Psr\Log\LoggerInterface;
 
 
 /**
 /**
  * The Input/Output helper interface.
  * The Input/Output helper interface.
  *
  *
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  */
  */
-interface IOInterface
+interface IOInterface extends LoggerInterface
 {
 {
     const QUIET = 1;
     const QUIET = 1;
     const NORMAL = 2;
     const NORMAL = 2;
@@ -80,6 +81,24 @@ interface IOInterface
      */
      */
     public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
     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.
      * 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
      * @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
      * @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);
     public function ask($question, $default = null);
 
 
@@ -145,7 +164,7 @@ interface IOInterface
      *
      *
      * @param string $question The question to ask
      * @param string $question The question to ask
      *
      *
-     * @return string The answer
+     * @return string|null The answer
      */
      */
     public function askAndHideAnswer($question);
     public function askAndHideAnswer($question);
 
 
@@ -160,7 +179,7 @@ interface IOInterface
      * @param bool        $multiselect  Select more than one value separated by comma
      * @param bool        $multiselect  Select more than one value separated by comma
      *
      *
      * @throws \InvalidArgumentException
      * @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);
     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\UninstallOperation;
 use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
 use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
 use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
 use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\StreamContextFactory;
 use Composer\Util\StreamContextFactory;
+use Composer\Util\Loop;
 
 
 /**
 /**
  * Package operation manager.
  * Package operation manager.
@@ -37,6 +39,16 @@ class InstallationManager
     private $installers = array();
     private $installers = array();
     private $cache = array();
     private $cache = array();
     private $notifiablePackages = 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()
     public function reset()
     {
     {
@@ -151,13 +163,105 @@ class InstallationManager
     /**
     /**
      * Executes solver operation.
      * 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();
         $package = $operation->getPackage();
         $installer = $this->getInstaller($package->getType());
         $installer = $this->getInstaller($package->getType());
-        $installer->install($repo, $package);
+        $promise = $installer->install($repo, $package);
         $this->markForNotification($package);
         $this->markForNotification($package);
+
+        return $promise;
     }
     }
 
 
     /**
     /**
@@ -190,12 +296,15 @@ class InstallationManager
 
 
         if ($initialType === $targetType) {
         if ($initialType === $targetType) {
             $installer = $this->getInstaller($initialType);
             $installer = $this->getInstaller($initialType);
-            $installer->update($repo, $initial, $target);
+            $promise = $installer->update($repo, $initial, $target);
             $this->markForNotification($target);
             $this->markForNotification($target);
         } else {
         } else {
             $this->getInstaller($initialType)->uninstall($repo, $initial);
             $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();
         $package = $operation->getPackage();
         $installer = $this->getInstaller($package->getType());
         $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\Composer;
 use Composer\DependencyResolver\PolicyInterface;
 use Composer\DependencyResolver\PolicyInterface;
-use Composer\DependencyResolver\Operation\OperationInterface;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
 use Composer\DependencyResolver\Request;
+use Composer\DependencyResolver\Pool;
+use Composer\DependencyResolver\Transaction;
 use Composer\EventDispatcher\Event;
 use Composer\EventDispatcher\Event;
 use Composer\IO\IOInterface;
 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
 class InstallerEvent extends Event
 {
 {
     /**
     /**
@@ -44,29 +39,14 @@ class InstallerEvent extends Event
     private $devMode;
     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.
      * Constructor.
@@ -75,24 +55,18 @@ class InstallerEvent extends Event
      * @param Composer             $composer
      * @param Composer             $composer
      * @param IOInterface          $io
      * @param IOInterface          $io
      * @param bool                 $devMode
      * @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);
         parent::__construct($eventName);
 
 
         $this->composer = $composer;
         $this->composer = $composer;
         $this->io = $io;
         $this->io = $io;
         $this->devMode = $devMode;
         $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;
 namespace Composer\Installer;
 
 
-/**
- * The Installer Events.
- *
- * @author François Pluchino <francois.pluchino@gmail.com>
- */
 class InstallerEvents
 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
      * @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\Package\PackageInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use InvalidArgumentException;
 use InvalidArgumentException;
+use React\Promise\PromiseInterface;
 
 
 /**
 /**
  * Interface for the package installation manager.
  * Interface for the package installation manager.
@@ -42,20 +43,46 @@ interface InstallerInterface
      */
      */
     public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package);
     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.
      * 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);
     public function install(InstalledRepositoryInterface $repo, PackageInterface $package);
 
 
     /**
     /**
      * Updates specific 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
      * @throws InvalidArgumentException if $initial package is not installed
      */
      */
@@ -64,11 +91,26 @@ interface InstallerInterface
     /**
     /**
      * Uninstalls specific package.
      * 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);
     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
      * 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 IOInterface     $io
      * @param Composer        $composer
      * @param Composer        $composer
-     * @param string          $type
+     * @param string|null     $type
      * @param Filesystem      $filesystem
      * @param Filesystem      $filesystem
      * @param BinaryInstaller $binaryInstaller
      * @param BinaryInstaller $binaryInstaller
      */
      */
@@ -85,6 +85,39 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
         return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath);
         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}
      * {@inheritDoc}
      */
      */
@@ -194,7 +227,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
     protected function installCode(PackageInterface $package)
     protected function installCode(PackageInterface $package)
     {
     {
         $downloadPath = $this->getInstallPath($package);
         $downloadPath = $this->getInstallPath($package);
-        $this->downloadManager->download($package, $downloadPath);
+        $this->downloadManager->install($package, $downloadPath);
     }
     }
 
 
     protected function updateCode(PackageInterface $initial, PackageInterface $target)
     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);
         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}
      * {@inheritDoc}
      */
      */
@@ -69,7 +93,7 @@ class MetapackageInstaller implements InstallerInterface
         $name = $target->getName();
         $name = $target->getName();
         $from = $initial->getFullPrettyVersion();
         $from = $initial->getFullPrettyVersion();
         $to = $target->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>)");
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>)");
 
 
         $repo->removePackage($initial);
         $repo->removePackage($initial);

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

@@ -40,6 +40,27 @@ class NoopInstaller implements InstallerInterface
         return $repo->hasPackage($package);
         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}
      * {@inheritDoc}
      */
      */

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

@@ -16,19 +16,45 @@ use Composer\Composer;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\PolicyInterface;
 use Composer\DependencyResolver\PolicyInterface;
-use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
 use Composer\DependencyResolver\Request;
-use Composer\Repository\CompositeRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Repository\RepositorySet;
+use Composer\EventDispatcher\Event;
 
 
 /**
 /**
  * The Package Event.
  * The Package Event.
  *
  *
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @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;
     private $operation;
 
 
@@ -39,20 +65,63 @@ class PackageEvent extends InstallerEvent
      * @param Composer             $composer
      * @param Composer             $composer
      * @param IOInterface          $io
      * @param IOInterface          $io
      * @param bool                 $devMode
      * @param bool                 $devMode
-     * @param PolicyInterface      $policy
-     * @param Pool                 $pool
-     * @param CompositeRepository  $installedRepo
+     * @param RepositoryInterface  $localRepo
      * @param Request              $request
      * @param Request              $request
      * @param OperationInterface[] $operations
      * @param OperationInterface[] $operations
      * @param OperationInterface   $operation
      * @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;
         $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.
      * Returns the package instance.
      *
      *

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

@@ -50,19 +50,27 @@ class PluginInstaller extends LibraryInstaller
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
     {
         $extra = $package->getExtra();
         $extra = $package->getExtra();
         if (empty($extra['class'])) {
         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.');
             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);
         parent::install($repo, $package);
         try {
         try {
             $this->composer->getPluginManager()->registerPackage($package, true);
             $this->composer->getPluginManager()->registerPackage($package, true);
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             // Rollback installation
             // Rollback installation
-            $this->io->writeError('Plugin installation failed, rolling back');
+            $this->io->writeError('Plugin initialization failed, uninstalling plugin');
             parent::uninstall($repo, $package);
             parent::uninstall($repo, $package);
             throw $e;
             throw $e;
         }
         }
@@ -73,12 +81,22 @@ class PluginInstaller extends LibraryInstaller
      */
      */
     public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
     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}
      * {@inheritDoc}
      */
      */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
     {
         $installPath = $this->installPath;
         $installPath = $this->installPath;
         if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
         if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@@ -67,7 +67,32 @@ class ProjectInstaller implements InstallerInterface
         if (!is_dir($installPath)) {
         if (!is_dir($installPath)) {
             mkdir($installPath, 0777, true);
             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
 class SuggestedPackagesReporter
 {
 {
+    const MODE_LIST = 1;
+    const MODE_BY_PACKAGE = 2;
+    const MODE_BY_SUGGESTION = 4;
+
     /**
     /**
      * @var array
      * @var array
      */
      */
@@ -91,38 +95,105 @@ class SuggestedPackagesReporter
 
 
     /**
     /**
      * Output suggested packages.
      * Output suggested packages.
+     *
      * Do not list the ones already installed if installed repository provided.
      * 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
      * @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();
         $suggestedPackages = $this->getPackages();
-        $installedPackages = array();
-        if (null !== $installedRepo && ! empty($suggestedPackages)) {
+        $installedNames = array();
+        if (null !== $installedRepo && !empty($suggestedPackages)) {
             foreach ($installedRepo->getPackages() as $package) {
             foreach ($installedRepo->getPackages() as $package) {
-                $installedPackages = array_merge(
-                    $installedPackages,
+                $installedNames = array_merge(
+                    $installedNames,
                     $package->getNames()
                     $package->getNames()
                 );
                 );
             }
             }
         }
         }
 
 
+        $suggestions = array();
         foreach ($suggestedPackages as $suggestion) {
         foreach ($suggestedPackages as $suggestion) {
-            if (in_array($suggestion['target'], $installedPackages)) {
+            if (in_array($suggestion['target'], $installedNames)) {
                 continue;
                 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 JsonSchema\Validator;
 use Seld\JsonLint\JsonParser;
 use Seld\JsonLint\JsonParser;
 use Seld\JsonLint\ParsingException;
 use Seld\JsonLint\ParsingException;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Downloader\TransportException;
 use Composer\Downloader\TransportException;
 
 
@@ -37,25 +37,25 @@ class JsonFile
     const COMPOSER_SCHEMA_PATH = '/../../../res/composer-schema.json';
     const COMPOSER_SCHEMA_PATH = '/../../../res/composer-schema.json';
 
 
     private $path;
     private $path;
-    private $rfs;
+    private $httpDownloader;
     private $io;
     private $io;
 
 
     /**
     /**
      * Initializes json file reader/parser.
      * 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
      * @param  IOInterface               $io
      * @throws \InvalidArgumentException
      * @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;
         $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;
         $this->io = $io;
     }
     }
 
 
@@ -86,8 +86,8 @@ class JsonFile
     public function read()
     public function read()
     {
     {
         try {
         try {
-            if ($this->rfs) {
-                $json = $this->rfs->getContents($this->path, $this->path, false);
+            if ($this->httpDownloader) {
+                $json = $this->httpDownloader->get($this->path)->getBody();
             } else {
             } else {
                 if ($this->io && $this->io->isDebug()) {
                 if ($this->io && $this->io->isDebug()) {
                     $this->io->writeError('Reading ' . $this->path);
                     $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);
         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\PackageInterface;
 use Composer\Package\RootPackageInterface;
 use Composer\Package\RootPackageInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonFile;
 
 
 /**
 /**
@@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
 class ArchiveManager
 class ArchiveManager
 {
 {
     protected $downloadManager;
     protected $downloadManager;
+    protected $loop;
 
 
     protected $archivers = array();
     protected $archivers = array();
 
 
@@ -36,9 +38,10 @@ class ArchiveManager
     /**
     /**
      * @param DownloadManager $downloadManager A manager used to download package sources
      * @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->downloadManager = $downloadManager;
+        $this->loop = $loop;
     }
     }
 
 
     /**
     /**
@@ -149,7 +152,9 @@ class ArchiveManager
 
 
             try {
             try {
                 // Download sources
                 // 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) {
             } catch (\Exception $e) {
                 $filesystem->removeDirectory($sourcePath);
                 $filesystem->removeDirectory($sourcePath);
                 throw  $e;
                 throw  $e;

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

@@ -210,18 +210,30 @@ abstract class BasePackage implements PackageInterface
     /**
     /**
      * {@inheritDoc}
      * {@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();
             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 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()
     public function getStabilityPriority()
@@ -238,14 +250,14 @@ abstract class BasePackage implements PackageInterface
     /**
     /**
      * Build a regexp from a package name, expanding * globs as required
      * 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
      * @param  string $wrap Wrap the cleaned string by the given string
      * @return 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