diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 30c035bd4..f09ed0fe2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -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 }}" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 87cd149eb..d2894e09b 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -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 diff --git a/doc/03-cli.md b/doc/03-cli.md index 374ef952e..edfaaa9ed 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -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 diff --git a/doc/articles/http-basic-authentication.md b/doc/articles/http-basic-authentication.md index 7284e1c03..fad17d28f 100644 --- a/doc/articles/http-basic-authentication.md +++ b/doc/articles/http-basic-authentication.md @@ -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. diff --git a/phpstan/config.neon b/phpstan/config.neon index d9bf7f8c6..06d2ad838 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -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 diff --git a/res/composer-schema.json b/res/composer-schema.json index 165ffcf85..04db6d3a3 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -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." diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index b74e4bd9b..07cb9aaa8 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -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(''.$e->getMessage().''); } diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index b86a4e9ce..ae6371fa4 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -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; } diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index fd6d454f5..b521e4927 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -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) { diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index de6701ce4..27d9919d7 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -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); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 80e6f8423..c0b82ada7 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -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 'You are not running the latest '.$versionsUtil->getChannel().' version, run `composer self-update` to update ('.Composer::VERSION.' => '.$latest['version'].')'; diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 93523cba7..f8294c47b 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -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')) diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index 85cb64a7f..c9a099f26 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -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 @@ -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)); } diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index e540056f8..53c040412 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -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'); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index d8ba23458..9525b9e77 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -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); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 9576b141f..809db532c 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -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(); diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index af735bcce..4160a69ac 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -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') ?: '/'); diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index 9ac6beabe..822d0ab69 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -61,7 +61,12 @@ class InstallOperation extends SolverOperation */ public function show($lock) { - return ($lock ? 'Locking ' : 'Installing ').''.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + return self::format($this->package, $lock); + } + + public static function format(PackageInterface $package, $lock = false) + { + return ($lock ? 'Locking ' : 'Installing ').''.$package->getPrettyName().' ('.$package->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index 4618e0722..13c0d4831 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -61,7 +61,12 @@ class UninstallOperation extends SolverOperation */ public function show($lock) { - return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + return self::format($this->package, $lock); + } + + public static function format(PackageInterface $package, $lock = false) + { + return 'Removing '.$package->getPrettyName().' ('.$package->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index 528efdd14..37fd78892 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -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.' '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; + return $actionName.' '.$initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; } /** diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 26ca947d3..b46447f72 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -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; } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 91381bee2..2db4e723b 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -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 $stabilityFlags * @param array[] $rootAliases - * @psalm-param list $rootAliases + * @psalm-param array> $rootAliases * @param string[] $rootReferences an array of package name => source reference * @psalm-param array $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; - } } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 8ba9756d6..4cb36258c 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -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)'; } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 283d0103e..7cf19deee 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): 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); - } } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index d7c4bcabe..8fbb48e9c 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $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 . " " . $name . " (" . $from . " => " . $to . "): ", 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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $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'); diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 37e7f94da..177644735 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -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()); } diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 0b12b4380..3ebff597a 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -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); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 51e9f7709..190267c5c 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -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 %s (%s): 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 %s (%s): ', - $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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . "), 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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); } if (!$this->filesystem->removeJunction($path)) { $this->io->writeError(" Could not remove junction at " . $path . " - is another process locking it?"); diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 0ca15de1f..b0484b661 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -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; diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index da078e76f..4b5f94f0c 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -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 @@ -120,7 +123,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): ", 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 . " " . $name . " (" . $from . " => " . $to . "): ", 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 " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package)); if (!$this->filesystem->removeDirectory($path)) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 34956d997..b043af333 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -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); diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index d0b4e8255..83b904a8a 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -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(' '.$processError->getMessage().''); + $io->writeError(' This most likely is due to a custom installer plugin not handling the returned Promise from the downloader'); + $io->writeError(' See https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb for an example fix'); + } else { + $io->writeError(' '.$processError->getMessage().''); + $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(' '.$processError->getMessage().''); $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(' '.$processError->getMessage().''); $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); } /** diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 04da08e31..8eb2d24cf 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -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 diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index 925a528be..ebe38f26a 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -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} */ diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8c508eaa0..40a041927 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -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 diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 06e8d65b9..7dfb2ff3e 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -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(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); - - 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(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); + + 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) { diff --git a/src/Composer/Installer/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index fbd983fa3..fb07e9bb1 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $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 . " " . $name . " (" . $from . " => " . $to . ")"); + $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 " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package)); $repo->removePackage($package); } diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 1f9d5061b..7d843c6ba 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -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(); diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index baf3a2292..09190f9e7 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -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); } diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index 46bcf639a..903997601 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -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) { diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index f50358776..b2330e6e2 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -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; } } diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index ac3d8f433..536d461ad 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -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(); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 8cc3d1eb8..6009082cd 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -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; diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 5e393a11c..29d700d13 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -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} */ diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 1c2ad2e06..bf94efc5c 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -371,6 +371,11 @@ interface PackageInterface */ public function getArchiveExcludes(); + /** + * @return bool + */ + public function isDefaultBranch(); + /** * Returns a list of options to download package dist files * diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 353ad00fb..24bd33b75 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -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; } diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index c2751da02..f7b523b94 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -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; + } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8848452f2..f7462c89f 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -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(); diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 27803455f..dc9659e5f 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -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); } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 24d935f4a..9417dec97 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -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 + * @psalm-var array> */ 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; + } } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 7b609a80c..137989cda 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -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']; } } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 1351c4694..f3d7fe56b 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -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('Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json'); + if (preg_match('{(^dev-|[.-]?dev$)}i', $parsedTag)) { + $this->io->writeError('Skipped tag '.$tag.', invalid tag name, tags can not use dev prefixes or suffixes'); + } else { + $this->io->writeError('Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json'); + } } 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 ' . ($this->packageName ?: $this->url) . ' (' . $branch . ')'; 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('Skipped cached version '.$version.', it conflicts with an another tag ('.$existingPackage->getPrettyVersion().') as both resolve to '.$cachedPackage['version_normalized'].' internally'); diff --git a/src/Composer/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index a03a75054..6d1379d41 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -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) { diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index a4b5a7336..8a934cf30 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -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; } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 365caf899..257a29115 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -23,6 +23,7 @@ use Composer\Util\HttpDownloader; use React\Promise\Promise; /** + * @internal * @author Jordi Boggiano * @author Nicolas Grekas */ @@ -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 (failed)", false); } if ($job['attributes']['storeAuth']) { diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 2fa8fa716..889fae07e 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -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) diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index dfaa2ac53..00159d562 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -14,6 +14,7 @@ namespace Composer\Util; use Composer\Util\HttpDownloader; use React\Promise\Promise; +use Symfony\Component\Console\Helper\ProgressBar; /** * @author Jordi Boggiano @@ -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(); + } + } + } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index a30a04d15..96b9235c8 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -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 + * @author Jordi Boggiano */ 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[^:/\s]+):(?P[^@\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); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index c7077afbb..4bac6a88f 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -20,6 +20,7 @@ use Composer\Util\HttpDownloader; use Composer\Util\Http\Response; /** + * @internal * @author FranƧois Pluchino * @author Jordi Boggiano * @author Nils Adermann @@ -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); } /** diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index c86ffa2f7..ba8f95db9 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -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); diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 4c1e37806..6618618e1 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -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)); diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index f770b0d35..6996d67f6 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -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'); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 4436c6ad7..dc0f55aec 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -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); } } diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test index 19d0ea287..7158057d1 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test @@ -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"} ] diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test index 2e3598ce2..d206764f2 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test @@ -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": [], diff --git a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test index 451e1f8b9..c37b6e5f3 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test +++ b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test @@ -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", diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index c13d5fc0e..b409a5cfc 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -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", diff --git a/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test new file mode 100644 index 000000000..28229eb87 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index b8968c35b..62bda3d7d 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -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-- { diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index dc722c379..39803f725 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -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-- { diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index 6fd1f7b53..c8070275c 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -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", diff --git a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test index 400e932ed..3c171a4b0 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -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": [], diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index b996ff65b..4160fac55 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/install-reference.test b/tests/Composer/Test/Fixtures/installer/install-reference.test index 74bf6e40a..33e7f5946 100644 --- a/tests/Composer/Test/Fixtures/installer/install-reference.test +++ b/tests/Composer/Test/Fixtures/installer/install-reference.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test index f45f6d528..614c87515 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test @@ -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": [ diff --git a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test index 6997e5a77..900028457 100644 --- a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test +++ b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test @@ -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" } ] } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test index 0f009ae6f..317f2396d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test index 95fd639f2..5c796181a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test index d40a924ab..d82138384 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test index 55a07b118..5cabcbb4a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test index 6cd1d7778..6ceec16b2 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test index 738f0af74..fde1c9eab 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test @@ -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": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test index 9360bc2f6..7f2299566 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test index 079ad9d2b..5b02b7d0d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test index 299c505cb..abdc48979 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test @@ -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-- diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list.test b/tests/Composer/Test/Fixtures/installer/update-allow-list.test index a02e00c4b..593375442 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 4831c7705..15600aefe 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -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": [], diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test index 181e039ea..7985ec7fa 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test @@ -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) diff --git a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test index b213e480d..7b0a87e59 100644 --- a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test +++ b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test @@ -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-- diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test index 89e94d781..1a2227cec 100644 --- a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test @@ -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": [], diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index a950b247a..e9fc88ef9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -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-- { diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index ec92ea607..d5613a648 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -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-- diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 94a56147e..0be8af711 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -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'); diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index a073f0ab7..d8b618647 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -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) { } diff --git a/tests/Composer/Test/Mock/VersionGuesserMock.php b/tests/Composer/Test/Mock/VersionGuesserMock.php new file mode 100644 index 000000000..bcfa1d19d --- /dev/null +++ b/tests/Composer/Test/Mock/VersionGuesserMock.php @@ -0,0 +1,33 @@ + + * Jordi Boggiano + * + * 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; + } + +} diff --git a/tests/Composer/Test/Package/BasePackageTest.php b/tests/Composer/Test/Package/BasePackageTest.php index 33d384d69..5f5f17cd1 100644 --- a/tests/Composer/Test/Package/BasePackageTest.php +++ b/tests/Composer/Test/Package/BasePackageTest.php @@ -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'])); diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index 93cde83f5..e5aebd1d0 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -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 = ""; diff --git a/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 9527b628f..0b7486afd 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -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'; diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 4fcbbb431..01e3be4ce 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -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( diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index fe4f213c6..361dd1669 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -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 '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(); + } }