1
0
Fork 0

Merge branch 'master' into filter-packages

pull/8850/head
Yanick Witschi 2020-06-22 18:38:16 +02:00
commit ea0ce9dd7d
100 changed files with 1898 additions and 744 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 || 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

@ -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

@ -4,7 +4,7 @@
# HTTP basic authentication # HTTP basic authentication
Your [Satis or Toran Proxy](handling-private-packages-with-satis.md) server Your [Satis or Private Packagist](handling-private-packages-with-satis.md) server
could be secured with http basic authentication. In order to allow your project could be secured with http basic authentication. In order to allow your project
to have access to these packages you will have to tell composer how to to have access to these packages you will have to tell composer how to
authenticate with your credentials. authenticate with your credentials.

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/*'
@ -27,6 +25,7 @@ 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\(\)\.$~'
paths: paths:
- ../src - ../src
- ../tests - ../tests

View File

@ -565,6 +565,10 @@
"type": "string" "type": "string"
} }
}, },
"default-branch": {
"type": ["boolean"],
"description": "Internal use only, do not specify this in composer.json. Indicates whether this version is the default branch of the linked VCS repository. Defaults to false."
},
"abandoned": { "abandoned": {
"type": ["boolean", "string"], "type": ["boolean", "string"],
"description": "Indicates whether this package has been abandoned, it can be boolean or a package name/URL pointing to a recommended alternative. Defaults to false." "description": "Indicates whether this package has been abandoned, it can be boolean or a package name/URL pointing to a recommended alternative. Defaults to false."

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

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

@ -201,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)
@ -212,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();
} }
@ -405,7 +411,8 @@ EOT
->setPreferDist($preferDist); ->setPreferDist($preferDist);
$projectInstaller = new ProjectInstaller($directory, $dm, $fs); $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);
$latest = $versionsUtil->getLatest(); try {
$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');

View File

@ -286,6 +286,8 @@ EOT
$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()) { public static function format(PackageInterface $initialPackage, PackageInterface $targetPackage, $lock = false)
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); {
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); $fromVersion = $initialPackage->getFullPrettyVersion();
} elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) { $toVersion = $targetPackage->getFullPrettyVersion();
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); 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($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; $actionName = VersionParser::isUpgrade($initialPackage->getVersion(), $targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading';
return $actionName.' <info>'.$this->initialPackage->getPrettyName().'</info> (<comment>'.$fromVersion.'</comment> => <comment>'.$toVersion.'</comment>)'; 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;
use Composer\Semver\Intervals; use Composer\Semver\Intervals;
/** /**
@ -103,7 +103,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
*/ */
@ -111,7 +111,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;
@ -193,7 +193,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;
} }
} }
@ -497,19 +497,5 @@ class PoolBuilder
unset($this->aliasMap[spl_object_hash($package)]); unset($this->aliasMap[spl_object_hash($package)]);
} }
} }
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

@ -264,7 +264,7 @@ abstract class Rule
return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals, $isVerbose) . '.'; return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals, $isVerbose) . '.';
case self::RULE_LEARNED: case self::RULE_LEARNED:
if (isset($learnedPool[$this->reasonData])) { if (isset($learnedPool[$this->reasonData])) {
$learnedString = ', learned rules:' . Problem::formatDeduplicatedRules($learnedPool[$this->reasonData], ' ', $repositorySet, $request, $pool, $installedMap, $learnedPool); $learnedString = ', learned rules:' . Problem::formatDeduplicatedRules($learnedPool[$this->reasonData], ' ', $repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool);
} else { } else {
$learnedString = ' (reasoning unavailable)'; $learnedString = ' (reasoning unavailable)';
} }

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);
} catch (\Exception $e) {
// remove cache if the file was corrupted
parent::clearLastCacheWrite($package);
throw $e;
}
$this->filesystem->unlink($fileName); $cleanup = function () use ($path, $filesystem, $temporaryDir, $package, $self) {
// remove cache if the file was corrupted
$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;
}
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

@ -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
@ -51,10 +56,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
protected $cache; protected $cache;
/** @var EventDispatcher */ /** @var EventDispatcher */
protected $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 $additionalCleanupPaths = array();
/** /**
* Constructor. * Constructor.
@ -66,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'));
@ -125,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();
@ -258,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);
@ -271,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);
@ -291,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;
});
} }
/** /**
@ -315,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.');
@ -380,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

@ -61,7 +61,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
GitUtil::cleanEnv(); GitUtil::cleanEnv();
$cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/'; $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
$gitVersion = $this->gitUtil->getVersion(); $gitVersion = GitUtil::getVersion($this->process);
// --dissociate option is only available since git 2.3.0-rc0 // --dissociate option is only available since git 2.3.0-rc0
if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) {
@ -479,7 +479,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
protected function getCommitLogs($fromReference, $toReference, $path) protected function getCommitLogs($fromReference, $toReference, $path)
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
$command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"', ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); $command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"'.GitUtil::getNoShowSignatureFlag($this->process), ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference));
if (0 !== $this->process->execute($command, $output, $path)) { if (0 !== $this->process->execute($command, $output, $path)) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
@ -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

@ -29,15 +29,6 @@ use Composer\Util\Filesystem;
*/ */
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, Filesystem $fs = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs);
}
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

@ -27,6 +27,9 @@ use Composer\Util\Filesystem;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; 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.
@ -39,15 +42,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
const STRATEGY_SYMLINK = 10; const STRATEGY_SYMLINK = 10;
const STRATEGY_MIRROR = 20; const STRATEGY_MIRROR = 20;
/** @var ProcessExecutor */
private $process;
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -91,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);
} }
@ -133,11 +123,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$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;
@ -196,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;
@ -209,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>");

View File

@ -33,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, Filesystem $fs = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs);
}
protected function extract(PackageInterface $package, $file, $path) protected function extract(PackageInterface $package, $file, $path)
{ {
$processError = null; $processError = null;

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

@ -29,16 +29,6 @@ use Composer\Util\Filesystem;
*/ */
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, Filesystem $fs = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs);
}
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

@ -34,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, Filesystem $fs = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -86,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
@ -98,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());
@ -121,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);
} }
/** /**
@ -134,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
@ -146,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;
@ -159,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"));
@ -170,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);
} }
/** /**
@ -192,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

@ -336,7 +336,7 @@ class Factory
$httpDownloader = self::createHttpDownloader($io, $config); $httpDownloader = self::createHttpDownloader($io, $config);
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
$loop = new Loop($httpDownloader); $loop = new Loop($httpDownloader, $process);
$composer->setLoop($loop); $composer->setLoop($loop);
// initialize event dispatcher // initialize event dispatcher
@ -356,7 +356,7 @@ class Factory
// load package // load package
$parser = new VersionParser; $parser = new VersionParser;
$guesser = new VersionGuesser($config, $process, $parser); $guesser = new VersionGuesser($config, $process, $parser);
$loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser, $io); $loader = $this->loadRootPackage($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);
@ -495,11 +495,11 @@ class Factory
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config, $process, $fs)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config, $process, $fs));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
return $dm; return $dm;
@ -567,6 +567,11 @@ class Factory
} }
} }
protected function loadRootPackage(RepositoryManager $rm, Config $config, VersionParser $parser, VersionGuesser $guesser, IOInterface $io)
{
return new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser, $io);
}
/** /**
* @param IOInterface $io IO instance * @param IOInterface $io IO instance
* @param mixed $config either a configuration array or a filename to read from, if null it will read from * @param mixed $config either a configuration array or a filename to read from, if null it will read from

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

@ -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;
@ -49,6 +50,8 @@ class InstallationManager
private $io; private $io;
/** @var EventDispatcher */ /** @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)
{ {
@ -173,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)
{ {
@ -184,6 +187,8 @@ 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();
@ -266,69 +271,44 @@ 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) { foreach ($operations as $index => $operation) {
$opType = $operation->getOperationType(); 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();
// ignoring alias ops as they don't need to execute anything continue;
if (!in_array($opType, array('update', 'install', 'uninstall'))) {
// output alias ops in debug verbosity as they have no output otherwise
if ($this->io->isDebug()) {
$this->io->writeError(' - ' . $operation->show(false));
} }
$this->$opType($repo, $operation);
continue;
} }
unset($operations[$index]);
if ($opType === 'update') { $batch[$index] = $operation;
$package = $operation->getTargetPackage();
$initialPackage = $operation->getInitialPackage();
} else {
$package = $operation->getPackage();
$initialPackage = null;
}
$installer = $this->getInstaller($package->getType());
$event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType);
if (defined($event) && $runScripts && $this->eventDispatcher) {
$this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
}
$dispatcher = $this->eventDispatcher;
$installManager = $this;
$loop = $this->loop;
$io = $this->io;
$promise = $installer->prepare($opType, $package, $initialPackage);
if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) {
return $installManager->$opType($repo, $operation);
})->then($cleanupPromises[$index])
->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) {
$repo->write($devMode, $installManager);
$event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType);
if (defined($event) && $runScripts && $dispatcher) {
$dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
}
}, function ($e) use ($opType, $package, $io) {
$io->writeError(' <error>' . ucfirst($opType) .' of '.$package->getPrettyName().' failed</error>');
throw $e;
});
$promises[] = $promise;
} }
// execute all prepare => installs/updates/removes => cleanup steps if ($batch) {
if (!empty($promises)) { $batches[] = $batch;
$this->loop->wait($promises); }
foreach ($batches as $batch) {
$this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$runCleanup(); $runCleanup();
@ -356,6 +336,77 @@ class InstallationManager
$repo->write($devMode, $this); $repo->write($devMode, $this);
} }
private function executeBatch(RepositoryInterface $repo, array $operations, array $cleanupPromises, $devMode, $runScripts)
{
foreach ($operations as $index => $operation) {
$opType = $operation->getOperationType();
// ignoring alias ops as they don't need to execute anything
if (!in_array($opType, array('update', 'install', 'uninstall'))) {
// output alias ops in debug verbosity as they have no output otherwise
if ($this->io->isDebug()) {
$this->io->writeError(' - ' . $operation->show(false));
}
$this->$opType($repo, $operation);
continue;
}
if ($opType === 'update') {
$package = $operation->getTargetPackage();
$initialPackage = $operation->getInitialPackage();
} else {
$package = $operation->getPackage();
$initialPackage = null;
}
$installer = $this->getInstaller($package->getType());
$event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType);
if (defined($event) && $runScripts && $this->eventDispatcher) {
$this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
}
$dispatcher = $this->eventDispatcher;
$installManager = $this;
$io = $this->io;
$promise = $installer->prepare($opType, $package, $initialPackage);
if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) {
return $installManager->$opType($repo, $operation);
})->then($cleanupPromises[$index])
->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) {
$repo->write($devMode, $installManager);
$event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType);
if (defined($event) && $runScripts && $dispatcher) {
$dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
}
}, function ($e) use ($opType, $package, $io) {
$io->writeError(' <error>' . ucfirst($opType) .' of '.$package->getPrettyName().' failed</error>');
throw $e;
});
$promises[] = $promise;
}
// execute all prepare => installs/updates/removes => cleanup steps
if (!empty($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();
}
}
}
/** /**
* Executes install operation. * Executes install operation.
* *
@ -454,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

@ -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

@ -414,6 +414,11 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
return $this->aliasOf->getArchiveExcludes(); return $this->aliasOf->getArchiveExcludes();
} }
public function isDefaultBranch()
{
return $this->aliasOf->isDefaultBranch();
}
public function isAbandoned() public function isAbandoned()
{ {
return $this->aliasOf->isAbandoned(); return $this->aliasOf->isAbandoned();

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

@ -95,6 +95,10 @@ class ArrayDumper
$data['time'] = $package->getReleaseDate()->format(DATE_RFC3339); $data['time'] = $package->getReleaseDate()->format(DATE_RFC3339);
} }
if ($package->isDefaultBranch()) {
$data['default-branch'] = true;
}
$data = $this->dumpValues($package, $keys, $data); $data = $this->dumpValues($package, $keys, $data);
if ($package instanceof CompletePackageInterface) { if ($package instanceof CompletePackageInterface) {

View File

@ -124,6 +124,10 @@ class ArrayLoader implements LoaderInterface
$package->setInstallationSource($config['installation-source']); $package->setInstallationSource($config['installation-source']);
} }
if (isset($config['default-branch']) && $config['default-branch'] === true) {
$package->setIsDefaultBranch(true);
}
if (isset($config['source'])) { if (isset($config['source'])) {
if (!isset($config['source']['type']) || !isset($config['source']['url']) || !isset($config['source']['reference'])) { if (!isset($config['source']['type']) || !isset($config['source']['url']) || !isset($config['source']['reference'])) {
throw new \UnexpectedValueException(sprintf( throw new \UnexpectedValueException(sprintf(
@ -364,7 +368,7 @@ class ArrayLoader implements LoaderInterface
} }
} }
if (\in_array($config['version'], array('dev-master', 'dev-default', 'dev-trunk'), true)) { if (isset($config['default-branch']) && $config['default-branch'] === true) {
return VersionParser::DEV_MASTER_ALIAS; return VersionParser::DEV_MASTER_ALIAS;
} }
} }

View File

@ -110,6 +110,11 @@ class RootPackageLoader extends ArrayLoader
} }
} }
$defaultBranch = $this->versionGuesser->getDefaultBranchName($cwd ?: getcwd());
if ($defaultBranch && $config['version'] === 'dev-'.$defaultBranch) {
$config['default-branch'] = true;
}
$realPackage = $package = parent::load($config, $class); $realPackage = $package = parent::load($config, $class);
if ($realPackage instanceof AliasPackage) { if ($realPackage instanceof AliasPackage) {
$realPackage = $package->getAliasOf(); $realPackage = $package->getAliasOf();

View File

@ -432,7 +432,7 @@ class Locker
case 'git': case 'git':
GitUtil::cleanEnv(); GitUtil::cleanEnv();
if (0 === $this->process->execute('git log -n1 --pretty=%ct '.ProcessExecutor::escape($sourceRef), $output, $path) && preg_match('{^\s*\d+\s*$}', $output)) { if (0 === $this->process->execute('git log -n1 --pretty=%ct '.ProcessExecutor::escape($sourceRef).GitUtil::getNoShowSignatureFlag($this->process), $output, $path) && preg_match('{^\s*\d+\s*$}', $output)) {
$datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); $datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC'));
} }
break; break;

View File

@ -59,6 +59,7 @@ class Package extends BasePackage
protected $includePaths = array(); protected $includePaths = array();
protected $archiveName; protected $archiveName;
protected $archiveExcludes = array(); protected $archiveExcludes = array();
protected $isDefaultBranch = false;
/** /**
* Creates a new in memory package. * Creates a new in memory package.
@ -588,6 +589,22 @@ class Package extends BasePackage
return $this->archiveExcludes; return $this->archiveExcludes;
} }
/**
* @param bool $defaultBranch
*/
public function setIsDefaultBranch($defaultBranch)
{
$this->isDefaultBranch = $defaultBranch;
}
/**
* {@inheritDoc}
*/
public function isDefaultBranch()
{
return $this->isDefaultBranch;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -371,6 +371,11 @@ interface PackageInterface
*/ */
public function getArchiveExcludes(); public function getArchiveExcludes();
/**
* @return bool
*/
public function isDefaultBranch();
/** /**
* Returns a list of options to download package dist files * Returns a list of options to download package dist files
* *

View File

@ -20,6 +20,7 @@ use Composer\Util\Git as GitUtil;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Svn as SvnUtil; use Composer\Util\Svn as SvnUtil;
use Composer\Util\Platform;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
@ -110,6 +111,43 @@ class VersionGuesser
return $versionData; return $versionData;
} }
/**
* Tries to find name of default branch from VCS info
*
* @param string $path Path to guess into
*/
public function getDefaultBranchName($path)
{
if (version_compare(GitUtil::getVersion($this->process), '2.3.0-rc0', '>=')) {
GitUtil::cleanEnv();
$oldVal = getenv('GIT_SSH_COMMAND');
putenv("GIT_SSH_COMMAND=ssh".(Platform::isWindows() ? '.exe' : '')." -o StrictHostKeyChecking=yes");
$hasGitRemote = 0 === $this->process->execute('git remote show origin', $output, $path);
if ($oldVal) {
putenv("GIT_SSH_COMMAND=$oldVal");
} else {
putenv("GIT_SSH_COMMAND");
}
if ($hasGitRemote && preg_match('{^ HEAD branch: (.+)$}m', $output, $match)) {
return trim($match[1]);
}
}
if (is_dir($path.'/.git')) {
return 'master';
}
if (is_dir($path.'/.hg')) {
return 'default';
}
if (is_dir($path.'/.svn')) {
return 'trunk';
}
return null;
}
private function guessGitVersion(array $packageConfig, $path) private function guessGitVersion(array $packageConfig, $path)
{ {
GitUtil::cleanEnv(); GitUtil::cleanEnv();
@ -154,6 +192,7 @@ class VersionGuesser
if ($isFeatureBranch) { if ($isFeatureBranch) {
$featureVersion = $version; $featureVersion = $version;
$featurePrettyVersion = $prettyVersion; $featurePrettyVersion = $prettyVersion;
// try to find the best (nearest) version branch to assume this feature's version // try to find the best (nearest) version branch to assume this feature's version
$result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path); $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path);
$version = $result['version']; $version = $result['version'];
@ -172,7 +211,7 @@ class VersionGuesser
} }
if (!$commit) { if (!$commit) {
$command = 'git log --pretty="%H" -n1 HEAD'; $command = 'git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($this->process);
if (0 === $this->process->execute($command, $output, $path)) { if (0 === $this->process->execute($command, $output, $path)) {
$commit = trim($output) ?: null; $commit = trim($output) ?: null;
} }
@ -248,14 +287,16 @@ class VersionGuesser
$nonFeatureBranches = implode('|', $packageConfig['non-feature-branches']); $nonFeatureBranches = implode('|', $packageConfig['non-feature-branches']);
} }
foreach ($branches as $candidate) { // return directly, if branch is configured to be non-feature branch
// return directly, if branch is configured to be non-feature branch if (preg_match('{^(' . $nonFeatureBranches . ')$}', $branch)) {
if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) { return array('version' => $version, 'pretty_version' => $prettyVersion);
break; }
}
$defaultBranch = $this->getDefaultBranchName($path);
foreach ($branches as $candidate) {
// do not compare against itself or other feature branches // do not compare against itself or other feature branches
if ($candidate === $branch || !preg_match('{^(' . $nonFeatureBranches . '|master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) { if ($candidate === $branch || !preg_match('{^(' . $nonFeatureBranches . ($defaultBranch ? '|'.preg_quote($defaultBranch) : '').'|master|main|latest|next|current|support|tip|trunk|default|develop|\d+\..+)$}', $candidate, $match)) {
continue; continue;
} }

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;
@ -1165,8 +1168,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$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

@ -22,6 +22,7 @@ use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Url; use Composer\Util\Url;
use Composer\Util\Git as GitUtil;
/** /**
* This repository allows installing local packages that are not necessarily under their own VCS. * This repository allows installing local packages that are not necessarily under their own VCS.
@ -182,7 +183,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
} }
$output = ''; $output = '';
if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) { if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H'.GitUtil::getNoShowSignatureFlag($this->process), $output, $path)) {
$package['dist']['reference'] = trim($output); $package['dist']['reference'] = trim($output);
} }

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

@ -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

@ -167,8 +167,10 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$this->loader = new ArrayLoader($this->versionParser); $this->loader = new ArrayLoader($this->versionParser);
} }
$hasRootIdentifierComposerJson = false;
try { try {
if ($driver->hasComposerFile($driver->getRootIdentifier())) { $hasRootIdentifierComposerJson = $driver->hasComposerFile($driver->getRootIdentifier());
if ($hasRootIdentifierComposerJson) {
$data = $driver->getComposerInformation($driver->getRootIdentifier()); $data = $driver->getComposerInformation($driver->getRootIdentifier());
$this->packageName = !empty($data['name']) ? $data['name'] : null; $this->packageName = !empty($data['name']) ? $data['name'] : null;
} }
@ -229,10 +231,17 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']); $data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']);
$data['version_normalized'] = preg_replace('{(^dev-|[.-]?dev$)}i', '', $data['version_normalized']); $data['version_normalized'] = preg_replace('{(^dev-|[.-]?dev$)}i', '', $data['version_normalized']);
// make sure tag do not contain the default-branch marker
unset($data['default-branch']);
// broken package, version doesn't match tag // broken package, version doesn't match tag
if ($data['version_normalized'] !== $parsedTag) { if ($data['version_normalized'] !== $parsedTag) {
if ($isVeryVerbose) { if ($isVeryVerbose) {
$this->io->writeError('<warning>Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json</warning>'); if (preg_match('{(^dev-|[.-]?dev$)}i', $parsedTag)) {
$this->io->writeError('<warning>Skipped tag '.$tag.', invalid tag name, tags can not use dev prefixes or suffixes</warning>');
} else {
$this->io->writeError('<warning>Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json</warning>');
}
} }
continue; continue;
} }
@ -269,6 +278,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
} }
$branches = $driver->getBranches(); $branches = $driver->getBranches();
// make sure the root identifier branch gets loaded first
if ($hasRootIdentifierComposerJson && isset($branches[$driver->getRootIdentifier()])) {
$branches = array($driver->getRootIdentifier() => $branches[$driver->getRootIdentifier()]) + $branches;
}
foreach ($branches as $branch => $identifier) { foreach ($branches as $branch => $identifier) {
$msg = 'Reading composer.json of <info>' . ($this->packageName ?: $this->url) . '</info> (<comment>' . $branch . '</comment>)'; $msg = 'Reading composer.json of <info>' . ($this->packageName ?: $this->url) . '</info> (<comment>' . $branch . '</comment>)';
if ($isVeryVerbose) { if ($isVeryVerbose) {
@ -299,7 +313,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$version = $prefix . preg_replace('{(\.9{7})+}', '.x', $parsedBranch); $version = $prefix . preg_replace('{(\.9{7})+}', '.x', $parsedBranch);
} }
$cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose); $cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose, $driver->getRootIdentifier() === $branch);
if ($cachedPackage) { if ($cachedPackage) {
$this->addPackage($cachedPackage); $this->addPackage($cachedPackage);
@ -323,6 +337,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$data['version'] = $version; $data['version'] = $version;
$data['version_normalized'] = $parsedBranch; $data['version_normalized'] = $parsedBranch;
unset($data['default-branch']);
if ($driver->getRootIdentifier() === $branch) {
$data['default-branch'] = true;
}
if ($isVeryVerbose) { if ($isVeryVerbose) {
$this->io->writeError('Importing branch '.$branch.' ('.$data['version'].')'); $this->io->writeError('Importing branch '.$branch.' ('.$data['version'].')');
} }
@ -404,7 +423,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
return false; return false;
} }
private function getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose) private function getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose, $isDefaultBranch = false)
{ {
if (!$this->versionCache) { if (!$this->versionCache) {
return; return;
@ -427,6 +446,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$this->io->overwriteError($msg, false); $this->io->overwriteError($msg, false);
} }
unset($cachedPackage['default-branch']);
if ($isDefaultBranch) {
$cachedPackage['default-branch'] = true;
}
if ($existingPackage = $this->findPackage($cachedPackage['name'], new Constraint('=', $cachedPackage['version_normalized']))) { if ($existingPackage = $this->findPackage($cachedPackage['name'], new Constraint('=', $cachedPackage['version_normalized']))) {
if ($isVeryVerbose) { if ($isVeryVerbose) {
$this->io->writeError('<warning>Skipped cached version '.$version.', it conflicts with an another tag ('.$existingPackage->getPrettyVersion().') as both resolve to '.$cachedPackage['version_normalized'].' internally</warning>'); $this->io->writeError('<warning>Skipped cached version '.$version.', it conflicts with an another tag ('.$existingPackage->getPrettyVersion().') as both resolve to '.$cachedPackage['version_normalized'].' internally</warning>');

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

@ -20,7 +20,7 @@ use Composer\IO\IOInterface;
*/ */
class Git class Git
{ {
private static $version; private static $version = false;
/** @var IOInterface */ /** @var IOInterface */
protected $io; protected $io;
@ -297,6 +297,16 @@ class Git
return false; return false;
} }
public static function getNoShowSignatureFlag(ProcessExecutor $process)
{
$gitVersion = self::getVersion($process);
if ($gitVersion && version_compare($gitVersion, '2.10.0-rc0', '>=')) {
return ' --no-show-signature';
}
return '';
}
private function checkRefIsInMirror($url, $dir, $ref) private function checkRefIsInMirror($url, $dir, $ref)
{ {
if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') {
@ -393,16 +403,18 @@ class Git
* *
* @return string|null The git version number. * @return string|null The git version number.
*/ */
public function getVersion() public static function getVersion(ProcessExecutor $process)
{ {
if (isset(self::$version)) { if (false === self::$version) {
return self::$version; self::$version = null;
} if (!$process) {
if (0 !== $this->process->execute('git --version', $output)) { $process = new ProcessExecutor;
return; }
} if (0 === $process->execute('git --version', $output) && preg_match('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) {
if (preg_match('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) { self::$version = $matches[1];
return self::$version = $matches[1]; }
} }
return self::$version;
} }
} }

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,51 +269,60 @@ class HttpDownloader
$this->runningJobs--; $this->runningJobs--;
} }
/** public function wait($index = null)
* @private
*/
public function scheduleNextJob()
{
foreach ($this->jobs as $job) {
if ($job['status'] === self::STATUS_QUEUED) {
$this->startJob($job['id']);
if ($this->runningJobs >= $this->maxJobs) {
return;
}
}
}
}
public function wait($index = null, $progress = false)
{ {
while (true) { while (true) {
if ($this->curl) { if (!$this->countActiveJobs($index)) {
$this->curl->tick(); return;
}
if (null !== $index) {
if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) {
return;
}
} else {
$done = true;
foreach ($this->jobs as $job) {
if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) {
$done = false;
break;
} elseif (!$job['sync']) {
unset($this->jobs[$job['id']]);
}
}
if ($done) {
return;
}
} }
usleep(1000); 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']);
}
}
}
if ($this->curl) {
$this->curl->tick();
}
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++;
} elseif (!$job['sync']) {
unset($this->jobs[$job['id']]);
}
}
return $active;
}
private function getResponse($index) private function getResponse($index)
{ {
if (!isset($this->jobs[$index])) { if (!isset($this->jobs[$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

@ -18,6 +18,8 @@ use Composer\Test\TestCase;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform; use Composer\Util\Platform;
use Prophecy\Argument; use Prophecy\Argument;
use Composer\Util\ProcessExecutor;
use Composer\Util\Git as GitUtil;
class GitDownloaderTest extends TestCase class GitDownloaderTest extends TestCase
{ {
@ -30,6 +32,8 @@ class GitDownloaderTest extends TestCase
{ {
$this->skipIfNotExecutable('git'); $this->skipIfNotExecutable('git');
$this->initGitVersion('1.0.0');
$this->fs = new Filesystem; $this->fs = new Filesystem;
$this->workingDir = $this->getUniqueTmpDirectory(); $this->workingDir = $this->getUniqueTmpDirectory();
} }
@ -40,10 +44,15 @@ class GitDownloaderTest extends TestCase
$this->fs->removeDirectory($this->workingDir); $this->fs->removeDirectory($this->workingDir);
} }
$this->initGitVersion(false);
}
private function initGitVersion($version)
{
// reset the static version cache // reset the static version cache
$refl = new \ReflectionProperty('Composer\Util\Git', 'version'); $refl = new \ReflectionProperty('Composer\Util\Git', 'version');
$refl->setAccessible(true); $refl->setAccessible(true);
$refl->setValue(null, null); $refl->setValue(null, $version);
} }
protected function setupConfig($config = null) protected function setupConfig($config = null)
@ -103,32 +112,23 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('dev-master')); ->will($this->returnValue('dev-master'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
->method('execute')
->with($this->equalTo($this->winCompat('git --version')))
->will($this->returnCallback(function ($command, &$output = null) {
$output = 'git version 1.0.0';
return 0;
}));
$expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer && git remote set-url origin 'https://example.com/composer/composer' && git remote set-url composer 'https://example.com/composer/composer'"); $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer && git remote set-url origin 'https://example.com/composer/composer' && git remote set-url composer 'https://example.com/composer/composer'");
$processExecutor->expects($this->at(1)) $processExecutor->expects($this->at(0))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(2)) $processExecutor->expects($this->at(1))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(3)) $processExecutor->expects($this->at(2))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git checkout 'master' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git checkout 'master' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(4)) $processExecutor->expects($this->at(3))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
@ -157,14 +157,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('dev-master')); ->will($this->returnValue('dev-master'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0)) $this->initGitVersion('2.17.0');
->method('execute')
->with($this->equalTo($this->winCompat('git --version')))
->will($this->returnCallback(function ($command, &$output = null) {
$output = 'git version 2.3.1';
return 0;
}));
$config = new Config; $config = new Config;
$this->setupConfig($config); $this->setupConfig($config);
@ -174,7 +167,7 @@ class GitDownloaderTest extends TestCase
$filesystem->removeDirectory($cachePath); $filesystem->removeDirectory($cachePath);
$expectedGitCommand = $this->winCompat(sprintf("git clone --mirror 'https://example.com/composer/composer' '%s'", $cachePath)); $expectedGitCommand = $this->winCompat(sprintf("git clone --mirror 'https://example.com/composer/composer' '%s'", $cachePath));
$processExecutor->expects($this->at(1)) $processExecutor->expects($this->at(0))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnCallback(function () use ($cachePath) { ->will($this->returnCallback(function () use ($cachePath) {
@ -182,7 +175,7 @@ class GitDownloaderTest extends TestCase
return 0; return 0;
})); }));
$processExecutor->expects($this->at(2)) $processExecutor->expects($this->at(1))
->method('execute') ->method('execute')
->with($this->equalTo('git rev-parse --git-dir'), $this->anything(), $this->equalTo($this->winCompat($cachePath))) ->with($this->equalTo('git rev-parse --git-dir'), $this->anything(), $this->equalTo($this->winCompat($cachePath)))
->will($this->returnCallback(function ($command, &$output = null) { ->will($this->returnCallback(function ($command, &$output = null) {
@ -190,28 +183,28 @@ class GitDownloaderTest extends TestCase
return 0; return 0;
})); }));
$processExecutor->expects($this->at(3)) $processExecutor->expects($this->at(2))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat('git rev-parse --quiet --verify \'1234567890123456789012345678901234567890^{commit}\'')), $this->equalTo(null), $this->equalTo($this->winCompat($cachePath))) ->with($this->equalTo($this->winCompat('git rev-parse --quiet --verify \'1234567890123456789012345678901234567890^{commit}\'')), $this->equalTo(null), $this->equalTo($this->winCompat($cachePath)))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$expectedGitCommand = $this->winCompat(sprintf("git clone --no-checkout '%1\$s' 'composerPath' --dissociate --reference '%1\$s' && cd 'composerPath' && git remote set-url origin 'https://example.com/composer/composer' && git remote add composer 'https://example.com/composer/composer'", $cachePath)); $expectedGitCommand = $this->winCompat(sprintf("git clone --no-checkout '%1\$s' 'composerPath' --dissociate --reference '%1\$s' && cd 'composerPath' && git remote set-url origin 'https://example.com/composer/composer' && git remote add composer 'https://example.com/composer/composer'", $cachePath));
$processExecutor->expects($this->at(4)) $processExecutor->expects($this->at(3))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(5)) $processExecutor->expects($this->at(4))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(6)) $processExecutor->expects($this->at(5))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git checkout 'master' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git checkout 'master' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(7)) $processExecutor->expects($this->at(6))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
@ -241,50 +234,41 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
->method('execute')
->with($this->equalTo($this->winCompat('git --version')))
->will($this->returnCallback(function ($command, &$output = null) {
$output = 'git version 1.0.0';
return 0;
}));
$expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer && git remote set-url origin 'https://github.com/mirrors/composer' && git remote set-url composer 'https://github.com/mirrors/composer'"); $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer && git remote set-url origin 'https://github.com/mirrors/composer' && git remote set-url composer 'https://github.com/mirrors/composer'");
$processExecutor->expects($this->at(1)) $processExecutor->expects($this->at(0))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(1)); ->will($this->returnValue(1));
$processExecutor->expects($this->at(2)) $processExecutor->expects($this->at(1))
->method('getErrorOutput') ->method('getErrorOutput')
->with() ->with()
->will($this->returnValue('Error1')); ->will($this->returnValue('Error1'));
$expectedGitCommand = $this->winCompat("git clone --no-checkout 'git@github.com:mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git@github.com:mirrors/composer' && git fetch composer && git remote set-url origin 'git@github.com:mirrors/composer' && git remote set-url composer 'git@github.com:mirrors/composer'"); $expectedGitCommand = $this->winCompat("git clone --no-checkout 'git@github.com:mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git@github.com:mirrors/composer' && git fetch composer && git remote set-url origin 'git@github.com:mirrors/composer' && git remote set-url composer 'git@github.com:mirrors/composer'");
$processExecutor->expects($this->at(3)) $processExecutor->expects($this->at(2))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$expectedGitCommand = $this->winCompat("git remote set-url origin 'https://github.com/composer/composer'"); $expectedGitCommand = $this->winCompat("git remote set-url origin 'https://github.com/composer/composer'");
$processExecutor->expects($this->at(4)) $processExecutor->expects($this->at(3))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'"); $expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'");
$processExecutor->expects($this->at(5)) $processExecutor->expects($this->at(4))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(6)) $processExecutor->expects($this->at(5))
->method('execute') ->method('execute')
->with($this->equalTo('git branch -r')) ->with($this->equalTo('git branch -r'))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(7)) $processExecutor->expects($this->at(6))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
@ -328,28 +312,19 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
->method('execute')
->with($this->equalTo($this->winCompat('git --version')))
->will($this->returnCallback(function ($command, &$output = null) {
$output = 'git version 1.0.0';
return 0;
}));
$expectedGitCommand = $this->winCompat("git clone --no-checkout '{$url}' 'composerPath' && cd 'composerPath' && git remote add composer '{$url}' && git fetch composer && git remote set-url origin '{$url}' && git remote set-url composer '{$url}'"); $expectedGitCommand = $this->winCompat("git clone --no-checkout '{$url}' 'composerPath' && cd 'composerPath' && git remote add composer '{$url}' && git fetch composer && git remote set-url origin '{$url}' && git remote set-url composer '{$url}'");
$processExecutor->expects($this->at(1)) $processExecutor->expects($this->at(0))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$expectedGitCommand = $this->winCompat("git remote set-url --push origin '{$pushUrl}'"); $expectedGitCommand = $this->winCompat("git remote set-url --push origin '{$pushUrl}'");
$processExecutor->expects($this->at(2)) $processExecutor->expects($this->at(1))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath')))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->exactly(5)) $processExecutor->expects($this->exactly(4))
->method('execute') ->method('execute')
->will($this->returnValue(0)); ->will($this->returnValue(0));
@ -375,14 +350,6 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(array('https://example.com/composer/composer'))); ->will($this->returnValue(array('https://example.com/composer/composer')));
$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')
->with($this->equalTo($this->winCompat('git --version')))
->will($this->returnCallback(function ($command, &$output = null) {
$output = 'git version 1.0.0';
return 0;
}));
$processExecutor->expects($this->at(1))
->method('execute') ->method('execute')
->with($this->equalTo($expectedGitCommand)) ->with($this->equalTo($expectedGitCommand))
->will($this->returnValue(1)); ->will($this->returnValue(1));
@ -437,7 +404,6 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0.0')); ->will($this->returnValue('1.0.0.0'));
$process = $this->prophesize('Composer\Util\ProcessExecutor'); $process = $this->prophesize('Composer\Util\ProcessExecutor');
$process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0);
$process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0); $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0);
$process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0); $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0);
$process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0); $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0);
@ -474,33 +440,29 @@ class GitDownloaderTest extends TestCase
$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('execute')
->with($this->equalTo($this->winCompat("git --version"))) ->with($this->equalTo($this->winCompat("git show-ref --head -d")))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(1)) $processExecutor->expects($this->at(1))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git show-ref --head -d"))) ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no")))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(2)) $processExecutor->expects($this->at(2))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) ->with($this->equalTo($this->winCompat("git remote -v")))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(3)) $processExecutor->expects($this->at(3))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git remote -v"))) ->with($this->equalTo($this->winCompat($expectedGitUpdateCommand)), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(4)) $processExecutor->expects($this->at(4))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat($expectedGitUpdateCommand)), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->with($this->equalTo('git branch -r'))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(5)) $processExecutor->expects($this->at(5))
->method('execute')
->with($this->equalTo('git branch -r'))
->will($this->returnValue(0));
$processExecutor->expects($this->at(6))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(7)) $processExecutor->expects($this->at(6))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git remote -v"))) ->with($this->equalTo($this->winCompat("git remote -v")))
->will($this->returnCallback(function ($cmd, &$output, $cwd) { ->will($this->returnCallback(function ($cmd, &$output, $cwd) {
@ -512,11 +474,11 @@ composer https://github.com/old/url (push)
return 0; return 0;
})); }));
$processExecutor->expects($this->at(8)) $processExecutor->expects($this->at(7))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git remote set-url origin 'https://github.com/composer/composer'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->with($this->equalTo($this->winCompat("git remote set-url origin 'https://github.com/composer/composer'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
->will($this->returnValue(0)); ->will($this->returnValue(0));
$processExecutor->expects($this->at(9)) $processExecutor->expects($this->at(8))
->method('execute') ->method('execute')
->with($this->equalTo($this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->with($this->equalTo($this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir)))
->will($this->returnValue(0)); ->will($this->returnValue(0));

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, 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, 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))
@ -240,7 +268,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, 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))
@ -272,7 +312,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, 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()
@ -300,7 +341,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, 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);
} }
/** /**
@ -332,7 +374,26 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, 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

@ -6,8 +6,8 @@ Test the error output of solver problems with dev-master aliases.
{ {
"type": "package", "type": "package",
"package": [ "package": [
{"name": "a/a", "version": "dev-master", "require": {"d/d": "1.0.0"}}, {"name": "a/a", "version": "dev-master", "require": {"d/d": "1.0.0"}, "default-branch": true},
{"name": "b/b", "version": "dev-master", "require": {"d/d": "2.0.0"}}, {"name": "b/b", "version": "dev-master", "require": {"d/d": "2.0.0"}, "default-branch": true},
{"name": "d/d", "version": "1.0.0"}, {"name": "d/d", "version": "1.0.0"},
{"name": "d/d", "version": "2.0.0"} {"name": "d/d", "version": "2.0.0"}
] ]

View File

@ -6,7 +6,7 @@ Test the error output of solver problems with dev-master aliases.
{ {
"type": "package", "type": "package",
"package": [ "package": [
{ "name": "locked/pkg", "version": "dev-master", "require": {"locked/dependency": "1.0.0"} } { "name": "locked/pkg", "version": "dev-master", "require": {"locked/dependency": "1.0.0"}, "default-branch": true }
] ]
} }
], ],
@ -18,7 +18,7 @@ Test the error output of solver problems with dev-master aliases.
--LOCK-- --LOCK--
{ {
"packages": [ "packages": [
{ "name": "locked/pkg", "version": "dev-master", "require": {"locked/dependency": "1.0.0"} }, { "name": "locked/pkg", "version": "dev-master", "require": {"locked/dependency": "1.0.0"}, "default-branch": true },
{ "name": "locked/dependency", "version": "1.0.0" } { "name": "locked/dependency", "version": "1.0.0" }
], ],
"packages-dev": [], "packages-dev": [],

View File

@ -8,7 +8,8 @@ Aliases of referenced packages work
"package": [ "package": [
{ {
"name": "a/aliased", "version": "dev-master", "name": "a/aliased", "version": "dev-master",
"source": { "reference": "orig", "type": "git", "url": "" } "source": { "reference": "orig", "type": "git", "url": "" },
"default-branch": true
}, },
{ {
"name": "b/requirer", "version": "1.0.0", "name": "b/requirer", "version": "1.0.0",
@ -31,7 +32,8 @@ update
{ {
"name": "a/aliased", "version": "dev-master", "name": "a/aliased", "version": "dev-master",
"source": { "reference": "abcd", "type": "git", "url": "" }, "source": { "reference": "abcd", "type": "git", "url": "" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "b/requirer", "version": "1.0.0", "name": "b/requirer", "version": "1.0.0",

View File

@ -13,20 +13,24 @@ Aliases take precedence over default package even if default is selected
{ {
"name": "a/req", "version": "dev-master", "name": "a/req", "version": "dev-master",
"extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } },
"source": { "reference": "forked", "type": "git", "url": "" } "source": { "reference": "forked", "type": "git", "url": "" },
"default-branch": true
}, },
{ {
"name": "a/req", "version": "dev-master", "name": "a/req", "version": "dev-master",
"extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } },
"source": { "reference": "master", "type": "git", "url": "" } "source": { "reference": "master", "type": "git", "url": "" },
"default-branch": true
}, },
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"require": { "a/req": "dev-master" } "require": { "a/req": "dev-master" },
"default-branch": true
}, },
{ {
"name": "a/b", "version": "dev-master", "name": "a/b", "version": "dev-master",
"require": { "a/req": "dev-master" } "require": { "a/req": "dev-master" },
"default-branch": true
} }
] ]
} }
@ -44,12 +48,14 @@ Aliases take precedence over default package even if default is selected
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"require": { "a/req": "dev-master" }, "require": { "a/req": "dev-master" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "a/b", "version": "dev-master", "name": "a/b", "version": "dev-master",
"require": { "a/req": "dev-master" }, "require": { "a/req": "dev-master" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "a/req", "version": "dev-feature-foo", "name": "a/req", "version": "dev-feature-foo",

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

@ -11,7 +11,8 @@ Installing double aliased package
"dist": { "type": "file", "url": "" }, "dist": { "type": "file", "url": "" },
"require": { "require": {
"b/b": "dev-master" "b/b": "dev-master"
} },
"default-branch": true
}, },
{ {
"name": "b/b", "version": "dev-foo", "name": "b/b", "version": "dev-foo",

View File

@ -14,7 +14,8 @@ Installs a dev package from lock using dist
"type": "zip", "type": "zip",
"url": "http://www.example.com/dist.zip", "url": "http://www.example.com/dist.zip",
"reference": "459720ff3b74ee0c0d159277c6f2f5df89d8a4f6" "reference": "459720ff3b74ee0c0d159277c6f2f5df89d8a4f6"
} },
"default-branch": true
} }
] ]
} }
@ -25,7 +26,7 @@ Installs a dev package from lock using dist
"minimum-stability": "dev" "minimum-stability": "dev"
} }
--RUN-- --RUN--
install --prefer-dist install
--EXPECT-LOCK-- --EXPECT-LOCK--
{ {
"packages": [ "packages": [
@ -37,7 +38,8 @@ install --prefer-dist
"url": "http://www.example.com/dist.zip", "url": "http://www.example.com/dist.zip",
"reference": "459720ff3b74ee0c0d159277c6f2f5df89d8a4f6" "reference": "459720ff3b74ee0c0d159277c6f2f5df89d8a4f6"
}, },
"type": "library" "type": "library",
"default-branch": true
} }
], ],
"packages-dev": [], "packages-dev": [],

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

@ -7,18 +7,19 @@ Installs a dev package forcing it's reference
"type": "package", "type": "package",
"package": [ "package": [
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-main",
"source": { "reference": "abc123", "url": "", "type": "git" } "source": { "reference": "abc123", "url": "", "type": "git" },
"default-branch": true
} }
] ]
} }
], ],
"require": { "require": {
"a/a": "dev-master#def000" "a/a": "dev-main#def000"
} }
} }
--RUN-- --RUN--
install install
--EXPECT-- --EXPECT--
Installing a/a (dev-master def000) Installing a/a (dev-main def000)
Marking a/a (9999999-dev def000) as installed, alias of a/a (dev-master def000) Marking a/a (9999999-dev def000) as installed, alias of a/a (dev-main def000)

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

@ -7,7 +7,7 @@ Test that a conflict against >=5 does not include dev-master or other dev-x
"type": "package", "type": "package",
"package": [ "package": [
{ "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": ">=5", "victim/pkg2": ">=5" } }, { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": ">=5", "victim/pkg2": ">=5" } },
{ "name": "victim/pkg", "version": "dev-master" }, { "name": "victim/pkg", "version": "dev-master", "default-branch": true },
{ "name": "victim/pkg2", "version": "dev-foo" } { "name": "victim/pkg2", "version": "dev-foo" }
] ]
} }

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--
{ {
@ -17,7 +17,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" } "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
"default-branch": true
}, },
{ {
"name": "b/b", "version": "2.0.3", "name": "b/b", "version": "2.0.3",
@ -32,23 +33,27 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
{ {
"name": "d/d", "version": "dev-master", "name": "d/d", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/d/newd", "type": "git" }, "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/d/newd", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" } "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
"default-branch": true
}, },
{ {
"name": "e/e", "version": "dev-master", "name": "e/e", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/e/newe", "type": "git" }, "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/e/newe", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" } "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
"default-branch": true
}, },
{ {
"name": "f/f", "version": "dev-master", "name": "f/f", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/f/newf", "type": "git" }, "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/f/newf", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }, "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
"transport-options": { "foo": "bar2" } "transport-options": { "foo": "bar2" },
"default-branch": true
}, },
{ {
"name": "g/g", "version": "dev-master", "name": "g/g", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" }, "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" } "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
"default-branch": true
} }
] ]
} }
@ -68,7 +73,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" } "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"default-branch": true
}, },
{ {
"name": "b/b", "version": "2.0.3", "name": "b/b", "version": "2.0.3",
@ -83,19 +89,22 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
{ {
"name": "d/d", "version": "dev-master", "name": "d/d", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" } "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"default-branch": true
}, },
{ {
"name": "f/f", "version": "dev-master", "name": "f/f", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"transport-options": { "foo": "bar" } "transport-options": { "foo": "bar" },
"default-branch": true
}, },
{ {
"name": "g/g", "version": "dev-master", "name": "g/g", "version": "dev-master",
"source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
"dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
"transport-options": { "foo": "bar" } "transport-options": { "foo": "bar" },
"default-branch": true
} }
] ]
--LOCK-- --LOCK--
@ -105,7 +114,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "b/b", "version": "2.0.3", "name": "b/b", "version": "2.0.3",
@ -123,21 +133,24 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
"name": "d/d", "version": "dev-master", "name": "d/d", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "f/f", "version": "dev-master", "name": "f/f", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"type": "library", "type": "library",
"transport-options": { "foo": "bar" } "transport-options": { "foo": "bar" },
"default-branch": true
}, },
{ {
"name": "g/g", "version": "dev-master", "name": "g/g", "version": "dev-master",
"source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
"dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
"type": "library", "type": "library",
"transport-options": { "foo": "bar" } "transport-options": { "foo": "bar" },
"default-branch": true
} }
], ],
"packages-dev": [], "packages-dev": [],
@ -156,7 +169,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" }, "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/tarball/2222222222222222222222222222222222222222", "type": "tar", "shasum": "newsum" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "b/b", "version": "2.0.3", "name": "b/b", "version": "2.0.3",
@ -174,26 +188,30 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
"name": "d/d", "version": "dev-master", "name": "d/d", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/newd", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/newd", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "e/e", "version": "dev-master", "name": "e/e", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
"type": "library" "type": "library",
"default-branch": true
}, },
{ {
"name": "f/f", "version": "dev-master", "name": "f/f", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"type": "library", "type": "library",
"transport-options": { "foo": "bar" } "transport-options": { "foo": "bar" },
"default-branch": true
}, },
{ {
"name": "g/g", "version": "dev-master", "name": "g/g", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
"type": "library" "type": "library",
"default-branch": true
} }
], ],
"packages-dev": [], "packages-dev": [],

View File

@ -7,36 +7,39 @@ Updating a dev package to its latest ref should pick up new dependencies
"type": "package", "type": "package",
"package": [ "package": [
{ {
"name": "a/devpackage", "version": "dev-master", "name": "a/devpackage", "version": "dev-main",
"source": { "reference": "newref", "url": "", "type": "git" }, "source": { "reference": "newref", "url": "", "type": "git" },
"require": { "require": {
"a/dependency": "*" "a/dependency": "*"
} },
"default-branch": true
}, },
{ {
"name": "a/dependency", "version": "dev-master", "name": "a/dependency", "version": "dev-main",
"source": { "reference": "ref", "url": "", "type": "git" }, "source": { "reference": "ref", "url": "", "type": "git" },
"require": {} "require": {},
"default-branch": true
} }
] ]
} }
], ],
"require": { "require": {
"a/devpackage": "dev-master" "a/devpackage": "dev-main"
}, },
"minimum-stability": "dev" "minimum-stability": "dev"
} }
--INSTALLED-- --INSTALLED--
[ [
{ {
"name": "a/devpackage", "version": "dev-master", "name": "a/devpackage", "version": "dev-main",
"source": { "reference": "oldref", "url": "", "type": "git" }, "source": { "reference": "oldref", "url": "", "type": "git" },
"require": {} "require": {},
"default-branch": true
} }
] ]
--RUN-- --RUN--
update update
--EXPECT-- --EXPECT--
Installing a/dependency (dev-master ref) Installing a/dependency (dev-main ref)
Marking a/dependency (9999999-dev ref) as installed, alias of a/dependency (dev-master ref) Marking a/dependency (9999999-dev ref) as installed, alias of a/dependency (dev-main ref)
Upgrading a/devpackage (dev-master oldref => dev-master newref) Upgrading a/devpackage (dev-main oldref => dev-main newref)

View File

@ -8,7 +8,8 @@ Downgrading from unstable to more stable package should work even if already ins
"package": [ "package": [
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "abcd", "url": "", "type": "git" } "source": { "reference": "abcd", "url": "", "type": "git" },
"default-branch": true
}, },
{ {
"name": "a/a", "version": "1.0.0", "name": "a/a", "version": "1.0.0",
@ -17,7 +18,8 @@ Downgrading from unstable to more stable package should work even if already ins
}, },
{ {
"name": "b/b", "version": "dev-master", "name": "b/b", "version": "dev-master",
"source": { "reference": "abcd", "url": "", "type": "git" } "source": { "reference": "abcd", "url": "", "type": "git" },
"default-branch": true
}, },
{ {
"name": "b/b", "version": "1.0.0", "name": "b/b", "version": "1.0.0",
@ -36,11 +38,13 @@ Downgrading from unstable to more stable package should work even if already ins
[ [
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "abcd", "url": "", "type": "git" } "source": { "reference": "abcd", "url": "", "type": "git" },
"default-branch": true
}, },
{ {
"name": "b/b", "version": "dev-master", "name": "b/b", "version": "dev-master",
"source": { "reference": "abcd", "url": "", "type": "git" } "source": { "reference": "abcd", "url": "", "type": "git" },
"default-branch": true
} }
] ]
--RUN-- --RUN--

View File

@ -8,7 +8,8 @@ Update to a state without dependency works well from locked with dependency
[ [
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "1234", "type": "git", "url": "" } "source": { "reference": "1234", "type": "git", "url": "" },
"default-branch": true
} }
] ]
--LOCK-- --LOCK--
@ -17,7 +18,8 @@ Update to a state without dependency works well from locked with dependency
{ {
"name": "a/a", "version": "dev-master", "name": "a/a", "version": "dev-master",
"source": { "reference": "1234", "type": "git", "url": "" }, "source": { "reference": "1234", "type": "git", "url": "" },
"type": "library" "type": "library",
"default-branch": true
} }
], ],
"packages-dev": [], "packages-dev": [],

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

@ -13,7 +13,8 @@ Installing locked dev packages should remove old dependencies
{ {
"name": "a/devpackage", "version": "dev-master", "name": "a/devpackage", "version": "dev-master",
"source": { "reference": "newref", "url": "", "type": "git" }, "source": { "reference": "newref", "url": "", "type": "git" },
"require": {} "require": {},
"default-branch": true
} }
], ],
"packages-dev": [], "packages-dev": [],
@ -30,12 +31,14 @@ Installing locked dev packages should remove old dependencies
"source": { "reference": "oldref", "url": "", "type": "git" }, "source": { "reference": "oldref", "url": "", "type": "git" },
"require": { "require": {
"a/dependency": "*" "a/dependency": "*"
} },
"default-branch": true
}, },
{ {
"name": "a/dependency", "version": "dev-master", "name": "a/dependency", "version": "dev-master",
"source": { "reference": "ref", "url": "", "type": "git" }, "source": { "reference": "ref", "url": "", "type": "git" },
"require": {} "require": {},
"default-branch": true
} }
] ]
--RUN-- --RUN--

View File

@ -14,7 +14,10 @@ namespace Composer\Test;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\Installer; use Composer\Installer;
use Composer\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Composer\IO\BufferIO; use Composer\IO\BufferIO;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Dumper\ArrayDumper;
@ -263,7 +266,12 @@ class InstallerTest extends TestCase
$installer = Installer::create($io, $composer); $installer = Installer::create($io, $composer);
$application = new Application; $application = new Application;
$application->get('install')->setCode(function ($input, $output) use ($installer) { $install = new Command('install');
$install->addOption('ignore-platform-reqs', null, InputOption::VALUE_NONE);
$install->addOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY);
$install->addOption('no-dev', null, InputOption::VALUE_NONE);
$install->addOption('dry-run', null, InputOption::VALUE_NONE);
$install->setCode(function ($input, $output) use ($installer) {
$ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false);
$installer $installer
@ -273,8 +281,21 @@ class InstallerTest extends TestCase
return $installer->run(); return $installer->run();
}); });
$application->add($install);
$application->get('update')->setCode(function ($input, $output) use ($installer) { $update = new Command('update');
$update->addOption('ignore-platform-reqs', null, InputOption::VALUE_NONE);
$update->addOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY);
$update->addOption('no-dev', null, InputOption::VALUE_NONE);
$update->addOption('no-install', null, InputOption::VALUE_NONE);
$update->addOption('dry-run', null, InputOption::VALUE_NONE);
$update->addOption('lock', null, InputOption::VALUE_NONE);
$update->addOption('with-all-dependencies', null, InputOption::VALUE_NONE);
$update->addOption('with-dependencies', null, InputOption::VALUE_NONE);
$update->addOption('prefer-stable', null, InputOption::VALUE_NONE);
$update->addOption('prefer-lowest', null, InputOption::VALUE_NONE);
$update->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL);
$update->setCode(function ($input, $output) use ($installer) {
$packages = $input->getArgument('packages'); $packages = $input->getArgument('packages');
$filteredPackages = array_filter($packages, function ($package) { $filteredPackages = array_filter($packages, function ($package) {
return !in_array($package, array('lock', 'nothing', 'mirrors'), true); return !in_array($package, array('lock', 'nothing', 'mirrors'), true);
@ -305,6 +326,7 @@ class InstallerTest extends TestCase
return $installer->run(); return $installer->run();
}); });
$application->add($update);
if (!preg_match('{^(install|update)\b}', $run)) { if (!preg_match('{^(install|update)\b}', $run)) {
throw new \UnexpectedValueException('The run command only supports install and update'); throw new \UnexpectedValueException('The run command only supports install and update');

View File

@ -17,6 +17,8 @@ use Composer\Config;
use Composer\Factory; use Composer\Factory;
use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryManager;
use Composer\Repository\WritableRepositoryInterface; use Composer\Repository\WritableRepositoryInterface;
use Composer\Package\Version\VersionGuesser;
use Composer\Package\Version\VersionParser;
use Composer\Package\RootPackageInterface; use Composer\Package\RootPackageInterface;
use Composer\Installer; use Composer\Installer;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
@ -39,6 +41,11 @@ class FactoryMock extends Factory
return $config; return $config;
} }
protected function loadRootPackage(RepositoryManager $rm, Config $config, VersionParser $parser, VersionGuesser $guesser, IOInterface $io)
{
return new \Composer\Package\Loader\RootPackageLoader($rm, $config, $parser, new VersionGuesserMock(), $io);
}
protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage) protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage)
{ {
} }

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Mock;
use Composer\Package\Version\VersionGuesser;
class VersionGuesserMock extends VersionGuesser
{
public function __construct()
{
}
public function guessVersion(array $packageConfig, $path)
{
return null;
}
public function getDefaultBranchName($path)
{
return null;
}
}

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

@ -105,6 +105,8 @@ class RootPackageLoaderTest extends TestCase
'pretty_version' => '3.0-dev', 'pretty_version' => '3.0-dev',
'commit' => 'aabbccddee', 'commit' => 'aabbccddee',
)); ));
$versionGuesser->getDefaultBranchName(Argument::cetera())
->willReturn('main');
$config = new Config; $config = new Config;
$config->merge(array('repositories' => array('packagist' => false))); $config->merge(array('repositories' => array('packagist' => false)));
$loader = new RootPackageLoader($manager->reveal(), $config, null, $versionGuesser->reveal()); $loader = new RootPackageLoader($manager->reveal(), $config, null, $versionGuesser->reveal());
@ -113,6 +115,28 @@ class RootPackageLoaderTest extends TestCase
$this->assertEquals('3.0-dev', $package->getPrettyVersion()); $this->assertEquals('3.0-dev', $package->getPrettyVersion());
} }
public function testDefaultBranchIsSetForRootPackageInDefaultBranch()
{
// see #6845
$manager = $this->prophesize('\\Composer\\Repository\\RepositoryManager');
$versionGuesser = $this->prophesize('\\Composer\\Package\\Version\\VersionGuesser');
$versionGuesser->guessVersion(Argument::cetera())
->willReturn(array(
'name' => 'A',
'version' => 'dev-main',
'pretty_version' => 'dev-main',
'commit' => 'aabbccddee',
));
$versionGuesser->getDefaultBranchName(Argument::cetera())
->willReturn('main');
$config = new Config;
$config->merge(array('repositories' => array('packagist' => false)));
$loader = new RootPackageLoader($manager->reveal(), $config, null, $versionGuesser->reveal());
$package = $loader->load(array());
$this->assertTrue($package->isDefaultBranch());
}
public function testFeatureBranchPrettyVersion() public function testFeatureBranchPrettyVersion()
{ {
if (!function_exists('proc_open')) { if (!function_exists('proc_open')) {
@ -147,6 +171,17 @@ class RootPackageLoaderTest extends TestCase
$executor $executor
->expects($this->at(1)) ->expects($this->at(1))
->method('execute') ->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git remote show origin', $command);
$output = " HEAD branch: master";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) { ->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git rev-list master..latest-production', $command); $self->assertEquals('git rev-list master..latest-production', $command);
$output = ""; $output = "";

View File

@ -16,6 +16,8 @@ use Composer\Config;
use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionGuesser;
use Composer\Semver\VersionParser; use Composer\Semver\VersionParser;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use Composer\Util\Git as GitUtil;
use Composer\Util\ProcessExecutor;
class VersionGuesserTest extends TestCase class VersionGuesserTest extends TestCase
{ {
@ -30,7 +32,7 @@ class VersionGuesserTest extends TestCase
{ {
$branch = 'default'; $branch = 'default';
$executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor') $executor = $this->getMockBuilder('Composer\\Util\\ProcessExecutor')
->setMethods(array('execute')) ->setMethods(array('execute'))
->disableArgumentCloning() ->disableArgumentCloning()
->disableOriginalConstructor() ->disableOriginalConstructor()
@ -40,6 +42,8 @@ class VersionGuesserTest extends TestCase
$self = $this; $self = $this;
$step = 0; $step = 0;
GitUtil::getVersion(new ProcessExecutor);
$executor $executor
->expects($this->at($step)) ->expects($this->at($step))
->method('execute') ->method('execute')
@ -65,8 +69,8 @@ class VersionGuesserTest extends TestCase
$executor $executor
->expects($this->at($step)) ->expects($this->at($step))
->method('execute') ->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) { ->willReturnCallback(function ($command, &$output) use ($self, $executor) {
$self->assertEquals('git log --pretty="%H" -n1 HEAD', $command); $self->assertEquals('git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($executor), $command);
return 128; return 128;
}) })
@ -131,6 +135,64 @@ class VersionGuesserTest extends TestCase
$this->assertEquals($commitHash, $versionArray['commit']); $this->assertEquals($commitHash, $versionArray['commit']);
} }
public function testGuessVersionReadsAndRespectsDefaultBranchAsNonFeatureBranch()
{
$commitHash = '03a15d220da53c52eddd5f32ffca64a7b3801bea';
$anotherCommitHash = '13a15d220da53c52eddd5f32ffca64a7b3801bea';
$executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
->setMethods(array('execute'))
->disableArgumentCloning()
->disableOriginalConstructor()
->getMock()
;
$self = $this;
$executor
->expects($this->at(0))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self, $commitHash, $anotherCommitHash) {
$self->assertEquals('git branch --no-color --no-abbrev -v', $command);
$output = " arbitrary $commitHash Commit message\n* current $anotherCommitHash Another message\n";
return 0;
})
;
$executor
->expects($this->at(1))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git remote show origin', $command);
$output = " HEAD branch: arbitrary\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output, $path) use ($self, $anotherCommitHash) {
$self->assertEquals('git rev-list arbitrary..current', $command);
$output = "$anotherCommitHash\n";
return 0;
})
;
$config = new Config;
$config->merge(array('repositories' => array('packagist' => false)));
$guesser = new VersionGuesser($config, $executor, new VersionParser());
$versionArray = $guesser->guessVersion(array('version' => 'self.version'), 'dummy/path');
$this->assertEquals("dev-arbitrary", $versionArray['version']);
$this->assertEquals($anotherCommitHash, $versionArray['commit']);
$this->assertEquals("dev-current", $versionArray['feature_version']);
$this->assertEquals("dev-current", $versionArray['feature_pretty_version']);
}
public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationForArbitraryNaming() public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationForArbitraryNaming()
{ {
$commitHash = '03a15d220da53c52eddd5f32ffca64a7b3801bea'; $commitHash = '03a15d220da53c52eddd5f32ffca64a7b3801bea';
@ -159,6 +221,17 @@ class VersionGuesserTest extends TestCase
$executor $executor
->expects($this->at(1)) ->expects($this->at(1))
->method('execute') ->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git remote show origin', $command);
$output = " HEAD branch: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output, $path) use ($self, $anotherCommitHash) { ->willReturnCallback(function ($command, &$output, $path) use ($self, $anotherCommitHash) {
$self->assertEquals('git rev-list arbitrary..current', $command); $self->assertEquals('git rev-list arbitrary..current', $command);
$output = "$anotherCommitHash\n"; $output = "$anotherCommitHash\n";
@ -202,10 +275,19 @@ class VersionGuesserTest extends TestCase
return 0; return 0;
}) })
; ;
$executor $executor
->expects($this->at(1)) ->expects($this->at(1))
->method('execute') ->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git remote show origin', $command);
$output = " HEAD branch: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output, $path) use ($self, $anotherCommitHash) { ->willReturnCallback(function ($command, &$output, $path) use ($self, $anotherCommitHash) {
$self->assertEquals('git rev-list latest-testing..current', $command); $self->assertEquals('git rev-list latest-testing..current', $command);
$output = "$anotherCommitHash\n"; $output = "$anotherCommitHash\n";
@ -374,10 +456,19 @@ class VersionGuesserTest extends TestCase
return 0; return 0;
}) })
; ;
$executor $executor
->expects($this->at(1)) ->expects($this->at(1))
->method('execute') ->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git remote show origin', $command);
$output = " HEAD branch: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) { ->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git describe --exact-match --tags', $command); $self->assertEquals('git describe --exact-match --tags', $command);
$output = "v2.0.5-alpha2"; $output = "v2.0.5-alpha2";
@ -415,10 +506,19 @@ class VersionGuesserTest extends TestCase
return 0; return 0;
}) })
; ;
$executor $executor
->expects($this->at(1)) ->expects($this->at(1))
->method('execute') ->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git remote show origin', $command);
$output = " HEAD branch: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) { ->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git describe --exact-match --tags', $command); $self->assertEquals('git describe --exact-match --tags', $command);
$output = '1.0.0'; $output = '1.0.0';

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

@ -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();
}
} }