Merge branch 'master' into filter-packages

main
Yanick Witschi 4 years ago
commit ea0ce9dd7d

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

@ -31,7 +31,7 @@ jobs:
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
extensions: "intl"
extensions: "intl, zip"
ini-values: "memory_limit=-1"
php-version: "${{ matrix.php-version }}"
tools: "cs2pr"
@ -52,5 +52,5 @@ jobs:
- name: Run PHPStan
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

@ -785,7 +785,7 @@ Lists the name, version and license of every package installed. Use
### 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
## run-script

@ -4,7 +4,7 @@
# 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
to have access to these packages you will have to tell composer how to
authenticate with your credentials.

@ -1,7 +1,5 @@
parameters:
level: 1
autoload_files:
- '../src/bootstrap.php'
excludes_analyse:
- '../tests/Composer/Test/Fixtures/*'
- '../tests/Composer/Test/Autoload/Fixtures/*'
@ -27,6 +25,7 @@ parameters:
# BC with older PHPUnit
- '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~'
paths:
- ../src
- ../tests

@ -565,6 +565,10 @@
"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": {
"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."

@ -257,16 +257,16 @@ EOF;
EOF;
}
$blacklist = null;
$excluded = null;
if (!empty($autoloads['exclude-from-classmap'])) {
$blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}';
$excluded = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}';
}
$classMap = array();
$ambiguousClasses = array();
$scannedFiles = array();
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) {
@ -289,7 +289,7 @@ EOF;
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);
}
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";
if (!isset($classMap[$class])) {
$classMap[$class] = $pathCode;
@ -382,9 +382,9 @@ EOF;
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)
@ -488,15 +488,15 @@ EOF;
}
if (isset($autoloads['classmap'])) {
$blacklist = null;
$excluded = null;
if (!empty($autoloads['exclude-from-classmap'])) {
$blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}';
$excluded = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}';
}
$scannedFiles = array();
foreach ($autoloads['classmap'] as $dir) {
try {
$loader->addClassMap($this->generateClassMap($dir, $blacklist, null, null, false, $scannedFiles));
$loader->addClassMap($this->generateClassMap($dir, $excluded, null, null, false, $scannedFiles));
} catch (\RuntimeException $e) {
$this->io->writeError('<warning>'.$e->getMessage().'</warning>');
}

@ -51,7 +51,7 @@ class ClassMapGenerator
* Iterate over all files in the given directory searching for classes
*
* @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 string $namespace Optional namespace prefix to filter by
* @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
* @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;
if (is_string($path)) {
@ -102,12 +102,12 @@ class ClassMapGenerator
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
if ($blacklist && preg_match($blacklist, strtr($realPath, '\\', '/'))) {
// 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 ($excluded && preg_match($excluded, strtr($realPath, '\\', '/'))) {
continue;
}
// 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;
}

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

@ -201,6 +201,8 @@ EOT
// install dependencies of the created project
if ($noInstall === false) {
$composer->getInstallationManager()->setOutputProgress(!$noProgress);
$installer = Installer::create($io, $composer);
$installer->setPreferSource($preferSource)
->setPreferDist($preferDist)
@ -212,6 +214,10 @@ EOT
->setClassMapAuthoritative($config->get('classmap-authoritative'))
->setApcuAutoloader($config->get('apcu-autoloader'));
if (!$composer->getLocker()->isLocked()) {
$installer->setUpdate(true);
}
if ($disablePlugins) {
$installer->disablePlugins();
}
@ -405,7 +411,8 @@ EOT
->setPreferDist($preferDist);
$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->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package)));
$im->notifyInstalls($io);

@ -431,7 +431,11 @@ EOT
}
$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@') {
return '<comment>You are not running the latest '.$versionsUtil->getChannel().' version, run `composer self-update` to update ('.Composer::VERSION.' => '.$latest['version'].')</comment>';

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

@ -21,6 +21,7 @@ use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* @author Benoît Merlet <benoit.merlet@gmail.com>
@ -111,6 +112,28 @@ EOT
)));
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:
throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format));
}

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

@ -286,6 +286,8 @@ EOT
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
$composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress'));
$install = Installer::create($io, $composer);
$ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false);

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

@ -156,7 +156,7 @@ class Application extends BaseApplication
}
// 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());
$home = realpath(getenv('HOME') ?: getenv('USERPROFILE') ?: '/');

@ -61,7 +61,12 @@ class InstallOperation extends SolverOperation
*/
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>)';
}
/**

@ -61,7 +61,12 @@ class UninstallOperation extends SolverOperation
*/
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>)';
}
/**

@ -75,20 +75,25 @@ class UpdateOperation extends SolverOperation
*/
public function show($lock)
{
$fromVersion = $this->initialPackage->getFullPrettyVersion();
$toVersion = $this->targetPackage->getFullPrettyVersion();
return self::format($this->initialPackage, $this->targetPackage, $lock);
}
public static function format(PackageInterface $initialPackage, PackageInterface $targetPackage, $lock = false)
{
$fromVersion = $initialPackage->getFullPrettyVersion();
$toVersion = $targetPackage->getFullPrettyVersion();
if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) {
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
} elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) {
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
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>)';
}
/**

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

@ -12,21 +12,21 @@
namespace Composer\DependencyResolver;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Package\Version\StabilityFilter;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Semver\CompilingMatcher;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Plugin\PluginEvents;
use Composer\Semver\Intervals;
/**
@ -103,7 +103,7 @@ class PoolBuilder
* @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value
* @psalm-param array<string, int> $stabilityFlags
* @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
* @psalm-param array<string, string> $rootReferences
*/
@ -111,7 +111,7 @@ class PoolBuilder
{
$this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $this->getRootAliasesPerPackage($rootAliases);
$this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher;
$this->io = $io;
@ -193,7 +193,7 @@ class PoolBuilder
$found = false;
foreach ($aliasedPackages as $packageOrAlias) {
if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) {
if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) {
$found = true;
}
}
@ -497,19 +497,5 @@ class PoolBuilder
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;
}
}

@ -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) . '.';
case self::RULE_LEARNED:
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 {
$learnedString = ' (reasoning unavailable)';
}

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

@ -18,6 +18,9 @@ use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\IO\NullIO;
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\Version\VersionParser;
use Composer\Plugin\PluginEvents;
@ -27,7 +30,9 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\Filesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Url as UrlUtil;
use Composer\Util\ProcessExecutor;
use Composer\Downloader\TransportException;
use React\Promise\PromiseInterface;
/**
* Base downloader for files
@ -51,10 +56,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
protected $cache;
/** @var EventDispatcher */
protected $eventDispatcher;
/** @var ProcessExecutor */
protected $process;
/**
* @private this is only public for php 5.3 support in closures
*/
public $lastCacheWrites = array();
private $additionalCleanupPaths = array();
/**
* Constructor.
@ -66,14 +74,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
* @param Cache $cache Cache instance
* @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->config = $config;
$this->eventDispatcher = $eventDispatcher;
$this->httpDownloader = $httpDownloader;
$this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache;
$this->process = $process ?: new ProcessExecutor($io);
$this->filesystem = $filesystem ?: new Filesystem($this->process);
if ($this->cache && $this->cache->gcIsNecessary()) {
$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);
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);
$url['processed'] = $preFileDownloadEvent->getProcessedUrl();
}
$checksum = $package->getDistSha1Checksum();
@ -258,6 +268,12 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$path,
);
if (isset($this->additionalCleanupPaths[$package->getName()])) {
foreach ($this->additionalCleanupPaths[$package->getName()] as $path) {
$this->filesystem->remove($path);
}
}
foreach ($dirsToCleanUp as $dir) {
if (is_dir($dir) && $this->filesystem->isDirEmpty($dir)) {
$this->filesystem->removeDirectory($dir);
@ -271,7 +287,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
public function install(PackageInterface $package, $path, $output = true)
{
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
$this->io->writeError(" - " . InstallOperation::format($package));
}
$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}
*/
public function update(PackageInterface $initial, PackageInterface $target, $path)
{
$name = $target->getName();
$from = $initial->getFullPrettyVersion();
$to = $target->getFullPrettyVersion();
$this->io->writeError(" - " . UpdateOperation::format($initial, $target) . ": ", false);
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$promise = $this->remove($initial, $path, false);
if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve();
}
$self = $this;
$io = $this->io;
$this->remove($initial, $path, false);
$this->install($target, $path, false);
return $promise->then(function () use ($self, $target, $path, $io) {
$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)
{
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)) {
throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
@ -380,9 +423,14 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$output = '';
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();
$res = $this->install($package, $targetDir.'_compare', false);
$this->install($package, $targetDir.'_compare', false);
$this->process->wait();
$comparer = new Comparer();
$comparer->setSource($targetDir.'_compare');

@ -61,7 +61,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
GitUtil::cleanEnv();
$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
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)
{
$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)) {
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)
{
$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());
}

@ -29,15 +29,6 @@ use Composer\Util\Filesystem;
*/
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)
{
$filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME);

@ -27,6 +27,9 @@ use Composer\Util\Filesystem;
use Composer\EventDispatcher\EventDispatcher;
use Symfony\Component\Filesystem\Exception\IOException;
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.
@ -39,15 +42,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
const STRATEGY_SYMLINK = 10;
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}
*/
@ -91,11 +85,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
if (realpath($path) === $realUrl) {
if ($output) {
$this->io->writeError(sprintf(
' - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
$package->getName(),
$package->getFullPrettyVersion()
));
$this->io->writeError(" - " . InstallOperation::format($package).': Source already present');
} else {
$this->io->writeError('Source already present', false);
}
@ -133,11 +123,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$this->filesystem->removeDirectory($path);
if ($output) {
$this->io->writeError(sprintf(
' - Installing <info>%s</info> (<comment>%s</comment>): ',
$package->getName(),
$package->getFullPrettyVersion()
), false);
$this->io->writeError(" - " . InstallOperation::format($package).': ', false);
}
$isFallback = false;
@ -196,7 +182,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
if ($path === $realUrl) {
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;
@ -209,7 +195,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
*/
if (Platform::isWindows() && $this->filesystem->isJunction($path)) {
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)) {
$this->io->writeError(" <warning>Could not remove junction at " . $path . " - is another process locking it?</warning>");

@ -33,15 +33,6 @@ use RarArchive;
*/
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)
{
$processError = null;

@ -21,6 +21,9 @@ use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
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>
@ -120,7 +123,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
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());
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');
}
$name = $target->getName();
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);
$this->io->writeError(" - " . UpdateOperation::format($initial, $target).': ', false);
$urls = $this->prepareUrls($target->getSourceUrls());
@ -227,7 +214,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
*/
public function remove(PackageInterface $package, $path)
{
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
$this->io->writeError(" - " . UninstallOperation::format($package));
if (!$this->filesystem->removeDirectory($path)) {
throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
}

@ -29,16 +29,6 @@ use Composer\Util\Filesystem;
*/
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)
{
$command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);

@ -34,17 +34,9 @@ class ZipDownloader extends ArchiveDownloader
private static $hasZipArchive;
private static $isWindows;
/** @var ProcessExecutor */
protected $process;
/** @var ZipArchive|null */
private $zipArchiveObject;
public function __construct(IOInterface $io, Config $config, 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}
*/
@ -86,9 +78,8 @@ class ZipDownloader extends ArchiveDownloader
* @param string $file File to extract
* @param string $path Path where to extract file
* @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) {
// Force Exception throwing if the Other alternative is not available
@ -98,18 +89,56 @@ class ZipDownloader extends ArchiveDownloader
if (!self::$hasSystemUnzip && !$isLastChance) {
// This was call as the favorite extract way, but is not available
// We switch to the alternative
return $this->extractWithZipArchive($file, $path, true);
return $this->extractWithZipArchive($package, $file, $path, true);
}
$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);
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;
try {
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());
@ -121,11 +150,11 @@ class ZipDownloader extends ArchiveDownloader
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(' 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 $path Path where to extract file
* @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) {
// Force Exception throwing if the Other alternative is not available
@ -146,7 +177,7 @@ class ZipDownloader extends ArchiveDownloader
if (!self::$hasZipArchive && !$isLastChance) {
// This was call as the favorite extract way, but is not available
// We switch to the alternative
return $this->extractWithSystemUnzip($file, $path, true);
return $this->extractWithSystemUnzip($package, $file, $path, true);
}
$processError = null;
@ -159,7 +190,7 @@ class ZipDownloader extends ArchiveDownloader
if (true === $extractResult) {
$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"));
@ -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);
} catch (\Exception $e) {
$processError = $e;
} catch (\Throwable $e) {
$processError = $e;
}
if ($isLastChance) {
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');
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
if (self::$isWindows) {
$this->extractWithZipArchive($file, $path, false);
} else {
$this->extractWithSystemUnzip($file, $path, false);
return $this->extractWithZipArchive($package, $file, $path, false);
}
return $this->extractWithSystemUnzip($package, $file, $path, false, true);
}
/**

@ -336,7 +336,7 @@ class Factory
$httpDownloader = self::createHttpDownloader($io, $config);
$process = new ProcessExecutor($io);
$loop = new Loop($httpDownloader);
$loop = new Loop($httpDownloader, $process);
$composer->setLoop($loop);
// initialize event dispatcher
@ -356,7 +356,7 @@ class Factory
// load package
$parser = new VersionParser;
$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);
$composer->setPackage($package);
@ -495,11 +495,11 @@ class Factory
$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('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('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('file', new Downloader\FileDownloader($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, $process));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process));
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 mixed $config either a configuration array or a filename to read from, if null it will read from

@ -14,6 +14,7 @@ namespace Composer\IO;
use Composer\Question\StrictConfirmationQuestion;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
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}
*/

@ -685,7 +685,7 @@ class Installer
}
if ($this->executeOperations) {
$this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode);
$this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts);
} else {
foreach ($localRepoTransaction->getOperations() as $operation) {
// output op, but alias op only in debug verbosity

@ -13,6 +13,7 @@
namespace Composer\Installer;
use Composer\IO\IOInterface;
use Composer\IO\ConsoleIO;
use Composer\Package\PackageInterface;
use Composer\Package\AliasPackage;
use Composer\Repository\RepositoryInterface;
@ -49,6 +50,8 @@ class InstallationManager
private $io;
/** @var EventDispatcher */
private $eventDispatcher;
/** @var bool */
private $outputProgress;
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 OperationInterface[] $operations operations to execute
* @param bool $devMode whether the install is being run in dev mode
* @param bool $operation whether to dispatch script events
* @param bool $runScripts whether to dispatch script events
*/
public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true)
{
@ -184,6 +187,8 @@ class InstallationManager
$runCleanup = function () use (&$cleanupPromises, $loop) {
$promises = array();
$loop->abortJobs();
foreach ($cleanupPromises as $cleanup) {
$promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) {
$promise = $cleanup();
@ -266,69 +271,44 @@ class InstallationManager
// execute all downloads first
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) {
$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));
if (in_array($operation->getOperationType(), array('update', 'install'), true)) {
$package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage();
if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') {
if ($batch) {
$batches[] = $batch;
}
unset($operations[$index]);
$batches[] = array($index => $operation);
$batch = array();
continue;
}
$this->$opType($repo, $operation);
continue;
}
unset($operations[$index]);
$batch[$index] = $operation;
}
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;
$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;
if ($batch) {
$batches[] = $batch;
}
// execute all prepare => installs/updates/removes => cleanup steps
if (!empty($promises)) {
$this->loop->wait($promises);
foreach ($batches as $batch) {
$this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts);
}
} catch (\Exception $e) {
$runCleanup();
@ -356,6 +336,77 @@ class InstallationManager
$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.
*
@ -454,6 +505,11 @@ class InstallationManager
return $installer->getInstallPath($package);
}
public function setOutputProgress($outputProgress)
{
$this->outputProgress = $outputProgress;
}
public function notifyInstalls(IOInterface $io)
{
foreach ($this->notifiablePackages as $repoUrl => $packages) {

@ -16,6 +16,9 @@ use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\IO\IOInterface;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
/**
* Metapackage installation manager.
@ -76,7 +79,7 @@ class MetapackageInstaller implements InstallerInterface
*/
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);
}
@ -90,11 +93,7 @@ class MetapackageInstaller implements InstallerInterface
throw new \InvalidArgumentException('Package is not installed: '.$initial);
}
$name = $target->getName();
$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>)");
$this->io->writeError(" - " . UpdateOperation::format($initial, $target));
$repo->removePackage($initial);
$repo->addPackage(clone $target);
@ -109,7 +108,7 @@ class MetapackageInstaller implements InstallerInterface
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);
}

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

@ -233,7 +233,7 @@ abstract class BasePackage implements PackageInterface
}
// 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);
}

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

@ -124,6 +124,10 @@ class ArrayLoader implements LoaderInterface
$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']['type']) || !isset($config['source']['url']) || !isset($config['source']['reference'])) {
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;
}
}

@ -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);
if ($realPackage instanceof AliasPackage) {
$realPackage = $package->getAliasOf();

@ -432,7 +432,7 @@ class Locker
case 'git':
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'));
}
break;

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

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

@ -20,6 +20,7 @@ use Composer\Util\Git as GitUtil;
use Composer\Util\HttpDownloader;
use Composer\Util\ProcessExecutor;
use Composer\Util\Svn as SvnUtil;
use Composer\Util\Platform;
use Composer\Package\Version\VersionParser;
@ -110,6 +111,43 @@ class VersionGuesser
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)
{
GitUtil::cleanEnv();
@ -154,6 +192,7 @@ class VersionGuesser
if ($isFeatureBranch) {
$featureVersion = $version;
$featurePrettyVersion = $prettyVersion;
// 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);
$version = $result['version'];
@ -172,7 +211,7 @@ class VersionGuesser
}
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)) {
$commit = trim($output) ?: null;
}
@ -248,14 +287,16 @@ class VersionGuesser
$nonFeatureBranches = implode('|', $packageConfig['non-feature-branches']);
}
foreach ($branches as $candidate) {
// return directly, if branch is configured to be non-feature branch
if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) {
break;
}
// return directly, if branch is configured to be non-feature branch
if (preg_match('{^(' . $nonFeatureBranches . ')$}', $branch)) {
return array('version' => $version, 'pretty_version' => $prettyVersion);
}
$defaultBranch = $this->getDefaultBranchName($path);
foreach ($branches as $candidate) {
// 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;
}

@ -32,18 +32,32 @@ class PreFileDownloadEvent extends Event
*/
private $processedUrl;
/**
* @var string
*/
private $type;
/**
* @var mixed
*/
private $context;
/**
* Constructor.
*
* @param string $name The event name
* @param HttpDownloader $httpDownloader
* @param string $processedUrl
* @param string $name The event name
* @param HttpDownloader $httpDownloader
* @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);
$this->httpDownloader = $httpDownloader;
$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
*/
@ -63,4 +77,36 @@ class PreFileDownloadEvent extends Event
{
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;
}
}

@ -23,6 +23,7 @@ use Composer\Config;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Semver\CompilingMatcher;
use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
use Composer\Plugin\PluginEvents;
@ -764,7 +765,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
continue;
}
if ($constraint && !$constraint->matches(new Constraint('==', $version))) {
if ($constraint && !CompilingMatcher::match($constraint, Constraint::OP_EQ, $version)) {
continue;
}
@ -1013,8 +1014,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
while ($retries--) {
try {
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);
$filename = $preFileDownloadEvent->getProcessedUrl();
}
$response = $this->httpDownloader->get($filename, $this->options);
@ -1099,8 +1101,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
while ($retries--) {
try {
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);
$filename = $preFileDownloadEvent->getProcessedUrl();
}
$options = $this->options;
@ -1165,8 +1168,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$httpDownloader = $this->httpDownloader;
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);
$filename = $preFileDownloadEvent->getProcessedUrl();
}
$options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array();

@ -22,6 +22,7 @@ use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\Filesystem;
use Composer\Util\Url;
use Composer\Util\Git as GitUtil;
/**
* 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 = '';
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);
}

@ -19,6 +19,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface;
use Composer\IO\NullIO;
use Composer\Package\BasePackage;
use Composer\Package\AliasPackage;
use Composer\Package\Version\VersionParser;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
@ -44,7 +45,7 @@ class RepositorySet
/**
* @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;
@ -91,7 +92,7 @@ class RepositorySet
*/
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->acceptableStabilities = array();
@ -249,8 +250,22 @@ class RepositorySet
$packages = array();
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);
}
@ -270,4 +285,18 @@ class RepositorySet
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;
}
}

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

@ -167,8 +167,10 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$this->loader = new ArrayLoader($this->versionParser);
}
$hasRootIdentifierComposerJson = false;
try {
if ($driver->hasComposerFile($driver->getRootIdentifier())) {
$hasRootIdentifierComposerJson = $driver->hasComposerFile($driver->getRootIdentifier());
if ($hasRootIdentifierComposerJson) {
$data = $driver->getComposerInformation($driver->getRootIdentifier());
$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_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
if ($data['version_normalized'] !== $parsedTag) {
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;
}
@ -269,6 +278,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
}
$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) {
$msg = 'Reading composer.json of <info>' . ($this->packageName ?: $this->url) . '</info> (<comment>' . $branch . '</comment>)';
if ($isVeryVerbose) {
@ -299,7 +313,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$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) {
$this->addPackage($cachedPackage);
@ -323,6 +337,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$data['version'] = $version;
$data['version_normalized'] = $parsedBranch;
unset($data['default-branch']);
if ($driver->getRootIdentifier() === $branch) {
$data['default-branch'] = true;
}
if ($isVeryVerbose) {
$this->io->writeError('Importing branch '.$branch.' ('.$data['version'].')');
}
@ -404,7 +423,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
return false;
}
private function getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose)
private function getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose, $isDefaultBranch = false)
{
if (!$this->versionCache) {
return;
@ -427,6 +446,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$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 ($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>');

@ -63,7 +63,11 @@ class Versions
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();
foreach ($versions[$channel ?: $this->getChannel()] as $version) {

@ -20,7 +20,7 @@ use Composer\IO\IOInterface;
*/
class Git
{
private static $version;
private static $version = false;
/** @var IOInterface */
protected $io;
@ -297,6 +297,16 @@ class Git
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)
{
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.
*/
public function getVersion()
public static function getVersion(ProcessExecutor $process)
{
if (isset(self::$version)) {
return self::$version;
}
if (0 !== $this->process->execute('git --version', $output)) {
return;
}
if (preg_match('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) {
return self::$version = $matches[1];
if (false === self::$version) {
self::$version = null;
if (!$process) {
$process = new ProcessExecutor;
}
if (0 === $process->execute('git --version', $output) && preg_match('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) {
self::$version = $matches[1];
}
}
return self::$version;
}
}

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

@ -31,6 +31,7 @@ class HttpDownloader
const STATUS_STARTED = 2;
const STATUS_COMPLETED = 3;
const STATUS_FAILED = 4;
const STATUS_ABORTED = 5;
private $io;
private $config;
@ -44,6 +45,7 @@ class HttpDownloader
private $rfs;
private $idGen = 0;
private $disabled;
private $allowAsync = false;
/**
* @param IOInterface $io The IO instance
@ -139,6 +141,10 @@ class HttpDownloader
'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
if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) {
$this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
@ -179,8 +185,20 @@ class HttpDownloader
$downloader = $this;
$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->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
$downloader->markJobDone();
$downloader->scheduleNextJob();
return $response;
}, function ($e) use (&$job, $downloader) {
@ -197,7 +214,6 @@ class HttpDownloader
$job['exception'] = $e;
$downloader->markJobDone();
$downloader->scheduleNextJob();
throw $e;
});
@ -239,9 +255,9 @@ class HttpDownloader
}
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 {
$this->curl->download($resolve, $reject, $origin, $url, $options);
$job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options);
}
}
@ -253,49 +269,58 @@ class HttpDownloader
$this->runningJobs--;
}
/**
* @private
*/
public function scheduleNextJob()
public function wait($index = null)
{
foreach ($this->jobs as $job) {
if ($job['status'] === self::STATUS_QUEUED) {
$this->startJob($job['id']);
if ($this->runningJobs >= $this->maxJobs) {
return;
}
while (true) {
if (!$this->countActiveJobs($index)) {
return;
}
usleep(1000);
}
}
public function wait($index = null, $progress = false)
/**
* @internal
*/
public function enableAsync()
{
while (true) {
if ($this->curl) {
$this->curl->tick();
}
$this->allowAsync = true;
}
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;
/**
* @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']);
}
}
}
usleep(1000);
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)

@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\Util\HttpDownloader;
use React\Promise\Promise;
use Symfony\Component\Console\Helper\ProgressBar;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -21,13 +22,22 @@ use React\Promise\Promise;
class Loop
{
private $httpDownloader;
private $processExecutor;
private $currentPromises;
public function __construct(HttpDownloader $httpDownloader)
public function __construct(HttpDownloader $httpDownloader = null, ProcessExecutor $processExecutor = null)
{
$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 */
$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) {
throw $uncaught;
}
}
public function abortJobs()
{
if ($this->currentPromises) {
foreach ($this->currentPromises as $promise) {
$promise->cancel();
}
}
}
}

@ -16,18 +16,32 @@ use Composer\IO\IOInterface;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ProcessUtils;
use Symfony\Component\Process\Exception\RuntimeException;
use React\Promise\Promise;
/**
* @author Robert Schönthal <seroscho@googlemail.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
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 $captureOutput;
protected $errorOutput;
protected $io;
private $jobs = array();
private $runningJobs = 0;
private $maxJobs = 10;
private $idGen = 0;
private $allowAsync = false;
public function __construct(IOInterface $io = null)
{
$this->io = $io;
@ -112,6 +126,192 @@ class ProcessExecutor
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)
{
$output = trim($output);

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

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

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

@ -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);
try {
$promise = $downloader->download($packageMock, $this->testDir.'/install-path');
$loop = new Loop($httpDownloader);
$promise = $downloader->download($packageMock, $this->testDir.'/install-path');
$loop->wait(array($promise));
$downloader->install($packageMock, $this->testDir.'/install-path');

@ -60,9 +60,6 @@ class ZipDownloaderTest extends TestCase
}
}
/**
* @group only
*/
public function testErrorMessages()
{
if (!class_exists('ZipArchive')) {
@ -92,8 +89,8 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false);
try {
$promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop = new Loop($this->httpDownloader);
$promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop->wait(array($promise));
$downloader->install($this->package, $path);
@ -125,7 +122,8 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(false));
$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')));
$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()
{
if (!class_exists('ZipArchive')) {
@ -174,45 +170,66 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(true));
$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
* @expectedExceptionMessage Failed to execute (1) unzip
* @expectedExceptionMessage Failed to extract : (1) unzip
*/
public function testSystemUnzipOnlyFailed()
{
if (!class_exists('ZipArchive')) {
$this->markTestSkipped('zip extension missing');
}
$this->setPrivateProperty('isWindows', false);
$this->setPrivateProperty('hasSystemUnzip', true);
$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->expects($this->at(0))
->method('execute')
->will($this->returnValue(1));
->method('executeAsync')
->will($this->returnValue(\React\Promise\resolve($procMock)));
$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()
{
if (!class_exists('ZipArchive')) {
$this->markTestSkipped('zip extension missing');
}
$this->setPrivateProperty('isWindows', false);
$this->setPrivateProperty('hasSystemUnzip', true);
$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->expects($this->at(0))
->method('execute')
->will($this->returnValue(0));
->method('executeAsync')
->will($this->returnValue(\React\Promise\resolve($procMock)));
$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()
@ -225,10 +242,21 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', 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->expects($this->at(0))
->method('execute')
->will($this->returnValue(1));
->method('executeAsync')
->will($this->returnValue(\React\Promise\resolve($procMock)));
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$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);
$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('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->expects($this->at(0))
->method('execute')
->will($this->returnValue(1));
->method('executeAsync')
->will($this->returnValue(\React\Promise\resolve($procMock)));
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$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);
$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()
@ -300,7 +341,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$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);
$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)
{
parent::extract($package, $file, $path);
return parent::extract($package, $file, $path);
}
}

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

@ -6,7 +6,7 @@ Test the error output of solver problems with dev-master aliases.
{
"type": "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--
{
"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" }
],
"packages-dev": [],

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

@ -13,20 +13,24 @@ Aliases take precedence over default package even if default is selected
{
"name": "a/req", "version": "dev-master",
"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",
"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",
"require": { "a/req": "dev-master" }
"require": { "a/req": "dev-master" },
"default-branch": true
},
{
"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",
"require": { "a/req": "dev-master" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"name": "a/b", "version": "dev-master",
"require": { "a/req": "dev-master" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"name": "a/req", "version": "dev-feature-foo",

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

@ -2,8 +2,8 @@
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
that are also a root package, when that root package is also explicitly whitelisted.
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 allowed.
--COMPOSER--
{

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

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

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

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

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

@ -1,5 +1,5 @@
--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--
{
"repositories": [

@ -7,7 +7,7 @@ Test that a conflict against >=5 does not include dev-master or other dev-x
"type": "package",
"package": [
{ "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" }
]
}

@ -1,13 +1,13 @@
--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--
{
"repositories": [
{
"type": "package",
"package": [
{ "name": "whitelisted/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.1.0", "require": { "dependency/pkg": "1.1.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.0.0" },
{ "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": {
"whitelisted/pkg": "1.*",
"allowed/pkg": "1.*",
"fixed/dependency": "1.*"
}
}
--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": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } },
{ "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--
{
"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": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } },
{ "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": []
}
--RUN--
update whitelisted/pkg dependency/pkg
update allowed/pkg dependency/pkg
--EXPECT--
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)

@ -1,5 +1,5 @@
--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--
{
"repositories": [
@ -8,10 +8,10 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
"package": [
{ "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/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-component1", "version": "1.1.0" },
{ "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "allowed/pkg-component2", "version": "1.1.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.0.0" },
{ "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": {
"fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*",
"allowed/pkg-component1": "1.*",
"allowed/pkg-component2": "1.*",
"dependency/pkg": "1.*",
"unrelated/pkg": "1.*"
}
@ -32,8 +32,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
--INSTALLED--
[
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "allowed/pkg-component1", "version": "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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": [
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "allowed/pkg-component1", "version": "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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": []
}
--RUN--
update whitelisted/pkg-* --with-all-dependencies
update allowed/pkg-* --with-all-dependencies
--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 whitelisted/pkg-component2 (1.0.0 => 1.1.0)
Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)

@ -1,5 +1,5 @@
--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--
{
"repositories": [
@ -8,10 +8,10 @@ Update with a package whitelist only updates those packages and their dependenci
"package": [
{ "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/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-component1", "version": "1.1.0" },
{ "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "allowed/pkg-component2", "version": "1.1.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.0.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": {
"fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*",
"allowed/pkg-component1": "1.*",
"allowed/pkg-component2": "1.*",
"root/pkg-dependency": "1.*",
"unrelated/pkg": "1.*"
}
@ -34,8 +34,8 @@ Update with a package whitelist only updates those packages and their dependenci
--INSTALLED--
[
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "allowed/pkg-component1", "version": "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": "dependency/pkg", "version": "1.0.0" },
{ "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": [
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "allowed/pkg-component1", "version": "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": "dependency/pkg", "version": "1.0.0" },
{ "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
}
--RUN--
update whitelisted/pkg-* --with-dependencies
update allowed/pkg-* --with-dependencies
--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 whitelisted/pkg-component2 (1.0.0 => 1.1.0)
Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)

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

@ -1,5 +1,5 @@
--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--
{
"repositories": [
@ -8,10 +8,10 @@ Update with a package whitelist only updates those packages matching the pattern
"package": [
{ "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.1.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/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-component1", "version": "1.1.0" },
{ "name": "allowed/pkg-component1", "version": "1.0.0" },
{ "name": "allowed/pkg-component2", "version": "1.1.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.0.0" },
{ "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": {
"fixed/pkg": "1.*",
"whitelisted/pkg-component1": "1.*",
"whitelisted/pkg-component2": "1.*",
"allowed/pkg-component1": "1.*",
"allowed/pkg-component2": "1.*",
"unrelated/pkg": "1.*"
}
}
--INSTALLED--
[
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "allowed/pkg-component1", "version": "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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": [
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component1", "version": "1.0.0" },
{ "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } },
{ "name": "allowed/pkg-component1", "version": "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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
}
--RUN--
update whitelisted/pkg-*
update allowed/pkg-*
--EXPECT--
Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0)
Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0)
Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0)
Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0)

@ -1,5 +1,5 @@
--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--
{
"repositories": [

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

@ -1,5 +1,5 @@
--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--
{
"repositories": [
@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages and their dependenci
"package": [
{ "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/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.1.0", "require": { "dependency/pkg": "1.1.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.0.0" },
{ "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": {
"fixed/pkg": "1.*",
"whitelisted/pkg": "1.*",
"allowed/pkg": "1.*",
"unrelated/pkg": "1.*"
}
}
--INSTALLED--
[
{ "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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": [
{ "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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
}
--RUN--
update whitelisted/pkg --with-dependencies
update allowed/pkg --with-dependencies
--EXPECT--
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)

@ -1,5 +1,5 @@
--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--
{
"repositories": [
@ -8,8 +8,8 @@ Update with a package whitelist only updates whitelisted packages if no dependen
"package": [
{ "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/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.1.0", "require": { "dependency/pkg": "1.1.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.0.0" },
{ "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": {
"fixed/pkg": "1.*",
"whitelisted/pkg": "1.*",
"allowed/pkg": "1.*",
"unrelated/pkg": "1.*"
}
}
--INSTALLED--
[
{ "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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": [
{ "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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
}
--RUN--
update whitelisted/pkg
update allowed/pkg
--EXPECT--

@ -1,5 +1,5 @@
--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--
{
"repositories": [
@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages listed as command ar
"package": [
{ "name": "fixed/pkg", "version": "1.1.0" },
{ "name": "fixed/pkg", "version": "1.0.0" },
{ "name": "whitelisted/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.1.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.0.0" },
{ "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": {
"fixed/pkg": "1.*",
"whitelisted/pkg": "1.*",
"allowed/pkg": "1.*",
"unrelated/pkg": "1.*"
}
}
--INSTALLED--
[
{ "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
{ "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": [
{ "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": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "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": []
}
--RUN--
update whitelisted/pkg
update allowed/pkg
--EXPECT--
Upgrading whitelisted/pkg (1.0.0 => 1.1.0)
Upgrading allowed/pkg (1.0.0 => 1.1.0)

@ -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
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
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
--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",
"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",
@ -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",
"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",
"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",
"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" },
"transport-options": { "foo": "bar2" }
"transport-options": { "foo": "bar2" },
"default-branch": true
},
{
"name": "g/g", "version": "dev-master",
"source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" },
"dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/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",
"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",
@ -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",
"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",
"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" },
"transport-options": { "foo": "bar" }
"transport-options": { "foo": "bar" },
"default-branch": true
},
{
"name": "g/g", "version": "dev-master",
"source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
"dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
"transport-options": { "foo": "bar" }
"transport-options": { "foo": "bar" },
"default-branch": true
}
]
--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",
"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" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"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",
"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" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"name": "f/f", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"type": "library",
"transport-options": { "foo": "bar" }
"transport-options": { "foo": "bar" },
"default-branch": true
},
{
"name": "g/g", "version": "dev-master",
"source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
"dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
"type": "library",
"transport-options": { "foo": "bar" }
"transport-options": { "foo": "bar" },
"default-branch": true
}
],
"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",
"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" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"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",
"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" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"name": "e/e", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
"type": "library"
"type": "library",
"default-branch": true
},
{
"name": "f/f", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
"type": "library",
"transport-options": { "foo": "bar" }
"transport-options": { "foo": "bar" },
"default-branch": true
},
{
"name": "g/g", "version": "dev-master",
"source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" },
"dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
"type": "library"
"type": "library",
"default-branch": true
}
],
"packages-dev": [],

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

@ -8,7 +8,8 @@ Downgrading from unstable to more stable package should work even if already ins
"package": [
{
"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",
@ -17,7 +18,8 @@ Downgrading from unstable to more stable package should work even if already ins
},
{
"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",
@ -36,11 +38,13 @@ Downgrading from unstable to more stable package should work even if already ins
[
{
"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",
"source": { "reference": "abcd", "url": "", "type": "git" }
"source": { "reference": "abcd", "url": "", "type": "git" },
"default-branch": true
}
]
--RUN--

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

@ -2,7 +2,7 @@
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--
{

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

@ -14,7 +14,10 @@ namespace Composer\Test;
use Composer\DependencyResolver\Request;
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\Json\JsonFile;
use Composer\Package\Dumper\ArrayDumper;
@ -263,7 +266,12 @@ class InstallerTest extends TestCase
$installer = Installer::create($io, $composer);
$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);
$installer
@ -273,8 +281,21 @@ class InstallerTest extends TestCase
return $installer->run();
});
$application->get('update')->setCode(function ($input, $output) use ($installer) {
$application->add($install);
$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');
$filteredPackages = array_filter($packages, function ($package) {
return !in_array($package, array('lock', 'nothing', 'mirrors'), true);
@ -305,6 +326,7 @@ class InstallerTest extends TestCase
return $installer->run();
});
$application->add($update);
if (!preg_match('{^(install|update)\b}', $run)) {
throw new \UnexpectedValueException('The run command only supports install and update');

@ -17,6 +17,8 @@ use Composer\Config;
use Composer\Factory;
use Composer\Repository\RepositoryManager;
use Composer\Repository\WritableRepositoryInterface;
use Composer\Package\Version\VersionGuesser;
use Composer\Package\Version\VersionParser;
use Composer\Package\RootPackageInterface;
use Composer\Installer;
use Composer\EventDispatcher\EventDispatcher;
@ -39,6 +41,11 @@ class FactoryMock extends Factory
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)
{
}

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

@ -81,7 +81,7 @@ class BasePackageTest extends TestCase
$createPackage = function ($arr) use ($self) {
$package = $self->getMockForAbstractClass('\Composer\Package\BasePackage', array(), '', false);
$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->any())->method('getSourceReference')->will($self->returnValue($arr['sourceReference']));

@ -105,6 +105,8 @@ class RootPackageLoaderTest extends TestCase
'pretty_version' => '3.0-dev',
'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());
@ -113,6 +115,28 @@ class RootPackageLoaderTest extends TestCase
$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()
{
if (!function_exists('proc_open')) {
@ -147,6 +171,17 @@ class RootPackageLoaderTest extends TestCase
$executor
->expects($this->at(1))
->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) {
$self->assertEquals('git rev-list master..latest-production', $command);
$output = "";

@ -16,6 +16,8 @@ use Composer\Config;
use Composer\Package\Version\VersionGuesser;
use Composer\Semver\VersionParser;
use Composer\Test\TestCase;
use Composer\Util\Git as GitUtil;
use Composer\Util\ProcessExecutor;
class VersionGuesserTest extends TestCase
{
@ -30,7 +32,7 @@ class VersionGuesserTest extends TestCase
{
$branch = 'default';
$executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
$executor = $this->getMockBuilder('Composer\\Util\\ProcessExecutor')
->setMethods(array('execute'))
->disableArgumentCloning()
->disableOriginalConstructor()
@ -40,6 +42,8 @@ class VersionGuesserTest extends TestCase
$self = $this;
$step = 0;
GitUtil::getVersion(new ProcessExecutor);
$executor
->expects($this->at($step))
->method('execute')
@ -65,8 +69,8 @@ class VersionGuesserTest extends TestCase
$executor
->expects($this->at($step))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git log --pretty="%H" -n1 HEAD', $command);
->willReturnCallback(function ($command, &$output) use ($self, $executor) {
$self->assertEquals('git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($executor), $command);
return 128;
})
@ -131,6 +135,64 @@ class VersionGuesserTest extends TestCase
$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()
{
$commitHash = '03a15d220da53c52eddd5f32ffca64a7b3801bea';
@ -159,6 +221,17 @@ class VersionGuesserTest extends TestCase
$executor
->expects($this->at(1))
->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) {
$self->assertEquals('git rev-list arbitrary..current', $command);
$output = "$anotherCommitHash\n";
@ -202,10 +275,19 @@ class VersionGuesserTest extends TestCase
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: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output, $path) use ($self, $anotherCommitHash) {
$self->assertEquals('git rev-list latest-testing..current', $command);
$output = "$anotherCommitHash\n";
@ -374,10 +456,19 @@ class VersionGuesserTest extends TestCase
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: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git describe --exact-match --tags', $command);
$output = "v2.0.5-alpha2";
@ -415,10 +506,19 @@ class VersionGuesserTest extends TestCase
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: foo\r\n";
return 0;
})
;
$executor
->expects($this->at(2))
->method('execute')
->willReturnCallback(function ($command, &$output) use ($self) {
$self->assertEquals('git describe --exact-match --tags', $command);
$output = '1.0.0';

@ -189,16 +189,19 @@ class ComposerRepositoryTest extends TestCase
->getMock();
$httpDownloader->expects($this->at(0))
->method('enableAsync');
$httpDownloader->expects($this->at(1))
->method('get')
->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%'))));
$httpDownloader->expects($this->at(1))
$httpDownloader->expects($this->at(2))
->method('get')
->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)));
$httpDownloader->expects($this->at(2))
$httpDownloader->expects($this->at(3))
->method('get')
->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())));
@ -291,6 +294,9 @@ class ComposerRepositoryTest extends TestCase
->getMock();
$httpDownloader->expects($this->at(0))
->method('enableAsync');
$httpDownloader->expects($this->at(1))
->method('get')
->with($url = 'http://example.org/packages.json')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array(

@ -12,32 +12,25 @@
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\Test\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use ReflectionMethod;
use ReflectionProperty;
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()
{
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io = $this->getIOInterfaceMock();
$io
->expects($this->once())
->method('hasAuthentication')
->will($this->returnValue(false))
->willReturn(false)
;
$res = $this->callGetOptionsForUrl($io, array('http://example.org', array()));
@ -46,16 +39,16 @@ class RemoteFilesystemTest extends TestCase
public function testGetOptionsForUrlWithAuthorization()
{
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io = $this->getIOInterfaceMock();
$io
->expects($this->once())
->method('hasAuthentication')
->will($this->returnValue(true))
->willReturn(true)
;
$io
->expects($this->once())
->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()));
@ -71,17 +64,17 @@ class RemoteFilesystemTest extends TestCase
public function testGetOptionsForUrlWithStreamOptions()
{
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io = $this->getIOInterfaceMock();
$io
->expects($this->once())
->method('hasAuthentication')
->will($this->returnValue(true))
->willReturn(true)
;
$io
->expects($this->once())
->method('getAuthentication')
->will($this->returnValue(array('username' => null, 'password' => null)))
->willReturn(array('username' => null, 'password' => null))
;
$streamOptions = array('ssl' => array(
@ -94,17 +87,17 @@ class RemoteFilesystemTest extends TestCase
public function testGetOptionsForUrlWithCallOptionsKeepsHeader()
{
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io = $this->getIOInterfaceMock();
$io
->expects($this->once())
->method('hasAuthentication')
->will($this->returnValue(true))
->willReturn(true)
;
$io
->expects($this->once())
->method('getAuthentication')
->will($this->returnValue(array('username' => null, 'password' => null)))
->willReturn(array('username' => null, 'password' => null))
;
$streamOptions = array('http' => array(
@ -127,14 +120,14 @@ class RemoteFilesystemTest extends TestCase
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->assertAttributeEquals(20, 'bytesMax', $fs);
}
public function testCallbackGetNotifyProgress()
{
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io = $this->getIOInterfaceMock();
$io
->expects($this->once())
->method('overwriteError')
@ -150,21 +143,21 @@ class RemoteFilesystemTest extends TestCase
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));
}
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__));
}
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');
$this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file));
@ -173,17 +166,96 @@ class RemoteFilesystemTest extends TestCase
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
*/
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');
$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']['SNI_enabled']);
$this->assertEquals(7, $res['ssl']['verify_depth']);
@ -220,6 +292,7 @@ class RemoteFilesystemTest extends TestCase
*/
public function testBitBucketPublicDownload($url, $contents)
{
/** @var ConsoleIO $io */
$io = $this
->getMockBuilder('Composer\IO\ConsoleIO')
->disableOriginalConstructor()
@ -242,6 +315,7 @@ class RemoteFilesystemTest extends TestCase
*/
public function testBitBucketPublicDownloadWithAuthConfigured($url, $contents)
{
/** @var MockObject|ConsoleIO $io */
$io = $this
->getMockBuilder('Composer\IO\ConsoleIO')
->disableOriginalConstructor()
@ -249,13 +323,12 @@ class RemoteFilesystemTest extends TestCase
$domains = array();
$io
->expects($this->any())
->method('hasAuthentication')
->will($this->returnCallback(function ($arg) use (&$domains) {
->willReturnCallback(function ($arg) use (&$domains) {
$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
return $arg === 'bitbucket.org';
}));
});
$io
->expects($this->at(1))
->method('getAuthentication')
@ -275,11 +348,11 @@ class RemoteFilesystemTest extends TestCase
$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);
$ref = new \ReflectionMethod($fs, 'getOptionsForUrl');
$prop = new \ReflectionProperty($fs, 'fileUrl');
$ref = new ReflectionMethod($fs, 'getOptionsForUrl');
$prop = new ReflectionProperty($fs, 'fileUrl');
$ref->setAccessible(true);
$prop->setAccessible(true);
@ -288,17 +361,80 @@ class RemoteFilesystemTest extends TestCase
return $ref->invokeArgs($fs, $args);
}
protected function callCallbackGet(RemoteFilesystem $fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax)
/**
* @return MockObject|Config
*/
private function getConfigMock()
{
$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 = new ReflectionMethod($fs, 'callbackGet');
$ref->setAccessible(true);
$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->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();
}
}

Loading…
Cancel
Save