Browse Source

Merge remote-tracking branch 'github-composer/2.0' into solve-without-installed

* github-composer/2.0: (48 commits)
  Fix missing use/undefined var
  Split up steps on VCS downloaders to allow doing network operations before touching the filesystem on GitDownloader, fixes #7903
  Fix use statement
  Deduplicate findHeaderValue code
  Add install-path to the installed.json for every package, fixes #2174, closes #2424
  Remove unnecessary config from phpstan
  Make sure the directory exists and will not block installation later when downloading
  Avoid wiping the whole target package if download of the new one fails, refs #7929
  Only empty dir before actually installing packages, fixes #7929
  Improve output when installing packages
  Show best possible version in diagnose command
  Remove extra arg
  Allow path repos to point to their own source dir as install target, resulting in noop, fixes #8254
  Fix use of decodeJson
  Fix update mirrors to also update transport-options, fixes #7672
  Fix updating or URLs to include dist type and shasum, fixes #8216
  Fix origin computation
  Improve handling of non-standard ports for GitLab and GitHub installs, fixes #8173
  Load packages from the lock file for check-platform-reqs if no dependencies have been installed yet, fixes #8058
  Fix error_handler return type declaration
  ...
Nils Adermann 5 years ago
parent
commit
f5e18250e6
91 changed files with 1565 additions and 516 deletions
  1. 3 1
      .travis.yml
  2. 4 0
      doc/03-cli.md
  3. 13 0
      doc/articles/handling-private-packages-with-satis.md
  4. 0 1
      phpstan/config.neon
  5. 6 0
      src/Composer/Command/CheckPlatformReqsCommand.php
  6. 3 3
      src/Composer/Command/DiagnoseCommand.php
  7. 17 5
      src/Composer/Command/InitCommand.php
  8. 15 6
      src/Composer/Command/RequireCommand.php
  9. 3 0
      src/Composer/Console/Application.php
  10. 6 2
      src/Composer/DependencyResolver/RuleSetGenerator.php
  11. 5 5
      src/Composer/Downloader/ArchiveDownloader.php
  12. 58 14
      src/Composer/Downloader/DownloadManager.php
  13. 34 3
      src/Composer/Downloader/DownloaderInterface.php
  14. 21 4
      src/Composer/Downloader/FileDownloader.php
  15. 10 2
      src/Composer/Downloader/FossilDownloader.php
  16. 70 33
      src/Composer/Downloader/GitDownloader.php
  17. 10 2
      src/Composer/Downloader/HgDownloader.php
  18. 32 2
      src/Composer/Downloader/PathDownloader.php
  19. 9 3
      src/Composer/Downloader/PerforceDownloader.php
  20. 10 2
      src/Composer/Downloader/SvnDownloader.php
  21. 99 34
      src/Composer/Downloader/VcsDownloader.php
  22. 2 2
      src/Composer/Downloader/ZipDownloader.php
  23. 3 1
      src/Composer/EventDispatcher/EventDispatcher.php
  24. 1 1
      src/Composer/Factory.php
  25. 8 0
      src/Composer/IO/BaseIO.php
  26. 1 1
      src/Composer/Installer.php
  27. 52 6
      src/Composer/Installer/InstallationManager.php
  28. 41 9
      src/Composer/Installer/InstallerInterface.php
  29. 25 0
      src/Composer/Installer/LibraryInstaller.php
  30. 16 0
      src/Composer/Installer/MetapackageInstaller.php
  31. 14 0
      src/Composer/Installer/NoopInstaller.php
  32. 16 6
      src/Composer/Installer/PluginInstaller.php
  33. 16 0
      src/Composer/Installer/ProjectInstaller.php
  34. 3 2
      src/Composer/Json/JsonManipulator.php
  35. 1 5
      src/Composer/Package/Locker.php
  36. 18 0
      src/Composer/Plugin/PluginInterface.php
  37. 106 2
      src/Composer/Plugin/PluginManager.php
  38. 4 64
      src/Composer/Repository/ArtifactRepository.php
  39. 17 5
      src/Composer/Repository/FilesystemRepository.php
  40. 6 3
      src/Composer/Repository/Vcs/GitHubDriver.php
  41. 21 14
      src/Composer/Repository/Vcs/GitLabDriver.php
  42. 2 1
      src/Composer/Repository/WritableArrayRepository.php
  43. 4 1
      src/Composer/Repository/WritableRepositoryInterface.php
  44. 44 0
      src/Composer/Script/Event.php
  45. 3 0
      src/Composer/Util/ErrorHandler.php
  46. 5 3
      src/Composer/Util/Git.php
  47. 10 1
      src/Composer/Util/GitLab.php
  48. 0 1
      src/Composer/Util/Http/CurlDownloader.php
  49. 24 14
      src/Composer/Util/Http/Response.php
  50. 0 2
      src/Composer/Util/Perforce.php
  51. 11 27
      src/Composer/Util/RemoteFilesystem.php
  52. 0 2
      src/Composer/Util/TlsHelper.php
  53. 3 0
      src/Composer/Util/Url.php
  54. 108 0
      src/Composer/Util/Zip.php
  55. 4 4
      tests/Composer/Test/AllFunctionalTest.php
  56. 0 7
      tests/Composer/Test/DependencyResolver/RuleSetTest.php
  57. 6 2
      tests/Composer/Test/Downloader/FossilDownloaderTest.php
  58. 118 117
      tests/Composer/Test/Downloader/GitDownloaderTest.php
  59. 6 0
      tests/Composer/Test/Downloader/HgDownloaderTest.php
  60. 1 1
      tests/Composer/Test/Downloader/ZipDownloaderTest.php
  61. 35 29
      tests/Composer/Test/Fixtures/installer/update-changes-url.test
  62. 1 1
      tests/Composer/Test/InstallerTest.php
  63. 16 0
      tests/Composer/Test/Json/JsonManipulatorTest.php
  64. 2 1
      tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php
  65. 0 1
      tests/Composer/Test/Package/Loader/ArrayLoaderTest.php
  66. 9 32
      tests/Composer/Test/Package/LockerTest.php
  67. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php
  68. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php
  69. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php
  70. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php
  71. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php
  72. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php
  73. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php
  74. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php
  75. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php
  76. 11 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php
  77. 31 1
      tests/Composer/Test/Plugin/PluginInstallerTest.php
  78. 3 1
      tests/Composer/Test/Repository/ComposerRepositoryTest.php
  79. 13 2
      tests/Composer/Test/Repository/FilesystemRepositoryTest.php
  80. 0 9
      tests/Composer/Test/Repository/Vcs/FossilDriverTest.php
  81. 0 9
      tests/Composer/Test/Repository/Vcs/SvnDriverTest.php
  82. 80 0
      tests/Composer/Test/Script/EventTest.php
  83. BIN
      tests/Composer/Test/Util/Fixtures/Zip/empty.zip
  84. BIN
      tests/Composer/Test/Util/Fixtures/Zip/folder.zip
  85. BIN
      tests/Composer/Test/Util/Fixtures/Zip/multiple.zip
  86. BIN
      tests/Composer/Test/Util/Fixtures/Zip/nojson.zip
  87. BIN
      tests/Composer/Test/Util/Fixtures/Zip/root.zip
  88. BIN
      tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip
  89. 0 3
      tests/Composer/Test/Util/GitHubTest.php
  90. 0 1
      tests/Composer/Test/Util/GitLabTest.php
  91. 117 0
      tests/Composer/Test/Util/ZipTest.php

+ 3 - 1
.travis.yml

@@ -30,9 +30,11 @@ matrix:
       env:
         - deps=high
     - php: nightly
+    - php: 7.4snapshot
   fast_finish: true
   allow_failures:
     - php: nightly
+    - php: 7.4snapshot
 
 before_install:
   # disable xdebug if available
@@ -62,7 +64,7 @@ script:
   - 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);'
   # Run PHPStan
   - if [[ $PHPSTAN == "1" ]]; then
-      composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs &&
+      bin/composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs &&
       vendor/bin/phpstan.phar analyse src tests --configuration=phpstan/config.neon --autoload-file=phpstan/autoload.php;
     fi
 

+ 4 - 0
doc/03-cli.md

@@ -259,6 +259,10 @@ match the platform requirements of the installed packages. This can be used
 to verify that a production server has all the extensions needed to run a
 project after installing it for example.
 
+Unlike update/install, this command will ignore config.platform settings and
+check the real platform packages so you can be certain you have the required
+platform dependencies.
+
 ## global
 
 The global command allows you to run other commands like `install`, `remove`, `require`

+ 13 - 0
doc/articles/handling-private-packages-with-satis.md

@@ -112,6 +112,19 @@ Note that this will still need to pull and scan all of your VCS repositories
 because any VCS repository might contain (on any branch) one of the selected
 packages.
 
+If you want to scan only the selected package and not all VCS repositories you need
+to declare a *name* for all your package (this only work on VCS repositories type) :
+
+```json
+{
+  "repositories": [
+    { "name": "company/privaterepo", "type": "vcs", "url": "https://github.com/mycompany/privaterepo" },
+    { "name": "private/repo", "type": "vcs", "url": "http://svn.example.org/private/repo" },
+    { "name": "mycompany/privaterepo2", "type": "vcs", "url": "https://github.com/mycompany/privaterepo2" }
+  ]
+}
+```
+
 If you want to scan only a single repository and update all packages found in
 it, pass the VCS repository URL as an optional argument:
 

+ 0 - 1
phpstan/config.neon

@@ -16,7 +16,6 @@ parameters:
         - '~^Anonymous function has an unused use \$io\.$~'
         - '~^Anonymous function has an unused use \$cache\.$~'
         - '~^Anonymous function has an unused use \$path\.$~'
-        - '~^Anonymous function has an unused use \$fileName\.$~'
 
         # ion cube is not installed
         - '~^Function ioncube_loader_\w+ not found\.$~'

+ 6 - 0
src/Composer/Command/CheckPlatformReqsCommand.php

@@ -34,6 +34,8 @@ class CheckPlatformReqsCommand extends BaseCommand
                 <<<EOT
 Checks that your PHP and extensions versions match the platform requirements of the installed packages.
 
+Unlike update/install, this command will ignore config.platform settings and check the real platform packages so you can be certain you have the required platform dependencies.
+
 <info>php composer.phar check-platform-reqs</info>
 
 EOT
@@ -49,6 +51,10 @@ EOT
             $dependencies = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'))->getPackages();
         } else {
             $dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages();
+            // fallback to lockfile if installed repo is empty
+            if (!$dependencies) {
+                $dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages();
+            }
             $requires += $composer->getPackage()->getDevRequires();
         }
         foreach ($requires as $require => $link) {

+ 3 - 3
src/Composer/Command/DiagnoseCommand.php

@@ -156,7 +156,7 @@ EOT
             $this->outputResult($this->checkVersion($config));
         }
 
-        $io->write(sprintf('Composer version: <comment>%s</comment>', Composer::VERSION));
+        $io->write(sprintf('Composer version: <comment>%s</comment>', Composer::getVersion()));
 
         $platformOverrides = $config->get('platform') ?: array();
         $platformRepo = new PlatformRepository(array(), $platformOverrides);
@@ -254,7 +254,7 @@ EOT
 
         $protocol = extension_loaded('openssl') ? 'https' : 'http';
         try {
-            $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->parseJson();
+            $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson();
             $hash = reset($json['provider-includes']);
             $hash = $hash['sha256'];
             $path = str_replace('%hash%', $hash, key($json['provider-includes']));
@@ -375,7 +375,7 @@ EOT
         }
 
         $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit';
-        $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->parseJson();
+        $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson();
 
         return $data['resources']['core'];
     }

+ 17 - 5
src/Composer/Command/InitCommand.php

@@ -168,13 +168,25 @@ EOT
         if ($repositories) {
             $config = Factory::createConfig($io);
             $repos = array(new PlatformRepository);
+            $createDefaultPackagistRepo = true;
             foreach ($repositories as $repo) {
-                $repos[] = RepositoryFactory::fromString($io, $config, $repo);
+                $repoConfig = RepositoryFactory::configFromString($io, $config, $repo);
+                if (
+                    (isset($repoConfig['packagist']) && $repoConfig === array('packagist' => false))
+                    || (isset($repoConfig['packagist.org']) && $repoConfig === array('packagist.org' => false))
+                ) {
+                    $createDefaultPackagistRepo = false;
+                    continue;
+                }
+                $repos[] = RepositoryFactory::createRepo($io, $config, $repoConfig);
+            }
+
+            if ($createDefaultPackagistRepo) {
+                $repos[] = RepositoryFactory::createRepo($io, $config, array(
+                    'type' => 'composer',
+                    'url' => 'https://repo.packagist.org',
+                ));
             }
-            $repos[] = RepositoryFactory::createRepo($io, $config, array(
-                'type' => 'composer',
-                'url' => 'https://repo.packagist.org',
-            ));
 
             $this->repos = new CompositeRepository($repos);
             unset($repos, $config, $repositories);

+ 15 - 6
src/Composer/Command/RequireCommand.php

@@ -26,6 +26,7 @@ use Composer\Plugin\PluginEvents;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\IO\IOInterface;
+use Composer\Util\Silencer;
 
 /**
  * @author Jérémy Romey <jeremy@free-agent.fr>
@@ -103,11 +104,6 @@ EOT
 
             return 1;
         }
-        if (!is_writable($this->file)) {
-            $io->writeError('<error>'.$this->file.' is not writable.</error>');
-
-            return 1;
-        }
 
         if (filesize($this->file) === 0) {
             file_put_contents($this->file, "{\n}\n");
@@ -116,6 +112,14 @@ EOT
         $this->json = new JsonFile($this->file);
         $this->composerBackup = file_get_contents($this->json->getPath());
 
+        // 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
+        if (!is_writable($this->file) && !Silencer::call('file_put_contents', $this->file, $this->composerBackup)) {
+            $io->writeError('<error>'.$this->file.' is not writable.</error>');
+
+            return 1;
+        }
+
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $repos = $composer->getRepositoryManager()->getRepositories();
 
@@ -141,7 +145,12 @@ EOT
 
         // validate requirements format
         $versionParser = new VersionParser();
-        foreach ($requirements as $constraint) {
+        foreach ($requirements as $package => $constraint) {
+            if (strtolower($package) === $composer->getPackage()->getName()) {
+                $io->writeError(sprintf('<error>Root package \'%s\' cannot require itself in its composer.json</error>', $package));
+
+                return 1;
+            }
             $versionParser->parseConstraints($constraint);
         }
 

+ 3 - 0
src/Composer/Console/Application.php

@@ -379,6 +379,9 @@ class Application extends BaseApplication
     public function resetComposer()
     {
         $this->composer = null;
+        if ($this->getIO() && method_exists($this->getIO(), 'resetAuthentications')) {
+            $this->getIO()->resetAuthentications();
+        }
     }
 
     /**

+ 6 - 2
src/Composer/DependencyResolver/RuleSetGenerator.php

@@ -195,7 +195,7 @@ class RuleSetGenerator
         }
     }
 
-    protected function addConflictRules()
+    protected function addConflictRules($ignorePlatformReqs = false)
     {
         /** @var PackageInterface $package */
         foreach ($this->addedPackages as $package) {
@@ -204,6 +204,10 @@ class RuleSetGenerator
                     continue;
                 }
 
+                if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) {
+                    continue;
+                }
+
                 /** @var PackageInterface $possibleConflict */
                 foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) {
                     $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true);
@@ -305,7 +309,7 @@ class RuleSetGenerator
 
         $this->addRulesForRequest($request, $ignorePlatformReqs);
 
-        $this->addConflictRules();
+        $this->addConflictRules($ignorePlatformReqs);
 
         // Remove references to packages
         $this->addedPackages = $this->addedPackagesByNames = null;

+ 5 - 5
src/Composer/Downloader/ArchiveDownloader.php

@@ -33,16 +33,16 @@ abstract class ArchiveDownloader extends FileDownloader
     public function install(PackageInterface $package, $path, $output = true)
     {
         if ($output) {
-            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
+            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): Extracting archive");
+        } else {
+            $this->io->writeError('Extracting archive', false);
         }
 
+        $this->filesystem->emptyDirectory($path);
+
         $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
         $fileName = $this->getFileName($package, $path);
 
-        if ($output) {
-            $this->io->writeError('    Extracting archive', true, IOInterface::VERBOSE);
-        }
-
         try {
             $this->filesystem->ensureDirectoryExists($temporaryDir);
             try {

+ 58 - 14
src/Composer/Downloader/DownloadManager.php

@@ -165,9 +165,9 @@ class DownloadManager
     /**
      * Downloads package into target dir.
      *
-     * @param PackageInterface $package      package instance
-     * @param string           $targetDir    target dir
-     * @param PackageInterface $prevPackage  previous package instance in case of updates
+     * @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
@@ -182,7 +182,7 @@ class DownloadManager
         $io = $this->io;
         $self = $this;
 
-        $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download) {
+        $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>');
@@ -214,7 +214,7 @@ class DownloadManager
             };
 
             try {
-                $result = $downloader->download($package, $targetDir);
+                $result = $downloader->download($package, $targetDir, $prevPackage);
             } catch (\Exception $e) {
                 return $handleError($e);
             }
@@ -232,12 +232,31 @@ class DownloadManager
         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)
+    {
+        $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
      */
@@ -245,7 +264,7 @@ class DownloadManager
     {
         $downloader = $this->getDownloaderForPackage($package);
         if ($downloader) {
-            $downloader->install($package, $targetDir);
+            return $downloader->install($package, $targetDir);
         }
     }
 
@@ -256,6 +275,7 @@ class DownloadManager
      * @param PackageInterface $target    target package version
      * @param string           $targetDir target dir
      *
+     * @return PromiseInterface|null
      * @throws \InvalidArgumentException if initial package is not installed
      */
     public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
@@ -270,17 +290,14 @@ class DownloadManager
 
         // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
         if (!$downloader) {
-            $initialDownloader->remove($initial, $targetDir);
-            return;
+            return $initialDownloader->remove($initial, $targetDir);
         }
 
         $initialType = $this->getDownloaderType($initialDownloader);
         $targetType = $this->getDownloaderType($downloader);
         if ($initialType === $targetType) {
             try {
-                $downloader->update($initial, $target, $targetDir);
-
-                return;
+                return $downloader->update($initial, $target, $targetDir);
             } catch (\RuntimeException $e) {
                 if (!$this->io->isInteractive()) {
                     throw $e;
@@ -294,8 +311,15 @@ class DownloadManager
 
         // 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
-        $initialDownloader->remove($initial, $targetDir);
-        $this->install($target, $targetDir);
+        $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);
     }
 
     /**
@@ -303,12 +327,32 @@ class DownloadManager
      *
      * @param PackageInterface $package   package instance
      * @param string           $targetDir target dir
+     *
+     * @return PromiseInterface|null
      */
     public function remove(PackageInterface $package, $targetDir)
     {
         $downloader = $this->getDownloaderForPackage($package);
         if ($downloader) {
-            $downloader->remove($package, $targetDir);
+            return $downloader->remove($package, $targetDir);
+        }
+    }
+
+    /**
+     * Cleans up a failed operation
+     *
+     * @param string                $type         one of install/update/uninstall
+     * @param PackageInterface      $package      package instance
+     * @param string                $targetDir    target dir
+     * @param PackageInterface|null $prevPackage  previous package instance in case of updates
+     *
+     * @return PromiseInterface|null
+     */
+    public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
+    {
+        $downloader = $this->getDownloaderForPackage($package);
+        if ($downloader) {
+            return $downloader->cleanup($type, $package, $targetDir, $prevPackage);
         }
     }
 

+ 34 - 3
src/Composer/Downloader/DownloaderInterface.php

@@ -31,14 +31,30 @@ interface DownloaderInterface
     public function getInstallationSource();
 
     /**
-     * This should do any network-related tasks to prepare for install/update
+     * This should do any network-related tasks to prepare for an upcoming install/update
      *
      * @return PromiseInterface|null
      */
-    public function download(PackageInterface $package, $path);
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null);
 
     /**
-     * Downloads specific package into specific folder.
+     * Do anything that needs to be done between all downloads have been completed and the actual operation is executed
+     *
+     * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore
+     * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or
+     * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can
+     * be undone as much as possible.
+     *
+     * @param  string                $type        one of install/update/uninstall
+     * @param  PackageInterface      $package     package instance
+     * @param  string                $path        download path
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
+     */
+    public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null);
+
+    /**
+     * Installs specific package into specific folder.
      *
      * @param PackageInterface $package package instance
      * @param string           $path    download path
@@ -61,4 +77,19 @@ interface DownloaderInterface
      * @param string           $path    download path
      */
     public function remove(PackageInterface $package, $path);
+
+    /**
+     * 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  string                $path        download path
+     * @param  PackageInterface      $prevPackage previous package instance in case of an update
+     * @return PromiseInterface|null
+     */
+    public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null);
 }

+ 21 - 4
src/Composer/Downloader/FileDownloader.php

@@ -84,7 +84,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
     /**
      * {@inheritDoc}
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         if (!$package->getDistUrl()) {
             throw new \InvalidArgumentException('The given package is missing url information');
@@ -101,7 +101,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
             );
         }
 
-        $this->filesystem->emptyDirectory($path);
+        $this->filesystem->ensureDirectoryExists($path);
         $fileName = $this->getFileName($package, $path);
 
         $io = $this->io;
@@ -176,7 +176,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
 
         $reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) {
             // clean up
-            $filesystem->removeDirectory($path);
+            if (file_exists($fileName)) {
+                $filesystem->unlink($fileName);
+            }
             $self->clearLastCacheWrite($package);
 
             if ($e instanceof TransportException) {
@@ -220,6 +222,20 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         return $download();
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
+    {
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -229,6 +245,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
             $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
         }
 
+        $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));
     }
@@ -333,7 +350,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         $e = null;
 
         try {
-            $res = $this->download($package, $targetDir.'_compare', false);
+            $res = $this->download($package, $targetDir.'_compare', null, false);
             $this->httpDownloader->wait();
             $res = $this->install($package, $targetDir.'_compare', false);
 

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

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

+ 70 - 33
src/Composer/Downloader/GitDownloader.php

@@ -29,6 +29,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     private $hasStashedChanges = false;
     private $hasDiscardedChanges = false;
     private $gitUtil;
+    private $cachedPackages = array();
 
     public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null)
     {
@@ -39,34 +40,49 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
      * {@inheritDoc}
      */
-    public function doInstall(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
     {
         GitUtil::cleanEnv();
-        $path = $this->normalizePath($path);
-        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
-        $ref = $package->getSourceReference();
-        $flag = Platform::isWindows() ? '/D ' : '';
 
-        // --dissociate option is only available since git 2.3.0-rc0
+        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
         $gitVersion = $this->gitUtil->getVersion();
-        $msg = "Cloning ".$this->getShortHash($ref);
 
-        $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer';
+        // --dissociate option is only available since git 2.3.0-rc0
         if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) {
-            $this->io->writeError('', true, IOInterface::DEBUG);
+            $this->io->writeError("  - Syncing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) into cache");
             $this->io->writeError(sprintf('    Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG);
-            try {
-                $this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref);
-                if (is_dir($cachePath)) {
-                    $command =
-                        'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% '
-                        . '&& cd '.$flag.'%path% '
-                        . '&& git remote set-url origin %url% && git remote add composer %url%';
-                    $msg = "Cloning ".$this->getShortHash($ref).' from cache';
-                }
-            } catch (\RuntimeException $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 %url% && git remote add composer %url%';
+        } else {
+            $msg = "Cloning ".$this->getShortHash($ref);
+            $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer';
+            if (getenv('COMPOSER_DISABLE_NETWORK')) {
+                throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting');
+            }
+        }
+
         $this->io->writeError($msg);
 
         $commandCallable = function ($url) use ($path, $command, $cachePath) {
@@ -99,30 +115,41 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         GitUtil::cleanEnv();
+        $path = $this->normalizePath($path);
         if (!$this->hasMetadataRepository($path)) {
             throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
         }
 
-        $updateOriginUrl = false;
-        if (
-            0 === $this->process->execute('git remote -v', $output, $path)
-            && preg_match('{^origin\s+(?P<url>\S+)}m', $output, $originMatch)
-            && preg_match('{^composer\s+(?P<url>\S+)}m', $output, $composerMatch)
-        ) {
-            if ($originMatch['url'] === $composerMatch['url'] && $composerMatch['url'] !== $target->getSourceUrl()) {
-                $updateOriginUrl = true;
+        $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
+        $ref = $target->getSourceReference();
+        $flag = Platform::isWindows() ? '/D ' : '';
+
+        if (!empty($this->cachedPackages[$target->getId()][$ref])) {
+            $msg = "Checking out ".$this->getShortHash($ref).' from cache';
+            $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %url%';
+        } 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)';
+            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)';
+        $this->io->writeError($msg);
 
-        $commandCallable = function ($url) use ($command, $ref) {
-            return sprintf($command, ProcessExecutor::escape($url), ProcessExecutor::escape($ref.'^{commit}'));
+        $commandCallable = function ($url) use ($ref, $command, $cachePath) {
+            return str_replace(
+                array('%url%', '%ref%', '%cachePath%'),
+                array(
+                    ProcessExecutor::escape($url),
+                    ProcessExecutor::escape($ref.'^{commit}'),
+                    ProcessExecutor::escape($cachePath),
+                ),
+                $command
+            );
         };
 
         $this->gitUtil->runCommand($commandCallable, $url, $path);
@@ -133,6 +160,16 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
             $target->setSourceReference($newRef);
         }
 
+        $updateOriginUrl = false;
+        if (
+            0 === $this->process->execute('git remote -v', $output, $path)
+            && preg_match('{^origin\s+(?P<url>\S+)}m', $output, $originMatch)
+            && preg_match('{^composer\s+(?P<url>\S+)}m', $output, $composerMatch)
+        ) {
+            if ($originMatch['url'] === $composerMatch['url'] && $composerMatch['url'] !== $target->getSourceUrl()) {
+                $updateOriginUrl = true;
+            }
+        }
         if ($updateOriginUrl) {
             $this->updateOriginUrl($path, $target->getSourceUrl());
         }

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

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

+ 32 - 2
src/Composer/Downloader/PathDownloader.php

@@ -37,7 +37,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
     /**
      * {@inheritdoc}
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         $url = $package->getDistUrl();
         $realUrl = realpath($url);
@@ -49,6 +49,10 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
             ));
         }
 
+        if (realpath($path) === $realUrl) {
+            return;
+        }
+
         if (strpos(realpath($path) . DIRECTORY_SEPARATOR, $realUrl . DIRECTORY_SEPARATOR) === 0) {
             // IMPORTANT NOTICE: If you wish to change this, don't. You are wasting your time and ours.
             //
@@ -71,6 +75,20 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
         $url = $package->getDistUrl();
         $realUrl = realpath($url);
 
+        if (realpath($path) === $realUrl) {
+            if ($output) {
+                $this->io->writeError(sprintf(
+                    '  - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
+                    $package->getName(),
+                    $package->getFullPrettyVersion()
+                ));
+            } else {
+                $this->io->writeError('Source already present', false);
+            }
+
+            return;
+        }
+
         // Get the transport options with default values
         $transportOptions = $package->getTransportOptions() + array('symlink' => null);
 
@@ -147,7 +165,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
             $fileSystem->mirror($realUrl, $path, $iterator);
         }
 
-        $this->io->writeError('');
+        if ($output) {
+            $this->io->writeError('');
+        }
     }
 
     /**
@@ -155,6 +175,16 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
      */
     public function remove(PackageInterface $package, $path, $output = true)
     {
+        $realUrl = realpath($package->getDistUrl());
+
+        if ($path === $realUrl) {
+            if ($output) {
+                $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>), source is still present in $path");
+            }
+
+            return;
+        }
+
         /**
          * For junctions don't blindly rely on Filesystem::removeDirectory as it may be overzealous. If a process
          * inadvertently locks the file the removal will fail, but it would fall back to recursive delete which

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

@@ -24,6 +24,14 @@ class PerforceDownloader extends VcsDownloader
     /** @var Perforce */
     protected $perforce;
 
+    /**
+     * {@inheritDoc}
+     */
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
+    {
+
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -76,7 +84,7 @@ class PerforceDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         $this->doInstall($target, $path, $url);
     }
@@ -87,8 +95,6 @@ class PerforceDownloader extends VcsDownloader
     public function getLocalChanges(PackageInterface $package, $path)
     {
         $this->io->writeError('Perforce driver does not check for local changes before overriding', true);
-
-        return null;
     }
 
     /**

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

@@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doInstall(PackageInterface $package, $path, $url)
+    protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
+    {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function doInstall(PackageInterface $package, $path, $url)
     {
         SvnUtil::cleanEnv();
         $ref = $package->getSourceReference();
@@ -48,7 +56,7 @@ class SvnDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
     {
         SvnUtil::cleanEnv();
         $ref = $target->getSourceReference();

+ 99 - 34
src/Composer/Downloader/VcsDownloader.php

@@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -54,9 +55,57 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     /**
      * {@inheritDoc}
      */
-    public function download(PackageInterface $package, $path)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null)
     {
-        // noop for now, ideally we would do a git fetch already here, or make sure the cached git repo is synced, etc.
+        if (!$package->getSourceReference()) {
+            throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
+        }
+
+        $urls = $this->prepareUrls($package->getSourceUrls());
+
+        while ($url = array_shift($urls)) {
+            try {
+                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');
+                }
+                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);
+        }
     }
 
     /**
@@ -69,32 +118,10 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
         }
 
         $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
-        $this->filesystem->emptyDirectory($path);
 
-        $urls = $package->getSourceUrls();
+        $urls = $this->prepareUrls($package->getSourceUrls());
         while ($url = array_shift($urls)) {
             try {
-                if (Filesystem::isLocalPath($url)) {
-                    // realpath() below will not understand
-                    // url that starts with "file://"
-                    $needle = 'file://';
-                    $isFileProtocol = false;
-                    if (0 === strpos($url, $needle)) {
-                        $url = substr($url, strlen($needle));
-                        $isFileProtocol = true;
-                    }
-
-                    // realpath() below will not understand %20 spaces etc.
-                    if (false !== strpos($url, '%')) {
-                        $url = rawurldecode($url);
-                    }
-
-                    $url = realpath($url);
-
-                    if ($isFileProtocol) {
-                        $url = $needle . $url;
-                    }
-                }
                 $this->doInstall($package, $path, $url);
                 break;
             } catch (\Exception $e) {
@@ -141,15 +168,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
         $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
 
-        $this->cleanChanges($initial, $path, true);
-        $urls = $target->getSourceUrls();
+        $urls = $this->prepareUrls($target->getSourceUrls());
 
         $exception = null;
         while ($url = array_shift($urls)) {
             try {
-                if (Filesystem::isLocalPath($url)) {
-                    $url = realpath($url);
-                }
                 $this->doUpdate($initial, $target, $path, $url);
 
                 $exception = null;
@@ -167,8 +190,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
             }
         }
 
-        $this->reapplyChanges($path);
-
         // print the commit logs if in verbose mode and VCS metadata is present
         // because in case of missing metadata code would trigger another exception
         if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) {
@@ -204,7 +225,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     public function remove(PackageInterface $package, $path)
     {
         $this->io->writeError("  - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
-        $this->cleanChanges($package, $path, false);
         if (!$this->filesystem->removeDirectory($path)) {
             throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
         }
@@ -243,7 +263,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     }
 
     /**
-     * Guarantee that no changes have been made to the local copy
+     * Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not)
      *
      * @param  string            $path
      * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly
@@ -252,12 +272,26 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
     {
     }
 
+    /**
+     * Downloads data needed to run an install/update later
+     *
+     * @param PackageInterface      $package     package instance
+     * @param string                $path        download path
+     * @param string                $url         package url
+     * @param PackageInterface|null $prevPackage previous package (in case of an update)
+     *
+     * @return PromiseInterface|null
+     */
+    abstract protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null);
+
     /**
      * Downloads specific package into specific folder.
      *
      * @param PackageInterface $package package instance
      * @param string           $path    download path
      * @param string           $url     package url
+     *
+     * @return PromiseInterface|null
      */
     abstract protected function doInstall(PackageInterface $package, $path, $url);
 
@@ -268,6 +302,8 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * @param PackageInterface $target  updated package
      * @param string           $path    download path
      * @param string           $url     package url
+     *
+     * @return PromiseInterface|null
      */
     abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url);
 
@@ -289,4 +325,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * @return bool
      */
     abstract protected function hasMetadataRepository($path);
+
+    private function prepareUrls(array $urls)
+    {
+        foreach ($urls as $index => $url) {
+            if (Filesystem::isLocalPath($url)) {
+                // realpath() below will not understand
+                // url that starts with "file://"
+                $fileProtocol = 'file://';
+                $isFileProtocol = false;
+                if (0 === strpos($url, $fileProtocol)) {
+                    $url = substr($url, strlen($fileProtocol));
+                    $isFileProtocol = true;
+                }
+
+                // realpath() below will not understand %20 spaces etc.
+                if (false !== strpos($url, '%')) {
+                    $url = rawurldecode($url);
+                }
+
+                $urls[$index] = realpath($url);
+
+                if ($isFileProtocol) {
+                    $urls[$index] = $fileProtocol . $urls[$index];
+                }
+            }
+        }
+
+        return $urls;
+    }
 }

+ 2 - 2
src/Composer/Downloader/ZipDownloader.php

@@ -47,7 +47,7 @@ class ZipDownloader extends ArchiveDownloader
     /**
      * {@inheritDoc}
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         if (null === self::$hasSystemUnzip) {
             $finder = new ExecutableFinder;
@@ -76,7 +76,7 @@ class ZipDownloader extends ArchiveDownloader
             }
         }
 
-        return parent::download($package, $path, $output);
+        return parent::download($package, $path, $prevPackage, $output);
     }
 
     /**

+ 3 - 1
src/Composer/EventDispatcher/EventDispatcher.php

@@ -201,7 +201,9 @@ class EventDispatcher
 
                     try {
                         /** @var InstallerEvent $event */
-                        $return = $this->dispatch($scriptName, 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);
+                        $return = $this->dispatch($scriptName, $scriptEvent);
                     } catch (ScriptExecutionException $e) {
                         $this->io->writeError(sprintf('<error>Script %s was called via %s</error>', $callable, $event->getName()), true, IOInterface::QUIET);
                         throw $e;

+ 1 - 1
src/Composer/Factory.php

@@ -392,7 +392,7 @@ class Factory
                 ? substr($composerFile, 0, -4).'lock'
                 : $composerFile . '.lock';
 
-            $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);
         }
 

+ 8 - 0
src/Composer/IO/BaseIO.php

@@ -28,6 +28,14 @@ abstract class BaseIO implements IOInterface
         return $this->authentications;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function resetAuthentications()
+    {
+        $this->authentications = array();
+    }
+
     /**
      * {@inheritDoc}
      */

+ 1 - 1
src/Composer/Installer.php

@@ -632,7 +632,7 @@ class Installer
             $this->installationManager->execute($localRepo, $operation);
 
             if ($this->executeOperations) {
-                $localRepo->write();
+                $localRepo->write($this->devMode, $this->installationManager);
             }
 
             $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType);

+ 52 - 6
src/Composer/Installer/InstallationManager.php

@@ -177,11 +177,52 @@ class InstallationManager
             $promise = $installer->download($target, $operation->getInitialPackage());
         }
 
-        if (isset($promise)) {
+        if (!empty($promise)) {
             $this->loop->wait(array($promise));
         }
 
-        $this->$method($repo, $operation);
+        $e = null;
+        try {
+            if ($method === 'install' || $method === 'uninstall') {
+                $package = $operation->getPackage();
+                $installer = $this->getInstaller($package->getType());
+                $promise = $installer->prepare($method, $package);
+            } elseif ($method === 'update') {
+                $target = $operation->getTargetPackage();
+                $targetType = $target->getType();
+                $installer = $this->getInstaller($targetType);
+                $promise = $installer->prepare('update', $target, $operation->getInitialPackage());
+            }
+
+            if (!empty($promise)) {
+                $this->loop->wait(array($promise));
+            }
+
+            $promise = $this->$method($repo, $operation);
+            if (!empty($promise)) {
+                $this->loop->wait(array($promise));
+            }
+        } catch (\Exception $e) {
+        }
+
+        if ($method === 'install' || $method === 'uninstall') {
+            $package = $operation->getPackage();
+            $installer = $this->getInstaller($package->getType());
+            $promise = $installer->cleanup($method, $package);
+        } elseif ($method === 'update') {
+            $target = $operation->getTargetPackage();
+            $targetType = $target->getType();
+            $installer = $this->getInstaller($targetType);
+            $promise = $installer->cleanup('update', $target, $operation->getInitialPackage());
+        }
+
+        if (!empty($promise)) {
+            $this->loop->wait(array($promise));
+        }
+
+        if ($e) {
+            throw $e;
+        }
     }
 
     /**
@@ -194,8 +235,10 @@ class InstallationManager
     {
         $package = $operation->getPackage();
         $installer = $this->getInstaller($package->getType());
-        $installer->install($repo, $package);
+        $promise = $installer->install($repo, $package);
         $this->markForNotification($package);
+
+        return $promise;
     }
 
     /**
@@ -214,13 +257,15 @@ class InstallationManager
 
         if ($initialType === $targetType) {
             $installer = $this->getInstaller($initialType);
-            $installer->update($repo, $initial, $target);
+            $promise = $installer->update($repo, $initial, $target);
             $this->markForNotification($target);
         } else {
             $this->getInstaller($initialType)->uninstall($repo, $initial);
             $installer = $this->getInstaller($targetType);
-            $installer->install($repo, $target);
+            $promise = $installer->install($repo, $target);
         }
+
+        return $promise;
     }
 
     /**
@@ -233,7 +278,8 @@ class InstallationManager
     {
         $package = $operation->getPackage();
         $installer = $this->getInstaller($package->getType());
-        $installer->uninstall($repo, $package);
+
+        return $installer->uninstall($repo, $package);
     }
 
     /**

+ 41 - 9
src/Composer/Installer/InstallerInterface.php

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

+ 25 - 0
src/Composer/Installer/LibraryInstaller.php

@@ -85,6 +85,9 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
         return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath);
     }
 
+    /**
+     * {@inheritDoc}
+     */
     public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
         $this->initializeVendorDir();
@@ -93,6 +96,28 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
         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}
      */

+ 16 - 0
src/Composer/Installer/MetapackageInstaller.php

@@ -55,6 +55,22 @@ class MetapackageInstaller implements InstallerInterface
         // noop
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        // noop
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        // noop
+    }
+
     /**
      * {@inheritDoc}
      */

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

@@ -47,6 +47,20 @@ class NoopInstaller implements InstallerInterface
     {
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+    }
+
     /**
      * {@inheritDoc}
      */

+ 16 - 6
src/Composer/Installer/PluginInstaller.php

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

+ 16 - 0
src/Composer/Installer/ProjectInstaller.php

@@ -71,6 +71,22 @@ class ProjectInstaller implements InstallerInterface
         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}
      */

+ 3 - 2
src/Composer/Json/JsonManipulator.php

@@ -326,9 +326,10 @@ class JsonManipulator
         }
 
         // try and find a match for the subkey
-        if ($this->pregMatch('{"'.preg_quote($name).'"\s*:}i', $children)) {
+        $keyRegex = str_replace('/', '\\\\?/', preg_quote($name));
+        if ($this->pregMatch('{"'.$keyRegex.'"\s*:}i', $children)) {
             // find best match for the value of "name"
-            if (preg_match_all('{'.self::$DEFINES.'"'.preg_quote($name).'"\s*:\s*(?:(?&json))}x', $children, $matches)) {
+            if (preg_match_all('{'.self::$DEFINES.'"'.$keyRegex.'"\s*:\s*(?:(?&json))}x', $children, $matches)) {
                 $bestMatch = '';
                 foreach ($matches[0] as $match) {
                     if (strlen($bestMatch) < strlen($match)) {

+ 1 - 5
src/Composer/Package/Locker.php

@@ -33,8 +33,6 @@ class Locker
 {
     /** @var JsonFile */
     private $lockFile;
-    /** @var RepositoryManager */
-    private $repositoryManager;
     /** @var InstallationManager */
     private $installationManager;
     /** @var string */
@@ -55,14 +53,12 @@ class Locker
      *
      * @param IOInterface         $io
      * @param JsonFile            $lockFile             lockfile loader
-     * @param RepositoryManager   $repositoryManager    repository manager instance
      * @param InstallationManager $installationManager  installation manager instance
      * @param string              $composerFileContents The contents of the composer file
      */
-    public function __construct(IOInterface $io, JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $composerFileContents)
+    public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents)
     {
         $this->lockFile = $lockFile;
-        $this->repositoryManager = $repositoryManager;
         $this->installationManager = $installationManager;
         $this->hash = md5($composerFileContents);
         $this->contentHash = self::getContentHash($composerFileContents);

+ 18 - 0
src/Composer/Plugin/PluginInterface.php

@@ -36,4 +36,22 @@ interface PluginInterface
      * @param IOInterface $io
      */
     public function activate(Composer $composer, IOInterface $io);
+
+    /**
+     * Remove any hooks from Composer
+     *
+     * @param Composer    $composer
+     * @param IOInterface $io
+     */
+    public function deactivate(Composer $composer, IOInterface $io);
+
+    /**
+     * Prepare the plugin to be uninstalled
+     *
+     * This will be called after deactivate
+     *
+     * @param Composer    $composer
+     * @param IOInterface $io
+     */
+    public function uninstall(Composer $composer, IOInterface $io);
 }

+ 106 - 2
src/Composer/Plugin/PluginManager.php

@@ -145,7 +145,7 @@ class PluginManager
 
         $oldInstallerPlugin = ($package->getType() === 'composer-installer');
 
-        if (in_array($package->getName(), $this->registeredPlugins)) {
+        if (isset($this->registeredPlugins[$package->getName()])) {
             return;
         }
 
@@ -201,16 +201,82 @@ class PluginManager
             if ($oldInstallerPlugin) {
                 $installer = new $class($this->io, $this->composer);
                 $this->composer->getInstallationManager()->addInstaller($installer);
+                $this->registeredPlugins[$package->getName()] = $installer;
             } elseif (class_exists($class)) {
                 $plugin = new $class();
                 $this->addPlugin($plugin);
-                $this->registeredPlugins[] = $package->getName();
+                $this->registeredPlugins[$package->getName()] = $plugin;
             } elseif ($failOnMissingClasses) {
                 throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class);
             }
         }
     }
 
+    /**
+     * Deactivates a plugin package
+     *
+     * If it's of type composer-installer it is unregistered from the installers
+     * instead for BC
+     *
+     * @param PackageInterface $package
+     *
+     * @throws \UnexpectedValueException
+     */
+    public function deactivatePackage(PackageInterface $package)
+    {
+        if ($this->disablePlugins) {
+            return;
+        }
+
+        $oldInstallerPlugin = ($package->getType() === 'composer-installer');
+
+        if (!isset($this->registeredPlugins[$package->getName()])) {
+            return;
+        }
+
+        if ($oldInstallerPlugin) {
+            $installer = $this->registeredPlugins[$package->getName()];
+            unset($this->registeredPlugins[$package->getName()]);
+            $this->composer->getInstallationManager()->removeInstaller($installer);
+        } else {
+            $plugin = $this->registeredPlugins[$package->getName()];
+            unset($this->registeredPlugins[$package->getName()]);
+            $this->removePlugin($plugin);
+        }
+    }
+
+    /**
+     * Uninstall a plugin package
+     *
+     * If it's of type composer-installer it is unregistered from the installers
+     * instead for BC
+     *
+     * @param PackageInterface $package
+     *
+     * @throws \UnexpectedValueException
+     */
+    public function uninstallPackage(PackageInterface $package)
+    {
+        if ($this->disablePlugins) {
+            return;
+        }
+
+        $oldInstallerPlugin = ($package->getType() === 'composer-installer');
+
+        if (!isset($this->registeredPlugins[$package->getName()])) {
+            return;
+        }
+
+        if ($oldInstallerPlugin) {
+            $this->deactivatePackage($package);
+        } else {
+            $plugin = $this->registeredPlugins[$package->getName()];
+            unset($this->registeredPlugins[$package->getName()]);
+            $this->removePlugin($plugin);
+            $this->uninstallPlugin($plugin);
+        }
+    }
+
     /**
      * Returns the version of the internal composer-plugin-api package.
      *
@@ -241,6 +307,44 @@ class PluginManager
         }
     }
 
+    /**
+     * Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance
+     *
+     * Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer
+     * programmatically and want to deregister a plugin class directly this is a valid way
+     * to do it.
+     *
+     * @param PluginInterface $plugin plugin instance
+     */
+    public function removePlugin(PluginInterface $plugin)
+    {
+        $index = array_search($plugin, $this->plugins, true);
+        if ($index === false) {
+            return;
+        }
+
+        $this->io->writeError('Unloading plugin '.get_class($plugin), true, IOInterface::DEBUG);
+        unset($this->plugins[$index]);
+        $plugin->deactivate($this->composer, $this->io);
+
+        $this->composer->getEventDispatcher()->removeListener($plugin);
+    }
+
+    /**
+     * Notifies a plugin it is being uninstalled and should clean up
+     *
+     * Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer
+     * programmatically and want to deregister a plugin class directly this is a valid way
+     * to do it.
+     *
+     * @param PluginInterface $plugin plugin instance
+     */
+    public function uninstallPlugin(PluginInterface $plugin)
+    {
+        $this->io->writeError('Uninstalling plugin '.get_class($plugin), true, IOInterface::DEBUG);
+        $plugin->uninstall($this->composer, $this->io);
+    }
+
     /**
      * Load all plugins and installers from a repository
      *

+ 4 - 64
src/Composer/Repository/ArtifactRepository.php

@@ -16,6 +16,7 @@ use Composer\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\Loader\LoaderInterface;
+use Composer\Util\Zip;
 
 /**
  * @author Serge Smertin <serg.smertin@gmail.com>
@@ -80,76 +81,15 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
         }
     }
 
-    /**
-     * Find a file by name, returning the one that has the shortest path.
-     *
-     * @param \ZipArchive $zip
-     * @param string $filename
-     * @return bool|int
-     */
-    private function locateFile(\ZipArchive $zip, $filename)
-    {
-        $indexOfShortestMatch = false;
-        $lengthOfShortestMatch = -1;
-
-        for ($i = 0; $i < $zip->numFiles; $i++) {
-            $stat = $zip->statIndex($i);
-            if (strcmp(basename($stat['name']), $filename) === 0) {
-                $directoryName = dirname($stat['name']);
-                if ($directoryName == '.') {
-                    //if composer.json is in root directory
-                    //it has to be the one to use.
-                    return $i;
-                }
-
-                if (strpos($directoryName, '\\') !== false ||
-                   strpos($directoryName, '/') !== false) {
-                    //composer.json files below first directory are rejected
-                    continue;
-                }
-
-                $length = strlen($stat['name']);
-                if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) {
-                    //Check it's not a directory.
-                    $contents = $zip->getFromIndex($i);
-                    if ($contents !== false) {
-                        $indexOfShortestMatch = $i;
-                        $lengthOfShortestMatch = $length;
-                    }
-                }
-            }
-        }
-
-        return $indexOfShortestMatch;
-    }
-
     private function getComposerInformation(\SplFileInfo $file)
     {
-        $zip = new \ZipArchive();
-        if ($zip->open($file->getPathname()) !== true) {
-            return false;
-        }
-
-        if (0 == $zip->numFiles) {
-            $zip->close();
+        $json = Zip::getComposerJson($file->getPathname());
 
+        if (null === $json) {
             return false;
         }
 
-        $foundFileIndex = $this->locateFile($zip, 'composer.json');
-        if (false === $foundFileIndex) {
-            $zip->close();
-
-            return false;
-        }
-
-        $configurationFileName = $zip->getNameIndex($foundFileIndex);
-        $zip->close();
-
-        $composerFile = "zip://{$file->getPathname()}#$configurationFileName";
-        $json = file_get_contents($composerFile);
-
-        $package = JsonFile::parseJson($json, $composerFile);
+        $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json');
         $package['dist'] = array(
             'type' => 'zip',
             'url' => strtr($file->getPathname(), '\\', '/'),

+ 17 - 5
src/Composer/Repository/FilesystemRepository.php

@@ -15,6 +15,8 @@ namespace Composer\Repository;
 use Composer\Json\JsonFile;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\Dumper\ArrayDumper;
+use Composer\Installer\InstallationManager;
+use Composer\Util\Filesystem;
 
 /**
  * Filesystem repository.
@@ -49,7 +51,12 @@ class FilesystemRepository extends WritableArrayRepository
         }
 
         try {
-            $packages = $this->file->read();
+            $data = $this->file->read();
+            if (isset($data['packages'])) {
+                $packages = $data['packages'];
+            } else {
+                $packages = $data;
+            }
 
             // forward compatibility for composer v2 installed.json
             if (isset($packages['packages'])) {
@@ -79,16 +86,21 @@ class FilesystemRepository extends WritableArrayRepository
     /**
      * Writes writable repository.
      */
-    public function write()
+    public function write($devMode, InstallationManager $installationManager)
     {
-        $data = array();
+        $data = array('packages' => array(), 'dev' => $devMode);
         $dumper = new ArrayDumper();
+        $fs = new Filesystem();
+        $repoDir = dirname($fs->normalizePath($this->file->getPath()));
 
         foreach ($this->getCanonicalPackages() as $package) {
-            $data[] = $dumper->dump($package);
+            $pkgArray = $dumper->dump($package);
+            $path = $installationManager->getInstallPath($package);
+            $pkgArray['install-path'] = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $path, true) : null;
+            $data['packages'][] = $pkgArray;
         }
 
-        usort($data, function ($a, $b) {
+        usort($data['packages'], function ($a, $b) {
             return strcmp($a['name'], $b['name']);
         });
 

+ 6 - 3
src/Composer/Repository/Vcs/GitHubDriver.php

@@ -19,7 +19,6 @@ use Composer\Cache;
 use Composer\IO\IOInterface;
 use Composer\Util\GitHub;
 use Composer\Util\Http\Response;
-use Composer\Util\RemoteFilesystem;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -308,6 +307,10 @@ class GitHubDriver extends VcsDriver
      */
     protected function generateSshUrl()
     {
+        if (false !== strpos($this->originUrl, ':')) {
+            return 'ssh://git@' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git';
+        }
+
         return 'git@' . $this->originUrl . ':'.$this->owner.'/'.$this->repository.'.git';
     }
 
@@ -342,10 +345,10 @@ class GitHubDriver extends VcsDriver
                     $scopesIssued = array();
                     $scopesNeeded = array();
                     if ($headers = $e->getHeaders()) {
-                        if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-OAuth-Scopes')) {
+                        if ($scopes = Response::findHeaderValue($headers, 'X-OAuth-Scopes')) {
                             $scopesIssued = explode(' ', $scopes);
                         }
-                        if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) {
+                        if ($scopes = Response::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) {
                             $scopesNeeded = explode(' ', $scopes);
                         }
                     }

+ 21 - 14
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -68,9 +68,9 @@ class GitLabDriver extends VcsDriver
     private $isPrivate = true;
 
     /**
-     * @var int port number
+     * @var bool true if the origin has a port number or a path component in it
      */
-    protected $portNumber;
+    private $hasNonstandardOrigin = false;
 
     const URL_REGEX = '#^(?:(?P<scheme>https?)://(?P<domain>.+?)(?::(?P<port>[0-9]+))?/|git@(?P<domain2>[^:]+):)(?P<parts>.+)/(?P<repo>[^/]+?)(?:\.git|/)?$#';
 
@@ -95,11 +95,10 @@ class GitLabDriver extends VcsDriver
             ? $match['scheme']
             : (isset($this->repoConfig['secure-http']) && $this->repoConfig['secure-http'] === false ? 'http' : 'https')
         ;
-        $this->originUrl = $this->determineOrigin($configuredDomains, $guessedDomain, $urlParts);
+        $this->originUrl = $this->determineOrigin($configuredDomains, $guessedDomain, $urlParts, $match['port']);
 
-        if (!empty($match['port']) && true === is_numeric($match['port'])) {
-            // If it is an HTTP based URL, and it has a port
-            $this->portNumber = (int) $match['port'];
+        if (false !== strpos($this->originUrl, ':') || false !== strpos($this->originUrl, '/')) {
+            $this->hasNonstandardOrigin = true;
         }
 
         $this->namespace = implode('/', $urlParts);
@@ -260,10 +259,7 @@ class GitLabDriver extends VcsDriver
      */
     public function getApiUrl()
     {
-        $domainName = $this->originUrl;
-        $portNumber = (true === is_numeric($this->portNumber)) ? sprintf(':%s', $this->portNumber) : '';
-
-        return $this->scheme.'://'.$domainName.$portNumber.'/api/v4/projects/'.$this->urlEncodeAll($this->namespace).'%2F'.$this->urlEncodeAll($this->repository);
+        return $this->scheme.'://'.$this->originUrl.'/api/v4/projects/'.$this->urlEncodeAll($this->namespace).'%2F'.$this->urlEncodeAll($this->repository);
     }
 
     /**
@@ -362,6 +358,10 @@ class GitLabDriver extends VcsDriver
      */
     protected function generateSshUrl()
     {
+        if ($this->hasNonstandardOrigin) {
+            return 'ssh://git@'.$this->originUrl.'/'.$this->namespace.'/'.$this->repository.'.git';
+        }
+
         return 'git@' . $this->originUrl . ':'.$this->namespace.'/'.$this->repository.'.git';
     }
 
@@ -464,7 +464,7 @@ class GitLabDriver extends VcsDriver
         $guessedDomain = !empty($match['domain']) ? $match['domain'] : $match['domain2'];
         $urlParts = explode('/', $match['parts']);
 
-        if (false === self::determineOrigin((array) $config->get('gitlab-domains'), $guessedDomain, $urlParts)) {
+        if (false === self::determineOrigin((array) $config->get('gitlab-domains'), $guessedDomain, $urlParts, $match['port'])) {
             return false;
         }
 
@@ -495,16 +495,23 @@ class GitLabDriver extends VcsDriver
      * @param  array       $urlParts
      * @return bool|string
      */
-    private static function determineOrigin(array $configuredDomains, $guessedDomain, array &$urlParts)
+    private static function determineOrigin(array $configuredDomains, $guessedDomain, array &$urlParts, $portNumber)
     {
-        if (in_array($guessedDomain, $configuredDomains)) {
+        if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array($guessedDomain.':'.$portNumber, $configuredDomains))) {
+            if ($portNumber) {
+                return $guessedDomain.':'.$portNumber;
+            }
             return $guessedDomain;
         }
 
+        if ($portNumber) {
+            $guessedDomain .= ':'.$portNumber;
+        }
+
         while (null !== ($part = array_shift($urlParts))) {
             $guessedDomain .= '/' . $part;
 
-            if (in_array($guessedDomain, $configuredDomains)) {
+            if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array(preg_replace('{:\d+}', '', $guessedDomain), $configuredDomains))) {
                 return $guessedDomain;
             }
         }

+ 2 - 1
src/Composer/Repository/WritableArrayRepository.php

@@ -13,6 +13,7 @@
 namespace Composer\Repository;
 
 use Composer\Package\AliasPackage;
+use Composer\Installer\InstallationManager;
 
 /**
  * Writable array repository.
@@ -24,7 +25,7 @@ class WritableArrayRepository extends ArrayRepository implements WritableReposit
     /**
      * {@inheritDoc}
      */
-    public function write()
+    public function write($devMode, InstallationManager $installationManager)
     {
     }
 

+ 4 - 1
src/Composer/Repository/WritableRepositoryInterface.php

@@ -13,6 +13,7 @@
 namespace Composer\Repository;
 
 use Composer\Package\PackageInterface;
+use Composer\Installer\InstallationManager;
 
 /**
  * Writable repository interface.
@@ -23,8 +24,10 @@ interface WritableRepositoryInterface extends RepositoryInterface
 {
     /**
      * Writes repository (f.e. to the disc).
+     *
+     * @param bool $devMode Whether dev requirements were included or not in this installation
      */
-    public function write();
+    public function write($devMode, InstallationManager $installationManager);
 
     /**
      * Adds package to the repository.

+ 44 - 0
src/Composer/Script/Event.php

@@ -39,6 +39,11 @@ class Event extends BaseEvent
      */
     private $devMode;
 
+    /**
+     * @var BaseEvent
+     */
+    private $originatingEvent;
+
     /**
      * Constructor.
      *
@@ -55,6 +60,7 @@ class Event extends BaseEvent
         $this->composer = $composer;
         $this->io = $io;
         $this->devMode = $devMode;
+        $this->originatingEvent = null;
     }
 
     /**
@@ -86,4 +92,42 @@ class Event extends BaseEvent
     {
         return $this->devMode;
     }
+
+    /**
+     * Set the originating event.
+     *
+     * @return \Composer\EventDispatcher\Event|null
+     */
+    public function getOriginatingEvent()
+    {
+        return $this->originatingEvent;
+    }
+
+    /**
+     * Set the originating event.
+     *
+     * @param \Composer\EventDispatcher\Event $event
+     * @return $this
+     */
+    public function setOriginatingEvent(BaseEvent $event)
+    {
+        $this->originatingEvent = $this->calculateOriginatingEvent($event);
+
+        return $this;
+    }
+
+    /**
+     * Returns the upper-most event in chain.
+     *
+     * @param \Composer\EventDispatcher\Event $event
+     * @return \Composer\EventDispatcher\Event
+     */
+    private function calculateOriginatingEvent(BaseEvent $event)
+    {
+        if ($event instanceof Event && $event->getOriginatingEvent()) {
+            return $this->calculateOriginatingEvent($event->getOriginatingEvent());
+        }
+
+        return $event;
+    }
 }

+ 3 - 0
src/Composer/Util/ErrorHandler.php

@@ -33,6 +33,7 @@ class ErrorHandler
      *
      * @static
      * @throws \ErrorException
+     * @return bool
      */
     public static function handle($level, $message, $file, $line)
     {
@@ -63,6 +64,8 @@ class ErrorHandler
                 }, array_slice(debug_backtrace(), 2))));
             }
         }
+
+        return true;
     }
 
     /**

+ 5 - 3
src/Composer/Util/Git.php

@@ -224,6 +224,10 @@ class Git
 
     public function syncMirror($url, $dir)
     {
+        if (getenv('COMPOSER_DISABLE_NETWORK')) {
+            return false;
+        }
+
         // update the repo if it is a valid git repository
         if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') {
             try {
@@ -260,9 +264,7 @@ class Git
             }
         }
 
-        $this->syncMirror($url, $dir);
-
-        return false;
+        return $this->syncMirror($url, $dir);
     }
 
     private function isAuthenticationFailure($url, &$match)

+ 10 - 1
src/Composer/Util/GitLab.php

@@ -57,7 +57,10 @@ class GitLab
      */
     public function authorizeOAuth($originUrl)
     {
-        if (!in_array($originUrl, $this->config->get('gitlab-domains'), true)) {
+        // before composer 1.9, origin URLs had no port number in them
+        $bcOriginUrl = preg_replace('{:\d+}', '', $originUrl);
+
+        if (!in_array($originUrl, $this->config->get('gitlab-domains'), true) && !in_array($bcOriginUrl, $this->config->get('gitlab-domains'), true)) {
             return false;
         }
 
@@ -77,6 +80,12 @@ class GitLab
             return true;
         }
 
+        if (isset($authTokens[$bcOriginUrl])) {
+            $this->io->setAuthentication($originUrl, $authTokens[$bcOriginUrl], 'private-token');
+
+            return true;
+        }
+
         return false;
     }
 

+ 0 - 1
src/Composer/Util/Http/CurlDownloader.php

@@ -16,7 +16,6 @@ use Composer\Config;
 use Composer\IO\IOInterface;
 use Composer\Downloader\TransportException;
 use Composer\CaBundle\CaBundle;
-use Composer\Util\RemoteFilesystem;
 use Composer\Util\StreamContextFactory;
 use Composer\Util\AuthHelper;
 use Composer\Util\Url;

+ 24 - 14
src/Composer/Util/Http/Response.php

@@ -61,20 +61,7 @@ class Response
 
     public function getHeader($name)
     {
-        $value = null;
-        foreach ($this->headers as $header) {
-            if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) {
-                $value = $match[1];
-            } elseif (preg_match('{^HTTP/}i', $header)) {
-                // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary
-                //
-                // In case of redirects, headers contains the headers of all responses
-                // so we reset the flag when a new response is being parsed as we are only interested in the last response
-                $value = null;
-            }
-        }
-
-        return $value;
+        return self::findHeaderValue($this->headers, $name);
     }
 
     public function getBody()
@@ -91,4 +78,27 @@ class Response
     {
         $this->request = $this->code = $this->headers = $this->body = null;
     }
+
+    /**
+     * @param  array       $headers array of returned headers like from getLastHeaders()
+     * @param  string      $name    header name (case insensitive)
+     * @return string|null
+     */
+    public static function findHeaderValue(array $headers, $name)
+    {
+        $value = null;
+        foreach ($headers as $header) {
+            if (preg_match('{^'.preg_quote($name).':\s*(.+?)\s*$}i', $header, $match)) {
+                $value = $match[1];
+            } elseif (preg_match('{^HTTP/}i', $header)) {
+                // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary
+                //
+                // In case of redirects, http_response_headers contains the headers of all responses
+                // so we reset the flag when a new response is being parsed as we are only interested in the last response
+                $value = null;
+            }
+        }
+
+        return $value;
+    }
 }

+ 0 - 2
src/Composer/Util/Perforce.php

@@ -363,8 +363,6 @@ class Perforce
         while ($line !== false) {
             $line = fgets($pipe);
         }
-
-        return;
     }
 
     public function windowsLogin($password)

+ 11 - 27
src/Composer/Util/RemoteFilesystem.php

@@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
 use Composer\Downloader\TransportException;
 use Composer\CaBundle\CaBundle;
 use Composer\Util\HttpDownloader;
+use Composer\Util\Http\Response;
 
 /**
  * @author François Pluchino <francois.pluchino@opendisplay.com>
@@ -143,27 +144,6 @@ class RemoteFilesystem
         return $this->lastHeaders;
     }
 
-    /**
-     * @param  array       $headers array of returned headers like from getLastHeaders()
-     * @param  string      $name    header name (case insensitive)
-     * @return string|null
-     */
-    public static function findHeaderValue(array $headers, $name)
-    {
-        $value = null;
-        foreach ($headers as $header) {
-            if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) {
-                $value = $match[1];
-            } elseif (preg_match('{^HTTP/}i', $header)) {
-                // In case of redirects, http_response_headers contains the headers of all responses
-                // so we reset the flag when a new response is being parsed as we are only interested in the last response
-                $value = null;
-            }
-        }
-
-        return $value;
-    }
-
     /**
      * @param  array    $headers array of returned headers like from getLastHeaders()
      * @return int|null
@@ -286,13 +266,15 @@ class RemoteFilesystem
                 $errorMessage .= "\n";
             }
             $errorMessage .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg);
+
+            return true;
         });
         try {
             $result = $this->getRemoteContents($originUrl, $fileUrl, $ctx, $http_response_header);
 
             if (!empty($http_response_header[0])) {
                 $statusCode = $this->findStatusCode($http_response_header);
-                if ($statusCode >= 400 && $this->findHeaderValue($http_response_header, 'content-type') === 'application/json') {
+                if ($statusCode >= 400 && Response::findHeaderValue($http_response_header, 'content-type') === 'application/json') {
                     HttpDownloader::outputWarnings($this->io, $originUrl, json_decode($result, true));
                 }
 
@@ -301,7 +283,7 @@ class RemoteFilesystem
                 }
             }
 
-            $contentLength = !empty($http_response_header[0]) ? $this->findHeaderValue($http_response_header, 'content-length') : null;
+            $contentLength = !empty($http_response_header[0]) ? Response::findHeaderValue($http_response_header, 'content-length') : null;
             if ($contentLength && Platform::strlen($result) < $contentLength) {
                 // alas, this is not possible via the stream callback because STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP
                 $e = new TransportException('Content-Length mismatch, received '.Platform::strlen($result).' bytes out of the expected '.$contentLength);
@@ -358,8 +340,8 @@ class RemoteFilesystem
         $locationHeader = null;
         if (!empty($http_response_header[0])) {
             $statusCode = $this->findStatusCode($http_response_header);
-            $contentType = $this->findHeaderValue($http_response_header, 'content-type');
-            $locationHeader = $this->findHeaderValue($http_response_header, 'location');
+            $contentType = Response::findHeaderValue($http_response_header, 'content-type');
+            $locationHeader = Response::findHeaderValue($http_response_header, 'location');
         }
 
         // check for bitbucket login page asking to authenticate
@@ -415,7 +397,7 @@ class RemoteFilesystem
 
         // decode gzip
         if ($result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http' && !$hasFollowedRedirect) {
-            $contentEncoding = $this->findHeaderValue($http_response_header, 'content-encoding');
+            $contentEncoding = Response::findHeaderValue($http_response_header, 'content-encoding');
             $decode = $contentEncoding && 'gzip' === strtolower($contentEncoding);
 
             if ($decode) {
@@ -459,6 +441,8 @@ class RemoteFilesystem
                     $errorMessage .= "\n";
                 }
                 $errorMessage .= preg_replace('{^file_put_contents\(.*?\): }', '', $msg);
+
+                return true;
             });
             $result = (bool) file_put_contents($fileName, $result);
             restore_error_handler();
@@ -696,7 +680,7 @@ class RemoteFilesystem
 
     private function handleRedirect(array $http_response_header, array $additionalOptions, $result)
     {
-        if ($locationHeader = $this->findHeaderValue($http_response_header, 'location')) {
+        if ($locationHeader = Response::findHeaderValue($http_response_header, 'location')) {
             if (parse_url($locationHeader, PHP_URL_SCHEME)) {
                 // Absolute URL; e.g. https://example.com/composer
                 $targetUrl = $locationHeader;

+ 0 - 2
src/Composer/Util/TlsHelper.php

@@ -19,8 +19,6 @@ use Composer\CaBundle\CaBundle;
  */
 final class TlsHelper
 {
-    private static $useOpensslParse;
-
     /**
      * Match hostname against a certificate.
      *

+ 3 - 0
src/Composer/Util/Url.php

@@ -70,6 +70,9 @@ class Url
         }
 
         $origin = (string) parse_url($url, PHP_URL_HOST);
+        if ($port = parse_url($url, PHP_URL_PORT)) {
+            $origin .= ':'.$port;
+        }
 
         if (strpos($origin, '.github.com') === (strlen($origin) - 11)) {
             return 'github.com';

+ 108 - 0
src/Composer/Util/Zip.php

@@ -0,0 +1,108 @@
+<?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\Util;
+
+/**
+ * @author Andreas Schempp <andreas.schempp@terminal42.ch>
+ */
+class Zip
+{
+    /**
+     * Gets content of the root composer.json inside a ZIP archive.
+     *
+     * @param string $pathToZip
+     * @param string $filename
+     *
+     * @return string|null
+     */
+    public static function getComposerJson($pathToZip)
+    {
+        if (!extension_loaded('zip')) {
+            throw new \RuntimeException('The Zip Util requires PHP\'s zip extension');
+        }
+
+        $zip = new \ZipArchive();
+        if ($zip->open($pathToZip) !== true) {
+            return null;
+        }
+
+        if (0 == $zip->numFiles) {
+            $zip->close();
+
+            return null;
+        }
+
+        $foundFileIndex = self::locateFile($zip, 'composer.json');
+        if (false === $foundFileIndex) {
+            $zip->close();
+
+            return null;
+        }
+
+        $content = null;
+        $configurationFileName = $zip->getNameIndex($foundFileIndex);
+        $stream = $zip->getStream($configurationFileName);
+
+        if (false !== $stream) {
+            $content = stream_get_contents($stream);
+        }
+
+        $zip->close();
+
+        return $content;
+    }
+
+    /**
+     * Find a file by name, returning the one that has the shortest path.
+     *
+     * @param \ZipArchive $zip
+     * @param string      $filename
+     *
+     * @return bool|int
+     */
+    private static function locateFile(\ZipArchive $zip, $filename)
+    {
+        $indexOfShortestMatch = false;
+        $lengthOfShortestMatch = -1;
+
+        for ($i = 0; $i < $zip->numFiles; $i++) {
+            $stat = $zip->statIndex($i);
+            if (strcmp(basename($stat['name']), $filename) === 0) {
+                $directoryName = dirname($stat['name']);
+                if ($directoryName === '.') {
+                    //if composer.json is in root directory
+                    //it has to be the one to use.
+                    return $i;
+                }
+
+                if (strpos($directoryName, '\\') !== false ||
+                    strpos($directoryName, '/') !== false) {
+                    //composer.json files below first directory are rejected
+                    continue;
+                }
+
+                $length = strlen($stat['name']);
+                if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) {
+                    //Check it's not a directory.
+                    $contents = $zip->getFromIndex($i);
+                    if ($contents !== false) {
+                        $indexOfShortestMatch = $i;
+                        $lengthOfShortestMatch = $length;
+                    }
+                }
+            }
+        }
+
+        return $indexOfShortestMatch;
+    }
+}

+ 4 - 4
tests/Composer/Test/AllFunctionalTest.php

@@ -162,18 +162,18 @@ class AllFunctionalTest extends TestCase
             }
         };
 
-        for ($i = 0, $c = count($tokens); $i < $c; $i++) {
-            if ('' === $tokens[$i] && null === $section) {
+        foreach ($tokens as $token) {
+            if ('' === $token && null === $section) {
                 continue;
             }
 
             // Handle section headers.
             if (null === $section) {
-                $section = $tokens[$i];
+                $section = $token;
                 continue;
             }
 
-            $sectionData = $tokens[$i];
+            $sectionData = $token;
 
             // Allow sections to validate, or modify their section data.
             switch ($section) {

+ 0 - 7
tests/Composer/Test/DependencyResolver/RuleSetTest.php

@@ -152,11 +152,4 @@ class RuleSetTest extends TestCase
 
         $this->assertContains('JOB     : Install command rule (install foo 2.1)', $ruleSet->getPrettyString($pool));
     }
-
-    private function getRuleMock()
-    {
-        return $this->getMockBuilder('Composer\DependencyResolver\Rule')
-            ->disableOriginalConstructor()
-            ->getMock();
-    }
 }

+ 6 - 2
tests/Composer/Test/Downloader/FossilDownloaderTest.php

@@ -48,7 +48,7 @@ class FossilDownloaderTest extends TestCase
     /**
      * @expectedException \InvalidArgumentException
      */
-    public function testDownloadForPackageWithoutSourceReference()
+    public function testInstallForPackageWithoutSourceReference()
     {
         $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
         $packageMock->expects($this->once())
@@ -59,7 +59,7 @@ class FossilDownloaderTest extends TestCase
         $downloader->install($packageMock, '/path');
     }
 
-    public function testDownload()
+    public function testInstall()
     {
         $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
         $packageMock->expects($this->any())
@@ -104,7 +104,9 @@ class FossilDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
+        $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock);
         $downloader->update($initialPackageMock, $sourcePackageMock, '/path');
+        $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock);
     }
 
     public function testUpdate()
@@ -140,7 +142,9 @@ class FossilDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
+        $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
         $downloader->update($packageMock, $packageMock, $this->workingDir);
+        $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
     }
 
     public function testRemove()

+ 118 - 117
tests/Composer/Test/Downloader/GitDownloaderTest.php

@@ -17,6 +17,7 @@ use Composer\Config;
 use Composer\Test\TestCase;
 use Composer\Util\Filesystem;
 use Composer\Util\Platform;
+use Prophecy\Argument;
 
 class GitDownloaderTest extends TestCase
 {
@@ -79,7 +80,10 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
+        $downloader->download($packageMock, '/path');
+        $downloader->prepare('install', $packageMock, '/path');
         $downloader->install($packageMock, '/path');
+        $downloader->cleanup('install', $packageMock, '/path');
     }
 
     public function testDownload()
@@ -130,7 +134,10 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
+        $downloader->download($packageMock, 'composerPath');
+        $downloader->prepare('install', $packageMock, 'composerPath');
         $downloader->install($packageMock, 'composerPath');
+        $downloader->cleanup('install', $packageMock, 'composerPath');
     }
 
     public function testDownloadWithCache()
@@ -195,7 +202,10 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, $config, $processExecutor);
+        $downloader->download($packageMock, 'composerPath');
+        $downloader->prepare('install', $packageMock, 'composerPath');
         $downloader->install($packageMock, 'composerPath');
+        $downloader->cleanup('install', $packageMock, 'composerPath');
         @rmdir($cachePath);
     }
 
@@ -265,7 +275,10 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
+        $downloader->download($packageMock, 'composerPath');
+        $downloader->prepare('install', $packageMock, 'composerPath');
         $downloader->install($packageMock, 'composerPath');
+        $downloader->cleanup('install', $packageMock, 'composerPath');
     }
 
     public function pushUrlProvider()
@@ -329,12 +342,12 @@ class GitDownloaderTest extends TestCase
         $config->merge(array('config' => array('github-protocols' => $protocols)));
 
         $downloader = $this->getDownloaderMock(null, $config, $processExecutor);
+        $downloader->download($packageMock, 'composerPath');
+        $downloader->prepare('install', $packageMock, 'composerPath');
         $downloader->install($packageMock, 'composerPath');
+        $downloader->cleanup('install', $packageMock, 'composerPath');
     }
 
-    /**
-     * @expectedException \RuntimeException
-     */
     public function testDownloadThrowsRuntimeExceptionIfGitCommandFails()
     {
         $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer");
@@ -359,8 +372,20 @@ class GitDownloaderTest extends TestCase
             ->with($this->equalTo($expectedGitCommand))
             ->will($this->returnValue(1));
 
-        $downloader = $this->getDownloaderMock(null, null, $processExecutor);
-        $downloader->install($packageMock, 'composerPath');
+        // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe
+        try {
+            $downloader = $this->getDownloaderMock(null, null, $processExecutor);
+            $downloader->download($packageMock, 'composerPath');
+            $downloader->prepare('install', $packageMock, 'composerPath');
+            $downloader->install($packageMock, 'composerPath');
+            $downloader->cleanup('install', $packageMock, 'composerPath');
+            $this->fail('This test should throw');
+        } catch (\RuntimeException $e) {
+            if ('RuntimeException' !== get_class($e)) {
+                throw $e;
+            }
+            $this->assertEquals('RuntimeException', get_class($e));
+        }
     }
 
     /**
@@ -375,7 +400,10 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
+        $downloader->download($sourcePackageMock, '/path', $initialPackageMock);
+        $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock);
         $downloader->update($initialPackageMock, $sourcePackageMock, '/path');
+        $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock);
     }
 
     public function testUpdate()
@@ -392,39 +420,22 @@ class GitDownloaderTest extends TestCase
         $packageMock->expects($this->any())
             ->method('getVersion')
             ->will($this->returnValue('1.0.0.0'));
-        $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
-        $processExecutor->expects($this->at(0))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git show-ref --head -d")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(1))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(2))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(3))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(4))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat($expectedGitUpdateCommand)), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(5))
-            ->method('execute')
-            ->with($this->equalTo('git branch -r'))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(6))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
-            ->will($this->returnValue(0));
+
+        $process = $this->prophesize('Composer\Util\ProcessExecutor');
+        $process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0);
+        $process->execute($expectedGitUpdateCommand, null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled();
+        $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled();
 
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
-        $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
+        $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal());
+        $downloader->download($packageMock, $this->workingDir, $packageMock);
+        $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
         $downloader->update($packageMock, $packageMock, $this->workingDir);
+        $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
     }
 
     public function testUpdateWithNewRepoUrl()
@@ -444,27 +455,20 @@ class GitDownloaderTest extends TestCase
         $packageMock->expects($this->any())
             ->method('getVersion')
             ->will($this->returnValue('1.0.0.0'));
+
         $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
         $processExecutor->expects($this->at(0))
             ->method('execute')
-            ->with($this->equalTo($this->winCompat("git show-ref --head -d")))
+            ->with($this->equalTo($this->winCompat("git --version")))
             ->will($this->returnValue(0));
         $processExecutor->expects($this->at(1))
             ->method('execute')
-            ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no")))
+            ->with($this->equalTo($this->winCompat("git show-ref --head -d")))
             ->will($this->returnValue(0));
         $processExecutor->expects($this->at(2))
             ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnCallback(function ($cmd, &$output, $cwd) {
-                $output = 'origin https://github.com/old/url (fetch)
-origin https://github.com/old/url (push)
-composer https://github.com/old/url (fetch)
-composer https://github.com/old/url (push)
-';
-
-                return 0;
-            }));
+            ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no")))
+            ->will($this->returnValue(0));
         $processExecutor->expects($this->at(3))
             ->method('execute')
             ->with($this->equalTo($this->winCompat("git remote -v")))
@@ -482,26 +486,41 @@ composer https://github.com/old/url (push)
             ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
             ->will($this->returnValue(0));
         $processExecutor->expects($this->at(7))
+            ->method('execute')
+            ->with($this->equalTo($this->winCompat("git remote -v")))
+            ->will($this->returnCallback(function ($cmd, &$output, $cwd) {
+                $output = 'origin https://github.com/old/url (fetch)
+origin https://github.com/old/url (push)
+composer https://github.com/old/url (fetch)
+composer https://github.com/old/url (push)
+';
+
+                return 0;
+            }));
+        $processExecutor->expects($this->at(8))
             ->method('execute')
             ->with($this->equalTo($this->winCompat("git remote set-url origin 'https://github.com/composer/composer'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
             ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(8))
+        $processExecutor->expects($this->at(9))
             ->method('execute')
             ->with($this->equalTo($this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
             ->will($this->returnValue(0));
 
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
         $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
+        $downloader->download($packageMock, $this->workingDir, $packageMock);
+        $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
         $downloader->update($packageMock, $packageMock, $this->workingDir);
+        $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
     }
 
     /**
      * @group failing
-     * @expectedException \RuntimeException
      */
     public function testUpdateThrowsRuntimeExceptionIfGitCommandFails()
     {
         $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)");
+        $expectedGitUpdateCommand2 = $this->winCompat("git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)");
 
         $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
         $packageMock->expects($this->any())
@@ -513,36 +532,38 @@ composer https://github.com/old/url (push)
         $packageMock->expects($this->any())
             ->method('getVersion')
             ->will($this->returnValue('1.0.0.0'));
-        $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
-        $processExecutor->expects($this->at(0))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git show-ref --head -d")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(1))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(2))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(3))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(4))
-            ->method('execute')
-            ->with($this->equalTo($expectedGitUpdateCommand))
-            ->will($this->returnValue(1));
+
+        $process = $this->prophesize('Composer\Util\ProcessExecutor');
+        $process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0);
+        $process->execute($expectedGitUpdateCommand, null, $this->winCompat($this->workingDir))->willReturn(1)->shouldBeCalled();
+        $process->execute($expectedGitUpdateCommand2, null, $this->winCompat($this->workingDir))->willReturn(1)->shouldBeCalled();
+        $process->getErrorOutput()->willReturn('');
 
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
-        $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
-        $downloader->update($packageMock, $packageMock, $this->workingDir);
+
+        // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe
+        try {
+            $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal());
+            $downloader->download($packageMock, $this->workingDir, $packageMock);
+            $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
+            $downloader->update($packageMock, $packageMock, $this->workingDir);
+            $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
+            $this->fail('This test should throw');
+        } catch (\RuntimeException $e) {
+            if ('RuntimeException' !== get_class($e)) {
+                throw $e;
+            }
+            $this->assertEquals('RuntimeException', get_class($e));
+        }
     }
 
     public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover()
     {
-        $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)");
+        $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)");
         $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)");
 
         $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
@@ -554,52 +575,24 @@ composer https://github.com/old/url (push)
             ->will($this->returnValue('1.0.0.0'));
         $packageMock->expects($this->any())
             ->method('getSourceUrls')
-            ->will($this->returnValue(array('/foo/bar', 'https://github.com/composer/composer')));
-        $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
-        $processExecutor->expects($this->at(0))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git show-ref --head -d")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(1))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(2))
-           ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(3))
-           ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(4))
-            ->method('execute')
-            ->with($this->equalTo($expectedFirstGitUpdateCommand))
-            ->will($this->returnValue(1));
-        $processExecutor->expects($this->at(6))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git --version")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(7))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(8))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git remote -v")))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(9))
-            ->method('execute')
-            ->with($this->equalTo($expectedSecondGitUpdateCommand))
-            ->will($this->returnValue(0));
-        $processExecutor->expects($this->at(11))
-            ->method('execute')
-            ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
-            ->will($this->returnValue(0));
+            ->will($this->returnValue(array(Platform::isWindows() ? 'C:\\' : '/', 'https://github.com/composer/composer')));
+
+        $process = $this->prophesize('Composer\Util\ProcessExecutor');
+        $process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0);
+        $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0);
+        $process->execute($expectedFirstGitUpdateCommand, Argument::cetera())->willReturn(1)->shouldBeCalled();
+        $process->execute($expectedSecondGitUpdateCommand, Argument::cetera())->willReturn(0)->shouldBeCalled();
+        $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled();
 
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
-        $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
+        $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal());
+        $downloader->download($packageMock, $this->workingDir, $packageMock);
+        $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
         $downloader->update($packageMock, $packageMock, $this->workingDir);
+        $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
     }
 
     public function testDowngradeShowsAppropriateMessage()
@@ -644,7 +637,10 @@ composer https://github.com/old/url (push)
 
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
         $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor);
+        $downloader->download($newPackage, $this->workingDir, $oldPackage);
+        $downloader->prepare('update', $newPackage, $this->workingDir, $oldPackage);
         $downloader->update($oldPackage, $newPackage, $this->workingDir);
+        $downloader->cleanup('update', $newPackage, $this->workingDir, $oldPackage);
     }
 
     public function testNotUsingDowngradingWithReferences()
@@ -679,11 +675,14 @@ composer https://github.com/old/url (push)
         $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
         $ioMock->expects($this->at(0))
             ->method('writeError')
-            ->with($this->stringContains('updating'));
+            ->with($this->stringContains('Updating'));
 
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
         $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor);
+        $downloader->download($newPackage, $this->workingDir, $oldPackage);
+        $downloader->prepare('update', $newPackage, $this->workingDir, $oldPackage);
         $downloader->update($oldPackage, $newPackage, $this->workingDir);
+        $downloader->cleanup('update', $newPackage, $this->workingDir, $oldPackage);
     }
 
     public function testRemove()
@@ -703,7 +702,9 @@ composer https://github.com/old/url (push)
             ->will($this->returnValue(true));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem);
+        $downloader->prepare('uninstall', $packageMock, 'composerPath');
         $downloader->remove($packageMock, 'composerPath');
+        $downloader->cleanup('uninstall', $packageMock, 'composerPath');
     }
 
     public function testGetInstallationSource()

+ 6 - 0
tests/Composer/Test/Downloader/HgDownloaderTest.php

@@ -98,7 +98,9 @@ class HgDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
+        $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock);
         $downloader->update($initialPackageMock, $sourcePackageMock, '/path');
+        $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock);
     }
 
     public function testUpdate()
@@ -129,7 +131,9 @@ class HgDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
+        $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
         $downloader->update($packageMock, $packageMock, $this->workingDir);
+        $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
     }
 
     public function testRemove()
@@ -148,7 +152,9 @@ class HgDownloaderTest extends TestCase
             ->will($this->returnValue(true));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem);
+        $downloader->prepare('uninstall', $packageMock, 'composerPath');
         $downloader->remove($packageMock, 'composerPath');
+        $downloader->cleanup('uninstall', $packageMock, 'composerPath');
     }
 
     public function testGetInstallationSource()

+ 1 - 1
tests/Composer/Test/Downloader/ZipDownloaderTest.php

@@ -338,7 +338,7 @@ class ZipDownloaderTest extends TestCase
 
 class MockedZipDownloader extends ZipDownloader
 {
-    public function download(PackageInterface $package, $path, $output = true)
+    public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
     {
         return;
     }

+ 35 - 29
tests/Composer/Test/Fixtures/installer/update-changes-url.test

@@ -17,37 +17,38 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
                 {
                     "name": "a/a", "version": "dev-master",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }
                 },
                 {
                     "name": "b/b", "version": "2.0.3",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }
                 },
                 {
                     "name": "c/c", "version": "1.0.0",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/c/newc", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/c/newc/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/c/newc/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }
                 },
                 {
                     "name": "d/d", "version": "dev-master",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/d/newd", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }
                 },
                 {
                     "name": "e/e", "version": "dev-master",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/e/newe", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }
                 },
                 {
                     "name": "f/f", "version": "dev-master",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/f/newf", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
+                    "transport-options": { "foo": "bar2" }
                 },
                 {
                     "name": "g/g", "version": "dev-master",
                     "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" },
-                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }
                 }
             ]
         }
@@ -67,32 +68,34 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
     {
         "name": "a/a", "version": "dev-master",
         "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
-        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }
     },
     {
         "name": "b/b", "version": "2.0.3",
         "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" },
-        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }
     },
     {
         "name": "c/c", "version": "1.0.0",
         "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
-        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }
     },
     {
         "name": "d/d", "version": "dev-master",
         "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
-        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }
     },
     {
         "name": "f/f", "version": "dev-master",
         "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
-        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+        "transport-options": { "foo": "bar" }
     },
     {
         "name": "g/g", "version": "dev-master",
         "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
-        "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" }
+        "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
+        "transport-options": { "foo": "bar" }
     }
 ]
 --LOCK--
@@ -101,38 +104,40 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
         {
             "name": "a/a", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library"
         },
         {
             "name": "b/b", "version": "2.0.3",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library"
         },
         {
             "name": "c/c", "version": "1.0.0",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library"
         },
         {
             "name": "d/d", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library"
         },
         {
             "name": "f/f", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" },
-            "type": "library"
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library",
+            "transport-options": { "foo": "bar" }
         },
         {
             "name": "g/g", "version": "dev-master",
             "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
-            "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" },
-            "type": "library"
+            "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
+            "type": "library",
+            "transport-options": { "foo": "bar" }
         }
     ],
     "packages-dev": [],
@@ -150,43 +155,44 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
         {
             "name": "a/a", "version": "dev-master",
             "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" },
-            "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" },
+            "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
             "type": "library"
         },
         {
             "name": "b/b", "version": "2.0.3",
             "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" },
-            "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" },
+            "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
             "type": "library"
         },
         {
             "name": "c/c", "version": "1.0.0",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library"
         },
         {
             "name": "d/d", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/newd", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
             "type": "library"
         },
         {
             "name": "e/e", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
             "type": "library"
         },
         {
             "name": "f/f", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" },
-            "type": "library"
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library",
+            "transport-options": { "foo": "bar" }
         },
         {
             "name": "g/g", "version": "dev-master",
             "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
             "type": "library"
         }
     ],

+ 1 - 1
tests/Composer/Test/InstallerTest.php

@@ -246,7 +246,7 @@ class InstallerTest extends TestCase
         }
 
         $contents = json_encode($composerConfig);
-        $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents);
+        $locker = new Locker($io, $lockJsonMock, $composer->getInstallationManager(), $contents);
         $composer->setLocker($locker);
 
         $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();

+ 16 - 0
tests/Composer/Test/Json/JsonManipulatorTest.php

@@ -1448,6 +1448,22 @@ class JsonManipulatorTest extends TestCase
     "repositories": {
     }
 }
+',
+            ),
+            'works on simple ones escaped slash' => array(
+                '{
+    "repositories": {
+        "foo\/bar": {
+            "bar": "baz"
+        }
+    }
+}',
+                'foo/bar',
+                true,
+                '{
+    "repositories": {
+    }
+}
 ',
             ),
             'works on simple ones middle' => array(

+ 2 - 1
tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php

@@ -13,6 +13,7 @@
 namespace Composer\Test\Mock;
 
 use Composer\Repository\InstalledFilesystemRepository;
+use Composer\Installer\InstallationManager;
 
 class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository
 {
@@ -20,7 +21,7 @@ class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository
     {
     }
 
-    public function write()
+    public function write($devMode, InstallationManager $installationManager)
     {
     }
 }

+ 0 - 1
tests/Composer/Test/Package/Loader/ArrayLoaderTest.php

@@ -148,7 +148,6 @@ class ArrayLoaderTest extends TestCase
     {
         $package = $this->loader->load($config);
         $dumper = new ArrayDumper;
-        $expectedConfig = $config;
         $expectedConfig = $this->fixConfigWhenLoadConfigIsFalse($config);
         $this->assertEquals($expectedConfig, $dumper->dump($package));
     }

+ 9 - 32
tests/Composer/Test/Package/LockerTest.php

@@ -24,7 +24,6 @@ class LockerTest extends TestCase
         $locker = new Locker(
             new NullIO,
             $json,
-            $this->createRepositoryManagerMock(),
             $this->createInstallationManagerMock(),
             $this->getJsonContent()
         );
@@ -44,10 +43,9 @@ class LockerTest extends TestCase
     public function testGetNotLockedPackages()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent());
+        $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent());
 
         $json
             ->expects($this->once())
@@ -62,10 +60,9 @@ class LockerTest extends TestCase
     public function testGetLockedPackages()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent());
+        $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent());
 
         $json
             ->expects($this->once())
@@ -89,11 +86,10 @@ class LockerTest extends TestCase
     public function testSetLockData()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
         $jsonContent = $this->getJsonContent() . '  ';
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent);
+        $locker = new Locker(new NullIO, $json, $inst, $jsonContent);
 
         $package1 = $this->createPackageMock();
         $package2 = $this->createPackageMock();
@@ -162,10 +158,9 @@ class LockerTest extends TestCase
     public function testLockBadPackages()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent());
+        $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent());
 
         $package1 = $this->createPackageMock();
         $package1
@@ -181,11 +176,10 @@ class LockerTest extends TestCase
     public function testIsFresh()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
         $jsonContent = $this->getJsonContent();
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent);
+        $locker = new Locker(new NullIO, $json, $inst, $jsonContent);
 
         $json
             ->expects($this->once())
@@ -198,10 +192,9 @@ class LockerTest extends TestCase
     public function testIsFreshFalse()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent());
+        $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent());
 
         $json
             ->expects($this->once())
@@ -214,11 +207,10 @@ class LockerTest extends TestCase
     public function testIsFreshWithContentHash()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
         $jsonContent = $this->getJsonContent();
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent);
+        $locker = new Locker(new NullIO, $json, $inst, $jsonContent);
 
         $json
             ->expects($this->once())
@@ -231,11 +223,10 @@ class LockerTest extends TestCase
     public function testIsFreshWithContentHashAndNoHash()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
         $jsonContent = $this->getJsonContent();
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent);
+        $locker = new Locker(new NullIO, $json, $inst, $jsonContent);
 
         $json
             ->expects($this->once())
@@ -248,10 +239,9 @@ class LockerTest extends TestCase
     public function testIsFreshFalseWithContentHash()
     {
         $json = $this->createJsonFileMock();
-        $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent());
+        $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent());
 
         $differentHash = md5($this->getJsonContent(array('name' => 'test2')));
 
@@ -270,19 +260,6 @@ class LockerTest extends TestCase
             ->getMock();
     }
 
-    private function createRepositoryManagerMock()
-    {
-        $mock = $this->getMockBuilder('Composer\Repository\RepositoryManager')
-            ->disableOriginalConstructor()
-            ->getMock();
-
-        $mock->expects($this->any())
-            ->method('getLocalRepository')
-            ->will($this->returnValue($this->getMockBuilder('Composer\Repository\ArrayRepository')->getMock()));
-
-        return $mock;
-    }
-
     private function createInstallationManagerMock()
     {
         $mock = $this->getMockBuilder('Composer\Installer\InstallationManager')

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php

@@ -12,5 +12,16 @@ class Plugin implements PluginInterface
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v1');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v1');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v1');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php

@@ -12,5 +12,16 @@ class Plugin2 implements PluginInterface
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v2');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v2');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v2');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php

@@ -12,5 +12,16 @@ class Plugin2 implements PluginInterface
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v3');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v3');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v3');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php

@@ -13,5 +13,16 @@ class Plugin1 implements PluginInterface
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v4-plugin1');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v4-plugin1');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v4-plugin1');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php

@@ -13,5 +13,16 @@ class Plugin2 implements PluginInterface
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v4-plugin2');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v4-plugin2');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v4-plugin2');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php

@@ -10,5 +10,16 @@ class Plugin5 implements PluginInterface
 {
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v5');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v5');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v5');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php

@@ -10,5 +10,16 @@ class Plugin6 implements PluginInterface
 {
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v6');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v6');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v6');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php

@@ -10,5 +10,16 @@ class Plugin7 implements PluginInterface
 {
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v7');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v7');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v7');
     }
 }

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php

@@ -13,6 +13,17 @@ class Plugin8 implements PluginInterface, Capable
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v8');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v8');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v8');
     }
 
     public function getCapabilities()

+ 11 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php

@@ -14,5 +14,16 @@ class Plugin implements PluginInterface
 
     public function activate(Composer $composer, IOInterface $io)
     {
+        $io->write('activate v9');
+    }
+
+    public function deactivate(Composer $composer, IOInterface $io)
+    {
+        $io->write('deactivate v9');
+    }
+
+    public function uninstall(Composer $composer, IOInterface $io)
+    {
+        $io->write('uninstall v9');
     }
 }

+ 31 - 1
tests/Composer/Test/Plugin/PluginInstallerTest.php

@@ -19,6 +19,9 @@ use Composer\Package\CompletePackage;
 use Composer\Package\Loader\JsonLoader;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Plugin\PluginManager;
+use Symfony\Component\Console\Output\OutputInterface;
+use Composer\IO\BufferIO;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Test\TestCase;
 use Composer\Util\Filesystem;
@@ -96,7 +99,7 @@ class PluginInstallerTest extends TestCase
                 return __DIR__.'/Fixtures/'.$package->getPrettyName();
             }));
 
-        $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
+        $this->io = new BufferIO();
 
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
         $this->autoloadGenerator = new AutoloadGenerator($dispatcher);
@@ -108,6 +111,7 @@ class PluginInstallerTest extends TestCase
         $this->composer->setRepositoryManager($rm);
         $this->composer->setInstallationManager($im);
         $this->composer->setAutoloadGenerator($this->autoloadGenerator);
+        $this->composer->setEventDispatcher(new EventDispatcher($this->composer, $this->io));
 
         $this->pm = new PluginManager($this->io, $this->composer);
         $this->composer->setPluginManager($this->pm);
@@ -140,6 +144,7 @@ class PluginInstallerTest extends TestCase
 
         $plugins = $this->pm->getPlugins();
         $this->assertEquals('installer-v1', $plugins[0]->version);
+        $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput());
     }
 
     public function testInstallMultiplePlugins()
@@ -158,6 +163,7 @@ class PluginInstallerTest extends TestCase
         $this->assertEquals('installer-v4', $plugins[0]->version);
         $this->assertEquals('plugin2', $plugins[1]->name);
         $this->assertEquals('installer-v4', $plugins[1]->version);
+        $this->assertEquals('activate v4-plugin1'.PHP_EOL.'activate v4-plugin2'.PHP_EOL, $this->io->getOutput());
     }
 
     public function testUpgradeWithNewClassName()
@@ -176,7 +182,29 @@ class PluginInstallerTest extends TestCase
         $installer->update($this->repository, $this->packages[0], $this->packages[1]);
 
         $plugins = $this->pm->getPlugins();
+        $this->assertCount(1, $plugins);
         $this->assertEquals('installer-v2', $plugins[1]->version);
+        $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'activate v2'.PHP_EOL, $this->io->getOutput());
+    }
+
+    public function testUninstall()
+    {
+        $this->repository
+            ->expects($this->once())
+            ->method('getPackages')
+            ->will($this->returnValue(array($this->packages[0])));
+        $this->repository
+            ->expects($this->exactly(1))
+            ->method('hasPackage')
+            ->will($this->onConsecutiveCalls(true, false));
+        $installer = new PluginInstaller($this->io, $this->composer);
+        $this->pm->loadInstalledPlugins();
+
+        $installer->uninstall($this->repository, $this->packages[0]);
+
+        $plugins = $this->pm->getPlugins();
+        $this->assertCount(0, $plugins);
+        $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'uninstall v1'.PHP_EOL, $this->io->getOutput());
     }
 
     public function testUpgradeWithSameClassName()
@@ -196,6 +224,7 @@ class PluginInstallerTest extends TestCase
 
         $plugins = $this->pm->getPlugins();
         $this->assertEquals('installer-v3', $plugins[1]->version);
+        $this->assertEquals('activate v2'.PHP_EOL.'deactivate v2'.PHP_EOL.'activate v3'.PHP_EOL, $this->io->getOutput());
     }
 
     public function testRegisterPluginOnlyOneTime()
@@ -213,6 +242,7 @@ class PluginInstallerTest extends TestCase
         $plugins = $this->pm->getPlugins();
         $this->assertCount(1, $plugins);
         $this->assertEquals('installer-v1', $plugins[0]->version);
+        $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput());
     }
 
     /**

+ 3 - 1
tests/Composer/Test/Repository/ComposerRepositoryTest.php

@@ -227,7 +227,9 @@ class ComposerRepositoryTest extends TestCase
         $repository = new ComposerRepository(
             array('url' => $repositoryUrl),
             new NullIO(),
-            FactoryMock::createConfig()
+            FactoryMock::createConfig(),
+            $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(),
+            $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock()
         );
 
         $object = new \ReflectionObject($repository);

+ 13 - 2
tests/Composer/Test/Repository/FilesystemRepositoryTest.php

@@ -82,11 +82,21 @@ class FilesystemRepositoryTest extends TestCase
         $json = $this->createJsonFileMock();
 
         $repository = new FilesystemRepository($json);
+        $im = $this->getMockBuilder('Composer\Installer\InstallationManager')
+            ->disableOriginalConstructor()
+            ->getMock();
+        $im->expects($this->once())
+            ->method('getInstallPath')
+            ->will($this->returnValue('/foo/bar/vendor/woop/woop'));
 
         $json
             ->expects($this->once())
             ->method('read')
             ->will($this->returnValue(array()));
+        $json
+            ->expects($this->once())
+            ->method('getPath')
+            ->will($this->returnValue('/foo/bar/vendor/composer/installed.json'));
         $json
             ->expects($this->once())
             ->method('exists')
@@ -95,11 +105,12 @@ class FilesystemRepositoryTest extends TestCase
             ->expects($this->once())
             ->method('write')
             ->with(array(
-                array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0'),
+                'packages' => array(array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0', 'install-path' => '../woop/woop')),
+                'dev' => true,
             ));
 
         $repository->addPackage($this->getPackage('mypkg', '0.1.10'));
-        $repository->write();
+        $repository->write(true, $im);
     }
 
     private function createJsonFileMock()

+ 0 - 9
tests/Composer/Test/Repository/Vcs/FossilDriverTest.php

@@ -40,15 +40,6 @@ class FossilDriverTest extends TestCase
         $fs->removeDirectory($this->home);
     }
 
-    private function getCmd($cmd)
-    {
-        if (Platform::isWindows()) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
-    }
-
     public static function supportProvider()
     {
         return array(

+ 0 - 9
tests/Composer/Test/Repository/Vcs/SvnDriverTest.php

@@ -71,15 +71,6 @@ class SvnDriverTest extends TestCase
         $svn->initialize();
     }
 
-    private function getCmd($cmd)
-    {
-        if (Platform::isWindows()) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
-    }
-
     public static function supportProvider()
     {
         return array(

+ 80 - 0
tests/Composer/Test/Script/EventTest.php

@@ -0,0 +1,80 @@
+<?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\Test\Script;
+
+use Composer\Composer;
+use Composer\Config;
+use Composer\Script\Event;
+use Composer\Test\TestCase;
+
+class EventTest extends TestCase
+{
+    public function testEventSetsOriginatingEvent()
+    {
+        $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
+        $composer = $this->createComposerInstance();
+
+        $originatingEvent = new \Composer\EventDispatcher\Event('originatingEvent');
+
+        $scriptEvent = new Event('test', $composer, $io, true);
+
+        $this->assertNull(
+            $scriptEvent->getOriginatingEvent(),
+            'originatingEvent is initialized as null'
+        );
+
+        $scriptEvent->setOriginatingEvent($originatingEvent);
+
+        $this->assertSame(
+            $originatingEvent,
+            $scriptEvent->getOriginatingEvent(),
+            'getOriginatingEvent() SHOULD return test event'
+        );
+    }
+
+    public function testEventCalculatesNestedOriginatingEvent()
+    {
+        $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
+        $composer = $this->createComposerInstance();
+
+        $originatingEvent = new \Composer\EventDispatcher\Event('upperOriginatingEvent');
+        $intermediateEvent = new Event('intermediate', $composer, $io, true);
+        $intermediateEvent->setOriginatingEvent($originatingEvent);
+
+        $scriptEvent = new Event('test', $composer, $io, true);
+        $scriptEvent->setOriginatingEvent($intermediateEvent);
+
+        $this->assertNotSame(
+            $intermediateEvent,
+            $scriptEvent->getOriginatingEvent(),
+            'getOriginatingEvent() SHOULD NOT return intermediate events'
+        );
+
+        $this->assertSame(
+            $originatingEvent,
+            $scriptEvent->getOriginatingEvent(),
+            'getOriginatingEvent() SHOULD return upper-most event'
+        );
+    }
+
+    private function createComposerInstance()
+    {
+        $composer = new Composer;
+        $config = new Config;
+        $composer->setConfig($config);
+        $package = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock();
+        $composer->setPackage($package);
+
+        return $composer;
+    }
+}

BIN
tests/Composer/Test/Util/Fixtures/Zip/empty.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/folder.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/multiple.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/nojson.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/root.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip


+ 0 - 3
tests/Composer/Test/Util/GitHubTest.php

@@ -24,12 +24,9 @@ use RecursiveIteratorIterator;
  */
 class GitHubTest extends TestCase
 {
-    private $username = 'username';
     private $password = 'password';
-    private $authcode = 'authcode';
     private $message = 'mymessage';
     private $origin = 'github.com';
-    private $token = 'githubtoken';
 
     public function testUsernamePasswordAuthenticationFlow()
     {

+ 0 - 1
tests/Composer/Test/Util/GitLabTest.php

@@ -24,7 +24,6 @@ class GitLabTest extends TestCase
 {
     private $username = 'username';
     private $password = 'password';
-    private $authcode = 'authcode';
     private $message = 'mymessage';
     private $origin = 'gitlab.com';
     private $token = 'gitlabtoken';

+ 117 - 0
tests/Composer/Test/Util/ZipTest.php

@@ -0,0 +1,117 @@
+<?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\Test\Util;
+
+use Composer\Util\Zip;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @author Andreas Schempp <andreas.schempp@terminal42.ch>
+ */
+class ZipTest extends TestCase
+{
+    public function testThrowsExceptionIfZipExcentionIsNotLoaded()
+    {
+        if (extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is loaded.');
+        }
+
+        $this->setExpectedException('\RuntimeException', 'The Zip Util requires PHP\'s zip extension');
+
+        Zip::getComposerJson('');
+    }
+
+    public function testReturnsNullifTheZipIsNotFound()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/invalid.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsNullIfTheZipIsEmpty()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/empty.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsNullIfTheZipHasNoComposerJson()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsNullIfTheComposerJsonIsInASubSubfolder()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolder.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsComposerJsonInZipRoot()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/root.zip');
+
+        $this->assertEquals("{\n    \"name\": \"foo/bar\"\n}\n", $result);
+    }
+
+    public function testReturnsComposerJsonInFirstFolder()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip');
+
+        $this->assertEquals("{\n    \"name\": \"foo/bar\"\n}\n", $result);
+    }
+
+    public function testReturnsRootComposerJsonAndSkipsSubfolders()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip');
+
+        $this->assertEquals("{\n    \"name\": \"foo/bar\"\n}\n", $result);
+    }
+}