1
0
Fork 0

Merge remote-tracking branch 'upstream/master' into OutdatedNoDevOption

pull/8955/head
Thomas Lamy 2020-06-17 13:03:15 +02:00
commit 722bbce72f
92 changed files with 1705 additions and 726 deletions

View File

@ -89,7 +89,7 @@ jobs:
uses: "shivammathur/setup-php@v2" uses: "shivammathur/setup-php@v2"
with: with:
coverage: "none" coverage: "none"
extensions: "intl" extensions: "intl, zip"
ini-values: "memory_limit=-1, phar.readonly=0" ini-values: "memory_limit=-1, phar.readonly=0"
php-version: "${{ matrix.php-version }}" php-version: "${{ matrix.php-version }}"

View File

@ -31,7 +31,7 @@ jobs:
uses: "shivammathur/setup-php@v2" uses: "shivammathur/setup-php@v2"
with: with:
coverage: "none" coverage: "none"
extensions: "intl" extensions: "intl, zip"
ini-values: "memory_limit=-1" ini-values: "memory_limit=-1"
php-version: "${{ matrix.php-version }}" php-version: "${{ matrix.php-version }}"
tools: "cs2pr" tools: "cs2pr"
@ -52,5 +52,5 @@ jobs:
- name: Run PHPStan - name: Run PHPStan
run: | run: |
bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --with-all-dependencies bin/composer require --dev phpstan/phpstan:^0.12.26 phpunit/phpunit:^7.5 --with-all-dependencies
vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr vendor/bin/phpstan analyse --configuration=phpstan/config.neon || vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr

View File

@ -17,6 +17,7 @@
- `PluginInterface` added a deactivate (so plugin can stop whatever it is doing) and an uninstall (so the plugin can remove any files it created or do general cleanup) method. - `PluginInterface` added a deactivate (so plugin can stop whatever it is doing) and an uninstall (so the plugin can remove any files it created or do general cleanup) method.
- Plugins implementing `EventSubscriberInterface` will be deregistered from the EventDispatcher automatically when being deactivated, nothing to do there. - Plugins implementing `EventSubscriberInterface` will be deregistered from the EventDispatcher automatically when being deactivated, nothing to do there.
- `Pool` objects are now created via the `RepositorySet` class, you should use that in case you were using the `Pool` class directly. - `Pool` objects are now created via the `RepositorySet` class, you should use that in case you were using the `Pool` class directly.
- Custom installers extending from LibraryInstaller should be aware that in Composer 2 it MAY return PromiseInterface instances when calling parent::install/update/uninstall/installCode/removeCode. See [composer/installers](https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb) for an example of how to handle this best.
- The `Composer\Installer` class changed quite a bit internally, but the inputs are almost the same: - The `Composer\Installer` class changed quite a bit internally, but the inputs are almost the same:
- `setAdditionalInstalledRepository` is now `setAdditionalFixedRepository` - `setAdditionalInstalledRepository` is now `setAdditionalFixedRepository`
- `setUpdateWhitelist` is now `setUpdateAllowList` - `setUpdateWhitelist` is now `setUpdateAllowList`

View File

@ -22,7 +22,7 @@
} }
], ],
"require": { "require": {
"php": "^5.3.2 || ^7.0", "php": "^5.3.2 || ^7.0 || ^8.0",
"composer/ca-bundle": "^1.0", "composer/ca-bundle": "^1.0",
"composer/semver": "^3.0", "composer/semver": "^3.0",
"composer/spdx-licenses": "^1.2", "composer/spdx-licenses": "^1.2",

6
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b39e04ca4a44810c96876dae02629707", "content-hash": "9b94ece2895724239b36aec399e5321a",
"packages": [ "packages": [
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
@ -268,7 +268,7 @@
"support": { "support": {
"irc": "irc://irc.freenode.org/composer", "irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/xdebug-handler/issues", "issues": "https://github.com/composer/xdebug-handler/issues",
"source": "https://github.com/composer/xdebug-handler/tree/master" "source": "https://github.com/composer/xdebug-handler/tree/1.4.2"
}, },
"funding": [ "funding": [
{ {
@ -1504,7 +1504,7 @@
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^5.3.2 || ^7.0" "php": "^5.3.2 || ^7.0 || ^8.0"
}, },
"platform-dev": [], "platform-dev": [],
"platform-overrides": { "platform-overrides": {

View File

@ -785,7 +785,7 @@ Lists the name, version and license of every package installed. Use
### Options ### Options
* **--format:** Format of the output: text or json (default: "text") * **--format:** Format of the output: text, json or summary (default: "text")
* **--no-dev:** Remove dev dependencies from the output * **--no-dev:** Remove dev dependencies from the output
## run-script ## run-script

View File

@ -1,7 +1,5 @@
parameters: parameters:
level: 1 level: 1
autoload_files:
- '../src/bootstrap.php'
excludes_analyse: excludes_analyse:
- '../tests/Composer/Test/Fixtures/*' - '../tests/Composer/Test/Fixtures/*'
- '../tests/Composer/Test/Autoload/Fixtures/*' - '../tests/Composer/Test/Autoload/Fixtures/*'
@ -28,11 +26,9 @@ parameters:
# BC with older PHPUnit # BC with older PHPUnit
- '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~' - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~'
# hhvm should have support for $this in closures # ZipArchive::* Class constants are already checked before use.
- - '~^Access to undefined constant ZipArchive::~'
count: 1
message: '~^Using \$this inside anonymous function is prohibited because of PHP 5\.3 support\.$~'
path: '../tests/Composer/Test/Repository/PlatformRepositoryTest.php'
paths: paths:
- ../src - ../src
- ../tests - ../tests

View File

@ -257,16 +257,16 @@ EOF;
EOF; EOF;
} }
$blacklist = null; $excluded = null;
if (!empty($autoloads['exclude-from-classmap'])) { if (!empty($autoloads['exclude-from-classmap'])) {
$blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; $excluded = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}';
} }
$classMap = array(); $classMap = array();
$ambiguousClasses = array(); $ambiguousClasses = array();
$scannedFiles = array(); $scannedFiles = array();
foreach ($autoloads['classmap'] as $dir) { foreach ($autoloads['classmap'] as $dir) {
$classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, null, null, $classMap, $ambiguousClasses, $scannedFiles); $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, null, null, $classMap, $ambiguousClasses, $scannedFiles);
} }
if ($scanPsrPackages) { if ($scanPsrPackages) {
@ -289,7 +289,7 @@ EOF;
continue; continue;
} }
$classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespace, $group['type'], $classMap, $ambiguousClasses, $scannedFiles); $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, $namespace, $group['type'], $classMap, $ambiguousClasses, $scannedFiles);
} }
} }
} }
@ -368,9 +368,9 @@ EOF;
return count($classMap); return count($classMap);
} }
private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles) private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles)
{ {
foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) { foreach ($this->generateClassMap($dir, $excluded, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) {
$pathCode = $this->getPathCode($filesystem, $basePath, $vendorPath, $path).",\n"; $pathCode = $this->getPathCode($filesystem, $basePath, $vendorPath, $path).",\n";
if (!isset($classMap[$class])) { if (!isset($classMap[$class])) {
$classMap[$class] = $pathCode; $classMap[$class] = $pathCode;
@ -382,9 +382,9 @@ EOF;
return $classMap; return $classMap;
} }
private function generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType, $showAmbiguousWarning, array &$scannedFiles) private function generateClassMap($dir, $excluded, $namespaceFilter, $autoloadType, $showAmbiguousWarning, array &$scannedFiles)
{ {
return ClassMapGenerator::createMap($dir, $blacklist, $showAmbiguousWarning ? $this->io : null, $namespaceFilter, $autoloadType, $scannedFiles); return ClassMapGenerator::createMap($dir, $excluded, $showAmbiguousWarning ? $this->io : null, $namespaceFilter, $autoloadType, $scannedFiles);
} }
public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages)
@ -488,15 +488,15 @@ EOF;
} }
if (isset($autoloads['classmap'])) { if (isset($autoloads['classmap'])) {
$blacklist = null; $excluded = null;
if (!empty($autoloads['exclude-from-classmap'])) { if (!empty($autoloads['exclude-from-classmap'])) {
$blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; $excluded = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}';
} }
$scannedFiles = array(); $scannedFiles = array();
foreach ($autoloads['classmap'] as $dir) { foreach ($autoloads['classmap'] as $dir) {
try { try {
$loader->addClassMap($this->generateClassMap($dir, $blacklist, null, null, false, $scannedFiles)); $loader->addClassMap($this->generateClassMap($dir, $excluded, null, null, false, $scannedFiles));
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
$this->io->writeError('<warning>'.$e->getMessage().'</warning>'); $this->io->writeError('<warning>'.$e->getMessage().'</warning>');
} }
@ -640,6 +640,10 @@ EOF;
} }
} }
if ($match[1] === 'zend-opcache') {
$match[1] = 'zend opcache';
}
$extension = var_export($match[1], true); $extension = var_export($match[1], true);
if ($match[1] === 'pcntl' || $match[1] === 'readline') { if ($match[1] === 'pcntl' || $match[1] === 'readline') {
$requiredExtensions[$extension] = "PHP_SAPI !== 'cli' || extension_loaded($extension) || \$missingExtensions[] = $extension;\n"; $requiredExtensions[$extension] = "PHP_SAPI !== 'cli' || extension_loaded($extension) || \$missingExtensions[] = $extension;\n";

View File

@ -51,7 +51,7 @@ class ClassMapGenerator
* Iterate over all files in the given directory searching for classes * Iterate over all files in the given directory searching for classes
* *
* @param \Iterator|string $path The path to search in or an iterator * @param \Iterator|string $path The path to search in or an iterator
* @param string $blacklist Regex that matches against the file path that exclude from the classmap. * @param string $excluded Regex that matches against the file path that exclude from the classmap.
* @param IOInterface $io IO object * @param IOInterface $io IO object
* @param string $namespace Optional namespace prefix to filter by * @param string $namespace Optional namespace prefix to filter by
* @param string $autoloadType psr-0|psr-4 Optional autoload standard to use mapping rules * @param string $autoloadType psr-0|psr-4 Optional autoload standard to use mapping rules
@ -59,7 +59,7 @@ class ClassMapGenerator
* @throws \RuntimeException When the path is neither an existing file nor directory * @throws \RuntimeException When the path is neither an existing file nor directory
* @return array A class map array * @return array A class map array
*/ */
public static function createMap($path, $blacklist = null, IOInterface $io = null, $namespace = null, $autoloadType = null, &$scannedFiles = array()) public static function createMap($path, $excluded = null, IOInterface $io = null, $namespace = null, $autoloadType = null, &$scannedFiles = array())
{ {
$basePath = $path; $basePath = $path;
if (is_string($path)) { if (is_string($path)) {
@ -102,12 +102,12 @@ class ClassMapGenerator
continue; continue;
} }
// check the realpath of the file against the blacklist as the path might be a symlink and the blacklist is realpath'd so symlink are resolved // check the realpath of the file against the excluded paths as the path might be a symlink and the excluded path is realpath'd so symlink are resolved
if ($blacklist && preg_match($blacklist, strtr($realPath, '\\', '/'))) { if ($excluded && preg_match($excluded, strtr($realPath, '\\', '/'))) {
continue; continue;
} }
// check non-realpath of file for directories symlink in project dir // check non-realpath of file for directories symlink in project dir
if ($blacklist && preg_match($blacklist, strtr($filePath, '\\', '/'))) { if ($excluded && preg_match($excluded, strtr($filePath, '\\', '/'))) {
continue; continue;
} }

View File

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

View File

@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\ProcessExecutor;
use Composer\Util\Loop; use Composer\Util\Loop;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
@ -186,7 +187,8 @@ EOT
$composer = Factory::create($io, null, $disablePlugins); $composer = Factory::create($io, null, $disablePlugins);
} }
$fs = new Filesystem(); $process = new ProcessExecutor($io);
$fs = new Filesystem($process);
if ($noScripts === false) { if ($noScripts === false) {
// dispatch event // dispatch event
@ -199,6 +201,8 @@ EOT
// install dependencies of the created project // install dependencies of the created project
if ($noInstall === false) { if ($noInstall === false) {
$composer->getInstallationManager()->setOutputProgress(!$noProgress);
$installer = Installer::create($io, $composer); $installer = Installer::create($io, $composer);
$installer->setPreferSource($preferSource) $installer->setPreferSource($preferSource)
->setPreferDist($preferDist) ->setPreferDist($preferDist)
@ -210,6 +214,10 @@ EOT
->setClassMapAuthoritative($config->get('classmap-authoritative')) ->setClassMapAuthoritative($config->get('classmap-authoritative'))
->setApcuAutoloader($config->get('apcu-autoloader')); ->setApcuAutoloader($config->get('apcu-autoloader'));
if (!$composer->getLocker()->isLocked()) {
$installer->setUpdate(true);
}
if ($disablePlugins) { if ($disablePlugins) {
$installer->disablePlugins(); $installer->disablePlugins();
} }
@ -307,7 +315,8 @@ EOT
$directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts);
} }
$fs = new Filesystem(); $process = new ProcessExecutor($io);
$fs = new Filesystem($process);
if (!$fs->isAbsolutePath($directory)) { if (!$fs->isAbsolutePath($directory)) {
$directory = getcwd() . DIRECTORY_SEPARATOR . $directory; $directory = getcwd() . DIRECTORY_SEPARATOR . $directory;
} }
@ -397,12 +406,13 @@ EOT
$factory = new Factory(); $factory = new Factory();
$httpDownloader = $factory->createHttpDownloader($io, $config); $httpDownloader = $factory->createHttpDownloader($io, $config);
$dm = $factory->createDownloadManager($io, $config, $httpDownloader); $dm = $factory->createDownloadManager($io, $config, $httpDownloader, $process);
$dm->setPreferSource($preferSource) $dm->setPreferSource($preferSource)
->setPreferDist($preferDist); ->setPreferDist($preferDist);
$projectInstaller = new ProjectInstaller($directory, $dm); $projectInstaller = new ProjectInstaller($directory, $dm, $fs);
$im = $factory->createInstallationManager(new Loop($httpDownloader), $io); $im = $factory->createInstallationManager(new Loop($httpDownloader, $process), $io);
$im->setOutputProgress(!$noProgress);
$im->addInstaller($projectInstaller); $im->addInstaller($projectInstaller);
$im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package))); $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package)));
$im->notifyInstalls($io); $im->notifyInstalls($io);

View File

@ -431,7 +431,11 @@ EOT
} }
$versionsUtil = new Versions($config, $this->httpDownloader); $versionsUtil = new Versions($config, $this->httpDownloader);
try {
$latest = $versionsUtil->getLatest(); $latest = $versionsUtil->getLatest();
} catch (\Exception $e) {
return $e;
}
if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') {
return '<comment>You are not running the latest '.$versionsUtil->getChannel().' version, run `composer self-update` to update ('.Composer::VERSION.' => '.$latest['version'].')</comment>'; return '<comment>You are not running the latest '.$versionsUtil->getChannel().' version, run `composer self-update` to update ('.Composer::VERSION.' => '.$latest['version'].')</comment>';

View File

@ -106,6 +106,8 @@ EOT
$ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false);
$composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress'));
$install $install
->setDryRun($input->getOption('dry-run')) ->setDryRun($input->getOption('dry-run'))
->setVerbose($input->getOption('verbose')) ->setVerbose($input->getOption('verbose'))

View File

@ -21,6 +21,7 @@ use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/** /**
* @author Benoît Merlet <benoit.merlet@gmail.com> * @author Benoît Merlet <benoit.merlet@gmail.com>
@ -111,6 +112,28 @@ EOT
))); )));
break; break;
case 'summary':
$dependencies = array();
foreach ($packages as $package) {
$license = $package->getLicense();
$licenseName = $license[0];
if (!isset($dependencies[$licenseName])) {
$dependencies[$licenseName] = 0;
}
$dependencies[$licenseName]++;
}
$rows = array();
foreach ($dependencies as $usedLicense => $numberOfDependencies) {
$rows[] = array($usedLicense, $numberOfDependencies);
}
$symfonyIo = new SymfonyStyle($input, $output);
$symfonyIo->table(
array('License', 'Number of dependencies'),
$rows
);
break;
default: default:
throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format)); throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format));
} }

View File

@ -220,6 +220,8 @@ EOT
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
$composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress'));
$install = Installer::create($io, $composer); $install = Installer::create($io, $composer);
$updateDevMode = !$input->getOption('update-no-dev'); $updateDevMode = !$input->getOption('update-no-dev');
@ -237,7 +239,7 @@ EOT
$flags .= ' --with-dependencies'; $flags .= ' --with-dependencies';
} }
$io->writeError('<info>Running composer update '.implode(' ', $packages).$flags); $io->writeError('<info>Running composer update '.implode(' ', $packages).$flags.'</info>');
$ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false);

View File

@ -281,11 +281,13 @@ EOT
$flags .= ' --with-dependencies'; $flags .= ' --with-dependencies';
} }
$io->writeError('<info>Running composer update '.implode(' ', array_keys($requirements)).$flags); $io->writeError('<info>Running composer update '.implode(' ', array_keys($requirements)).$flags.'</info>');
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
$composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress'));
$install = Installer::create($io, $composer); $install = Installer::create($io, $composer);
$ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false);

View File

@ -180,6 +180,8 @@ EOT
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
$composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress'));
$install = Installer::create($io, $composer); $install = Installer::create($io, $composer);
$config = $composer->getConfig(); $config = $composer->getConfig();

View File

@ -156,7 +156,7 @@ class Application extends BaseApplication
} }
// prompt user for dir change if no composer.json is present in current dir // prompt user for dir change if no composer.json is present in current dir
if ($io->isInteractive() && !$newWorkDir && !in_array($commandName, array('', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project'), true) && !file_exists(Factory::getComposerFile())) { if ($io->isInteractive() && !$newWorkDir && !in_array($commandName, array('', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project', 'outdated'), true) && !file_exists(Factory::getComposerFile())) {
$dir = dirname(getcwd()); $dir = dirname(getcwd());
$home = realpath(getenv('HOME') ?: getenv('USERPROFILE') ?: '/'); $home = realpath(getenv('HOME') ?: getenv('USERPROFILE') ?: '/');

View File

@ -61,7 +61,12 @@ class InstallOperation extends SolverOperation
*/ */
public function show($lock) public function show($lock)
{ {
return ($lock ? 'Locking ' : 'Installing ').'<info>'.$this->package->getPrettyName().'</info> (<comment>'.$this->package->getFullPrettyVersion().'</comment>)'; return self::format($this->package, $lock);
}
public static function format(PackageInterface $package, $lock = false)
{
return ($lock ? 'Locking ' : 'Installing ').'<info>'.$package->getPrettyName().'</info> (<comment>'.$package->getFullPrettyVersion().'</comment>)';
} }
/** /**

View File

@ -61,7 +61,12 @@ class UninstallOperation extends SolverOperation
*/ */
public function show($lock) public function show($lock)
{ {
return 'Removing <info>'.$this->package->getPrettyName().'</info> (<comment>'.$this->package->getFullPrettyVersion().'</comment>)'; return self::format($this->package, $lock);
}
public static function format(PackageInterface $package, $lock = false)
{
return 'Removing <info>'.$package->getPrettyName().'</info> (<comment>'.$package->getFullPrettyVersion().'</comment>)';
} }
/** /**

View File

@ -75,20 +75,25 @@ class UpdateOperation extends SolverOperation
*/ */
public function show($lock) public function show($lock)
{ {
$fromVersion = $this->initialPackage->getFullPrettyVersion(); return self::format($this->initialPackage, $this->targetPackage, $lock);
$toVersion = $this->targetPackage->getFullPrettyVersion();
if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) {
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
} elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) {
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
} }
$actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; public static function format(PackageInterface $initialPackage, PackageInterface $targetPackage, $lock = false)
{
$fromVersion = $initialPackage->getFullPrettyVersion();
$toVersion = $targetPackage->getFullPrettyVersion();
return $actionName.' <info>'.$this->initialPackage->getPrettyName().'</info> (<comment>'.$fromVersion.'</comment> => <comment>'.$toVersion.'</comment>)'; if ($fromVersion === $toVersion && $initialPackage->getSourceReference() !== $targetPackage->getSourceReference()) {
$fromVersion = $initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
$toVersion = $targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
} elseif ($fromVersion === $toVersion && $initialPackage->getDistReference() !== $targetPackage->getDistReference()) {
$fromVersion = $initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
$toVersion = $targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
}
$actionName = VersionParser::isUpgrade($initialPackage->getVersion(), $targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading';
return $actionName.' <info>'.$initialPackage->getPrettyName().'</info> (<comment>'.$fromVersion.'</comment> => <comment>'.$toVersion.'</comment>)';
} }
/** /**

View File

@ -14,6 +14,7 @@ namespace Composer\DependencyResolver;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Semver\CompilingMatcher;
use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
@ -146,9 +147,7 @@ class Pool implements \Countable
$candidateVersion = $candidate->getVersion(); $candidateVersion = $candidate->getVersion();
if ($candidateName === $name) { if ($candidateName === $name) {
$pkgConstraint = new Constraint('==', $candidateVersion); if ($constraint === null || CompilingMatcher::match($constraint, Constraint::OP_EQ, $candidateVersion)) {
if ($constraint === null || $constraint->matches($pkgConstraint)) {
return true; return true;
} }

View File

@ -12,21 +12,21 @@
namespace Composer\DependencyResolver; namespace Composer\DependencyResolver;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
use Composer\Package\Package;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\Version\StabilityFilter; use Composer\Package\Version\StabilityFilter;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository; use Composer\Repository\RootPackageRepository;
use Composer\Semver\CompilingMatcher;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Constraint\MultiConstraint;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Plugin\PluginEvents;
/** /**
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
@ -88,7 +88,7 @@ class PoolBuilder
* @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value
* @psalm-param array<string, int> $stabilityFlags * @psalm-param array<string, int> $stabilityFlags
* @param array[] $rootAliases * @param array[] $rootAliases
* @psalm-param list<array{package: string, version: string, alias: string, alias_normalized: string}> $rootAliases * @psalm-param array<string, array<string, array{alias: string, alias_normalized: string}>> $rootAliases
* @param string[] $rootReferences an array of package name => source reference * @param string[] $rootReferences an array of package name => source reference
* @psalm-param array<string, string> $rootReferences * @psalm-param array<string, string> $rootReferences
*/ */
@ -96,7 +96,7 @@ class PoolBuilder
{ {
$this->acceptableStabilities = $acceptableStabilities; $this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags; $this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $this->getRootAliasesPerPackage($rootAliases); $this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences; $this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->io = $io; $this->io = $io;
@ -209,7 +209,7 @@ class PoolBuilder
$found = false; $found = false;
foreach ($aliasedPackages as $packageOrAlias) { foreach ($aliasedPackages as $packageOrAlias) {
if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) { if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) {
$found = true; $found = true;
} }
} }
@ -425,19 +425,5 @@ class PoolBuilder
unset($this->skippedLoad[$name]); unset($this->skippedLoad[$name]);
unset($this->loadedNames[$name]); unset($this->loadedNames[$name]);
} }
private function getRootAliasesPerPackage(array $aliases)
{
$normalizedAliases = array();
foreach ($aliases as $alias) {
$normalizedAliases[$alias['package']][$alias['version']] = array(
'alias' => $alias['alias'],
'alias_normalized' => $alias['alias_normalized'],
);
}
return $normalizedAliases;
}
} }

View File

@ -16,6 +16,8 @@ use Composer\Package\PackageInterface;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Exception\IrrecoverableDownloadException; use Composer\Exception\IrrecoverableDownloadException;
use React\Promise\PromiseInterface;
use Composer\DependencyResolver\Operation\InstallOperation;
/** /**
* Base downloader for archives * Base downloader for archives
@ -28,14 +30,7 @@ abstract class ArchiveDownloader extends FileDownloader
{ {
public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{ {
$res = parent::download($package, $path, $prevPackage, $output); return parent::download($package, $path, $prevPackage, $output);
// if not downgrading and the dir already exists it seems we have an inconsistent state in the vendor dir and the user should fix it
if (!$prevPackage && is_dir($path) && !$this->filesystem->isDirEmpty($path)) {
throw new IrrecoverableDownloadException('Expected empty path to extract '.$package.' into but directory exists: '.$path);
}
return $res;
} }
/** /**
@ -46,40 +41,75 @@ abstract class ArchiveDownloader extends FileDownloader
public function install(PackageInterface $package, $path, $output = true) public function install(PackageInterface $package, $path, $output = true)
{ {
if ($output) { if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): Extracting archive"); $this->io->writeError(" - " . InstallOperation::format($package).": Extracting archive");
} else { } else {
$this->io->writeError('Extracting archive', false); $this->io->writeError('Extracting archive', false);
} }
$this->filesystem->ensureDirectoryExists($path); $this->filesystem->emptyDirectory($path);
if (!$this->filesystem->isDirEmpty($path)) {
throw new \UnexpectedValueException('Expected empty path to extract '.$package.' into but directory exists: '.$path);
}
do { do {
$temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
} while (is_dir($temporaryDir)); } while (is_dir($temporaryDir));
$this->addCleanupPath($package, $temporaryDir);
$this->addCleanupPath($package, $path);
$this->filesystem->ensureDirectoryExists($temporaryDir);
$fileName = $this->getFileName($package, $path); $fileName = $this->getFileName($package, $path);
try { $filesystem = $this->filesystem;
$this->filesystem->ensureDirectoryExists($temporaryDir); $self = $this;
try {
$this->extract($package, $fileName, $temporaryDir); $cleanup = function () use ($path, $filesystem, $temporaryDir, $package, $self) {
} catch (\Exception $e) {
// remove cache if the file was corrupted // remove cache if the file was corrupted
parent::clearLastCacheWrite($package); $self->clearLastCacheWrite($package);
// clean up
$filesystem->removeDirectory($path);
$filesystem->removeDirectory($temporaryDir);
$self->removeCleanupPath($package, $temporaryDir);
$self->removeCleanupPath($package, $path);
};
$promise = null;
try {
$promise = $this->extract($package, $fileName, $temporaryDir);
} catch (\Exception $e) {
$cleanup();
throw $e; throw $e;
} }
$this->filesystem->unlink($fileName); if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
return $promise->then(function () use ($self, $package, $filesystem, $fileName, $temporaryDir, $path) {
$filesystem->unlink($fileName);
/**
* Returns the folder content, excluding .DS_Store
*
* @param string $dir Directory
* @return \SplFileInfo[]
*/
$getFolderContent = function ($dir) {
$finder = Finder::create()
->ignoreVCS(false)
->ignoreDotFiles(false)
->notName('.DS_Store')
->depth(0)
->in($dir);
return iterator_to_array($finder);
};
$renameAsOne = false; $renameAsOne = false;
if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) { if (!file_exists($path) || ($filesystem->isDirEmpty($path) && $filesystem->removeDirectory($path))) {
$renameAsOne = true; $renameAsOne = true;
} }
$contentDir = $this->getFolderContent($temporaryDir); $contentDir = $getFolderContent($temporaryDir);
$singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir)); $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir));
if ($renameAsOne) { if ($renameAsOne) {
@ -89,28 +119,28 @@ abstract class ArchiveDownloader extends FileDownloader
} else { } else {
$extractedDir = $temporaryDir; $extractedDir = $temporaryDir;
} }
$this->filesystem->rename($extractedDir, $path); $filesystem->rename($extractedDir, $path);
} else { } else {
// only one dir in the archive, extract its contents out of it // only one dir in the archive, extract its contents out of it
if ($singleDirAtTopLevel) { if ($singleDirAtTopLevel) {
$contentDir = $this->getFolderContent((string) reset($contentDir)); $contentDir = $getFolderContent((string) reset($contentDir));
} }
// move files back out of the temp dir // move files back out of the temp dir
foreach ($contentDir as $file) { foreach ($contentDir as $file) {
$file = (string) $file; $file = (string) $file;
$this->filesystem->rename($file, $path . '/' . basename($file)); $filesystem->rename($file, $path . '/' . basename($file));
} }
} }
$this->filesystem->removeDirectory($temporaryDir); $filesystem->removeDirectory($temporaryDir);
} catch (\Exception $e) { $self->removeCleanupPath($package, $temporaryDir);
// clean up $self->removeCleanupPath($package, $path);
$this->filesystem->removeDirectory($path); }, function ($e) use ($cleanup) {
$this->filesystem->removeDirectory($temporaryDir); $cleanup();
throw $e; throw $e;
} });
} }
/** /**
@ -119,25 +149,8 @@ abstract class ArchiveDownloader extends FileDownloader
* @param string $file Extracted file * @param string $file Extracted file
* @param string $path Directory * @param string $path Directory
* *
* @return PromiseInterface|null
* @throws \UnexpectedValueException If can not extract downloaded file to path * @throws \UnexpectedValueException If can not extract downloaded file to path
*/ */
abstract protected function extract(PackageInterface $package, $file, $path); abstract protected function extract(PackageInterface $package, $file, $path);
/**
* Returns the folder content, excluding dotfiles
*
* @param string $dir Directory
* @return \SplFileInfo[]
*/
private function getFolderContent($dir)
{
$finder = Finder::create()
->ignoreVCS(false)
->ignoreDotFiles(false)
->notName('.DS_Store')
->depth(0)
->in($dir);
return iterator_to_array($finder);
}
} }

View File

@ -25,11 +25,17 @@ use React\Promise\PromiseInterface;
*/ */
class DownloadManager class DownloadManager
{ {
/** @var IOInterface */
private $io; private $io;
/** @var bool */
private $preferDist = false; private $preferDist = false;
/** @var bool */
private $preferSource = false; private $preferSource = false;
/** @var array<string, string> */
private $packagePreferences = array(); private $packagePreferences = array();
/** @var Filesystem */
private $filesystem; private $filesystem;
/** @var array<string, DownloaderInterface> */
private $downloaders = array(); private $downloaders = array();
/** /**

View File

@ -18,6 +18,9 @@ use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\IO\NullIO; use Composer\IO\NullIO;
use Composer\Package\Comparer\Comparer; use Composer\Package\Comparer\Comparer;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
@ -27,7 +30,9 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Util\Url as UrlUtil; use Composer\Util\Url as UrlUtil;
use Composer\Util\ProcessExecutor;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use React\Promise\PromiseInterface;
/** /**
* Base downloader for files * Base downloader for files
@ -39,16 +44,25 @@ use Composer\Downloader\TransportException;
*/ */
class FileDownloader implements DownloaderInterface, ChangeReportInterface class FileDownloader implements DownloaderInterface, ChangeReportInterface
{ {
/** @var IOInterface */
protected $io; protected $io;
/** @var Config */
protected $config; protected $config;
/** @var HttpDownloader */
protected $httpDownloader; protected $httpDownloader;
/** @var Filesystem */
protected $filesystem; protected $filesystem;
/** @var Cache */
protected $cache; protected $cache;
/** @var EventDispatcher */
protected $eventDispatcher;
/** @var ProcessExecutor */
protected $process;
/** /**
* @private this is only public for php 5.3 support in closures * @private this is only public for php 5.3 support in closures
*/ */
public $lastCacheWrites = array(); public $lastCacheWrites = array();
private $eventDispatcher; private $additionalCleanupPaths = array();
/** /**
* Constructor. * Constructor.
@ -60,14 +74,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
* @param Cache $cache Cache instance * @param Cache $cache Cache instance
* @param Filesystem $filesystem The filesystem * @param Filesystem $filesystem The filesystem
*/ */
public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null) public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null, ProcessExecutor $process = null)
{ {
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->httpDownloader = $httpDownloader; $this->httpDownloader = $httpDownloader;
$this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache; $this->cache = $cache;
$this->process = $process ?: new ProcessExecutor($io);
$this->filesystem = $filesystem ?: new Filesystem($this->process);
if ($this->cache && $this->cache->gcIsNecessary()) { if ($this->cache && $this->cache->gcIsNecessary()) {
$this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize'));
@ -119,8 +134,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$url = reset($urls); $url = reset($urls);
if ($eventDispatcher) { if ($eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed'], 'package', $package);
$eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
$url['processed'] = $preFileDownloadEvent->getProcessedUrl();
} }
$checksum = $package->getDistSha1Checksum(); $checksum = $package->getDistSha1Checksum();
@ -252,6 +268,12 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$path, $path,
); );
if (isset($this->additionalCleanupPaths[$package->getName()])) {
foreach ($this->additionalCleanupPaths[$package->getName()] as $path) {
$this->filesystem->remove($path);
}
}
foreach ($dirsToCleanUp as $dir) { foreach ($dirsToCleanUp as $dir) {
if (is_dir($dir) && $this->filesystem->isDirEmpty($dir)) { if (is_dir($dir) && $this->filesystem->isDirEmpty($dir)) {
$this->filesystem->removeDirectory($dir); $this->filesystem->removeDirectory($dir);
@ -265,7 +287,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
public function install(PackageInterface $package, $path, $output = true) public function install(PackageInterface $package, $path, $output = true)
{ {
if ($output) { if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)"); $this->io->writeError(" - " . InstallOperation::format($package));
} }
$this->filesystem->emptyDirectory($path); $this->filesystem->emptyDirectory($path);
@ -285,22 +307,49 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
} }
} }
/**
* TODO mark private in v3
* @protected This is public due to PHP 5.3
*/
public function addCleanupPath(PackageInterface $package, $path)
{
$this->additionalCleanupPaths[$package->getName()][] = $path;
}
/**
* TODO mark private in v3
* @protected This is public due to PHP 5.3
*/
public function removeCleanupPath(PackageInterface $package, $path)
{
if (isset($this->additionalCleanupPaths[$package->getName()])) {
$idx = array_search($path, $this->additionalCleanupPaths[$package->getName()]);
if (false !== $idx) {
unset($this->additionalCleanupPaths[$package->getName()][$idx]);
}
}
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function update(PackageInterface $initial, PackageInterface $target, $path) public function update(PackageInterface $initial, PackageInterface $target, $path)
{ {
$name = $target->getName(); $this->io->writeError(" - " . UpdateOperation::format($initial, $target) . ": ", false);
$from = $initial->getFullPrettyVersion();
$to = $target->getFullPrettyVersion();
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $promise = $this->remove($initial, $path, false);
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false); if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$self = $this;
$io = $this->io;
$this->remove($initial, $path, false); return $promise->then(function () use ($self, $target, $path, $io) {
$this->install($target, $path, false); $promise = $self->install($target, $path, false);
$io->writeError('');
$this->io->writeError(''); return $promise;
});
} }
/** /**
@ -309,7 +358,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
public function remove(PackageInterface $package, $path, $output = true) public function remove(PackageInterface $package, $path, $output = true)
{ {
if ($output) { if ($output) {
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)"); $this->io->writeError(" - " . UninstallOperation::format($package));
} }
if (!$this->filesystem->removeDirectory($path)) { if (!$this->filesystem->removeDirectory($path)) {
throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
@ -374,9 +423,14 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$output = ''; $output = '';
try { try {
$res = $this->download($package, $targetDir.'_compare', null, false); if (is_dir($targetDir.'_compare')) {
$this->filesystem->removeDirectory($targetDir.'_compare');
}
$this->download($package, $targetDir.'_compare', null, false);
$this->httpDownloader->wait(); $this->httpDownloader->wait();
$res = $this->install($package, $targetDir.'_compare', false); $this->install($package, $targetDir.'_compare', false);
$this->process->wait();
$comparer = new Comparer(); $comparer = new Comparer();
$comparer->setSource($targetDir.'_compare'); $comparer->setSource($targetDir.'_compare');

View File

@ -495,7 +495,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
protected function discardChanges($path) protected function discardChanges($path)
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
if (0 !== $this->process->execute('git reset --hard', $output, $path)) { if (0 !== $this->process->execute('git clean -df && git reset --hard', $output, $path)) {
throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput());
} }

View File

@ -20,6 +20,7 @@ use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
/** /**
* GZip archive downloader. * GZip archive downloader.
@ -28,15 +29,6 @@ use Composer\IO\IOInterface;
*/ */
class GzipDownloader extends ArchiveDownloader class GzipDownloader extends ArchiveDownloader
{ {
/** @var ProcessExecutor */
protected $process;
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract(PackageInterface $package, $file, $path) protected function extract(PackageInterface $package, $file, $path)
{ {
$filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME); $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME);

View File

@ -18,10 +18,18 @@ use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionGuesser;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\IO\IOInterface;
use Composer\Config;
use Composer\Cache;
use Composer\Util\HttpDownloader;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Filesystem as ComposerFilesystem; use Composer\Util\Filesystem;
use Composer\EventDispatcher\EventDispatcher;
use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
/** /**
* Download a package from a local path. * Download a package from a local path.
@ -77,11 +85,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
if (realpath($path) === $realUrl) { if (realpath($path) === $realUrl) {
if ($output) { if ($output) {
$this->io->writeError(sprintf( $this->io->writeError(" - " . InstallOperation::format($package).': Source already present');
' - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
$package->getName(),
$package->getFullPrettyVersion()
));
} else { } else {
$this->io->writeError('Source already present', false); $this->io->writeError('Source already present', false);
} }
@ -115,15 +119,11 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$allowedStrategies = array(self::STRATEGY_MIRROR); $allowedStrategies = array(self::STRATEGY_MIRROR);
} }
$fileSystem = new Filesystem(); $symfonyFilesystem = new SymfonyFilesystem();
$this->filesystem->removeDirectory($path); $this->filesystem->removeDirectory($path);
if ($output) { if ($output) {
$this->io->writeError(sprintf( $this->io->writeError(" - " . InstallOperation::format($package).': ', false);
' - Installing <info>%s</info> (<comment>%s</comment>): ',
$package->getName(),
$package->getFullPrettyVersion()
), false);
} }
$isFallback = false; $isFallback = false;
@ -142,9 +142,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$path = rtrim($path, "/"); $path = rtrim($path, "/");
$this->io->writeError(sprintf('Symlinking from %s', $url), false); $this->io->writeError(sprintf('Symlinking from %s', $url), false);
if ($transportOptions['relative']) { if ($transportOptions['relative']) {
$fileSystem->symlink($shortestPath, $path); $symfonyFilesystem->symlink($shortestPath, $path);
} else { } else {
$fileSystem->symlink($realUrl, $path); $symfonyFilesystem->symlink($realUrl, $path);
} }
} }
} catch (IOException $e) { } catch (IOException $e) {
@ -161,12 +161,11 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
// Fallback if symlink failed or if symlink is not allowed for the package // Fallback if symlink failed or if symlink is not allowed for the package
if (self::STRATEGY_MIRROR == $currentStrategy) { if (self::STRATEGY_MIRROR == $currentStrategy) {
$fs = new ComposerFilesystem(); $realUrl = $this->filesystem->normalizePath($realUrl);
$realUrl = $fs->normalizePath($realUrl);
$this->io->writeError(sprintf('%sMirroring from %s', $isFallback ? ' ' : '', $url), false); $this->io->writeError(sprintf('%sMirroring from %s', $isFallback ? ' ' : '', $url), false);
$iterator = new ArchivableFilesFinder($realUrl, array()); $iterator = new ArchivableFilesFinder($realUrl, array());
$fileSystem->mirror($realUrl, $path, $iterator); $symfonyFilesystem->mirror($realUrl, $path, $iterator);
} }
if ($output) { if ($output) {
@ -183,7 +182,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
if ($path === $realUrl) { if ($path === $realUrl) {
if ($output) { if ($output) {
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>), source is still present in $path"); $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path");
} }
return; return;
@ -196,7 +195,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
*/ */
if (Platform::isWindows() && $this->filesystem->isJunction($path)) { if (Platform::isWindows() && $this->filesystem->isJunction($path)) {
if ($output) { if ($output) {
$this->io->writeError(" - Removing junction for <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)"); $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path");
} }
if (!$this->filesystem->removeJunction($path)) { if (!$this->filesystem->removeJunction($path)) {
$this->io->writeError(" <warning>Could not remove junction at " . $path . " - is another process locking it?</warning>"); $this->io->writeError(" <warning>Could not remove junction at " . $path . " - is another process locking it?</warning>");
@ -213,7 +212,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
public function getVcsReference(PackageInterface $package, $path) public function getVcsReference(PackageInterface $package, $path)
{ {
$parser = new VersionParser; $parser = new VersionParser;
$guesser = new VersionGuesser($this->config, new ProcessExecutor($this->io), $parser); $guesser = new VersionGuesser($this->config, $this->process, $parser);
$dumper = new ArrayDumper; $dumper = new ArrayDumper;
$packageConfig = $dumper->dump($package); $packageConfig = $dumper->dump($package);

View File

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

View File

@ -65,7 +65,7 @@ class SvnDownloader extends VcsDownloader
throw new \RuntimeException('The .svn directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); throw new \RuntimeException('The .svn directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
} }
$util = new SvnUtil($url, $this->io, $this->config); $util = new SvnUtil($url, $this->io, $this->config, $this->process);
$flags = ""; $flags = "";
if (version_compare($util->binaryVersion(), '1.7.0', '>=')) { if (version_compare($util->binaryVersion(), '1.7.0', '>=')) {
$flags .= ' --ignore-ancestry'; $flags .= ' --ignore-ancestry';
@ -103,7 +103,7 @@ class SvnDownloader extends VcsDownloader
*/ */
protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null) protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null)
{ {
$util = new SvnUtil($baseUrl, $this->io, $this->config); $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process);
$util->setCacheCredentials($this->cacheCredentials); $util->setCacheCredentials($this->cacheCredentials);
try { try {
return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose());
@ -202,7 +202,7 @@ class SvnDownloader extends VcsDownloader
$command = sprintf('svn log -r%s:%s --incremental', ProcessExecutor::escape($fromRevision), ProcessExecutor::escape($toRevision)); $command = sprintf('svn log -r%s:%s --incremental', ProcessExecutor::escape($fromRevision), ProcessExecutor::escape($toRevision));
$util = new SvnUtil($baseUrl, $this->io, $this->config); $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process);
$util->setCacheCredentials($this->cacheCredentials); $util->setCacheCredentials($this->cacheCredentials);
try { try {
return $util->executeLocal($command, $path, null, $this->io->isVerbose()); return $util->executeLocal($command, $path, null, $this->io->isVerbose());

View File

@ -21,6 +21,9 @@ use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
@ -120,7 +123,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
} }
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false); $this->io->writeError(" - " . InstallOperation::format($package).': ', false);
$urls = $this->prepareUrls($package->getSourceUrls()); $urls = $this->prepareUrls($package->getSourceUrls());
while ($url = array_shift($urls)) { while ($url = array_shift($urls)) {
@ -153,23 +156,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
throw new \InvalidArgumentException('Package '.$target->getPrettyName().' is missing reference information'); throw new \InvalidArgumentException('Package '.$target->getPrettyName().' is missing reference information');
} }
$name = $target->getName(); $this->io->writeError(" - " . UpdateOperation::format($initial, $target).': ', false);
if ($initial->getPrettyVersion() == $target->getPrettyVersion()) {
if ($target->getSourceType() === 'svn') {
$from = $initial->getSourceReference();
$to = $target->getSourceReference();
} else {
$from = substr($initial->getSourceReference(), 0, 7);
$to = substr($target->getSourceReference(), 0, 7);
}
$name .= ' '.$initial->getPrettyVersion();
} else {
$from = $initial->getFullPrettyVersion();
$to = $target->getFullPrettyVersion();
}
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$urls = $this->prepareUrls($target->getSourceUrls()); $urls = $this->prepareUrls($target->getSourceUrls());
@ -227,7 +214,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
*/ */
public function remove(PackageInterface $package, $path) public function remove(PackageInterface $package, $path)
{ {
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)"); $this->io->writeError(" - " . UninstallOperation::format($package));
if (!$this->filesystem->removeDirectory($path)) { if (!$this->filesystem->removeDirectory($path)) {
throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
} }

View File

@ -19,6 +19,7 @@ use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
/** /**
* Xz archive downloader. * Xz archive downloader.
@ -28,16 +29,6 @@ use Composer\IO\IOInterface;
*/ */
class XzDownloader extends ArchiveDownloader class XzDownloader extends ArchiveDownloader
{ {
/** @var ProcessExecutor */
protected $process;
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract(PackageInterface $package, $file, $path) protected function extract(PackageInterface $package, $file, $path)
{ {
$command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);

View File

@ -19,6 +19,7 @@ use Composer\Package\PackageInterface;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Filesystem;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\ExecutableFinder;
@ -33,17 +34,9 @@ class ZipDownloader extends ArchiveDownloader
private static $hasZipArchive; private static $hasZipArchive;
private static $isWindows; private static $isWindows;
/** @var ProcessExecutor */
protected $process;
/** @var ZipArchive|null */ /** @var ZipArchive|null */
private $zipArchiveObject; private $zipArchiveObject;
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -85,9 +78,8 @@ class ZipDownloader extends ArchiveDownloader
* @param string $file File to extract * @param string $file File to extract
* @param string $path Path where to extract file * @param string $path Path where to extract file
* @param bool $isLastChance If true it is called as a fallback and should throw an exception * @param bool $isLastChance If true it is called as a fallback and should throw an exception
* @return bool Success status
*/ */
protected function extractWithSystemUnzip($file, $path, $isLastChance) private function extractWithSystemUnzip(PackageInterface $package, $file, $path, $isLastChance, $async = false)
{ {
if (!self::$hasZipArchive) { if (!self::$hasZipArchive) {
// Force Exception throwing if the Other alternative is not available // Force Exception throwing if the Other alternative is not available
@ -97,18 +89,56 @@ class ZipDownloader extends ArchiveDownloader
if (!self::$hasSystemUnzip && !$isLastChance) { if (!self::$hasSystemUnzip && !$isLastChance) {
// This was call as the favorite extract way, but is not available // This was call as the favorite extract way, but is not available
// We switch to the alternative // We switch to the alternative
return $this->extractWithZipArchive($file, $path, true); return $this->extractWithZipArchive($package, $file, $path, true);
}
// When called after a ZipArchive failed, perhaps there is some files to overwrite
$overwrite = $isLastChance ? '-o' : '';
$command = 'unzip -qq '.$overwrite.' '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path);
if ($async) {
$self = $this;
$io = $this->io;
$tryFallback = function ($processError) use ($isLastChance, $io, $self, $file, $path, $package) {
if ($isLastChance) {
throw $processError;
}
if (!is_file($file)) {
$io->writeError(' <warning>'.$processError->getMessage().'</warning>');
$io->writeError(' <warning>This most likely is due to a custom installer plugin not handling the returned Promise from the downloader</warning>');
$io->writeError(' <warning>See https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb for an example fix</warning>');
} else {
$io->writeError(' <warning>'.$processError->getMessage().'</warning>');
$io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)');
$io->writeError(' Unzip with unzip command failed, falling back to ZipArchive class');
}
return $self->extractWithZipArchive($package, $file, $path, true);
};
try {
$promise = $this->process->executeAsync($command);
return $promise->then(function ($process) use ($tryFallback, $command, $package, $file) {
if (!$process->isSuccessful()) {
$output = $process->getErrorOutput();
$output = str_replace(', '.$file.'.zip or '.$file.'.ZIP', '', $output);
return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$output));
}
});
} catch (\Exception $e) {
return $tryFallback($e);
} catch (\Throwable $e) {
return $tryFallback($e);
}
} }
$processError = null; $processError = null;
// When called after a ZipArchive failed, perhaps there is some files to overwrite
$overwrite = $isLastChance ? '-o' : '';
$command = 'unzip -qq '.$overwrite.' '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path);
try { try {
if (0 === $exitCode = $this->process->execute($command, $ignoredOutput)) { if (0 === $exitCode = $this->process->execute($command, $ignoredOutput)) {
return true; return \React\Promise\resolve();
} }
$processError = new \RuntimeException('Failed to execute ('.$exitCode.') '.$command."\n\n".$this->process->getErrorOutput()); $processError = new \RuntimeException('Failed to execute ('.$exitCode.') '.$command."\n\n".$this->process->getErrorOutput());
@ -120,11 +150,11 @@ class ZipDownloader extends ArchiveDownloader
throw $processError; throw $processError;
} }
$this->io->writeError(' '.$processError->getMessage()); $this->io->writeError(' <warning>'.$processError->getMessage().'</warning>');
$this->io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)'); $this->io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)');
$this->io->writeError(' Unzip with unzip command failed, falling back to ZipArchive class'); $this->io->writeError(' Unzip with unzip command failed, falling back to ZipArchive class');
return $this->extractWithZipArchive($file, $path, true); return $this->extractWithZipArchive($package, $file, $path, true);
} }
/** /**
@ -133,9 +163,11 @@ class ZipDownloader extends ArchiveDownloader
* @param string $file File to extract * @param string $file File to extract
* @param string $path Path where to extract file * @param string $path Path where to extract file
* @param bool $isLastChance If true it is called as a fallback and should throw an exception * @param bool $isLastChance If true it is called as a fallback and should throw an exception
* @return bool Success status *
* TODO v3 should make this private once we can drop PHP 5.3 support
* @protected
*/ */
protected function extractWithZipArchive($file, $path, $isLastChance) public function extractWithZipArchive(PackageInterface $package, $file, $path, $isLastChance)
{ {
if (!self::$hasSystemUnzip) { if (!self::$hasSystemUnzip) {
// Force Exception throwing if the Other alternative is not available // Force Exception throwing if the Other alternative is not available
@ -145,7 +177,7 @@ class ZipDownloader extends ArchiveDownloader
if (!self::$hasZipArchive && !$isLastChance) { if (!self::$hasZipArchive && !$isLastChance) {
// This was call as the favorite extract way, but is not available // This was call as the favorite extract way, but is not available
// We switch to the alternative // We switch to the alternative
return $this->extractWithSystemUnzip($file, $path, true); return $this->extractWithSystemUnzip($package, $file, $path, true);
} }
$processError = null; $processError = null;
@ -158,7 +190,7 @@ class ZipDownloader extends ArchiveDownloader
if (true === $extractResult) { if (true === $extractResult) {
$zipArchive->close(); $zipArchive->close();
return true; return \React\Promise\resolve();
} }
$processError = new \RuntimeException(rtrim("There was an error extracting the ZIP file, it is either corrupted or using an invalid format.\n")); $processError = new \RuntimeException(rtrim("There was an error extracting the ZIP file, it is either corrupted or using an invalid format.\n"));
@ -169,16 +201,18 @@ class ZipDownloader extends ArchiveDownloader
$processError = new \RuntimeException('The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems): '.$e->getMessage(), 0, $e); $processError = new \RuntimeException('The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems): '.$e->getMessage(), 0, $e);
} catch (\Exception $e) { } catch (\Exception $e) {
$processError = $e; $processError = $e;
} catch (\Throwable $e) {
$processError = $e;
} }
if ($isLastChance) { if ($isLastChance) {
throw $processError; throw $processError;
} }
$this->io->writeError(' '.$processError->getMessage()); $this->io->writeError(' <warning>'.$processError->getMessage().'</warning>');
$this->io->writeError(' Unzip with ZipArchive class failed, falling back to unzip command'); $this->io->writeError(' Unzip with ZipArchive class failed, falling back to unzip command');
return $this->extractWithSystemUnzip($file, $path, true); return $this->extractWithSystemUnzip($package, $file, $path, true);
} }
/** /**
@ -191,10 +225,10 @@ class ZipDownloader extends ArchiveDownloader
{ {
// Each extract calls its alternative if not available or fails // Each extract calls its alternative if not available or fails
if (self::$isWindows) { if (self::$isWindows) {
$this->extractWithZipArchive($file, $path, false); return $this->extractWithZipArchive($package, $file, $path, false);
} else {
$this->extractWithSystemUnzip($file, $path, false);
} }
return $this->extractWithSystemUnzip($package, $file, $path, false, true);
} }
/** /**

View File

@ -28,6 +28,7 @@ use Composer\Installer\PackageEvent;
use Composer\Installer\BinaryInstaller; use Composer\Installer\BinaryInstaller;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Script\Event as ScriptEvent; use Composer\Script\Event as ScriptEvent;
use Composer\ClassLoader;
use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\PhpExecutableFinder;
/** /**
@ -45,11 +46,17 @@ use Symfony\Component\Process\PhpExecutableFinder;
*/ */
class EventDispatcher class EventDispatcher
{ {
/** @var Composer */
protected $composer; protected $composer;
/** @var IOInterface */
protected $io; protected $io;
/** @var ?ClassLoader */
protected $loader; protected $loader;
/** @var ProcessExecutor */
protected $process; protected $process;
/** @var array<string, array<int, array<callable|string>>> */
protected $listeners = array(); protected $listeners = array();
/** @var list<string> */
private $eventStack; private $eventStack;
/** /**

View File

@ -335,15 +335,16 @@ class Factory
} }
$httpDownloader = self::createHttpDownloader($io, $config); $httpDownloader = self::createHttpDownloader($io, $config);
$loop = new Loop($httpDownloader); $process = new ProcessExecutor($io);
$loop = new Loop($httpDownloader, $process);
$composer->setLoop($loop); $composer->setLoop($loop);
// initialize event dispatcher // initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io); $dispatcher = new EventDispatcher($composer, $io, $process);
$composer->setEventDispatcher($dispatcher); $composer->setEventDispatcher($dispatcher);
// initialize repository manager // initialize repository manager
$rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher); $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher, $process);
$composer->setRepositoryManager($rm); $composer->setRepositoryManager($rm);
// force-set the version of the global package if not defined as // force-set the version of the global package if not defined as
@ -354,7 +355,7 @@ class Factory
// load package // load package
$parser = new VersionParser; $parser = new VersionParser;
$guesser = new VersionGuesser($config, new ProcessExecutor($io), $parser); $guesser = new VersionGuesser($config, $process, $parser);
$loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser, $io); $loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser, $io);
$package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd); $package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd);
$composer->setPackage($package); $composer->setPackage($package);
@ -368,7 +369,7 @@ class Factory
if ($fullLoad) { if ($fullLoad) {
// initialize download manager // initialize download manager
$dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher); $dm = $this->createDownloadManager($io, $config, $httpDownloader, $process, $dispatcher);
$composer->setDownloadManager($dm); $composer->setDownloadManager($dm);
// initialize autoload generator // initialize autoload generator
@ -381,7 +382,7 @@ class Factory
} }
// add installers to the manager (must happen after download manager is created since they read it out of $composer) // add installers to the manager (must happen after download manager is created since they read it out of $composer)
$this->createDefaultInstallers($im, $composer, $io); $this->createDefaultInstallers($im, $composer, $io, $process);
if ($fullLoad) { if ($fullLoad) {
$globalComposer = null; $globalComposer = null;
@ -399,7 +400,7 @@ class Factory
if ($fullLoad && isset($composerFile)) { if ($fullLoad && isset($composerFile)) {
$lockFile = self::getLockFile($composerFile); $lockFile = self::getLockFile($composerFile);
$locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)); $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile), $process);
$composer->setLocker($locker); $composer->setLocker($locker);
} }
@ -460,14 +461,16 @@ class Factory
* @param EventDispatcher $eventDispatcher * @param EventDispatcher $eventDispatcher
* @return Downloader\DownloadManager * @return Downloader\DownloadManager
*/ */
public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process, EventDispatcher $eventDispatcher = null)
{ {
$cache = null; $cache = null;
if ($config->get('cache-files-ttl') > 0) { if ($config->get('cache-files-ttl') > 0) {
$cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./'); $cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./');
} }
$dm = new Downloader\DownloadManager($io); $fs = new Filesystem($process);
$dm = new Downloader\DownloadManager($io, false, $fs);
switch ($preferred = $config->get('preferred-install')) { switch ($preferred = $config->get('preferred-install')) {
case 'dist': case 'dist':
$dm->setPreferDist(true); $dm->setPreferDist(true);
@ -485,22 +488,19 @@ class Factory
$dm->setPreferences($preferred); $dm->setPreferences($preferred);
} }
$executor = new ProcessExecutor($io); $dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $process, $fs));
$fs = new Filesystem($executor); $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $process, $fs));
$dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $process, $fs));
$dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $executor, $fs)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $process, $fs));
$dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $executor, $fs)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config, $process, $fs));
$dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs)); $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
return $dm; return $dm;
} }
@ -544,10 +544,13 @@ class Factory
* @param Composer $composer * @param Composer $composer
* @param IO\IOInterface $io * @param IO\IOInterface $io
*/ */
protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io) protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null)
{ {
$im->addInstaller(new Installer\LibraryInstaller($io, $composer, null)); $fs = new Filesystem($process);
$im->addInstaller(new Installer\PluginInstaller($io, $composer)); $binaryInstaller = new Installer\BinaryInstaller($io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $fs);
$im->addInstaller(new Installer\LibraryInstaller($io, $composer, null, $fs, $binaryInstaller));
$im->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller));
$im->addInstaller(new Installer\MetapackageInstaller($io)); $im->addInstaller(new Installer\MetapackageInstaller($io));
} }

View File

@ -14,6 +14,7 @@ namespace Composer\IO;
use Composer\Question\StrictConfirmationQuestion; use Composer\Question\StrictConfirmationQuestion;
use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -253,6 +254,15 @@ class ConsoleIO extends BaseIO
} }
} }
/**
* @param int $max
* @return ProgressBar
*/
public function getProgressBar($max = 0)
{
return new ProgressBar($this->getErrorOutput(), $max);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -226,7 +226,7 @@ class Installer
} }
if ($this->runScripts) { if ($this->runScripts) {
$_SERVER['COMPOSER_DEV_MODE'] = (int) $this->devMode; $_SERVER['COMPOSER_DEV_MODE'] = $this->devMode ? '1' : '0';
putenv('COMPOSER_DEV_MODE='.$_SERVER['COMPOSER_DEV_MODE']); putenv('COMPOSER_DEV_MODE='.$_SERVER['COMPOSER_DEV_MODE']);
// dispatch pre event // dispatch pre event
@ -685,7 +685,7 @@ class Installer
} }
if ($this->executeOperations) { if ($this->executeOperations) {
$this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode); $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts);
} else { } else {
foreach ($localRepoTransaction->getOperations() as $operation) { foreach ($localRepoTransaction->getOperations() as $operation) {
// output op, but alias op only in debug verbosity // output op, but alias op only in debug verbosity

View File

@ -13,6 +13,7 @@
namespace Composer\Installer; namespace Composer\Installer;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\IO\ConsoleIO;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryInterface;
@ -26,6 +27,7 @@ use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\Util\Loop; use Composer\Util\Loop;
use React\Promise\PromiseInterface;
/** /**
* Package operation manager. * Package operation manager.
@ -36,12 +38,20 @@ use Composer\Util\Loop;
*/ */
class InstallationManager class InstallationManager
{ {
/** @var array<InstallerInterface> */
private $installers = array(); private $installers = array();
/** @var array<string, InstallerInterface> */
private $cache = array(); private $cache = array();
/** @var array<string, array<PackageInterface>> */
private $notifiablePackages = array(); private $notifiablePackages = array();
/** @var Loop */
private $loop; private $loop;
/** @var IOInterface */
private $io; private $io;
/** @var EventDispatcher */
private $eventDispatcher; private $eventDispatcher;
/** @var bool */
private $outputProgress;
public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null)
{ {
@ -166,7 +176,7 @@ class InstallationManager
* @param RepositoryInterface $repo repository in which to add/remove/update packages * @param RepositoryInterface $repo repository in which to add/remove/update packages
* @param OperationInterface[] $operations operations to execute * @param OperationInterface[] $operations operations to execute
* @param bool $devMode whether the install is being run in dev mode * @param bool $devMode whether the install is being run in dev mode
* @param bool $operation whether to dispatch script events * @param bool $runScripts whether to dispatch script events
*/ */
public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true)
{ {
@ -177,10 +187,12 @@ class InstallationManager
$runCleanup = function () use (&$cleanupPromises, $loop) { $runCleanup = function () use (&$cleanupPromises, $loop) {
$promises = array(); $promises = array();
$loop->abortJobs();
foreach ($cleanupPromises as $cleanup) { foreach ($cleanupPromises as $cleanup) {
$promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) {
$promise = $cleanup(); $promise = $cleanup();
if (null === $promise) { if (!$promise instanceof PromiseInterface) {
$resolve(); $resolve();
} else { } else {
$promise->then(function () use ($resolve) { $promise->then(function () use ($resolve) {
@ -259,9 +271,73 @@ class InstallationManager
// execute all downloads first // execute all downloads first
if (!empty($promises)) { if (!empty($promises)) {
$this->loop->wait($promises); $progress = null;
if ($this->outputProgress && $this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1) {
$progress = $this->io->getProgressBar();
}
$this->loop->wait($promises, $progress);
if ($progress) {
$progress->clear();
}
} }
// execute operations in batches to make sure every plugin is installed in the
// right order and activated before the packages depending on it are installed
$batches = array();
$batch = array();
foreach ($operations as $index => $operation) {
if (in_array($operation->getOperationType(), array('update', 'install'), true)) {
$package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage();
if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') {
if ($batch) {
$batches[] = $batch;
}
unset($operations[$index]);
$batches[] = array($index => $operation);
$batch = array();
continue;
}
}
unset($operations[$index]);
$batch[$index] = $operation;
}
if ($batch) {
$batches[] = $batch;
}
foreach ($batches as $batch) {
$this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts);
}
} catch (\Exception $e) {
$runCleanup();
if ($handleInterruptsUnix) {
pcntl_signal(SIGINT, $prevHandler);
}
if ($handleInterruptsWindows) {
sapi_windows_set_ctrl_handler($prevHandler, false);
}
throw $e;
}
if ($handleInterruptsUnix) {
pcntl_signal(SIGINT, $prevHandler);
}
if ($handleInterruptsWindows) {
sapi_windows_set_ctrl_handler($prevHandler, false);
}
// do a last write so that we write the repository even if nothing changed
// as that can trigger an update of some files like InstalledVersions.php if
// running a new composer version
$repo->write($devMode, $this);
}
private function executeBatch(RepositoryInterface $repo, array $operations, array $cleanupPromises, $devMode, $runScripts)
{
foreach ($operations as $index => $operation) { foreach ($operations as $index => $operation) {
$opType = $operation->getOperationType(); $opType = $operation->getOperationType();
@ -292,12 +368,11 @@ class InstallationManager
$dispatcher = $this->eventDispatcher; $dispatcher = $this->eventDispatcher;
$installManager = $this; $installManager = $this;
$loop = $this->loop;
$io = $this->io; $io = $this->io;
$promise = $installer->prepare($opType, $package, $initialPackage); $promise = $installer->prepare($opType, $package, $initialPackage);
if (null === $promise) { if (!$promise instanceof PromiseInterface) {
$promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); $promise = \React\Promise\resolve();
} }
$promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) {
@ -321,32 +396,15 @@ class InstallationManager
// execute all prepare => installs/updates/removes => cleanup steps // execute all prepare => installs/updates/removes => cleanup steps
if (!empty($promises)) { if (!empty($promises)) {
$this->loop->wait($promises); $progress = null;
if ($this->outputProgress && $this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1) {
$progress = $this->io->getProgressBar();
} }
} catch (\Exception $e) { $this->loop->wait($promises, $progress);
$runCleanup(); if ($progress) {
$progress->clear();
if ($handleInterruptsUnix) {
pcntl_signal(SIGINT, $prevHandler);
} }
if ($handleInterruptsWindows) {
sapi_windows_set_ctrl_handler($prevHandler, false);
} }
throw $e;
}
if ($handleInterruptsUnix) {
pcntl_signal(SIGINT, $prevHandler);
}
if ($handleInterruptsWindows) {
sapi_windows_set_ctrl_handler($prevHandler, false);
}
// do a last write so that we write the repository even if nothing changed
// as that can trigger an update of some files like InstalledVersions.php if
// running a new composer version
$repo->write($devMode, $this);
} }
/** /**
@ -447,6 +505,11 @@ class InstallationManager
return $installer->getInstallPath($package); return $installer->getInstallPath($package);
} }
public function setOutputProgress($outputProgress)
{
$this->outputProgress = $outputProgress;
}
public function notifyInstalls(IOInterface $io) public function notifyInstalls(IOInterface $io)
{ {
foreach ($this->notifiablePackages as $repoUrl => $packages) { foreach ($this->notifiablePackages as $repoUrl => $packages) {

View File

@ -19,6 +19,7 @@ use Composer\Package\PackageInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Composer\Util\Platform; use Composer\Util\Platform;
use React\Promise\PromiseInterface;
/** /**
* Package installation manager. * Package installation manager.
@ -131,11 +132,19 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
$this->binaryInstaller->removeBinaries($package); $this->binaryInstaller->removeBinaries($package);
} }
$this->installCode($package); $promise = $this->installCode($package);
$this->binaryInstaller->installBinaries($package, $this->getInstallPath($package)); if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$binaryInstaller = $this->binaryInstaller;
$installPath = $this->getInstallPath($package);
return $promise->then(function () use ($binaryInstaller, $installPath, $package, $repo) {
$binaryInstaller->installBinaries($package, $installPath);
if (!$repo->hasPackage($package)) { if (!$repo->hasPackage($package)) {
$repo->addPackage(clone $package); $repo->addPackage(clone $package);
} }
});
} }
/** /**
@ -150,12 +159,20 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
$this->initializeVendorDir(); $this->initializeVendorDir();
$this->binaryInstaller->removeBinaries($initial); $this->binaryInstaller->removeBinaries($initial);
$this->updateCode($initial, $target); $promise = $this->updateCode($initial, $target);
$this->binaryInstaller->installBinaries($target, $this->getInstallPath($target)); if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$binaryInstaller = $this->binaryInstaller;
$installPath = $this->getInstallPath($target);
return $promise->then(function () use ($binaryInstaller, $installPath, $target, $initial, $repo) {
$binaryInstaller->installBinaries($target, $installPath);
$repo->removePackage($initial); $repo->removePackage($initial);
if (!$repo->hasPackage($target)) { if (!$repo->hasPackage($target)) {
$repo->addPackage(clone $target); $repo->addPackage(clone $target);
} }
});
} }
/** /**
@ -167,17 +184,25 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
throw new \InvalidArgumentException('Package is not installed: '.$package); throw new \InvalidArgumentException('Package is not installed: '.$package);
} }
$this->removeCode($package); $promise = $this->removeCode($package);
$this->binaryInstaller->removeBinaries($package); if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$binaryInstaller = $this->binaryInstaller;
$downloadPath = $this->getPackageBasePath($package);
$filesystem = $this->filesystem;
return $promise->then(function () use ($binaryInstaller, $filesystem, $downloadPath, $package, $repo) {
$binaryInstaller->removeBinaries($package);
$repo->removePackage($package); $repo->removePackage($package);
$downloadPath = $this->getPackageBasePath($package);
if (strpos($package->getName(), '/')) { if (strpos($package->getName(), '/')) {
$packageVendorDir = dirname($downloadPath); $packageVendorDir = dirname($downloadPath);
if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) { if (is_dir($packageVendorDir) && $filesystem->isDirEmpty($packageVendorDir)) {
Silencer::call('rmdir', $packageVendorDir); Silencer::call('rmdir', $packageVendorDir);
} }
} }
});
} }
/** /**
@ -227,7 +252,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
protected function installCode(PackageInterface $package) protected function installCode(PackageInterface $package)
{ {
$downloadPath = $this->getInstallPath($package); $downloadPath = $this->getInstallPath($package);
$this->downloadManager->install($package, $downloadPath); return $this->downloadManager->install($package, $downloadPath);
} }
protected function updateCode(PackageInterface $initial, PackageInterface $target) protected function updateCode(PackageInterface $initial, PackageInterface $target)
@ -240,21 +265,31 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
if (substr($initialDownloadPath, 0, strlen($targetDownloadPath)) === $targetDownloadPath if (substr($initialDownloadPath, 0, strlen($targetDownloadPath)) === $targetDownloadPath
|| substr($targetDownloadPath, 0, strlen($initialDownloadPath)) === $initialDownloadPath || substr($targetDownloadPath, 0, strlen($initialDownloadPath)) === $initialDownloadPath
) { ) {
$this->removeCode($initial); $promise = $this->removeCode($initial);
$this->installCode($target); if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
return; $self = $this;
return $promise->then(function () use ($self, $target) {
$reflMethod = new \ReflectionMethod($self, 'installCode');
$reflMethod->setAccessible(true);
// equivalent of $this->installCode($target) with php 5.3 support
// TODO remove this once 5.3 support is dropped
return $reflMethod->invoke($self, $target);
});
} }
$this->filesystem->rename($initialDownloadPath, $targetDownloadPath); $this->filesystem->rename($initialDownloadPath, $targetDownloadPath);
} }
$this->downloadManager->update($initial, $target, $targetDownloadPath); return $this->downloadManager->update($initial, $target, $targetDownloadPath);
} }
protected function removeCode(PackageInterface $package) protected function removeCode(PackageInterface $package)
{ {
$downloadPath = $this->getPackageBasePath($package); $downloadPath = $this->getPackageBasePath($package);
$this->downloadManager->remove($package, $downloadPath); return $this->downloadManager->remove($package, $downloadPath);
} }
protected function initializeVendorDir() protected function initializeVendorDir()

View File

@ -16,6 +16,9 @@ use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
/** /**
* Metapackage installation manager. * Metapackage installation manager.
@ -76,7 +79,7 @@ class MetapackageInstaller implements InstallerInterface
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{ {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)"); $this->io->writeError(" - " . InstallOperation::format($package));
$repo->addPackage(clone $package); $repo->addPackage(clone $package);
} }
@ -90,11 +93,7 @@ class MetapackageInstaller implements InstallerInterface
throw new \InvalidArgumentException('Package is not installed: '.$initial); throw new \InvalidArgumentException('Package is not installed: '.$initial);
} }
$name = $target->getName(); $this->io->writeError(" - " . UpdateOperation::format($initial, $target));
$from = $initial->getFullPrettyVersion();
$to = $target->getFullPrettyVersion();
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>)");
$repo->removePackage($initial); $repo->removePackage($initial);
$repo->addPackage(clone $target); $repo->addPackage(clone $target);
@ -109,7 +108,7 @@ class MetapackageInstaller implements InstallerInterface
throw new \InvalidArgumentException('Package is not installed: '.$package); throw new \InvalidArgumentException('Package is not installed: '.$package);
} }
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)"); $this->io->writeError(" - " . UninstallOperation::format($package));
$repo->removePackage($package); $repo->removePackage($package);
} }

View File

@ -16,6 +16,8 @@ use Composer\Composer;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/** /**
* Installer for plugin packages * Installer for plugin packages
@ -33,9 +35,9 @@ class PluginInstaller extends LibraryInstaller
* @param IOInterface $io * @param IOInterface $io
* @param Composer $composer * @param Composer $composer
*/ */
public function __construct(IOInterface $io, Composer $composer) public function __construct(IOInterface $io, Composer $composer, Filesystem $fs = null, BinaryInstaller $binaryInstaller = null)
{ {
parent::__construct($io, $composer, 'composer-plugin'); parent::__construct($io, $composer, 'composer-plugin', $fs, $binaryInstaller);
$this->installationManager = $composer->getInstallationManager(); $this->installationManager = $composer->getInstallationManager();
} }
@ -65,15 +67,20 @@ class PluginInstaller extends LibraryInstaller
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{ {
parent::install($repo, $package); $promise = parent::install($repo, $package);
try { if (!$promise instanceof PromiseInterface) {
$this->composer->getPluginManager()->registerPackage($package, true); $promise = \React\Promise\resolve();
} catch (\Exception $e) {
// Rollback installation
$this->io->writeError('Plugin initialization failed ('.$e->getMessage().'), uninstalling plugin');
parent::uninstall($repo, $package);
throw $e;
} }
$pluginManager = $this->composer->getPluginManager();
$self = $this;
return $promise->then(function () use ($self, $pluginManager, $package, $repo) {
try {
$pluginManager->registerPackage($package, true);
} catch (\Exception $e) {
$self->rollbackInstall($e, $repo, $package);
}
});
} }
/** /**
@ -81,22 +88,38 @@ class PluginInstaller extends LibraryInstaller
*/ */
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
{ {
parent::update($repo, $initial, $target); $promise = parent::update($repo, $initial, $target);
if (!$promise instanceof PromiseInterface) {
try { $promise = \React\Promise\resolve();
$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;
} }
$pluginManager = $this->composer->getPluginManager();
$self = $this;
return $promise->then(function () use ($self, $pluginManager, $initial, $target, $repo) {
try {
$pluginManager->deactivatePackage($initial, true);
$pluginManager->registerPackage($target, true);
} catch (\Exception $e) {
$self->rollbackInstall($e, $repo, $target);
}
});
} }
public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
{ {
$this->composer->getPluginManager()->uninstallPackage($package, true); $this->composer->getPluginManager()->uninstallPackage($package, true);
return parent::uninstall($repo, $package);
}
/**
* TODO v3 should make this private once we can drop PHP 5.3 support
* @private
*/
public function rollbackInstall(\Exception $e, InstalledRepositoryInterface $repo, PackageInterface $package)
{
$this->io->writeError('Plugin initialization failed ('.$e->getMessage().'), uninstalling plugin');
parent::uninstall($repo, $package); parent::uninstall($repo, $package);
throw $e;
} }
} }

View File

@ -29,11 +29,11 @@ class ProjectInstaller implements InstallerInterface
private $downloadManager; private $downloadManager;
private $filesystem; private $filesystem;
public function __construct($installPath, DownloadManager $dm) public function __construct($installPath, DownloadManager $dm, Filesystem $fs)
{ {
$this->installPath = rtrim(strtr($installPath, '\\', '/'), '/').'/'; $this->installPath = rtrim(strtr($installPath, '\\', '/'), '/').'/';
$this->downloadManager = $dm; $this->downloadManager = $dm;
$this->filesystem = new Filesystem; $this->filesystem = $fs;
} }
/** /**
@ -76,7 +76,7 @@ class ProjectInstaller implements InstallerInterface
*/ */
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
{ {
$this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage); return $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage);
} }
/** /**
@ -84,7 +84,7 @@ class ProjectInstaller implements InstallerInterface
*/ */
public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
{ {
$this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage); return $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage);
} }
/** /**
@ -92,7 +92,7 @@ class ProjectInstaller implements InstallerInterface
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{ {
$this->downloadManager->install($package, $this->installPath); return $this->downloadManager->install($package, $this->installPath);
} }
/** /**

View File

@ -25,7 +25,9 @@ use Composer\Json\JsonFile;
*/ */
class ArchiveManager class ArchiveManager
{ {
/** @var DownloadManager */
protected $downloadManager; protected $downloadManager;
/** @var Loop */
protected $loop; protected $loop;
/** /**

View File

@ -233,7 +233,7 @@ abstract class BasePackage implements PackageInterface
} }
// if source reference is a sha1 hash -- truncate // if source reference is a sha1 hash -- truncate
if ($truncate && \strlen($reference) === 40) { if ($truncate && \strlen($reference) === 40 && $this->getSourceType() !== 'svn') {
return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7); return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7);
} }

View File

@ -58,7 +58,7 @@ class RootPackageLoader extends ArrayLoader
$this->manager = $manager; $this->manager = $manager;
$this->config = $config; $this->config = $config;
$this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor(), $this->versionParser); $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser);
$this->io = $io; $this->io = $io;
} }

View File

@ -57,7 +57,7 @@ class Locker
* @param InstallationManager $installationManager installation manager instance * @param InstallationManager $installationManager installation manager instance
* @param string $composerFileContents The contents of the composer file * @param string $composerFileContents The contents of the composer file
*/ */
public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents) public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents, ProcessExecutor $process = null)
{ {
$this->lockFile = $lockFile; $this->lockFile = $lockFile;
$this->installationManager = $installationManager; $this->installationManager = $installationManager;
@ -65,7 +65,7 @@ class Locker
$this->contentHash = self::getContentHash($composerFileContents); $this->contentHash = self::getContentHash($composerFileContents);
$this->loader = new ArrayLoader(null, true); $this->loader = new ArrayLoader(null, true);
$this->dumper = new ArrayDumper(); $this->dumper = new ArrayDumper();
$this->process = new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
} }
/** /**

View File

@ -34,15 +34,23 @@ use Composer\Util\PackageSorter;
*/ */
class PluginManager class PluginManager
{ {
/** @var Composer */
protected $composer; protected $composer;
/** @var IOInterface */
protected $io; protected $io;
/** @var Composer */
protected $globalComposer; protected $globalComposer;
/** @var VersionParser */
protected $versionParser; protected $versionParser;
/** @var bool */
protected $disablePlugins = false; protected $disablePlugins = false;
/** @var array<PluginInterface> */
protected $plugins = array(); protected $plugins = array();
/** @var array<string, PluginInterface> */
protected $registeredPlugins = array(); protected $registeredPlugins = array();
/** @var int */
private static $classCounter = 0; private static $classCounter = 0;
/** /**

View File

@ -32,18 +32,32 @@ class PreFileDownloadEvent extends Event
*/ */
private $processedUrl; private $processedUrl;
/**
* @var string
*/
private $type;
/**
* @var mixed
*/
private $context;
/** /**
* Constructor. * Constructor.
* *
* @param string $name The event name * @param string $name The event name
* @param HttpDownloader $httpDownloader * @param HttpDownloader $httpDownloader
* @param string $processedUrl * @param string $processedUrl
* @param string $type
* @param mixed $context
*/ */
public function __construct($name, HttpDownloader $httpDownloader, $processedUrl) public function __construct($name, HttpDownloader $httpDownloader, $processedUrl, $type, $context = null)
{ {
parent::__construct($name); parent::__construct($name);
$this->httpDownloader = $httpDownloader; $this->httpDownloader = $httpDownloader;
$this->processedUrl = $processedUrl; $this->processedUrl = $processedUrl;
$this->type = $type;
$this->context = $context;
} }
/** /**
@ -55,7 +69,7 @@ class PreFileDownloadEvent extends Event
} }
/** /**
* Retrieves the processed URL this remote filesystem will be used for * Retrieves the processed URL that will be downloaded.
* *
* @return string * @return string
*/ */
@ -63,4 +77,36 @@ class PreFileDownloadEvent extends Event
{ {
return $this->processedUrl; return $this->processedUrl;
} }
/**
* Sets the processed URL that will be downloaded.
*
* @param string $processedUrl New processed URL
*/
public function setProcessedUrl($processedUrl)
{
$this->processedUrl = $processedUrl;
}
/**
* Returns the type of this download (package, metadata).
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Returns the context of this download, if any.
*
* If this download is of type package, the package object is returned.
*
* @return mixed
*/
public function getContext()
{
return $this->context;
}
} }

View File

@ -23,6 +23,7 @@ use Composer\Config;
use Composer\Composer; use Composer\Composer;
use Composer\Factory; use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Semver\CompilingMatcher;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Util\Loop; use Composer\Util\Loop;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
@ -764,7 +765,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
continue; continue;
} }
if ($constraint && !$constraint->matches(new Constraint('==', $version))) { if ($constraint && !CompilingMatcher::match($constraint, Constraint::OP_EQ, $version)) {
continue; continue;
} }
@ -1013,8 +1014,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
while ($retries--) { while ($retries--) {
try { try {
if ($this->eventDispatcher) { if ($this->eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata');
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
$filename = $preFileDownloadEvent->getProcessedUrl();
} }
$response = $this->httpDownloader->get($filename, $this->options); $response = $this->httpDownloader->get($filename, $this->options);
@ -1099,8 +1101,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
while ($retries--) { while ($retries--) {
try { try {
if ($this->eventDispatcher) { if ($this->eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata');
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
$filename = $preFileDownloadEvent->getProcessedUrl();
} }
$options = $this->options; $options = $this->options;
@ -1155,18 +1158,19 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$retries = 3; $retries = 3;
if (isset($this->packagesNotFoundCache[$filename])) { if (isset($this->packagesNotFoundCache[$filename])) {
return new Promise(function ($resolve, $reject) { $resolve(array('packages' => array())); }); return \React\Promise\resolve(array('packages' => array()));
} }
if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) { if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) {
// make it look like we got a 304 response // make it look like we got a 304 response
return new Promise(function ($resolve, $reject) { $resolve(true); }); return \React\Promise\resolve(true);
} }
$httpDownloader = $this->httpDownloader; $httpDownloader = $this->httpDownloader;
if ($this->eventDispatcher) { if ($this->eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata');
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
$filename = $preFileDownloadEvent->getProcessedUrl();
} }
$options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array();

View File

@ -30,6 +30,7 @@ class PlatformRepository extends ArrayRepository
{ {
const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer-(?:plugin|runtime)-api)$}iD'; const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer-(?:plugin|runtime)-api)$}iD';
private static $hhvmVersion;
private $versionParser; private $versionParser;
/** /**
@ -45,7 +46,7 @@ class PlatformRepository extends ArrayRepository
public function __construct(array $packages = array(), array $overrides = array(), ProcessExecutor $process = null) public function __construct(array $packages = array(), array $overrides = array(), ProcessExecutor $process = null)
{ {
$this->process = $process === null ? (new ProcessExecutor()) : $process; $this->process = $process;
foreach ($overrides as $name => $version) { foreach ($overrides as $name => $version) {
$this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version); $this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version);
} }
@ -251,22 +252,7 @@ class PlatformRepository extends ArrayRepository
$this->addPackage($lib); $this->addPackage($lib);
} }
$hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null; if ($hhvmVersion = self::getHHVMVersion($this->process)) {
if ($hhvmVersion === null && !Platform::isWindows()) {
$finder = new ExecutableFinder();
$hhvm = $finder->find('hhvm');
if ($hhvm !== null) {
$exitCode = $this->process->execute(
ProcessExecutor::escape($hhvm).
' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null',
$hhvmVersion
);
if ($exitCode !== 0) {
$hhvmVersion = null;
}
}
}
if ($hhvmVersion) {
try { try {
$prettyVersion = $hhvmVersion; $prettyVersion = $hhvmVersion;
$version = $this->versionParser->normalize($prettyVersion); $version = $this->versionParser->normalize($prettyVersion);
@ -362,4 +348,29 @@ class PlatformRepository extends ArrayRepository
{ {
return 'ext-' . str_replace(' ', '-', $name); return 'ext-' . str_replace(' ', '-', $name);
} }
private static function getHHVMVersion(ProcessExecutor $process = null)
{
if (null !== self::$hhvmVersion) {
return self::$hhvmVersion ?: null;
}
self::$hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null;
if (self::$hhvmVersion === null && !Platform::isWindows()) {
self::$hhvmVersion = false;
$finder = new ExecutableFinder();
$hhvmPath = $finder->find('hhvm');
if ($hhvmPath !== null) {
$process = $process ?: new ProcessExecutor();
$exitCode = $process->execute(
ProcessExecutor::escape($hhvmPath).
' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null',
self::$hhvmVersion
);
if ($exitCode !== 0) {
self::$hhvmVersion = false;
}
}
}
}
} }

View File

@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
use Composer\Config; use Composer\Config;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Util\ProcessExecutor;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
/** /**
@ -114,9 +115,9 @@ class RepositoryFactory
* @param HttpDownloader $httpDownloader * @param HttpDownloader $httpDownloader
* @return RepositoryManager * @return RepositoryManager
*/ */
public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, ProcessExecutor $process = null)
{ {
$rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher); $rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher, $process);
$rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
$rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');

View File

@ -17,6 +17,7 @@ use Composer\Config;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Util\ProcessExecutor;
/** /**
* Repositories manager. * Repositories manager.
@ -27,20 +28,30 @@ use Composer\Util\HttpDownloader;
*/ */
class RepositoryManager class RepositoryManager
{ {
/** @var InstalledRepositoryInterface */
private $localRepository; private $localRepository;
/** @var list<RepositoryInterface> */
private $repositories = array(); private $repositories = array();
/** @var array<string, string> */
private $repositoryClasses = array(); private $repositoryClasses = array();
/** @var IOInterface */
private $io; private $io;
/** @var Config */
private $config; private $config;
private $eventDispatcher; /** @var HttpDownloader */
private $httpDownloader; private $httpDownloader;
/** @var ?EventDispatcher */
private $eventDispatcher;
/** @var ProcessExecutor */
private $process;
public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, ProcessExecutor $process = null)
{ {
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->httpDownloader = $httpDownloader; $this->httpDownloader = $httpDownloader;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->process = $process ?: new ProcessExecutor($io);
} }
/** /**
@ -130,7 +141,7 @@ class RepositoryManager
unset($config['only'], $config['exclude'], $config['canonical']); unset($config['only'], $config['exclude'], $config['canonical']);
} }
$repository = new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher); $repository = new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher, $this->process);
if (isset($filterConfig)) { if (isset($filterConfig)) {
$repository = new FilterRepository($repository, $filterConfig); $repository = new FilterRepository($repository, $filterConfig);

View File

@ -19,6 +19,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\IO\NullIO; use Composer\IO\NullIO;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
use Composer\Package\AliasPackage;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
@ -44,7 +45,7 @@ class RepositorySet
/** /**
* @var array[] * @var array[]
* @psalm-var list<array{package: string, version: string, alias: string, alias_normalized: string}> * @psalm-var array<string, array<string, array{alias: string, alias_normalized: string}>>
*/ */
private $rootAliases; private $rootAliases;
@ -91,7 +92,7 @@ class RepositorySet
*/ */
public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array())
{ {
$this->rootAliases = $rootAliases; $this->rootAliases = self::getRootAliasesPerPackage($rootAliases);
$this->rootReferences = $rootReferences; $this->rootReferences = $rootReferences;
$this->acceptableStabilities = array(); $this->acceptableStabilities = array();
@ -249,8 +250,22 @@ class RepositorySet
$packages = array(); $packages = array();
foreach ($this->repositories as $repository) { foreach ($this->repositories as $repository) {
$packages = array_merge($packages, $repository->getPackages()); foreach ($repository->getPackages() as $package) {
$packages[] = $package;
if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) {
$alias = $this->rootAliases[$package->getName()][$package->getVersion()];
while ($package instanceof AliasPackage) {
$package = $package->getAliasOf();
} }
$aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']);
$aliasPackage->setRootPackageAlias(true);
$packages[] = $aliasPackage;
}
}
}
return new Pool($packages); return new Pool($packages);
} }
@ -270,4 +285,18 @@ class RepositorySet
return $this->createPool($request, new NullIO()); return $this->createPool($request, new NullIO());
} }
private static function getRootAliasesPerPackage(array $aliases)
{
$normalizedAliases = array();
foreach ($aliases as $alias) {
$normalizedAliases[$alias['package']][$alias['version']] = array(
'alias' => $alias['alias'],
'alias_normalized' => $alias['alias_normalized'],
);
}
return $normalizedAliases;
}
} }

View File

@ -223,7 +223,6 @@ class GitDriver extends VcsDriver
} }
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
return $process->execute('git ls-remote --heads ' . ProcessExecutor::escape($url), $output) === 0; return $process->execute('git ls-remote --heads ' . ProcessExecutor::escape($url), $output) === 0;
} }
} }

View File

@ -338,14 +338,12 @@ class GitHubDriver extends VcsDriver
$this->branches = array(); $this->branches = array();
$resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads?per_page=100'; $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads?per_page=100';
$branchBlacklist = array('gh-pages');
do { do {
$response = $this->getContents($resource); $response = $this->getContents($resource);
$branchData = $response->decodeJson(); $branchData = $response->decodeJson();
foreach ($branchData as $branch) { foreach ($branchData as $branch) {
$name = substr($branch['ref'], 11); $name = substr($branch['ref'], 11);
if (!in_array($name, $branchBlacklist)) { if ($name !== 'gh-pages') {
$this->branches[$name] = $branch['object']['sha']; $this->branches[$name] = $branch['object']['sha'];
} }
} }

View File

@ -228,8 +228,8 @@ class HgDriver extends VcsDriver
return false; return false;
} }
$processExecutor = new ProcessExecutor($io); $process = new ProcessExecutor($io);
$exit = $processExecutor->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored); $exit = $process->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored);
return $exit === 0; return $exit === 0;
} }

View File

@ -307,9 +307,8 @@ class SvnDriver extends VcsDriver
return false; return false;
} }
$processExecutor = new ProcessExecutor($io); $process = new ProcessExecutor($io);
$exit = $process->execute(
$exit = $processExecutor->execute(
"svn info --non-interactive ".ProcessExecutor::escape($url), "svn info --non-interactive ".ProcessExecutor::escape($url),
$ignoredOutput $ignoredOutput
); );
@ -320,14 +319,14 @@ class SvnDriver extends VcsDriver
} }
// Subversion client 1.7 and older // Subversion client 1.7 and older
if (false !== stripos($processExecutor->getErrorOutput(), 'authorization failed:')) { if (false !== stripos($process->getErrorOutput(), 'authorization failed:')) {
// This is likely a remote Subversion repository that requires // This is likely a remote Subversion repository that requires
// authentication. We will handle actual authentication later. // authentication. We will handle actual authentication later.
return true; return true;
} }
// Subversion client 1.8 and newer // Subversion client 1.8 and newer
if (false !== stripos($processExecutor->getErrorOutput(), 'Authentication failed')) { if (false !== stripos($process->getErrorOutput(), 'Authentication failed')) {
// This is likely a remote Subversion or newer repository that requires // This is likely a remote Subversion or newer repository that requires
// authentication. We will handle actual authentication later. // authentication. We will handle actual authentication later.
return true; return true;

View File

@ -53,7 +53,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
private $emptyReferences = array(); private $emptyReferences = array();
private $versionTransportExceptions = array(); private $versionTransportExceptions = array();
public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, ProcessExecutor $process = null, array $drivers = null, VersionCacheInterface $versionCache = null)
{ {
parent::__construct(); parent::__construct();
$this->drivers = $drivers ?: array( $this->drivers = $drivers ?: array(
@ -78,7 +78,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$this->repoConfig = $repoConfig; $this->repoConfig = $repoConfig;
$this->versionCache = $versionCache; $this->versionCache = $versionCache;
$this->httpDownloader = $httpDownloader; $this->httpDownloader = $httpDownloader;
$this->processExecutor = new ProcessExecutor($io); $this->processExecutor = $process ?: new ProcessExecutor($io);
} }
public function getRepoName() public function getRepoName()

View File

@ -63,7 +63,11 @@ class Versions
public function getLatest($channel = null) public function getLatest($channel = null)
{ {
$protocol = extension_loaded('openssl') ? 'https' : 'http'; if ($this->config->get('disable-tls') === true) {
$protocol = 'http';
} else {
$protocol = 'https';
}
$versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); $versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson();
foreach ($versions[$channel ?: $this->getChannel()] as $version) { foreach ($versions[$channel ?: $this->getChannel()] as $version) {

View File

@ -28,7 +28,7 @@ class Filesystem
public function __construct(ProcessExecutor $executor = null) public function __construct(ProcessExecutor $executor = null)
{ {
$this->processExecutor = $executor ?: new ProcessExecutor(); $this->processExecutor = $executor;
} }
public function remove($file) public function remove($file)
@ -320,7 +320,7 @@ class Filesystem
if (Platform::isWindows()) { if (Platform::isWindows()) {
// Try to copy & delete - this is a workaround for random "Access denied" errors. // Try to copy & delete - this is a workaround for random "Access denied" errors.
$command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
$result = $this->processExecutor->execute($command, $output); $result = $this->getProcess()->execute($command, $output);
// clear stat cache because external processes aren't tracked by the php stat cache // clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache(); clearstatcache();
@ -334,7 +334,7 @@ class Filesystem
// We do not use PHP's "rename" function here since it does not support // We do not use PHP's "rename" function here since it does not support
// the case where $source, and $target are located on different partitions. // the case where $source, and $target are located on different partitions.
$command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
$result = $this->processExecutor->execute($command, $output); $result = $this->getProcess()->execute($command, $output);
// clear stat cache because external processes aren't tracked by the php stat cache // clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache(); clearstatcache();
@ -546,6 +546,10 @@ class Filesystem
*/ */
protected function getProcess() protected function getProcess()
{ {
if (!$this->processExecutor) {
$this->processExecutor = new ProcessExecutor();
}
return $this->processExecutor; return $this->processExecutor;
} }

View File

@ -23,6 +23,7 @@ use Composer\Util\HttpDownloader;
use React\Promise\Promise; use React\Promise\Promise;
/** /**
* @internal
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nicolas Grekas <p@tchwork.com> * @author Nicolas Grekas <p@tchwork.com>
*/ */
@ -90,6 +91,9 @@ class CurlDownloader
$this->authHelper = new AuthHelper($io, $config); $this->authHelper = new AuthHelper($io, $config);
} }
/**
* @return int internal job id
*/
public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) public function download($resolve, $reject, $origin, $url, $options, $copyTo = null)
{ {
$attributes = array(); $attributes = array();
@ -101,6 +105,9 @@ class CurlDownloader
return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes); return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes);
} }
/**
* @return int internal job id
*/
private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array())
{ {
$attributes = array_merge(array( $attributes = array_merge(array(
@ -199,8 +206,29 @@ class CurlDownloader
} }
$this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle));
// TODO progress // TODO progress
//$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); //$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false);
return (int) $curlHandle;
}
public function abortRequest($id)
{
if (isset($this->jobs[$id]) && isset($this->jobs[$id]['handle'])) {
$job = $this->jobs[$id];
curl_multi_remove_handle($this->multiHandle, $job['handle']);
curl_close($job['handle']);
if (is_resource($job['headerHandle'])) {
fclose($job['headerHandle']);
}
if (is_resource($job['bodyHandle'])) {
fclose($job['bodyHandle']);
}
if ($job['filename']) {
@unlink($job['filename'].'~');
}
unset($this->jobs[$id]);
}
} }
public function tick() public function tick()
@ -235,7 +263,7 @@ class CurlDownloader
$statusCode = null; $statusCode = null;
$response = null; $response = null;
try { try {
// TODO progress // TODO progress
//$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']);
if (CURLE_OK !== $errno || $error) { if (CURLE_OK !== $errno || $error) {
throw new TransportException($error); throw new TransportException($error);
@ -285,8 +313,6 @@ class CurlDownloader
// fail 4xx and 5xx responses and capture the response // fail 4xx and 5xx responses and capture the response
if ($statusCode >= 400 && $statusCode <= 599) { if ($statusCode >= 400 && $statusCode <= 599) {
throw $this->failResponse($job, $response, $response->getStatusMessage()); throw $this->failResponse($job, $response, $response->getStatusMessage());
// TODO progress
// $this->io->overwriteError("Downloading (<error>failed</error>)", false);
} }
if ($job['attributes']['storeAuth']) { if ($job['attributes']['storeAuth']) {

View File

@ -31,6 +31,7 @@ class HttpDownloader
const STATUS_STARTED = 2; const STATUS_STARTED = 2;
const STATUS_COMPLETED = 3; const STATUS_COMPLETED = 3;
const STATUS_FAILED = 4; const STATUS_FAILED = 4;
const STATUS_ABORTED = 5;
private $io; private $io;
private $config; private $config;
@ -44,6 +45,7 @@ class HttpDownloader
private $rfs; private $rfs;
private $idGen = 0; private $idGen = 0;
private $disabled; private $disabled;
private $allowAsync = false;
/** /**
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
@ -139,6 +141,10 @@ class HttpDownloader
'origin' => Url::getOrigin($this->config, $request['url']), 'origin' => Url::getOrigin($this->config, $request['url']),
); );
if (!$sync && !$this->allowAsync) {
throw new \LogicException('You must use the HttpDownloader instance which is part of a Composer\Loop instance to be able to run async http requests');
}
// capture username/password from URL if there is one // capture username/password from URL if there is one
if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) {
$this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
@ -179,8 +185,20 @@ class HttpDownloader
$downloader = $this; $downloader = $this;
$io = $this->io; $io = $this->io;
$curl = $this->curl;
$canceler = function () {}; $canceler = function () use (&$job, $curl) {
if ($job['status'] === self::STATUS_QUEUED) {
$job['status'] = self::STATUS_ABORTED;
}
if ($job['status'] !== self::STATUS_STARTED) {
return;
}
$job['status'] = self::STATUS_ABORTED;
if (isset($job['curl_id'])) {
$curl->abortRequest($job['curl_id']);
}
};
$promise = new Promise($resolver, $canceler); $promise = new Promise($resolver, $canceler);
$promise->then(function ($response) use (&$job, $downloader) { $promise->then(function ($response) use (&$job, $downloader) {
@ -189,7 +207,6 @@ class HttpDownloader
// TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
$downloader->markJobDone(); $downloader->markJobDone();
$downloader->scheduleNextJob();
return $response; return $response;
}, function ($e) use (&$job, $downloader) { }, function ($e) use (&$job, $downloader) {
@ -197,7 +214,6 @@ class HttpDownloader
$job['exception'] = $e; $job['exception'] = $e;
$downloader->markJobDone(); $downloader->markJobDone();
$downloader->scheduleNextJob();
throw $e; throw $e;
}); });
@ -239,9 +255,9 @@ class HttpDownloader
} }
if ($job['request']['copyTo']) { if ($job['request']['copyTo']) {
$this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
} else { } else {
$this->curl->download($resolve, $reject, $origin, $url, $options); $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options);
} }
} }
@ -253,49 +269,58 @@ class HttpDownloader
$this->runningJobs--; $this->runningJobs--;
} }
/** public function wait($index = null)
* @private
*/
public function scheduleNextJob()
{ {
foreach ($this->jobs as $job) { while (true) {
if ($job['status'] === self::STATUS_QUEUED) { if (!$this->countActiveJobs($index)) {
$this->startJob($job['id']);
if ($this->runningJobs >= $this->maxJobs) {
return; return;
} }
usleep(1000);
}
}
/**
* @internal
*/
public function enableAsync()
{
$this->allowAsync = true;
}
/**
* @internal
*
* @return int number of active (queued or started) jobs
*/
public function countActiveJobs($index = null)
{
if ($this->runningJobs < $this->maxJobs) {
foreach ($this->jobs as $job) {
if ($job['status'] === self::STATUS_QUEUED && $this->runningJobs < $this->maxJobs) {
$this->startJob($job['id']);
} }
} }
} }
public function wait($index = null, $progress = false)
{
while (true) {
if ($this->curl) { if ($this->curl) {
$this->curl->tick(); $this->curl->tick();
} }
if (null !== $index) { if (null !== $index) {
if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0;
return;
} }
} else {
$done = true; $active = 0;
foreach ($this->jobs as $job) { foreach ($this->jobs as $job) {
if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { if ($job['status'] < self::STATUS_COMPLETED) {
$done = false; $active++;
break;
} elseif (!$job['sync']) { } elseif (!$job['sync']) {
unset($this->jobs[$job['id']]); unset($this->jobs[$job['id']]);
} }
} }
if ($done) {
return;
}
}
usleep(1000); return $active;
}
} }
private function getResponse($index) private function getResponse($index)

View File

@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use React\Promise\Promise; use React\Promise\Promise;
use Symfony\Component\Console\Helper\ProgressBar;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
@ -21,13 +22,22 @@ use React\Promise\Promise;
class Loop class Loop
{ {
private $httpDownloader; private $httpDownloader;
private $processExecutor;
private $currentPromises;
public function __construct(HttpDownloader $httpDownloader) public function __construct(HttpDownloader $httpDownloader = null, ProcessExecutor $processExecutor = null)
{ {
$this->httpDownloader = $httpDownloader; $this->httpDownloader = $httpDownloader;
if ($this->httpDownloader) {
$this->httpDownloader->enableAsync();
}
$this->processExecutor = $processExecutor;
if ($this->processExecutor) {
$this->processExecutor->enableAsync();
}
} }
public function wait(array $promises) public function wait(array $promises, ProgressBar $progress = null)
{ {
/** @var \Exception|null */ /** @var \Exception|null */
$uncaught = null; $uncaught = null;
@ -39,10 +49,52 @@ class Loop
} }
); );
$this->httpDownloader->wait(); $this->currentPromises = $promises;
if ($progress) {
$totalJobs = 0;
if ($this->httpDownloader) {
$totalJobs += $this->httpDownloader->countActiveJobs();
}
if ($this->processExecutor) {
$totalJobs += $this->processExecutor->countActiveJobs();
}
$progress->start($totalJobs);
}
while (true) {
$activeJobs = 0;
if ($this->httpDownloader) {
$activeJobs += $this->httpDownloader->countActiveJobs();
}
if ($this->processExecutor) {
$activeJobs += $this->processExecutor->countActiveJobs();
}
if ($progress) {
$progress->setProgress($progress->getMaxSteps() - $activeJobs);
}
if (!$activeJobs) {
break;
}
usleep(5000);
}
$this->currentPromises = null;
if ($uncaught) { if ($uncaught) {
throw $uncaught; throw $uncaught;
} }
} }
public function abortJobs()
{
if ($this->currentPromises) {
foreach ($this->currentPromises as $promise) {
$promise->cancel();
}
}
}
} }

View File

@ -16,18 +16,32 @@ use Composer\IO\IOInterface;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
use Symfony\Component\Process\ProcessUtils; use Symfony\Component\Process\ProcessUtils;
use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Exception\RuntimeException;
use React\Promise\Promise;
/** /**
* @author Robert Schönthal <seroscho@googlemail.com> * @author Robert Schönthal <seroscho@googlemail.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/ */
class ProcessExecutor class ProcessExecutor
{ {
const STATUS_QUEUED = 1;
const STATUS_STARTED = 2;
const STATUS_COMPLETED = 3;
const STATUS_FAILED = 4;
const STATUS_ABORTED = 5;
protected static $timeout = 300; protected static $timeout = 300;
protected $captureOutput; protected $captureOutput;
protected $errorOutput; protected $errorOutput;
protected $io; protected $io;
private $jobs = array();
private $runningJobs = 0;
private $maxJobs = 10;
private $idGen = 0;
private $allowAsync = false;
public function __construct(IOInterface $io = null) public function __construct(IOInterface $io = null)
{ {
$this->io = $io; $this->io = $io;
@ -112,6 +126,192 @@ class ProcessExecutor
return $process->getExitCode(); return $process->getExitCode();
} }
/**
* starts a process on the commandline in async mode
*
* @param string $command the command to execute
* @param mixed $output the output will be written into this var if passed by ref
* if a callable is passed it will be used as output handler
* @param string $cwd the working directory
* @return int statuscode
*/
public function executeAsync($command, $cwd = null)
{
if (!$this->allowAsync) {
throw new \LogicException('You must use the ProcessExecutor instance which is part of a Composer\Loop instance to be able to run async processes');
}
$job = array(
'id' => $this->idGen++,
'status' => self::STATUS_QUEUED,
'command' => $command,
'cwd' => $cwd,
);
$resolver = function ($resolve, $reject) use (&$job) {
$job['status'] = ProcessExecutor::STATUS_QUEUED;
$job['resolve'] = $resolve;
$job['reject'] = $reject;
};
$self = $this;
$io = $this->io;
$canceler = function () use (&$job) {
if ($job['status'] === self::STATUS_QUEUED) {
$job['status'] = self::STATUS_ABORTED;
}
if ($job['status'] !== self::STATUS_STARTED) {
return;
}
$job['status'] = self::STATUS_ABORTED;
try {
if (defined('SIGINT')) {
$job['process']->signal(SIGINT);
}
} catch (\Exception $e) {
// signal can throw in various conditions, but we don't care if it fails
}
$job['process']->stop(1);
};
$promise = new Promise($resolver, $canceler);
$promise = $promise->then(function () use (&$job, $self) {
if ($job['process']->isSuccessful()) {
$job['status'] = ProcessExecutor::STATUS_COMPLETED;
} else {
$job['status'] = ProcessExecutor::STATUS_FAILED;
}
// TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
$self->markJobDone();
return $job['process'];
}, function ($e) use (&$job, $self) {
$job['status'] = ProcessExecutor::STATUS_FAILED;
$self->markJobDone();
throw $e;
});
$this->jobs[$job['id']] =& $job;
if ($this->runningJobs < $this->maxJobs) {
$this->startJob($job['id']);
}
return $promise;
}
private function startJob($id)
{
$job =& $this->jobs[$id];
if ($job['status'] !== self::STATUS_QUEUED) {
return;
}
// start job
$job['status'] = self::STATUS_STARTED;
$this->runningJobs++;
$command = $job['command'];
$cwd = $job['cwd'];
if ($this->io && $this->io->isDebug()) {
$safeCommand = preg_replace_callback('{://(?P<user>[^:/\s]+):(?P<password>[^@\s/]+)@}i', function ($m) {
if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) {
return '://***:***@';
}
return '://'.$m['user'].':***@';
}, $command);
$safeCommand = preg_replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand);
$this->io->writeError('Executing async command ('.($cwd ?: 'CWD').'): '.$safeCommand);
}
// make sure that null translate to the proper directory in case the dir is a symlink
// and we call a git command, because msysgit does not handle symlinks properly
if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) {
$cwd = realpath(getcwd());
}
// TODO in v3, commands should be passed in as arrays of cmd + args
if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
$process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout());
} else {
$process = new Process($command, $cwd, null, null, static::getTimeout());
}
$job['process'] = $process;
$process->start();
}
public function wait($index = null)
{
while (true) {
if (!$this->countActiveJobs($index)) {
return;
}
usleep(1000);
}
}
/**
* @internal
*/
public function enableAsync()
{
$this->allowAsync = true;
}
/**
* @internal
*
* @return int number of active (queued or started) jobs
*/
public function countActiveJobs($index = null)
{
// tick
foreach ($this->jobs as $job) {
if ($job['status'] === self::STATUS_STARTED) {
if (!$job['process']->isRunning()) {
call_user_func($job['resolve'], $job['process']);
}
}
if ($this->runningJobs < $this->maxJobs) {
if ($job['status'] === self::STATUS_QUEUED) {
$this->startJob($job['id']);
}
}
}
if (null !== $index) {
return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0;
}
$active = 0;
foreach ($this->jobs as $job) {
if ($job['status'] < self::STATUS_COMPLETED) {
$active++;
} else {
unset($this->jobs[$job['id']]);
}
}
return $active;
}
/**
* @private
*/
public function markJobDone()
{
$this->runningJobs--;
}
public function splitLines($output) public function splitLines($output)
{ {
$output = trim($output); $output = trim($output);

View File

@ -20,6 +20,7 @@ use Composer\Util\HttpDownloader;
use Composer\Util\Http\Response; use Composer\Util\Http\Response;
/** /**
* @internal
* @author François Pluchino <francois.pluchino@opendisplay.com> * @author François Pluchino <francois.pluchino@opendisplay.com>
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
@ -54,8 +55,9 @@ class RemoteFilesystem
* @param Config $config The config * @param Config $config The config
* @param array $options The options * @param array $options The options
* @param bool $disableTls * @param bool $disableTls
* @param AuthHelper $authHelper
*/ */
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false, AuthHelper $authHelper = null)
{ {
$this->io = $io; $this->io = $io;
@ -70,7 +72,7 @@ class RemoteFilesystem
// handle the other externally set options normally. // handle the other externally set options normally.
$this->options = array_replace_recursive($this->options, $options); $this->options = array_replace_recursive($this->options, $options);
$this->config = $config; $this->config = $config;
$this->authHelper = new AuthHelper($io, $config); $this->authHelper = isset($authHelper) ? $authHelper : new AuthHelper($io, $config);
} }
/** /**

View File

@ -139,8 +139,8 @@ class FileDownloaderTest extends TestCase
->will($this->returnValue($path.'/vendor')); ->will($this->returnValue($path.'/vendor'));
try { try {
$promise = $downloader->download($packageMock, $path);
$loop = new Loop($this->httpDownloader); $loop = new Loop($this->httpDownloader);
$promise = $downloader->download($packageMock, $path);
$loop->wait(array($promise)); $loop->wait(array($promise));
$this->fail('Download was expected to throw'); $this->fail('Download was expected to throw');
@ -225,8 +225,8 @@ class FileDownloaderTest extends TestCase
touch($dlFile); touch($dlFile);
try { try {
$promise = $downloader->download($packageMock, $path);
$loop = new Loop($this->httpDownloader); $loop = new Loop($this->httpDownloader);
$promise = $downloader->download($packageMock, $path);
$loop->wait(array($promise)); $loop->wait(array($promise));
$this->fail('Download was expected to throw'); $this->fail('Download was expected to throw');
@ -296,8 +296,8 @@ class FileDownloaderTest extends TestCase
mkdir(dirname($dlFile), 0777, true); mkdir(dirname($dlFile), 0777, true);
touch($dlFile); touch($dlFile);
$promise = $downloader->download($newPackage, $path, $oldPackage);
$loop = new Loop($this->httpDownloader); $loop = new Loop($this->httpDownloader);
$promise = $downloader->download($newPackage, $path, $oldPackage);
$loop->wait(array($promise)); $loop->wait(array($promise));
$downloader->update($oldPackage, $newPackage, $path); $downloader->update($oldPackage, $newPackage, $path);

View File

@ -70,8 +70,8 @@ class XzDownloaderTest extends TestCase
$downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); $downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null);
try { try {
$promise = $downloader->download($packageMock, $this->testDir.'/install-path');
$loop = new Loop($httpDownloader); $loop = new Loop($httpDownloader);
$promise = $downloader->download($packageMock, $this->testDir.'/install-path');
$loop->wait(array($promise)); $loop->wait(array($promise));
$downloader->install($packageMock, $this->testDir.'/install-path'); $downloader->install($packageMock, $this->testDir.'/install-path');

View File

@ -60,9 +60,6 @@ class ZipDownloaderTest extends TestCase
} }
} }
/**
* @group only
*/
public function testErrorMessages() public function testErrorMessages()
{ {
if (!class_exists('ZipArchive')) { if (!class_exists('ZipArchive')) {
@ -92,8 +89,8 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasSystemUnzip', false);
try { try {
$promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop = new Loop($this->httpDownloader); $loop = new Loop($this->httpDownloader);
$promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop->wait(array($promise)); $loop->wait(array($promise));
$downloader->install($this->package, $path); $downloader->install($this->package, $path);
@ -125,7 +122,8 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(false)); ->will($this->returnValue(false));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
/** /**
@ -150,12 +148,10 @@ class ZipDownloaderTest extends TestCase
->will($this->throwException(new \ErrorException('Not a directory'))); ->will($this->throwException(new \ErrorException('Not a directory')));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
/**
* @group only
*/
public function testZipArchiveOnlyGood() public function testZipArchiveOnlyGood()
{ {
if (!class_exists('ZipArchive')) { if (!class_exists('ZipArchive')) {
@ -174,45 +170,66 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(true)); ->will($this->returnValue(true));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
/** /**
* @expectedException \Exception * @expectedException \Exception
* @expectedExceptionMessage Failed to execute (1) unzip * @expectedExceptionMessage Failed to extract : (1) unzip
*/ */
public function testSystemUnzipOnlyFailed() public function testSystemUnzipOnlyFailed()
{ {
if (!class_exists('ZipArchive')) { $this->setPrivateProperty('isWindows', false);
$this->markTestSkipped('zip extension missing');
}
$this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', false); $this->setPrivateProperty('hasZipArchive', false);
$procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
$procMock->expects($this->any())
->method('getExitCode')
->will($this->returnValue(1));
$procMock->expects($this->any())
->method('isSuccessful')
->will($this->returnValue(false));
$procMock->expects($this->any())
->method('getErrorOutput')
->will($this->returnValue('output'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0)) $processExecutor->expects($this->at(0))
->method('execute') ->method('executeAsync')
->will($this->returnValue(1)); ->will($this->returnValue(\React\Promise\resolve($procMock)));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
public function testSystemUnzipOnlyGood() public function testSystemUnzipOnlyGood()
{ {
if (!class_exists('ZipArchive')) { $this->setPrivateProperty('isWindows', false);
$this->markTestSkipped('zip extension missing');
}
$this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', false); $this->setPrivateProperty('hasZipArchive', false);
$procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
$procMock->expects($this->any())
->method('getExitCode')
->will($this->returnValue(0));
$procMock->expects($this->any())
->method('isSuccessful')
->will($this->returnValue(true));
$procMock->expects($this->any())
->method('getErrorOutput')
->will($this->returnValue('output'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0)) $processExecutor->expects($this->at(0))
->method('execute') ->method('executeAsync')
->will($this->returnValue(0)); ->will($this->returnValue(\React\Promise\resolve($procMock)));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
public function testNonWindowsFallbackGood() public function testNonWindowsFallbackGood()
@ -225,10 +242,21 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', true); $this->setPrivateProperty('hasZipArchive', true);
$procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
$procMock->expects($this->any())
->method('getExitCode')
->will($this->returnValue(1));
$procMock->expects($this->any())
->method('isSuccessful')
->will($this->returnValue(false));
$procMock->expects($this->any())
->method('getErrorOutput')
->will($this->returnValue('output'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0)) $processExecutor->expects($this->at(0))
->method('execute') ->method('executeAsync')
->will($this->returnValue(1)); ->will($this->returnValue(\React\Promise\resolve($procMock)));
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0)) $zipArchive->expects($this->at(0))
@ -238,9 +266,10 @@ class ZipDownloaderTest extends TestCase
->method('extractTo') ->method('extractTo')
->will($this->returnValue(true)); ->will($this->returnValue(true));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
/** /**
@ -257,10 +286,21 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', true); $this->setPrivateProperty('hasZipArchive', true);
$procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
$procMock->expects($this->any())
->method('getExitCode')
->will($this->returnValue(1));
$procMock->expects($this->any())
->method('isSuccessful')
->will($this->returnValue(false));
$procMock->expects($this->any())
->method('getErrorOutput')
->will($this->returnValue('output'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0)) $processExecutor->expects($this->at(0))
->method('execute') ->method('executeAsync')
->will($this->returnValue(1)); ->will($this->returnValue(\React\Promise\resolve($procMock)));
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0)) $zipArchive->expects($this->at(0))
@ -270,9 +310,10 @@ class ZipDownloaderTest extends TestCase
->method('extractTo') ->method('extractTo')
->will($this->returnValue(false)); ->will($this->returnValue(false));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
public function testWindowsFallbackGood() public function testWindowsFallbackGood()
@ -298,9 +339,10 @@ class ZipDownloaderTest extends TestCase
->method('extractTo') ->method('extractTo')
->will($this->returnValue(false)); ->will($this->returnValue(false));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
} }
/** /**
@ -330,9 +372,28 @@ class ZipDownloaderTest extends TestCase
->method('extractTo') ->method('extractTo')
->will($this->returnValue(false)); ->will($this->returnValue(false));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
$this->wait($promise);
}
private function wait($promise)
{
if (null === $promise) {
return;
}
$e = null;
$promise->then(function () {
// noop
}, function ($ex) use (&$e) {
$e = $ex;
});
if ($e) {
throw $e;
}
} }
} }
@ -350,6 +411,6 @@ class MockedZipDownloader extends ZipDownloader
public function extract(PackageInterface $package, $file, $path) public function extract(PackageInterface $package, $file, $path)
{ {
parent::extract($package, $file, $path); return parent::extract($package, $file, $path);
} }
} }

View File

@ -0,0 +1,90 @@
--TEST--
Aliases are loaded when splitting require-dev from require (https://github.com/composer/composer/issues/8954)
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{
"name": "a/aliased", "version": "dev-next", "replace": { "a/aliased-replaced": "self.version" }
},
{
"name": "b/requirer", "version": "2.3.0",
"require": { "a/aliased-replaced": "^4.0" }
},
{
"name": "a/aliased2", "version": "dev-next", "replace": { "a/aliased-replaced2": "self.version" }
},
{
"name": "b/requirer2", "version": "2.3.0",
"require": { "a/aliased-replaced": "^4.0", "a/aliased-replaced2": "^4.0" }
}
]
}
],
"require": {
"a/aliased": "dev-next as 4.1.0-RC2",
"b/requirer": "2.3.0"
},
"require-dev": {
"a/aliased2": "dev-next as 4.1.0-RC2",
"b/requirer2": "2.3.0"
}
}
--RUN--
update
--EXPECT-LOCK--
{
"packages": [
{
"name": "a/aliased", "version": "dev-next",
"type": "library",
"replace": { "a/aliased-replaced": "self.version" }
},
{
"name": "b/requirer", "version": "2.3.0",
"require": { "a/aliased-replaced": "^4.0" },
"type": "library"
}
],
"packages-dev": [
{
"name": "a/aliased2", "version": "dev-next",
"type": "library",
"replace": { "a/aliased-replaced2": "self.version" }
},
{
"name": "b/requirer2", "version": "2.3.0",
"require": { "a/aliased-replaced": "^4.0", "a/aliased-replaced2": "^4.0" },
"type": "library"
}
],
"aliases": [{
"package": "a/aliased2",
"version": "dev-next",
"alias": "4.1.0-RC2",
"alias_normalized": "4.1.0.0-RC2"
}, {
"package": "a/aliased",
"version": "dev-next",
"alias": "4.1.0-RC2",
"alias_normalized": "4.1.0.0-RC2"
}],
"minimum-stability": "stable",
"stability-flags": {
"a/aliased": 20,
"a/aliased2": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}
--EXPECT--
Installing a/aliased (dev-next)
Marking a/aliased (4.1.0-RC2) as installed, alias of a/aliased (dev-next)
Installing b/requirer (2.3.0)
Installing a/aliased2 (dev-next)
Marking a/aliased2 (4.1.0-RC2) as installed, alias of a/aliased2 (dev-next)
Installing b/requirer2 (2.3.0)

View File

@ -2,8 +2,8 @@
See Github issue #4795 ( github.com/composer/composer/issues/4795 ). See Github issue #4795 ( github.com/composer/composer/issues/4795 ).
Composer\Installer::whitelistUpdateDependencies should not output a warning for dependencies that need to be updated Composer\Installer::allowListUpdateDependencies should not output a warning for dependencies that need to be updated
that are also a root package, when that root package is also explicitly whitelisted. that are also a root package, when that root package is also explicitly allowed.
--COMPOSER-- --COMPOSER--
{ {

View File

@ -2,8 +2,8 @@
See Github issue #4795 ( github.com/composer/composer/issues/4795 ). See Github issue #4795 ( github.com/composer/composer/issues/4795 ).
Composer\Installer::whitelistUpdateDependencies intentionally ignores root requirements even if said package is also a Composer\Installer::allowListUpdateDependencies intentionally ignores root requirements even if said package is also a
dependency of one the requirements that is whitelisted for update. dependency of one the requirements that is allowed for update.
--COMPOSER-- --COMPOSER--
{ {

View File

@ -6,8 +6,8 @@ Install from a lock file that deleted a package
{ {
"type": "package", "type": "package",
"package": [ "package": [
{ "name": "whitelisted/pkg", "version": "1.1.0" }, { "name": "allowed/pkg", "version": "1.1.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } },
{ "name": "fixed/dependency", "version": "1.1.0" }, { "name": "fixed/dependency", "version": "1.1.0" },
{ "name": "fixed/dependency", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0" },
{ "name": "old/dependency", "version": "1.0.0" } { "name": "old/dependency", "version": "1.0.0" }
@ -15,14 +15,14 @@ Install from a lock file that deleted a package
} }
], ],
"require": { "require": {
"whitelisted/pkg": "1.*", "allowed/pkg": "1.*",
"fixed/dependency": "1.*" "fixed/dependency": "1.*"
} }
} }
--LOCK-- --LOCK--
{ {
"packages": [ "packages": [
{ "name": "whitelisted/pkg", "version": "1.1.0" }, { "name": "allowed/pkg", "version": "1.1.0" },
{ "name": "fixed/dependency", "version": "1.0.0" } { "name": "fixed/dependency", "version": "1.0.0" }
], ],
"packages-dev": [], "packages-dev": [],
@ -33,7 +33,7 @@ Install from a lock file that deleted a package
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } },
{ "name": "fixed/dependency", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0" },
{ "name": "old/dependency", "version": "1.0.0" } { "name": "old/dependency", "version": "1.0.0" }
] ]
@ -41,4 +41,4 @@ Install from a lock file that deleted a package
install install
--EXPECT-- --EXPECT--
Removing old/dependency (1.0.0) Removing old/dependency (1.0.0)
Upgrading whitelisted/pkg (1.0.0 => 1.1.0) Upgrading allowed/pkg (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Partial update forces updates dev reference from lock file for non whitelisted packages Partial update forces updates dev reference from lock file for non allowed packages
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [

View File

@ -1,13 +1,13 @@
--TEST-- --TEST--
Update with a package whitelist only updates those packages if they are not present in composer.json Update with a package allow list only updates those packages if they are not present in composer.json
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
{ {
"type": "package", "type": "package",
"package": [ "package": [
{ "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "fixed/dependency": "1.*" } }, { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "fixed/dependency": "1.*" } },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "fixed/dependency", "version": "1.1.0", "require": { "fixed/sub-dependency": "1.*" } }, { "name": "fixed/dependency", "version": "1.1.0", "require": { "fixed/sub-dependency": "1.*" } },
@ -18,13 +18,13 @@ Update with a package whitelist only updates those packages if they are not pres
} }
], ],
"require": { "require": {
"whitelisted/pkg": "1.*", "allowed/pkg": "1.*",
"fixed/dependency": "1.*" "fixed/dependency": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } },
{ "name": "fixed/sub-dependency", "version": "1.0.0" } { "name": "fixed/sub-dependency", "version": "1.0.0" }
@ -32,7 +32,7 @@ Update with a package whitelist only updates those packages if they are not pres
--LOCK-- --LOCK--
{ {
"packages": [ "packages": [
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } },
{ "name": "fixed/sub-dependency", "version": "1.0.0" } { "name": "fixed/sub-dependency", "version": "1.0.0" }
@ -47,7 +47,7 @@ Update with a package whitelist only updates those packages if they are not pres
"platform-dev": [] "platform-dev": []
} }
--RUN-- --RUN--
update whitelisted/pkg dependency/pkg update allowed/pkg dependency/pkg
--EXPECT-- --EXPECT--
Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg (1.0.0 => 1.1.0) Upgrading allowed/pkg (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist pattern and all-dependencies flag updates packages and their dependencies, even if defined as root dependency, matching the pattern Update with a package allow list pattern and all-dependencies flag updates packages and their dependencies, even if defined as root dependency, matching the pattern
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,10 +8,10 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0" }, { "name": "allowed/pkg-component1", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -23,8 +23,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*", "allowed/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*", "allowed/pkg-component2": "1.*",
"dependency/pkg": "1.*", "dependency/pkg": "1.*",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
@ -32,8 +32,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -42,8 +42,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -58,8 +58,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
"platform-dev": [] "platform-dev": []
} }
--RUN-- --RUN--
update whitelisted/pkg-* --with-all-dependencies update allowed/pkg-* --with-all-dependencies
--EXPECT-- --EXPECT--
Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0)
Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package Update with a package allow list only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,10 +8,10 @@ Update with a package whitelist only updates those packages and their dependenci
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0" }, { "name": "allowed/pkg-component1", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "root/pkg-dependency", "version": "1.1.0" }, { "name": "root/pkg-dependency", "version": "1.1.0" },
@ -25,8 +25,8 @@ Update with a package whitelist only updates those packages and their dependenci
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*", "allowed/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*", "allowed/pkg-component2": "1.*",
"root/pkg-dependency": "1.*", "root/pkg-dependency": "1.*",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
@ -34,8 +34,8 @@ Update with a package whitelist only updates those packages and their dependenci
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "root/pkg-dependency", "version": "1.0.0" }, { "name": "root/pkg-dependency", "version": "1.0.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -45,8 +45,8 @@ Update with a package whitelist only updates those packages and their dependenci
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "root/pkg-dependency", "version": "1.0.0" }, { "name": "root/pkg-dependency", "version": "1.0.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -60,8 +60,8 @@ Update with a package whitelist only updates those packages and their dependenci
"prefer-lowest": false "prefer-lowest": false
} }
--RUN-- --RUN--
update whitelisted/pkg-* --with-dependencies update allowed/pkg-* --with-dependencies
--EXPECT-- --EXPECT--
Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0)
Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates those packages and their dependencies matching the pattern Update with a package allow list only updates those packages and their dependencies matching the pattern
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,16 +8,16 @@ Update with a package whitelist only updates those packages and their dependenci
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0", "require": { "whitelisted/pkg-component2": "1.1.0" } }, { "name": "allowed/pkg-component1", "version": "1.1.0", "require": { "allowed/pkg-component2": "1.1.0" } },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, { "name": "allowed/pkg-component1", "version": "1.0.0", "require": { "allowed/pkg-component2": "1.0.0" } },
{ "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "whitelisted/pkg-component5": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "allowed/pkg-component5": "1.0.0" } },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "whitelisted/pkg-component3", "version": "1.1.0", "require": { "whitelisted/pkg-component4": "1.1.0" } }, { "name": "allowed/pkg-component3", "version": "1.1.0", "require": { "allowed/pkg-component4": "1.1.0" } },
{ "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, { "name": "allowed/pkg-component3", "version": "1.0.0", "require": { "allowed/pkg-component4": "1.0.0" } },
{ "name": "whitelisted/pkg-component4", "version": "1.1.0" }, { "name": "allowed/pkg-component4", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component4", "version": "1.0.0" }, { "name": "allowed/pkg-component4", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component5", "version": "1.1.0" }, { "name": "allowed/pkg-component5", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component5", "version": "1.0.0" }, { "name": "allowed/pkg-component5", "version": "1.0.0" },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -29,20 +29,20 @@ Update with a package whitelist only updates those packages and their dependenci
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*", "allowed/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*", "allowed/pkg-component2": "1.*",
"whitelisted/pkg-component3": "1.0.0", "allowed/pkg-component3": "1.0.0",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, { "name": "allowed/pkg-component1", "version": "1.0.0", "require": { "allowed/pkg-component2": "1.0.0" } },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, { "name": "allowed/pkg-component3", "version": "1.0.0", "require": { "allowed/pkg-component4": "1.0.0" } },
{ "name": "whitelisted/pkg-component4", "version": "1.0.0" }, { "name": "allowed/pkg-component4", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component5", "version": "1.0.0" }, { "name": "allowed/pkg-component5", "version": "1.0.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -51,11 +51,11 @@ Update with a package whitelist only updates those packages and their dependenci
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, { "name": "allowed/pkg-component1", "version": "1.0.0", "require": { "allowed/pkg-component2": "1.0.0" } },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, { "name": "allowed/pkg-component3", "version": "1.0.0", "require": { "allowed/pkg-component4": "1.0.0" } },
{ "name": "whitelisted/pkg-component4", "version": "1.0.0" }, { "name": "allowed/pkg-component4", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component5", "version": "1.0.0" }, { "name": "allowed/pkg-component5", "version": "1.0.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -70,8 +70,8 @@ Update with a package whitelist only updates those packages and their dependenci
"platform-dev": [] "platform-dev": []
} }
--RUN-- --RUN--
update whitelisted/pkg-* foobar --with-dependencies update allowed/pkg-* foobar --with-dependencies
--EXPECT-- --EXPECT--
Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates those packages matching the pattern Update with a package allow list only updates those packages matching the pattern
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,10 +8,10 @@ Update with a package whitelist only updates those packages matching the pattern
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0" }, { "name": "allowed/pkg-component1", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -23,16 +23,16 @@ Update with a package whitelist only updates those packages matching the pattern
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*", "allowed/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*", "allowed/pkg-component2": "1.*",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -41,8 +41,8 @@ Update with a package whitelist only updates those packages matching the pattern
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" }, { "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -55,7 +55,7 @@ Update with a package whitelist only updates those packages matching the pattern
"prefer-lowest": false "prefer-lowest": false
} }
--RUN-- --RUN--
update whitelisted/pkg-* update allowed/pkg-*
--EXPECT-- --EXPECT--
Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates those corresponding to the pattern Update with a package allow list only updates those corresponding to the pattern
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [

View File

@ -1,13 +1,13 @@
--TEST-- --TEST--
Update with a package whitelist removes unused packages Update with a package allow list removes unused packages
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
{ {
"type": "package", "type": "package",
"package": [ "package": [
{ "name": "whitelisted/pkg", "version": "1.1.0" }, { "name": "allowed/pkg", "version": "1.1.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } },
{ "name": "fixed/dependency", "version": "1.1.0" }, { "name": "fixed/dependency", "version": "1.1.0" },
{ "name": "fixed/dependency", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0" },
{ "name": "old/dependency", "version": "1.0.0" } { "name": "old/dependency", "version": "1.0.0" }
@ -15,20 +15,20 @@ Update with a package whitelist removes unused packages
} }
], ],
"require": { "require": {
"whitelisted/pkg": "1.*", "allowed/pkg": "1.*",
"fixed/dependency": "1.*" "fixed/dependency": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } },
{ "name": "fixed/dependency", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0" },
{ "name": "old/dependency", "version": "1.0.0" } { "name": "old/dependency", "version": "1.0.0" }
] ]
--LOCK-- --LOCK--
{ {
"packages": [ "packages": [
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } },
{ "name": "fixed/dependency", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0" },
{ "name": "old/dependency", "version": "1.0.0" } { "name": "old/dependency", "version": "1.0.0" }
], ],
@ -42,7 +42,7 @@ Update with a package whitelist removes unused packages
"platform-dev": [] "platform-dev": []
} }
--RUN-- --RUN--
update --with-dependencies whitelisted/pkg update --with-dependencies allowed/pkg
--EXPECT-- --EXPECT--
Removing old/dependency (1.0.0) Removing old/dependency (1.0.0)
Upgrading whitelisted/pkg (1.0.0 => 1.1.0) Upgrading allowed/pkg (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates those packages and their dependencies listed as command arguments Update with a package allow list only updates those packages and their dependencies listed as command arguments
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages and their dependenci
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -21,14 +21,14 @@ Update with a package whitelist only updates those packages and their dependenci
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg": "1.*", "allowed/pkg": "1.*",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -37,7 +37,7 @@ Update with a package whitelist only updates those packages and their dependenci
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -50,7 +50,7 @@ Update with a package whitelist only updates those packages and their dependenci
"prefer-lowest": false "prefer-lowest": false
} }
--RUN-- --RUN--
update whitelisted/pkg --with-dependencies update allowed/pkg --with-dependencies
--EXPECT-- --EXPECT--
Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg (1.0.0 => 1.1.0) Upgrading allowed/pkg (1.0.0 => 1.1.0)

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates whitelisted packages if no dependency conflicts Update with a package allow list only updates allowed packages if no dependency conflicts
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,8 +8,8 @@ Update with a package whitelist only updates whitelisted packages if no dependen
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -21,14 +21,14 @@ Update with a package whitelist only updates whitelisted packages if no dependen
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg": "1.*", "allowed/pkg": "1.*",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -37,7 +37,7 @@ Update with a package whitelist only updates whitelisted packages if no dependen
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -50,5 +50,5 @@ Update with a package whitelist only updates whitelisted packages if no dependen
"prefer-lowest": false "prefer-lowest": false
} }
--RUN-- --RUN--
update whitelisted/pkg update allowed/pkg
--EXPECT-- --EXPECT--

View File

@ -1,5 +1,5 @@
--TEST-- --TEST--
Update with a package whitelist only updates those packages listed as command arguments Update with a package allow list only updates those packages listed as command arguments
--COMPOSER-- --COMPOSER--
{ {
"repositories": [ "repositories": [
@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages listed as command ar
"package": [ "package": [
{ "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
@ -21,14 +21,14 @@ Update with a package whitelist only updates those packages listed as command ar
], ],
"require": { "require": {
"fixed/pkg": "1.*", "fixed/pkg": "1.*",
"whitelisted/pkg": "1.*", "allowed/pkg": "1.*",
"unrelated/pkg": "1.*" "unrelated/pkg": "1.*"
} }
} }
--INSTALLED-- --INSTALLED--
[ [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency": "1.*" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -37,7 +37,7 @@ Update with a package whitelist only updates those packages listed as command ar
{ {
"packages": [ "packages": [
{ "name": "fixed/pkg", "version": "1.0.0" }, { "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } },
{ "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" } { "name": "unrelated/pkg-dependency", "version": "1.0.0" }
@ -52,6 +52,6 @@ Update with a package whitelist only updates those packages listed as command ar
"platform-dev": [] "platform-dev": []
} }
--RUN-- --RUN--
update whitelisted/pkg update allowed/pkg
--EXPECT-- --EXPECT--
Upgrading whitelisted/pkg (1.0.0 => 1.1.0) Upgrading allowed/pkg (1.0.0 => 1.1.0)

View File

@ -3,10 +3,10 @@ Update updates URLs for updated packages if they have changed
a/a is dev and gets everything updated as it updates to a new ref a/a is dev and gets everything updated as it updates to a new ref
b/b is a tag and gets everything updated by updating the package URL directly b/b is a tag and gets everything updated by updating the package URL directly
c/c is a tag and not whitelisted and remains unchanged c/c is a tag and not allowlisted and remains unchanged
d/d is dev but with a #ref so it should get URL updated but not the reference d/d is dev but with a #ref so it should get URL updated but not the reference
e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref
f/f is dev but not whitelisted and remains unchanged f/f is dev but not allowlisted and remains unchanged
g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref
--COMPOSER-- --COMPOSER--
{ {

View File

@ -2,7 +2,7 @@
See Github issue #6661 ( github.com/composer/composer/issues/6661 ). See Github issue #6661 ( github.com/composer/composer/issues/6661 ).
When `--with-all-dependencies` is used, Composer\Installer::whitelistUpdateDependencies should update the dependencies of all whitelisted packages, even if the dependency is a root requirement. When `--with-all-dependencies` is used, Composer should update the dependencies of all allowed packages, even if the dependency is a root requirement.
--COMPOSER-- --COMPOSER--
{ {

View File

@ -23,6 +23,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use Composer\Util\Loop; use Composer\Util\Loop;
use Composer\Util\ProcessExecutor;
class FactoryMock extends Factory class FactoryMock extends Factory
{ {
@ -47,7 +48,7 @@ class FactoryMock extends Factory
return new InstallationManagerMock(); return new InstallationManagerMock();
} }
protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io) protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null)
{ {
} }

View File

@ -18,6 +18,7 @@ use Composer\Package\Archiver\ArchiveManager;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Loop; use Composer\Util\Loop;
use Composer\Test\Mock\FactoryMock; use Composer\Test\Mock\FactoryMock;
use Composer\Util\ProcessExecutor;
class ArchiveManagerTest extends ArchiverTest class ArchiveManagerTest extends ArchiverTest
{ {
@ -36,7 +37,8 @@ class ArchiveManagerTest extends ArchiverTest
$dm = $factory->createDownloadManager( $dm = $factory->createDownloadManager(
$io = new NullIO, $io = new NullIO,
$config = FactoryMock::createConfig(), $config = FactoryMock::createConfig(),
$httpDownloader = $factory->createHttpDownloader($io, $config) $httpDownloader = $factory->createHttpDownloader($io, $config),
new ProcessExecutor($io)
); );
$loop = new Loop($httpDownloader); $loop = new Loop($httpDownloader);
$this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop); $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop);

View File

@ -81,7 +81,7 @@ class BasePackageTest extends TestCase
$createPackage = function ($arr) use ($self) { $createPackage = function ($arr) use ($self) {
$package = $self->getMockForAbstractClass('\Composer\Package\BasePackage', array(), '', false); $package = $self->getMockForAbstractClass('\Composer\Package\BasePackage', array(), '', false);
$package->expects($self->once())->method('isDev')->will($self->returnValue(true)); $package->expects($self->once())->method('isDev')->will($self->returnValue(true));
$package->expects($self->once())->method('getSourceType')->will($self->returnValue('git')); $package->expects($self->any())->method('getSourceType')->will($self->returnValue('git'));
$package->expects($self->once())->method('getPrettyVersion')->will($self->returnValue('PrettyVersion')); $package->expects($self->once())->method('getPrettyVersion')->will($self->returnValue('PrettyVersion'));
$package->expects($self->any())->method('getSourceReference')->will($self->returnValue($arr['sourceReference'])); $package->expects($self->any())->method('getSourceReference')->will($self->returnValue($arr['sourceReference']));

View File

@ -189,16 +189,19 @@ class ComposerRepositoryTest extends TestCase
->getMock(); ->getMock();
$httpDownloader->expects($this->at(0)) $httpDownloader->expects($this->at(0))
->method('enableAsync');
$httpDownloader->expects($this->at(1))
->method('get') ->method('get')
->with($url = 'http://example.org/packages.json') ->with($url = 'http://example.org/packages.json')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array('search' => '/search.json?q=%query%&type=%type%')))); ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array('search' => '/search.json?q=%query%&type=%type%'))));
$httpDownloader->expects($this->at(1)) $httpDownloader->expects($this->at(2))
->method('get') ->method('get')
->with($url = 'http://example.org/search.json?q=foo&type=composer-plugin') ->with($url = 'http://example.org/search.json?q=foo&type=composer-plugin')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode($result))); ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode($result)));
$httpDownloader->expects($this->at(2)) $httpDownloader->expects($this->at(3))
->method('get') ->method('get')
->with($url = 'http://example.org/search.json?q=foo&type=library') ->with($url = 'http://example.org/search.json?q=foo&type=library')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array()))); ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array())));
@ -291,6 +294,9 @@ class ComposerRepositoryTest extends TestCase
->getMock(); ->getMock();
$httpDownloader->expects($this->at(0)) $httpDownloader->expects($this->at(0))
->method('enableAsync');
$httpDownloader->expects($this->at(1))
->method('get') ->method('get')
->with($url = 'http://example.org/packages.json') ->with($url = 'http://example.org/packages.json')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array( ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array(

View File

@ -14,11 +14,15 @@ namespace Composer\Test\Repository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use Composer\Util\ProcessExecutor;
use Composer\Package\Version\VersionParser;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\ExecutableFinder;
class PlatformRepositoryTest extends TestCase { class PlatformRepositoryTest extends TestCase
public function testHHVMVersionWhenExecutingInHHVM() { {
public function testHHVMVersionWhenExecutingInHHVM()
{
if (!defined('HHVM_VERSION_ID')) { if (!defined('HHVM_VERSION_ID')) {
$this->markTestSkipped('Not running with HHVM'); $this->markTestSkipped('Not running with HHVM');
return; return;
@ -36,7 +40,8 @@ class PlatformRepositoryTest extends TestCase {
); );
} }
public function testHHVMVersionWhenExecutingInPHP() { public function testHHVMVersionWhenExecutingInPHP()
{
if (defined('HHVM_VERSION_ID')) { if (defined('HHVM_VERSION_ID')) {
$this->markTestSkipped('Running with HHVM'); $this->markTestSkipped('Running with HHVM');
return; return;
@ -54,17 +59,18 @@ class PlatformRepositoryTest extends TestCase {
if ($hhvm === null) { if ($hhvm === null) {
$this->markTestSkipped('HHVM is not installed'); $this->markTestSkipped('HHVM is not installed');
} }
$process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $repository = new PlatformRepository(array(), array());
$process->expects($this->once())->method('execute')->will($this->returnCallback(
function($command, &$out) {
$this->assertContains('HHVM_VERSION', $command);
$out = '4.0.1-dev';
return 0;
}
));
$repository = new PlatformRepository(array(), array(), $process);
$package = $repository->findPackage('hhvm', '*'); $package = $repository->findPackage('hhvm', '*');
$this->assertNotNull($package, 'failed to find HHVM package'); $this->assertNotNull($package, 'failed to find HHVM package');
$this->assertSame('4.0.1.0-dev', $package->getVersion());
$process = new ProcessExecutor();
$exitCode = $process->execute(
ProcessExecutor::escape($hhvm).
' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null',
$version
);
$parser = new VersionParser;
$this->assertSame($parser->normalize($version), $package->getVersion());
} }
} }

View File

@ -12,32 +12,25 @@
namespace Composer\Test\Util; namespace Composer\Test\Util;
use Composer\Config;
use Composer\IO\ConsoleIO;
use Composer\IO\IOInterface;
use Composer\Util\AuthHelper;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use ReflectionMethod;
use ReflectionProperty;
class RemoteFilesystemTest extends TestCase class RemoteFilesystemTest extends TestCase
{ {
private function getConfigMock()
{
$config = $this->getMockBuilder('Composer\Config')->getMock();
$config->expects($this->any())
->method('get')
->will($this->returnCallback(function ($key) {
if ($key === 'github-domains' || $key === 'gitlab-domains') {
return array();
}
}));
return $config;
}
public function testGetOptionsForUrl() public function testGetOptionsForUrl()
{ {
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io = $this->getIOInterfaceMock();
$io $io
->expects($this->once()) ->expects($this->once())
->method('hasAuthentication') ->method('hasAuthentication')
->will($this->returnValue(false)) ->willReturn(false)
; ;
$res = $this->callGetOptionsForUrl($io, array('http://example.org', array())); $res = $this->callGetOptionsForUrl($io, array('http://example.org', array()));
@ -46,16 +39,16 @@ class RemoteFilesystemTest extends TestCase
public function testGetOptionsForUrlWithAuthorization() public function testGetOptionsForUrlWithAuthorization()
{ {
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io = $this->getIOInterfaceMock();
$io $io
->expects($this->once()) ->expects($this->once())
->method('hasAuthentication') ->method('hasAuthentication')
->will($this->returnValue(true)) ->willReturn(true)
; ;
$io $io
->expects($this->once()) ->expects($this->once())
->method('getAuthentication') ->method('getAuthentication')
->will($this->returnValue(array('username' => 'login', 'password' => 'password'))) ->willReturn(array('username' => 'login', 'password' => 'password'))
; ;
$options = $this->callGetOptionsForUrl($io, array('http://example.org', array())); $options = $this->callGetOptionsForUrl($io, array('http://example.org', array()));
@ -71,17 +64,17 @@ class RemoteFilesystemTest extends TestCase
public function testGetOptionsForUrlWithStreamOptions() public function testGetOptionsForUrlWithStreamOptions()
{ {
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io = $this->getIOInterfaceMock();
$io $io
->expects($this->once()) ->expects($this->once())
->method('hasAuthentication') ->method('hasAuthentication')
->will($this->returnValue(true)) ->willReturn(true)
; ;
$io $io
->expects($this->once()) ->expects($this->once())
->method('getAuthentication') ->method('getAuthentication')
->will($this->returnValue(array('username' => null, 'password' => null))) ->willReturn(array('username' => null, 'password' => null))
; ;
$streamOptions = array('ssl' => array( $streamOptions = array('ssl' => array(
@ -94,17 +87,17 @@ class RemoteFilesystemTest extends TestCase
public function testGetOptionsForUrlWithCallOptionsKeepsHeader() public function testGetOptionsForUrlWithCallOptionsKeepsHeader()
{ {
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io = $this->getIOInterfaceMock();
$io $io
->expects($this->once()) ->expects($this->once())
->method('hasAuthentication') ->method('hasAuthentication')
->will($this->returnValue(true)) ->willReturn(true)
; ;
$io $io
->expects($this->once()) ->expects($this->once())
->method('getAuthentication') ->method('getAuthentication')
->will($this->returnValue(array('username' => null, 'password' => null))) ->willReturn(array('username' => null, 'password' => null))
; ;
$streamOptions = array('http' => array( $streamOptions = array('http' => array(
@ -127,14 +120,14 @@ class RemoteFilesystemTest extends TestCase
public function testCallbackGetFileSize() public function testCallbackGetFileSize()
{ {
$fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock());
$this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20); $this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20);
$this->assertAttributeEquals(20, 'bytesMax', $fs); $this->assertAttributeEquals(20, 'bytesMax', $fs);
} }
public function testCallbackGetNotifyProgress() public function testCallbackGetNotifyProgress()
{ {
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io = $this->getIOInterfaceMock();
$io $io
->expects($this->once()) ->expects($this->once())
->method('overwriteError') ->method('overwriteError')
@ -150,21 +143,21 @@ class RemoteFilesystemTest extends TestCase
public function testCallbackGetPassesThrough404() public function testCallbackGetPassesThrough404()
{ {
$fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock());
$this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0));
} }
public function testGetContents() public function testGetContents()
{ {
$fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock());
$this->assertContains('testGetContents', $fs->getContents('http://example.org', 'file://'.__FILE__)); $this->assertContains('testGetContents', $fs->getContents('http://example.org', 'file://'.__FILE__));
} }
public function testCopy() public function testCopy()
{ {
$fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock());
$file = tempnam(sys_get_temp_dir(), 'c'); $file = tempnam(sys_get_temp_dir(), 'c');
$this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file)); $this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file));
@ -173,17 +166,96 @@ class RemoteFilesystemTest extends TestCase
unlink($file); unlink($file);
} }
/**
* @expectedException \Composer\Downloader\TransportException
*/
public function testCopyWithNoRetryOnFailure()
{
$fs = $this->getRemoteFilesystemWithMockedMethods(array('getRemoteContents'));
$fs->expects($this->once())->method('getRemoteContents')
->willReturnCallback(function ($originUrl, $fileUrl, $ctx, &$http_response_header) {
$http_response_header = array('http/1.1 401 unauthorized');
return '';
});
$file = tempnam(sys_get_temp_dir(), 'z');
unlink($file);
$fs->copy(
'http://example.org',
'file://' . __FILE__,
$file,
true,
array('retry-auth-failure' => false)
);
}
public function testCopyWithSuccessOnRetry()
{
$authHelper = $this->getAuthHelperWithMockedMethods(array('promptAuthIfNeeded'));
$fs = $this->getRemoteFilesystemWithMockedMethods(array('getRemoteContents'), $authHelper);
$authHelper->expects($this->once())
->method('promptAuthIfNeeded')
->willReturn(array(
'storeAuth' => true,
'retry' => true
));
$fs->expects($this->at(0))
->method('getRemoteContents')
->willReturnCallback(function ($originUrl, $fileUrl, $ctx, &$http_response_header) {
$http_response_header = array('http/1.1 401 unauthorized');
return '';
});
$fs->expects($this->at(1))
->method('getRemoteContents')
->willReturnCallback(function ($originUrl, $fileUrl, $ctx, &$http_response_header) {
$http_response_header = array('http/1.1 200 OK');
return '<?php $copied = "Copied"; ';
});
$file = tempnam(sys_get_temp_dir(), 'z');
$copyResult = $fs->copy(
'http://example.org',
'file://' . __FILE__,
$file,
true,
array('retry-auth-failure' => true)
);
$this->assertTrue($copyResult);
$this->assertFileExists($file);
$this->assertContains('Copied', file_get_contents($file));
unlink($file);
}
/** /**
* @group TLS * @group TLS
*/ */
public function testGetOptionsForUrlCreatesSecureTlsDefaults() public function testGetOptionsForUrlCreatesSecureTlsDefaults()
{ {
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io = $this->getIOInterfaceMock();
$res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl' => array('cafile' => '/some/path/file.crt'))), array(), 'http://www.example.org'); $res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl' => array('cafile' => '/some/path/file.crt'))), array(), 'http://www.example.org');
$this->assertTrue(isset($res['ssl']['ciphers'])); $this->assertTrue(isset($res['ssl']['ciphers']));
$this->assertRegExp("|!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA|", $res['ssl']['ciphers']); $this->assertRegExp('|!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA|', $res['ssl']['ciphers']);
$this->assertTrue($res['ssl']['verify_peer']); $this->assertTrue($res['ssl']['verify_peer']);
$this->assertTrue($res['ssl']['SNI_enabled']); $this->assertTrue($res['ssl']['SNI_enabled']);
$this->assertEquals(7, $res['ssl']['verify_depth']); $this->assertEquals(7, $res['ssl']['verify_depth']);
@ -220,6 +292,7 @@ class RemoteFilesystemTest extends TestCase
*/ */
public function testBitBucketPublicDownload($url, $contents) public function testBitBucketPublicDownload($url, $contents)
{ {
/** @var ConsoleIO $io */
$io = $this $io = $this
->getMockBuilder('Composer\IO\ConsoleIO') ->getMockBuilder('Composer\IO\ConsoleIO')
->disableOriginalConstructor() ->disableOriginalConstructor()
@ -242,6 +315,7 @@ class RemoteFilesystemTest extends TestCase
*/ */
public function testBitBucketPublicDownloadWithAuthConfigured($url, $contents) public function testBitBucketPublicDownloadWithAuthConfigured($url, $contents)
{ {
/** @var MockObject|ConsoleIO $io */
$io = $this $io = $this
->getMockBuilder('Composer\IO\ConsoleIO') ->getMockBuilder('Composer\IO\ConsoleIO')
->disableOriginalConstructor() ->disableOriginalConstructor()
@ -249,13 +323,12 @@ class RemoteFilesystemTest extends TestCase
$domains = array(); $domains = array();
$io $io
->expects($this->any())
->method('hasAuthentication') ->method('hasAuthentication')
->will($this->returnCallback(function ($arg) use (&$domains) { ->willReturnCallback(function ($arg) use (&$domains) {
$domains[] = $arg; $domains[] = $arg;
// first time is called with bitbucket.org, then it redirects to bbuseruploads.s3.amazonaws.com so next time we have no auth configured // first time is called with bitbucket.org, then it redirects to bbuseruploads.s3.amazonaws.com so next time we have no auth configured
return $arg === 'bitbucket.org'; return $arg === 'bitbucket.org';
})); });
$io $io
->expects($this->at(1)) ->expects($this->at(1))
->method('getAuthentication') ->method('getAuthentication')
@ -275,11 +348,11 @@ class RemoteFilesystemTest extends TestCase
$this->assertEquals(array('bitbucket.org', 'bbuseruploads.s3.amazonaws.com'), $domains); $this->assertEquals(array('bitbucket.org', 'bbuseruploads.s3.amazonaws.com'), $domains);
} }
protected function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '') private function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '')
{ {
$fs = new RemoteFilesystem($io, $this->getConfigMock(), $options); $fs = new RemoteFilesystem($io, $this->getConfigMock(), $options);
$ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); $ref = new ReflectionMethod($fs, 'getOptionsForUrl');
$prop = new \ReflectionProperty($fs, 'fileUrl'); $prop = new ReflectionProperty($fs, 'fileUrl');
$ref->setAccessible(true); $ref->setAccessible(true);
$prop->setAccessible(true); $prop->setAccessible(true);
@ -288,17 +361,80 @@ class RemoteFilesystemTest extends TestCase
return $ref->invokeArgs($fs, $args); return $ref->invokeArgs($fs, $args);
} }
protected function callCallbackGet(RemoteFilesystem $fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax) /**
* @return MockObject|Config
*/
private function getConfigMock()
{ {
$ref = new \ReflectionMethod($fs, 'callbackGet'); $config = $this->getMockBuilder('Composer\Config')->getMock();
$config
->method('get')
->willReturnCallback(function ($key) {
if ($key === 'github-domains' || $key === 'gitlab-domains') {
return array();
}
return null;
});
return $config;
}
private function callCallbackGet(RemoteFilesystem $fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax)
{
$ref = new ReflectionMethod($fs, 'callbackGet');
$ref->setAccessible(true); $ref->setAccessible(true);
$ref->invoke($fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax); $ref->invoke($fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax);
} }
protected function setAttribute($object, $attribute, $value) private function setAttribute($object, $attribute, $value)
{ {
$attr = new \ReflectionProperty($object, $attribute); $attr = new ReflectionProperty($object, $attribute);
$attr->setAccessible(true); $attr->setAccessible(true);
$attr->setValue($object, $value); $attr->setValue($object, $value);
} }
/**
* @return MockObject|IOInterface
*/
private function getIOInterfaceMock()
{
return $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
}
/**
* @param array $mockedMethods
* @param AuthHelper $authHelper
*
* @return RemoteFilesystem|MockObject
*/
private function getRemoteFilesystemWithMockedMethods(array $mockedMethods, AuthHelper $authHelper = null)
{
return $this->getMockBuilder('Composer\Util\RemoteFilesystem')
->setConstructorArgs(array(
$this->getIOInterfaceMock(),
$this->getConfigMock(),
array(),
false,
$authHelper
))
->setMethods($mockedMethods)
->getMock();
}
/**
* @param array $mockedMethods
*
* @return AuthHelper|MockObject
*/
private function getAuthHelperWithMockedMethods(array $mockedMethods)
{
return $this->getMockBuilder('Composer\Util\AuthHelper')
->setConstructorArgs(array(
$this->getIOInterfaceMock(),
$this->getConfigMock()
))
->setMethods($mockedMethods)
->getMock();
}
} }