From 95e6e16b78b62e104247d4850f60c92bc9b2076f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Wed, 29 Apr 2020 01:47:29 +0200 Subject: [PATCH 01/52] Use Semver compiled constraints --- src/Composer/DependencyResolver/Pool.php | 5 ++--- src/Composer/DependencyResolver/PoolBuilder.php | 10 +++++----- src/Composer/Repository/ComposerRepository.php | 3 ++- 3 files changed, 9 insertions(+), 9 deletions(-) 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 9e1f9f433..1e08e0ad7 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; /** * @author Nils Adermann @@ -209,7 +209,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; } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e960f63be..bb845aeca 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; } From fa799970ada7057b1b41c67dba182ee01d7b09ec Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 7 Jun 2020 22:15:09 +0100 Subject: [PATCH 02/52] Replace whitelist with allow list --- doc/01-basic-usage.md | 2 +- doc/03-cli.md | 4 +- src/Composer/Autoload/AutoloadGenerator.php | 22 ++--- src/Composer/Autoload/ClassMapGenerator.php | 10 +- src/Composer/Cache.php | 22 ++--- src/Composer/Command/InitCommand.php | 4 +- src/Composer/Command/RemoveCommand.php | 4 +- src/Composer/Command/RequireCommand.php | 6 +- src/Composer/Command/UpdateCommand.php | 10 +- src/Composer/DependencyResolver/Pool.php | 11 ++- .../DependencyResolver/RuleSetGenerator.php | 34 +++++-- src/Composer/Installer.php | 97 ++++++++++++++----- src/Composer/Package/BasePackage.php | 8 +- src/Composer/Repository/Vcs/GitHubDriver.php | 4 +- .../installer/github-issues-4795-2.test | 4 +- .../installer/github-issues-4795.test | 6 +- .../install-from-lock-removes-package.test | 12 +-- ...e-downgrades-non-whitelisted-unstable.test | 2 +- ...ce-from-lock-for-non-updated-packages.test | 2 +- .../partial-update-without-lock.test | 2 +- .../installer/update-changes-url.test | 4 +- .../update-whitelist-locked-require.test | 14 +-- ...telist-patterns-with-all-dependencies.test | 24 ++--- ...-whitelist-patterns-with-dependencies.test | 24 ++--- ...elist-patterns-with-root-dependencies.test | 44 ++++----- ...itelist-patterns-without-dependencies.test | 24 ++--- .../installer/update-whitelist-patterns.test | 2 +- .../update-whitelist-removes-unused.test | 14 +-- .../update-whitelist-with-dependencies.test | 14 +-- ...te-whitelist-with-dependency-conflict.test | 12 +-- .../Fixtures/installer/update-whitelist.test | 14 +-- .../update-with-all-dependencies.test | 2 +- tests/Composer/Test/InstallerTest.php | 6 +- 33 files changed, 269 insertions(+), 195 deletions(-) diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 8c634bcfd..ac8086491 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install or update one dependency, you can whitelist them: +If you only want to install or update one dependency, you can allow list them: ```sh php composer.phar update monolog/monolog [...] diff --git a/doc/03-cli.md b/doc/03-cli.md index 0c41e9ef7..833cf2c5c 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -157,8 +157,8 @@ php composer.phar update "vendor/*" * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. * **--no-suggest:** Skips suggested packages in the output. -* **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements. -* **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements. +* **--with-dependencies:** Add also dependencies of allowed packages to the allow list, except those that are root requirements. +* **--with-all-dependencies:** Add also all dependencies of allowed packages to the allow list, including those that are root requirements. * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 371f3ed76..863ceac43 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -229,16 +229,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) { @@ -261,7 +261,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); } } } @@ -336,9 +336,9 @@ EOF; return 0; } - 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; @@ -350,9 +350,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) @@ -456,15 +456,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 c0b011f3f..4adbcc8be 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()) { if (is_string($path)) { $basePath = $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/Cache.php b/src/Composer/Cache.php index 06c6a0996..069f59d5d 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -28,20 +28,20 @@ class Cache private $io; private $root; private $enabled = true; - private $whitelist; + private $allowList; private $filesystem; /** * @param IOInterface $io * @param string $cacheDir location of the cache - * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) + * @param string $allowList List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance */ - public function __construct(IOInterface $io, $cacheDir, $whitelist = 'a-z0-9.', Filesystem $filesystem = null) + public function __construct(IOInterface $io, $cacheDir, $allowList = 'a-z0-9.', Filesystem $filesystem = null) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; - $this->whitelist = $whitelist; + $this->allowList = $allowList; $this->filesystem = $filesystem ?: new Filesystem(); if (!self::isUsable($cacheDir)) { @@ -77,7 +77,7 @@ class Cache public function read($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); @@ -91,7 +91,7 @@ class Cache public function write($file, $contents) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); @@ -129,7 +129,7 @@ class Cache public function copyFrom($file, $source) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); if (!file_exists($source)) { @@ -150,7 +150,7 @@ class Cache public function copyTo($file, $target) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { try { touch($this->root . $file, filemtime($this->root . $file), time()); @@ -177,7 +177,7 @@ class Cache public function remove($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { return $this->filesystem->unlink($this->root . $file); } @@ -229,7 +229,7 @@ class Cache public function sha1($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { return sha1_file($this->root . $file); } @@ -241,7 +241,7 @@ class Cache public function sha256($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { return hash_file('sha256', $this->root . $file); } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index d234a8cba..59f0488d1 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -86,8 +86,8 @@ EOT { $io = $this->getIO(); - $whitelist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); - $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); + $allowList = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowList))); if (isset($options['author'])) { $options['authors'] = $this->formatAuthors($options['author']); diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index e4407d4cb..e6a2fbfb3 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -146,8 +146,8 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) + ->setUpdateAllowList($packages) + ->setAllowListTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) ; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 9b59e7feb..45bd315fe 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -237,9 +237,9 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist(array_keys($requirements)) - ->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies')) + ->setUpdatAllowList(array_keys($requirements)) + ->setAllowListTransitiveDependencies($input->getOption('update-with-dependencies')) + ->setAllowListAllDependencies($input->getOption('update-with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index e68c265c0..44f1e7dea 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -49,8 +49,8 @@ class UpdateCommand extends BaseCommand new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), - new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'), - new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'), + new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of allowed packages to the allow list, except those defined in root package.'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of allowed packages to the allow list, including those defined in root package.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), @@ -148,9 +148,9 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($input->getOption('lock') ? array('lock') : $packages) + ->setAllowListTransitiveDependencies($input->getOption('with-dependencies')) + ->setAllowListAllDependencies($input->getOption('with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 085aaa7bf..8021275b6 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -50,7 +50,7 @@ class Pool implements \Countable protected $versionParser; protected $providerCache = array(); protected $filterRequires; - protected $whitelist = null; + protected $whitelist = null; // TODO 2.0 rename to allowList protected $id = 1; public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) @@ -71,6 +71,15 @@ class Pool implements \Countable } } + public function setAllowList($allowList) + { + // call original method for BC + $this->setWhitelist($allowList); + } + + /** + * @deprecated use setAllowList instead + */ public function setWhitelist($whitelist) { $this->whitelist = $whitelist; diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index e8714a405..8638440bd 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -26,7 +26,7 @@ class RuleSetGenerator protected $rules; protected $jobs; protected $installedMap; - protected $whitelistedMap; + protected $allowListedMap; protected $addedMap; protected $conflictAddedMap; protected $addedPackages; @@ -147,6 +147,15 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } + protected function allowListFromPackage(PackageInterface $package) + { + // call original method for BC + $this->whitelistFromPackage($package); + } + + /** + * @deprecated use whitelistFromPackage instead + */ protected function whitelistFromPackage(PackageInterface $package) { $workQueue = new \SplQueue; @@ -154,11 +163,11 @@ class RuleSetGenerator while (!$workQueue->isEmpty()) { $package = $workQueue->dequeue(); - if (isset($this->whitelistedMap[$package->id])) { + if (isset($this->allowListedMap[$package->id])) { continue; } - $this->whitelistedMap[$package->id] = true; + $this->allowListedMap[$package->id] = true; foreach ($package->getRequires() as $link) { $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true); @@ -294,6 +303,15 @@ class RuleSetGenerator return $impossible; } + protected function allowListFromJobs() + { + // call original method for BC + $this->whitelistFromJobs(); + } + + /** + * @deprecated use allowListFromJobs instead + */ protected function whitelistFromJobs() { foreach ($this->jobs as $job) { @@ -301,7 +319,7 @@ class RuleSetGenerator case 'install': $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true); foreach ($packages as $package) { - $this->whitelistFromPackage($package); + $this->allowListFromPackage($package); } break; } @@ -348,13 +366,13 @@ class RuleSetGenerator $this->rules = new RuleSet; $this->installedMap = $installedMap; - $this->whitelistedMap = array(); + $this->allowListedMap = array(); foreach ($this->installedMap as $package) { - $this->whitelistFromPackage($package); + $this->allowListFromPackage($package); } - $this->whitelistFromJobs(); + $this->allowListFromJobs(); - $this->pool->setWhitelist($this->whitelistedMap); + $this->pool->setAllowList($this->allowListedMap); $this->addedMap = array(); $this->conflictAddedMap = array(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c5c0f7a21..c7af69427 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -127,9 +127,9 @@ class Installer * * @var array|null */ - protected $updateWhitelist = null; - protected $whitelistDependencies = false; // TODO 2.0 rename to whitelistTransitiveDependencies - protected $whitelistAllDependencies = false; + protected $updateWhitelist = null; // TODO 2.0 rename to updateAllowList + protected $whitelistDependencies = false; // TODO 2.0 rename to allowListTransitiveDependencies + protected $whitelistAllDependencies = false; // TODO 2.0 rename to allowListAllDependencies /** * @var SuggestedPackagesReporter @@ -360,7 +360,7 @@ class Installer $repositories = null; // initialize locked repo if we are installing from lock or in a partial update - // and a lock file is present as we need to force install non-whitelisted lock file + // and a lock file is present as we need to force install non-allowed lock file // packages in that case if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { try { @@ -375,7 +375,7 @@ class Installer } } - $this->whitelistUpdateDependencies( + $this->allowListUpdateDependencies( $lockedRepository ?: $localRepo, $this->package->getRequires(), $this->package->getDevRequires() @@ -1011,7 +1011,7 @@ class Installer } if ($this->update) { - // skip package if the whitelist is enabled and it is not in it + // skip package if the allow list is enabled and it is not in it if ($this->updateWhitelist && !$this->isUpdateable($package)) { // check if non-updateable packages are out of date compared to the lock file to ensure we don't corrupt it foreach ($currentPackages as $curPackage) { @@ -1280,11 +1280,11 @@ class Installer private function isUpdateable(PackageInterface $package) { if (!$this->updateWhitelist) { - throw new \LogicException('isUpdateable should only be called when a whitelist is present'); + throw new \LogicException('isUpdateable should only be called when an allow list is present'); } - foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $patternRegexp = BasePackage::packageNameToRegexp($whiteListedPattern); + foreach ($this->updateWhitelist as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); if (preg_match($patternRegexp, $package->getName())) { return true; } @@ -1310,11 +1310,11 @@ class Installer } /** - * Adds all dependencies of the update whitelist to the whitelist, too. + * Adds all dependencies of the update allow list to the allow list, too. * * Packages which are listed as requirements in the root package will be * skipped including their dependencies, unless they are listed in the - * update whitelist themselves or $whitelistAllDependencies is true. + * update allow list themselves or $whitelistAllDependencies is true. * * @param RepositoryInterface $localOrLockRepo Use the locked repo if available, otherwise installed repo will do * As we want the most accurate package list to work with, and installed @@ -1322,7 +1322,7 @@ class Installer * @param array $rootRequires An array of links to packages in require of the root package * @param array $rootDevRequires An array of links to packages in require-dev of the root package */ - private function whitelistUpdateDependencies($localOrLockRepo, array $rootRequires, array $rootDevRequires) + private function allowListUpdateDependencies($localOrLockRepo, array $rootRequires, array $rootDevRequires) { if (!$this->updateWhitelist) { return; @@ -1352,16 +1352,16 @@ class Installer $matchesByPattern = array(); // check if the name is a glob pattern that did not match directly if (empty($depPackages)) { - // add any installed package matching the whitelisted name/pattern - $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); - foreach ($localOrLockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { + // add any installed package matching the allow listed name/pattern + $allowListPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); + foreach ($localOrLockRepo->search($allowListPatternSearchRegexp) as $installedPackage) { $matchesByPattern[] = $pool->whatProvides($installedPackage['name']); } - // add root requirements which match the whitelisted name/pattern - $whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName); + // add root requirements which match the allow listed name/pattern + $allowListPatternRegexp = BasePackage::packageNameToRegexp($packageName); foreach ($rootRequiredPackageNames as $rootRequiredPackageName) { - if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) { + if (preg_match($allowListPatternRegexp, $rootRequiredPackageName)) { $nameMatchesRequiredPackage = true; break; } @@ -1404,7 +1404,7 @@ class Installer } if (isset($skipPackages[$requirePackage->getName()]) && !preg_match(BasePackage::packageNameToRegexp($packageName), $requirePackage->getName())) { - $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.'); + $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly allowed. Ignoring.'); continue; } @@ -1679,6 +1679,8 @@ class Installer * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version * + * @deprecated use setAllowList instead + * * @param array $packages * @return Installer */ @@ -1690,7 +1692,20 @@ class Installer } /** - * @deprecated use setWhitelistTransitiveDependencies instead + * restrict the update operation to a few packages, all other packages + * that are already installed will be kept at their current version + * + * @param array $packages + * @return Installer + */ + public function setUpdateAllowList(array $packages) + { + // call original method for BC + return $this->setUpdateWhitelist($packages); + } + + /** + * @deprecated use setAllowListTransitiveDependencies instead */ public function setWhitelistDependencies($updateDependencies = true) { @@ -1698,11 +1713,13 @@ class Installer } /** - * Should dependencies of whitelisted packages (but not direct dependencies) be updated? + * Should dependencies of allowed packages (but not direct dependencies) be updated? * - * This will NOT whitelist any dependencies that are also directly defined + * This will NOT allow list any dependencies that are also directly defined * in the root package. * + * @deprecated use setAllowListTransitiveDependencies instead + * * @param bool $updateTransitiveDependencies * @return Installer */ @@ -1714,11 +1731,28 @@ class Installer } /** - * Should all dependencies of whitelisted packages be updated recursively? + * Should dependencies of allowed packages (but not direct dependencies) be updated? + * + * This will NOT allow list any dependencies that are also directly defined + * in the root package. * - * This will whitelist any dependencies of the whitelisted packages, including + * @param bool $updateTransitiveDependencies + * @return Installer + */ + public function setAllowListTransitiveDependencies($updateTransitiveDependencies = true) + { + // call original method for BC + return $this->setWhitelistTransitiveDependencies($updateTransitiveDependencies); + } + + /** + * Should all dependencies of allowed packages be updated recursively? + * + * This will allow list any dependencies of the allow listed packages, including * those defined in the root package. * + * @deprecated use setAllowListAllDependencies instead + * * @param bool $updateAllDependencies * @return Installer */ @@ -1729,6 +1763,21 @@ class Installer return $this; } + /** + * Should all dependencies of allowed packages be updated recursively? + * + * This will allow list any dependencies of the allow listed packages, including + * those defined in the root package. + * + * @param bool $updateAllDependencies + * @return Installer + */ + public function setAllowListAllDependencies($updateAllDependencies = true) + { + // call original method for BC + return $this->setWhitelistAllDependencies($updateAllDependencies); + } + /** * Should packages be preferred in a stable version when updating? * diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 9630e7ef0..3987e7e87 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -238,14 +238,14 @@ abstract class BasePackage implements PackageInterface /** * Build a regexp from a package name, expanding * globs as required * - * @param string $whiteListedPattern + * @param string $allowListPattern * @param string $wrap Wrap the cleaned string by the given string * @return string */ - public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i') + public static function packageNameToRegexp($allowListPattern, $wrap = '{^%s$}i') { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + $cleanedAllowListPattern = str_replace('\\*', '.*', preg_quote($allowListPattern)); - return sprintf($wrap, $cleanedWhiteListedPattern); + return sprintf($wrap, $cleanedAllowListPattern); } } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 2fe7e872e..549625fa9 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -337,13 +337,11 @@ 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 { $branchData = JsonFile::parseJson($this->getContents($resource), $resource); 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/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 877ac3653..c8d10d4cd 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 1f4b1af27..64d8e0b39 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-- { @@ -34,7 +34,7 @@ dependency of one the requirements that is whitelisted for update. update b/b --with-dependencies --EXPECT-OUTPUT-- -Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. +Dependency "a/a" is also a root requirement, but is not explicitly allowed. Ignoring. Loading composer repositories with package information Updating dependencies (including require-dev) Nothing to install or update 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 6063abfee..8a2bf39a1 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", "version": "1.1.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0" }, + { "name": "allowed", "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": "1.*", + "allowed": "1.*", "fixed-dependency": "1.*" } } --LOCK-- { "packages": [ - { "name": "whitelisted", "version": "1.1.0" }, + { "name": "allowed", "version": "1.1.0" }, { "name": "fixed-dependency", "version": "1.0.0" } ], "packages-dev": null, @@ -33,7 +33,7 @@ Install from a lock file that deleted a package } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "allowed", "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-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index 3a428c97c..99c46a918 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -1,5 +1,5 @@ --TEST-- -Partial update from lock file should apply lock file and downgrade unstable packages even if not whitelisted +Partial update from lock file should apply lock file and downgrade unstable packages even if not allowed --COMPOSER-- { "repositories": [ 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 4533d5a94..756c52d42 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/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index 94be9176c..97fc4bb49 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -1,5 +1,5 @@ --TEST-- -Partial update without lock file should update everything whitelisted, remove overly unstable packages +Partial update without lock file should update everything allowed, remove overly unstable packages --COMPOSER-- { "repositories": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 0a0d47507..5ca2df792 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 gets the new URL but keeps its old ref +c/c is a tag and not allowed and gets the new URL but keeps its old ref d/d is dev but with a #ref so it should get URL updated but not the reference e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref -e/e is dev but not whitelisted and gets the new URL but keeps its old ref +e/e is dev but not allowed and gets the new URL but keeps its old ref g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref --COMPOSER-- { diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index 381416af1..cad697e0b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed list only updates those packages if they are not present in composer.json --COMPOSER-- { "repositories": [ { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0", "fixed-dependency": "1.*" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.1.0", "fixed-dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "fixed-dependency", "version": "1.1.0", "require": { "fixed-sub-dependency": "1.*" } }, @@ -18,19 +18,19 @@ Update with a package whitelist only updates those packages if they are not pres } ], "require": { - "whitelisted": "1.*", + "allowed": "1.*", "fixed-dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, { "name": "fixed-sub-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted dependency +update allowed dependency --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index 8ea177cad..ec507859c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component1", "version": "1.1.0" }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -23,8 +23,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", + "allowed-component1": "1.*", + "allowed-component2": "1.*", "dependency": "1.*", "unrelated": "1.*" } @@ -32,15 +32,15 @@ Update with a package whitelist pattern and all-dependencies flag updates packag --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* --with-all-dependencies +update allowed-* --with-all-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index c685f14ce..e9e21916d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, + { "name": "allowed-component1", "version": "1.1.0" }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "root-dependency", "version": "1.1.0" }, @@ -25,8 +25,8 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", + "allowed-component1": "1.*", + "allowed-component2": "1.*", "root-dependency": "1.*", "unrelated": "1.*" } @@ -34,16 +34,16 @@ Update with a package whitelist only updates those packages and their dependenci --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "root-dependency", "version": "1.0.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* --with-dependencies +update allowed-* --with-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index a24bafb91..8724b4a82 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0", "require": { "whitelisted-component2": "1.1.0" } }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.1.0", "whitelisted-component5": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.1.0", "require": { "whitelisted-component4": "1.1.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.1.0" }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.1.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, + { "name": "allowed-component1", "version": "1.1.0", "require": { "allowed-component2": "1.1.0" } }, + { "name": "allowed-component1", "version": "1.0.0", "require": { "allowed-component2": "1.0.0" } }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.1.0", "allowed-component5": "1.0.0" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component3", "version": "1.1.0", "require": { "allowed-component4": "1.1.0" } }, + { "name": "allowed-component3", "version": "1.0.0", "require": { "allowed-component4": "1.0.0" } }, + { "name": "allowed-component4", "version": "1.1.0" }, + { "name": "allowed-component4", "version": "1.0.0" }, + { "name": "allowed-component5", "version": "1.1.0" }, + { "name": "allowed-component5", "version": "1.0.0" }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -29,27 +29,27 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "whitelisted-component3": "1.0.0", + "allowed-component1": "1.*", + "allowed-component2": "1.*", + "allowed-component3": "1.0.0", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, + { "name": "allowed-component1", "version": "1.0.0", "require": { "allowed-component2": "1.0.0" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component3", "version": "1.0.0", "require": { "allowed-component4": "1.0.0" } }, + { "name": "allowed-component4", "version": "1.0.0" }, + { "name": "allowed-component5", "version": "1.0.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* --with-dependencies +update allowed-* --with-dependencies --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index e5551b43f..0dff8264a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component1", "version": "1.1.0" }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -23,22 +23,22 @@ Update with a package whitelist only updates those packages matching the pattern ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", + "allowed-component1": "1.*", + "allowed-component2": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* +update allowed-* --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index de1fb1b73..e4344cc7d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those corresponding to the pattern +Update with a package allowed list only updates those corresponding to the pattern --COMPOSER-- { "repositories": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index e658e8c06..87fc11b05 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -1,13 +1,13 @@ --TEST-- -Update with a package whitelist removes unused packages +Update with a package allowed list removes unused packages --COMPOSER-- { "repositories": [ { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0" }, + { "name": "allowed", "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,18 +15,18 @@ Update with a package whitelist removes unused packages } ], "require": { - "whitelisted": "1.*", + "allowed": "1.*", "fixed-dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "allowed", "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" } ] --RUN-- -update --with-dependencies whitelisted +update --with-dependencies allowed --EXPECT-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index bb2e04193..2c0e7b9bf 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -21,20 +21,20 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed": "1.*", - "whitelisted": "1.*", + "allowed": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted --with-dependencies +update allowed --with-dependencies --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test index f63229fbc..81c667463 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -21,18 +21,18 @@ Update with a package whitelist only updates whitelisted packages if no dependen ], "require": { "fixed": "1.*", - "whitelisted": "1.*", + "allowed": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted +update allowed --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 751d79e70..9cc43dba3 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages listed as command arguments +Update with a package allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -21,19 +21,19 @@ Update with a package whitelist only updates those packages listed as command ar ], "require": { "fixed": "1.*", - "whitelisted": "1.*", + "allowed": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted +update allowed --EXPECT-- -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) 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 c0019e6ca..12d507a7a 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\Installer::allowListUpdateDependencies should update the dependencies of all allowed packages, even if the dependency is a root requirement. --COMPOSER-- { diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 067baf17a..90f295d1d 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -230,9 +230,9 @@ class InstallerTest extends TestCase ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) - ->setUpdateWhitelist($input->getArgument('packages')) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($input->getArgument('packages')) + ->setAllowListTransitiveDependencies($input->getOption('with-dependencies')) + ->setAllowListAllDependencies($input->getOption('with-all-dependencies')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); From 491067f253f60b5f9c137236b3fbe36476c2a9e5 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 7 Jun 2020 22:31:24 +0100 Subject: [PATCH 03/52] Fixed wording --- doc/01-basic-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index ac8086491..e6b1e6772 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install or update one dependency, you can allow list them: +If you only want to install or update one dependency, you can allow them: ```sh php composer.phar update monolog/monolog [...] From a97d13fc6db7a4eecb69aa41231d3eafc799ea20 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 8 Jun 2020 09:33:40 +0100 Subject: [PATCH 04/52] Fixed typo Co-authored-by: ZhangWei --- src/Composer/Command/RequireCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 45bd315fe..039250766 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -237,7 +237,7 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdatAllowList(array_keys($requirements)) + ->setUpdateAllowList(array_keys($requirements)) ->setAllowListTransitiveDependencies($input->getOption('update-with-dependencies')) ->setAllowListAllDependencies($input->getOption('update-with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) From 13bdf8553a8a4a8ca369d95d75ac1ccdc207c30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4nz=20Friederes?= Date: Thu, 11 Jun 2020 21:53:31 +0200 Subject: [PATCH 05/52] Add setProcessedUrl method to PreFileDownloadEvent --- src/Composer/Downloader/FileDownloader.php | 1 + src/Composer/Plugin/PreFileDownloadEvent.php | 12 +++++++++++- src/Composer/Repository/ComposerRepository.php | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index d7c4bcabe..14ea40220 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -127,6 +127,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface if ($eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $url['processed'] = $preFileDownloadEvent->getProcessedUrl(); } $checksum = $package->getDistSha1Checksum(); diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index c2751da02..878ab851f 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -55,7 +55,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 +63,14 @@ class PreFileDownloadEvent extends Event { return $this->processedUrl; } + + /** + * Sets the processed URL that will be downloaded + * + * @return string + */ + public function setProcessedUrl($processedUrl) + { + $this->processedUrl = $processedUrl; + } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8848452f2..bc2844df9 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1015,6 +1015,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $response = $this->httpDownloader->get($filename, $this->options); @@ -1101,6 +1102,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $options = $this->options; @@ -1167,6 +1169,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); From a17bbec842f515842b2c01c2b2b02b12ec276058 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 15 Jun 2020 13:04:02 +0200 Subject: [PATCH 06/52] Avoid double warnings about composer.json when running outdated, fixes #8958 --- src/Composer/Console/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index cc39c587b..bc1cf0993 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') ?: '/'); From 54debe82102f72a80988c81b56e81d8b4f13cce5 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Fri, 28 Feb 2020 16:15:34 +0000 Subject: [PATCH 07/52] Respect disable-tls in Versions::getLatest Use http to get the latest version when disable-tls is true and error- trap DiagnoseCommand::checkVersion so that all checks can complete. Fixes #8657. --- src/Composer/Command/DiagnoseCommand.php | 6 +++++- src/Composer/SelfUpdate/Versions.php | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 6c9158630..b7739ff18 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -432,7 +432,11 @@ EOT } $versionsUtil = new Versions($config, $this->rfs); - $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/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index 01d01e7e3..6a0a1bdbf 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -63,7 +63,12 @@ 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 = JsonFile::parseJson($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/versions', false)); foreach ($versions[$channel ?: $this->getChannel()] as $version) { From 907367ff438481356ce86a1a27e297b02fd6c7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4nz=20Friederes?= Date: Mon, 15 Jun 2020 21:28:27 +0200 Subject: [PATCH 08/52] Fix PHPDoc issue --- src/Composer/Plugin/PreFileDownloadEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 878ab851f..2ae1e5380 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -67,7 +67,7 @@ class PreFileDownloadEvent extends Event /** * Sets the processed URL that will be downloaded * - * @return string + * @param string $processedUrl New processed URL */ public function setProcessedUrl($processedUrl) { From ae5904716610434a3fa62f123ebda7e2d5d79e84 Mon Sep 17 00:00:00 2001 From: Michael Stucki Date: Mon, 15 Jun 2020 21:42:41 +0200 Subject: [PATCH 09/52] Clean Git repos during discard --- src/Composer/Downloader/GitDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index edeaa7686..ede6dca10 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -451,7 +451,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()); } From 5c13c974286969e4c5298e74c1a3ee66ef6e2bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4nz=20Friederes?= Date: Mon, 15 Jun 2020 21:43:41 +0200 Subject: [PATCH 10/52] Implement type and context properties in PreFileDownloadEvent --- src/Composer/Downloader/FileDownloader.php | 2 +- src/Composer/Plugin/PreFileDownloadEvent.php | 43 +++++++++++++++++-- .../Repository/ComposerRepository.php | 6 +-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 14ea40220..5ea4bdfe8 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -125,7 +125,7 @@ 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(); } diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 2ae1e5380..03efe3ab2 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; } /** @@ -73,4 +87,25 @@ class PreFileDownloadEvent extends Event { $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 bc2844df9..6a2aba6ea 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1013,7 +1013,7 @@ 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(); } @@ -1100,7 +1100,7 @@ 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(); } @@ -1167,7 +1167,7 @@ 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(); } From 6d9bf426553ea944b2d0931114b992d965ecf741 Mon Sep 17 00:00:00 2001 From: Michael Chekin Date: Tue, 16 Jun 2020 09:35:33 +0200 Subject: [PATCH 11/52] Additional Util\RemoteFileSystem tests (#8960) * RemoteFilesystemTest: simplifying some mock expectations calls - will($this->returnValue()) to willReturn() - will($this->returnCallBack()) to willReturnCallback() * RemoteFilesystemTest: extracting identical mocks for IOInterface into a separate getIOInterfaceMock() method * RemoteFilesystemTest: converting protected helper methods to private. * RemoteFilesystemTest: moving getConfigMock() private method after the public methods (with other private methods) * adding RemoteFileSystemTest::testCopyWithRetryAuthFailureFalse() unit test. * Allow optional injecting of AuthHelper into RemoteFilesystem constructor. * adding RemoteFileSystemTest::testCopyWithSuccessOnRetry() unit test. * using backward compatible @expectedException in RemoteFilesystemTest.php * RemoteFilesystemTest: extracting RemoteFilesystem with mocked method creation into a separate method. * RemoteFilesystemTest: extracting AuthHelper with mocked method creation into a separate method. --- src/Composer/Util/RemoteFilesystem.php | 5 +- .../Test/Util/RemoteFilesystemTest.php | 220 ++++++++++++++---- 2 files changed, 181 insertions(+), 44 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index c7077afbb..b0dfbea94 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -54,8 +54,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 +71,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/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(); + } } From d906ff12c9257f50ade53d96f0e513f610b5dd87 Mon Sep 17 00:00:00 2001 From: Ayesh Karunaratne Date: Tue, 16 Jun 2020 15:05:44 +0700 Subject: [PATCH 12/52] PHPStan fixes: `autoload_files`, and `ignoreErrors` (#8974) * PHPStan: Remove autoload_files directive as it is not necessary anymore * PHPStan: Add error exclusions for sapi_windows_set_ctrl_handler function * PHPStan: Add error exclusions for ZipArchive::LIBZIP_VERSION * PHPStan: Require phpstan ^0.12.26 * Ensure zip ext is available on gh actions Co-authored-by: Jordi Boggiano --- .github/workflows/continuous-integration.yml | 2 +- .github/workflows/phpstan.yml | 4 ++-- phpstan/config.neon | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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/phpstan/config.neon b/phpstan/config.neon index d9bf7f8c6..c8c130a31 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,10 @@ parameters: # BC with older PHPUnit - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~' + + # ZipArchive::* Class constants are already checked before use. + - '~^Access to undefined constant ZipArchive::~' + paths: - ../src - ../tests From 8da2811dc3392c88d99c4b3011ce613cda5e7f8f Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Tue, 16 Jun 2020 10:07:53 +0200 Subject: [PATCH 13/52] Add new summary format for licenses (#8973) * Add new summary format to render the number of dependencies for each used license * Array dereferencing wasn't available on php 5.3 * Add summary format to documentation --- doc/03-cli.md | 2 +- src/Composer/Command/LicensesCommand.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) 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/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)); } From a797ee1322088415ce57639686562cdc74e8250d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 16 Jun 2020 11:56:08 +0200 Subject: [PATCH 14/52] Fix inline aliases not being loaded when extracting dev requirements, fixes #8954 --- .../DependencyResolver/PoolBuilder.php | 18 +--- src/Composer/Repository/RepositorySet.php | 35 +++++++- .../installer/aliases-with-require-dev.test | 90 +++++++++++++++++++ 3 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 29903f493..0da0eb87c 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -88,7 +88,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 */ @@ -96,7 +96,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; @@ -425,19 +425,5 @@ class PoolBuilder unset($this->skippedLoad[$name]); unset($this->loadedNames[$name]); } - - private function getRootAliasesPerPackage(array $aliases) - { - $normalizedAliases = array(); - - foreach ($aliases as $alias) { - $normalizedAliases[$alias['package']][$alias['version']] = array( - 'alias' => $alias['alias'], - 'alias_normalized' => $alias['alias_normalized'], - ); - } - - return $normalizedAliases; - } } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 24d935f4a..da352a7f3 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 = $this->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 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/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) From d5286d0cb8216d2a76f5e63c03e4bcb6f0fa0c0a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 08:30:59 +0200 Subject: [PATCH 15/52] Add a way for FileDownloader subclasses to add paths to the cleanup stage --- src/Composer/Downloader/FileDownloader.php | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index d7c4bcabe..f5674cf90 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -55,6 +55,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface * @private this is only public for php 5.3 support in closures */ public $lastCacheWrites = array(); + private $additionalCleanupPaths = array(); /** * Constructor. @@ -258,6 +259,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); @@ -291,6 +298,29 @@ 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} */ From 0dad963cd89be409c729830dea4128a021129090 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 08:33:44 +0200 Subject: [PATCH 16/52] Add executeAsync to ProcessExecutor and allow Loop class to wait on it in addition to HttpDownloader --- src/Composer/Factory.php | 2 +- src/Composer/Util/HttpDownloader.php | 71 +++--- src/Composer/Util/Loop.php | 35 ++- src/Composer/Util/ProcessExecutor.php | 204 ++++++++++++++++++ .../Test/Downloader/FileDownloaderTest.php | 6 +- .../Test/Downloader/XzDownloaderTest.php | 2 +- .../Test/Downloader/ZipDownloaderTest.php | 2 +- .../Repository/ComposerRepositoryTest.php | 10 +- 8 files changed, 293 insertions(+), 39 deletions(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 04da08e31..5bc41ee6b 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 diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 2fa8fa716..41ced41e3 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -44,6 +44,7 @@ class HttpDownloader private $rfs; private $idGen = 0; private $disabled; + private $allowAsync = false; /** * @param IOInterface $io The IO instance @@ -139,6 +140,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])); @@ -189,7 +194,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 +201,6 @@ class HttpDownloader $job['exception'] = $e; $downloader->markJobDone(); - $downloader->scheduleNextJob(); throw $e; }); @@ -251,13 +254,7 @@ class HttpDownloader public function markJobDone() { $this->runningJobs--; - } - /** - * @private - */ - public function scheduleNextJob() - { foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_QUEUED) { $this->startJob($job['id']); @@ -268,34 +265,50 @@ class HttpDownloader } } - public function wait($index = null, $progress = false) + public function wait($index = null) { while (true) { - if ($this->curl) { - $this->curl->tick(); + if (!$this->hasActiveJob($index)) { + return; } - if (null !== $index) { - if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { - return; - } - } else { - $done = true; - foreach ($this->jobs as $job) { - if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { - $done = false; - break; - } elseif (!$job['sync']) { - unset($this->jobs[$job['id']]); - } - } - if ($done) { - return; - } + usleep(1000); + } + } + + /** + * @internal + */ + public function enableAsync() + { + $this->allowAsync = true; + } + + /** + * @internal + */ + public function hasActiveJob($index = null) + { + if ($this->curl) { + $this->curl->tick(); + } + + if (null !== $index) { + if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { + return false; } + return true; + } - usleep(1000); + foreach ($this->jobs as $job) { + if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { + return true; + } elseif (!$job['sync']) { + unset($this->jobs[$job['id']]); + } } + + return false; } private function getResponse($index) diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index dfaa2ac53..b0061ba2d 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -21,10 +21,19 @@ 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) @@ -39,8 +48,30 @@ class Loop } ); - $this->httpDownloader->wait(); + $this->currentPromises = $promises; + + while (true) { + $hasActiveJob = false; + + if ($this->httpDownloader) { + if ($this->httpDownloader->hasActiveJob()) { + $hasActiveJob = true; + } + } + if ($this->processExecutor) { + if ($this->processExecutor->hasActiveJob()) { + $hasActiveJob = true; + } + } + + if (!$hasActiveJob) { + break; + } + + usleep(5000); + } + $this->currentPromises = null; if ($uncaught) { throw $uncaught; } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index a30a04d15..b443e541d 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,196 @@ 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 () use (&$job, $self) { + $job['status'] = ProcessExecutor::STATUS_FAILED; + + $self->markJobDone(); + + return \React\Promise\reject($job['process']); + }); + $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->hasActiveJob($index)) { + return; + } + + usleep(1000); + } + } + + /** + * @internal + */ + public function enableAsync() + { + $this->allowAsync = true; + } + + /** + * @internal + */ + public function hasActiveJob($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 (null !== $index) { + if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED || $this->jobs[$index]['status'] === self::STATUS_ABORTED) { + return false; + } + + return true; + } + + foreach ($this->jobs as $job) { + if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_ABORTED), true)) { + return true; + } else { + unset($this->jobs[$job['id']]); + } + } + + return false; + } + + /** + * @private + */ + public function markJobDone() + { + $this->runningJobs--; + + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_QUEUED) { + $this->startJob($job['id']); + if ($this->runningJobs >= $this->maxJobs) { + return; + } + } + } + } + public function splitLines($output) { $output = trim($output); 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/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..764af8feb 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -92,8 +92,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); 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( From 8f6e82f562ca2ba06c16d6325f079c386cdfdde0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 09:06:49 +0200 Subject: [PATCH 17/52] Add support for aborting running promises cleanly --- .../Installer/InstallationManager.php | 2 ++ src/Composer/Util/Http/CurlDownloader.php | 34 ++++++++++++++++--- src/Composer/Util/HttpDownloader.php | 26 +++++++++----- src/Composer/Util/Loop.php | 9 +++++ src/Composer/Util/ProcessExecutor.php | 8 ++--- src/Composer/Util/RemoteFilesystem.php | 1 + 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 06e8d65b9..9e55c4ac7 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -184,6 +184,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(); 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 41ced41e3..6fe53390e 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; @@ -184,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) { @@ -242,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); } } @@ -294,14 +307,11 @@ class HttpDownloader } if (null !== $index) { - if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { - return false; - } - return true; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED; } foreach ($this->jobs as $job) { - if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { + if ($job['status'] < self::STATUS_COMPLETED) { return true; } elseif (!$job['sync']) { unset($this->jobs[$job['id']]); diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index b0061ba2d..b7382f1ed 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -76,4 +76,13 @@ class Loop 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 b443e541d..59db0c0c0 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -281,15 +281,11 @@ class ProcessExecutor } if (null !== $index) { - if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED || $this->jobs[$index]['status'] === self::STATUS_ABORTED) { - return false; - } - - return true; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED; } foreach ($this->jobs as $job) { - if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_ABORTED), true)) { + if ($job['status'] < self::STATUS_COMPLETED) { return true; } else { unset($this->jobs[$job['id']]); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b0dfbea94..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 From 3af617efe892647ca1d2cfa59fa5c23215b82ba3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 09:07:40 +0200 Subject: [PATCH 18/52] Parallelize zip extraction using async unzip processes --- src/Composer/Downloader/ArchiveDownloader.php | 95 +++++++++++-------- src/Composer/Downloader/ZipDownloader.php | 66 +++++++++---- .../Test/Downloader/ZipDownloaderTest.php | 73 ++++++++++---- 3 files changed, 162 insertions(+), 72 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 283d0103e..a9091dbe6 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -16,6 +16,7 @@ use Composer\Package\PackageInterface; use Symfony\Component\Finder\Finder; use Composer\IO\IOInterface; use Composer\Exception\IrrecoverableDownloadException; +use React\Promise\PromiseInterface; /** * Base downloader for archives @@ -60,26 +61,62 @@ abstract class ArchiveDownloader extends FileDownloader $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); } while (is_dir($temporaryDir)); + $this->addCleanupPath($package, $temporaryDir); + + $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); + }; + + $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 dotfiles + * + * @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 +126,27 @@ 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); + }, function ($e) use ($cleanup) { + $cleanup(); throw $e; - } + }); } /** @@ -119,25 +155,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/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index d0b4e8255..c5fd2b11b 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -86,9 +86,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 +97,47 @@ 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; + } + + $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) { + if (!$process->isSuccessful()) { + return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$process->getErrorOutput())); + } + }); + } 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 +149,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 +162,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 +176,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 +189,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 +200,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 +224,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/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 764af8feb..d86d2cdf1 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -179,37 +179,65 @@ class ZipDownloaderTest extends TestCase /** * @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'); + $e = null; + $promise->then(function () { + // noop + }, function ($ex) use (&$e) { + $e = $ex; + }); + + if ($e) { + throw $e; + } } 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'); @@ -225,10 +253,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)) @@ -350,6 +389,6 @@ class MockedZipDownloader extends ZipDownloader public function extract(PackageInterface $package, $file, $path) { - parent::extract($package, $file, $path); + return parent::extract($package, $file, $path); } } From b1e15c77256dd5e05ceaef207dd5c6225061c4fa Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 09:39:11 +0200 Subject: [PATCH 19/52] Fix a couple async bugs --- src/Composer/Util/ProcessExecutor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 59db0c0c0..b59bced70 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -187,12 +187,12 @@ class ProcessExecutor $self->markJobDone(); return $job['process']; - }, function () use (&$job, $self) { + }, function ($e) use (&$job, $self) { $job['status'] = ProcessExecutor::STATUS_FAILED; $self->markJobDone(); - return \React\Promise\reject($job['process']); + throw $e; }); $this->jobs[$job['id']] =& $job; @@ -272,7 +272,7 @@ class ProcessExecutor public function hasActiveJob($index = null) { // tick - foreach ($this->jobs as &$job) { + foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_STARTED) { if (!$job['process']->isRunning()) { call_user_func($job['resolve'], $job['process']); From 9f380d606caa6901d10e5f3e1b917d605379cebd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 10:01:48 +0200 Subject: [PATCH 20/52] Add basic progress bar capability while waiting for jobs to complete --- src/Composer/IO/ConsoleIO.php | 10 +++++++ .../Installer/InstallationManager.php | 10 ++++++- src/Composer/Util/HttpDownloader.php | 30 ++++++++++--------- src/Composer/Util/Loop.php | 30 +++++++++++++------ src/Composer/Util/ProcessExecutor.php | 28 ++++++++--------- 5 files changed, 70 insertions(+), 38 deletions(-) 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/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 9e55c4ac7..47b3e2914 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; @@ -330,7 +331,14 @@ class InstallationManager // execute all prepare => installs/updates/removes => cleanup steps if (!empty($promises)) { - $this->loop->wait($promises); + $progress = null; + if ($io instanceof ConsoleIO && !$io->isDebug()) { + $progress = $io->getProgressBar(); + } + $this->loop->wait($promises, $progress); + if ($progress) { + $progress->clear(); + } } } catch (\Exception $e) { $runCleanup(); diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 6fe53390e..889fae07e 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -267,21 +267,12 @@ class HttpDownloader public function markJobDone() { $this->runningJobs--; - - foreach ($this->jobs as $job) { - if ($job['status'] === self::STATUS_QUEUED) { - $this->startJob($job['id']); - if ($this->runningJobs >= $this->maxJobs) { - return; - } - } - } } public function wait($index = null) { while (true) { - if (!$this->hasActiveJob($index)) { + if (!$this->countActiveJobs($index)) { return; } @@ -299,26 +290,37 @@ class HttpDownloader /** * @internal + * + * @return int number of active (queued or started) jobs */ - public function hasActiveJob($index = null) + public function countActiveJobs($index = null) { + if ($this->runningJobs < $this->maxJobs) { + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_QUEUED && $this->runningJobs < $this->maxJobs) { + $this->startJob($job['id']); + } + } + } + if ($this->curl) { $this->curl->tick(); } if (null !== $index) { - return $this->jobs[$index]['status'] < self::STATUS_COMPLETED; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } + $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { - return true; + $active++; } elseif (!$job['sync']) { unset($this->jobs[$job['id']]); } } - return false; + return $active; } private function getResponse($index) diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index b7382f1ed..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 @@ -36,7 +37,7 @@ class Loop } } - public function wait(array $promises) + public function wait(array $promises, ProgressBar $progress = null) { /** @var \Exception|null */ $uncaught = null; @@ -50,21 +51,32 @@ class Loop $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) { - $hasActiveJob = false; + $activeJobs = 0; if ($this->httpDownloader) { - if ($this->httpDownloader->hasActiveJob()) { - $hasActiveJob = true; - } + $activeJobs += $this->httpDownloader->countActiveJobs(); } if ($this->processExecutor) { - if ($this->processExecutor->hasActiveJob()) { - $hasActiveJob = true; - } + $activeJobs += $this->processExecutor->countActiveJobs(); + } + + if ($progress) { + $progress->setProgress($progress->getMaxSteps() - $activeJobs); } - if (!$hasActiveJob) { + if (!$activeJobs) { break; } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index b59bced70..96b9235c8 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -250,7 +250,7 @@ class ProcessExecutor public function wait($index = null) { while (true) { - if (!$this->hasActiveJob($index)) { + if (!$this->countActiveJobs($index)) { return; } @@ -268,8 +268,10 @@ class ProcessExecutor /** * @internal + * + * @return int number of active (queued or started) jobs */ - public function hasActiveJob($index = null) + public function countActiveJobs($index = null) { // tick foreach ($this->jobs as $job) { @@ -278,21 +280,28 @@ class ProcessExecutor 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; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } + $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { - return true; + $active++; } else { unset($this->jobs[$job['id']]); } } - return false; + return $active; } /** @@ -301,15 +310,6 @@ class ProcessExecutor public function markJobDone() { $this->runningJobs--; - - foreach ($this->jobs as $job) { - if ($job['status'] === self::STATUS_QUEUED) { - $this->startJob($job['id']); - if ($this->runningJobs >= $this->maxJobs) { - return; - } - } - } } public function splitLines($output) From 87a0fc5506687186b426f8e4386fd491d52ea5cb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 11:11:31 +0200 Subject: [PATCH 21/52] Execute operations in batches to make sure plugins install in the expected order --- src/Composer/Downloader/ArchiveDownloader.php | 3 + .../Installer/InstallationManager.php | 149 ++++++++++-------- 2 files changed, 88 insertions(+), 64 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index a9091dbe6..bcee49f9a 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -62,6 +62,7 @@ abstract class ArchiveDownloader extends FileDownloader } while (is_dir($temporaryDir)); $this->addCleanupPath($package, $temporaryDir); + $this->addCleanupPath($package, $path); $this->filesystem->ensureDirectoryExists($temporaryDir); $fileName = $this->getFileName($package, $path); @@ -77,6 +78,7 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->removeDirectory($path); $filesystem->removeDirectory($temporaryDir); $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); }; $promise = null; @@ -142,6 +144,7 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->removeDirectory($temporaryDir); $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); }, function ($e) use ($cleanup) { $cleanup(); diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 47b3e2914..54f359eaa 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -272,73 +272,23 @@ class InstallationManager $this->loop->wait($promises); } - 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)); + // 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 + while ($operations) { + $batch = array(); + + foreach ($operations as $index => $operation) { + unset($operations[$index]); + $batch[$index] = $operation; + if (in_array($operation->getOperationType(), array('update', 'install'), true)) { + $package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage(); + if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { + break; + } } - $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; - $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; - } - - // execute all prepare => installs/updates/removes => cleanup steps - if (!empty($promises)) { - $progress = null; - if ($io instanceof ConsoleIO && !$io->isDebug()) { - $progress = $io->getProgressBar(); - } - $this->loop->wait($promises, $progress); - if ($progress) { - $progress->clear(); - } + $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts); } } catch (\Exception $e) { $runCleanup(); @@ -366,6 +316,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 ($io instanceof ConsoleIO && !$io->isDebug() && count($promises) > 1) { + $progress = $io->getProgressBar(); + } + $this->loop->wait($promises, $progress); + if ($progress) { + $progress->clear(); + } + } + } + /** * Executes install operation. * From 9c78eda7db409125145aa2b94944c7fca55aeae1 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 13:15:50 +0200 Subject: [PATCH 22/52] Fix FileDownloader::update impl to handle promises --- src/Composer/Downloader/ArchiveDownloader.php | 4 +-- src/Composer/Downloader/FileDownloader.php | 29 ++++++++++++++----- src/Composer/Downloader/GzipDownloader.php | 9 ------ src/Composer/Downloader/PathDownloader.php | 9 ------ src/Composer/Downloader/RarDownloader.php | 9 ------ src/Composer/Downloader/XzDownloader.php | 10 ------- src/Composer/Downloader/ZipDownloader.php | 8 ----- src/Composer/Factory.php | 6 ++-- 8 files changed, 26 insertions(+), 58 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index bcee49f9a..d77d55738 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -29,14 +29,12 @@ 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); } /** diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index f5674cf90..5f88cf140 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -27,7 +27,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,6 +53,8 @@ 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 */ @@ -67,14 +71,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')); @@ -333,10 +338,19 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); - $this->remove($initial, $path, false); - $this->install($target, $path, false); + $promise = $this->remove($initial, $path, false); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); + } + $self = $this; + $io = $this->io; + + return $promise->then(function () use ($self, $target, $path, $io) { + $promise = $self->install($target, $path, false); + $io->writeError(''); - $this->io->writeError(''); + return $promise; + }); } /** @@ -410,9 +424,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $output = ''; try { - $res = $this->download($package, $targetDir.'_compare', null, false); + $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/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..2cb74d641 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -39,15 +39,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} */ 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/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 c5fd2b11b..d891fb33b 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} */ diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 5bc41ee6b..509fa62b0 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -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; From 085fe4e7e5c9b1c0122322d62e94bba2c7f0fe4f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 13:43:21 +0200 Subject: [PATCH 23/52] Add --no-progress support and a few more fixes --- src/Composer/Command/ArchiveCommand.php | 6 +++-- src/Composer/Command/CreateProjectCommand.php | 9 +++++++- src/Composer/Command/InstallCommand.php | 2 ++ src/Composer/Command/RemoveCommand.php | 2 ++ src/Composer/Command/RequireCommand.php | 2 ++ src/Composer/Command/UpdateCommand.php | 2 ++ src/Composer/Installer.php | 2 +- .../Installer/InstallationManager.php | 22 +++++++++++++++---- 8 files changed, 39 insertions(+), 8 deletions(-) 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/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/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/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 54f359eaa..a9bbe7c3a 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -50,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) { @@ -174,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) { @@ -269,7 +271,14 @@ 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 @@ -377,8 +386,8 @@ class InstallationManager // execute all prepare => installs/updates/removes => cleanup steps if (!empty($promises)) { $progress = null; - if ($io instanceof ConsoleIO && !$io->isDebug() && count($promises) > 1) { - $progress = $io->getProgressBar(); + if ($this->outputProgress && $this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1) { + $progress = $this->io->getProgressBar(); } $this->loop->wait($promises, $progress); if ($progress) { @@ -485,6 +494,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) { From ee58f25c001332bb8aaeaa442d6ca232093d911d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 14:05:19 +0200 Subject: [PATCH 24/52] Fix ZipDownloaderTest --- .../Test/Downloader/ZipDownloaderTest.php | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index d86d2cdf1..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')) { @@ -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,7 +170,8 @@ 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); } /** @@ -205,16 +202,7 @@ class ZipDownloaderTest extends TestCase $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); - $e = null; - $promise->then(function () { - // noop - }, function ($ex) use (&$e) { - $e = $ex; - }); - - if ($e) { - throw $e; - } + $this->wait($promise); } public function testSystemUnzipOnlyGood() @@ -240,7 +228,8 @@ class ZipDownloaderTest extends TestCase ->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() @@ -279,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); } /** @@ -296,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)) @@ -311,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() @@ -339,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); } /** @@ -371,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; + } } } From aea074308c642e86377a2fa0fef7d0f97e5ebb26 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 16 Jun 2020 14:07:30 +0200 Subject: [PATCH 25/52] Update batching to install plugin deps before the plugin (alone an own batch) --- src/Composer/Downloader/ArchiveDownloader.php | 2 +- .../Installer/InstallationManager.php | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index d77d55738..8f7f5ef83 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -95,7 +95,7 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->unlink($fileName); /** - * Returns the folder content, excluding dotfiles + * Returns the folder content, excluding .DS_Store * * @param string $dir Directory * @return \SplFileInfo[] diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index a9bbe7c3a..7dfb2ff3e 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -283,20 +283,31 @@ class InstallationManager // 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 - while ($operations) { - $batch = array(); - - foreach ($operations as $index => $operation) { - unset($operations[$index]); - $batch[$index] = $operation; - if (in_array($operation->getOperationType(), array('update', 'install'), true)) { - $package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage(); - if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { - break; + $batches = array(); + $batch = array(); + foreach ($operations as $index => $operation) { + if (in_array($operation->getOperationType(), array('update', 'install'), true)) { + $package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage(); + if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { + if ($batch) { + $batches[] = $batch; } + unset($operations[$index]); + $batches[] = array($index => $operation); + $batch = array(); + + continue; } } + unset($operations[$index]); + $batch[$index] = $operation; + } + + if ($batch) { + $batches[] = $batch; + } + foreach ($batches as $batch) { $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts); } } catch (\Exception $e) { From a4f41013463412ec39139f3f745d522b663208f4 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 16 Jun 2020 13:46:20 +0100 Subject: [PATCH 26/52] Phpdoc tweaks --- src/Composer/Plugin/PreFileDownloadEvent.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 03efe3ab2..f7b523b94 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -69,7 +69,7 @@ class PreFileDownloadEvent extends Event } /** - * Retrieves the processed URL that will be downloaded + * Retrieves the processed URL that will be downloaded. * * @return string */ @@ -79,7 +79,7 @@ class PreFileDownloadEvent extends Event } /** - * Sets the processed URL that will be downloaded + * Sets the processed URL that will be downloaded. * * @param string $processedUrl New processed URL */ @@ -89,7 +89,7 @@ class PreFileDownloadEvent extends Event } /** - * Returns the type of this download (package, metadata) + * Returns the type of this download (package, metadata). * * @return string */ @@ -100,6 +100,7 @@ class PreFileDownloadEvent extends Event /** * Returns the context of this download, if any. + * * If this download is of type package, the package object is returned. * * @return mixed From 643852a2b070e7f29fd4e32df7e8dd8efb6806d3 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 16 Jun 2020 13:48:59 +0100 Subject: [PATCH 27/52] Marked getRootAliasesPerPackage as static --- src/Composer/Repository/RepositorySet.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index da352a7f3..9417dec97 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -92,7 +92,7 @@ class RepositorySet */ public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) { - $this->rootAliases = $this->getRootAliasesPerPackage($rootAliases); + $this->rootAliases = self::getRootAliasesPerPackage($rootAliases); $this->rootReferences = $rootReferences; $this->acceptableStabilities = array(); @@ -286,7 +286,7 @@ class RepositorySet return $this->createPool($request, new NullIO()); } - private function getRootAliasesPerPackage(array $aliases) + private static function getRootAliasesPerPackage(array $aliases) { $normalizedAliases = array(); From 09fc263d373a4c44c62c71ef56f209d1daf22d92 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 16 Jun 2020 16:27:36 +0200 Subject: [PATCH 28/52] Fix status command bug --- src/Composer/Downloader/ArchiveDownloader.php | 2 +- src/Composer/Downloader/FileDownloader.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 8f7f5ef83..14b434b74 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -29,7 +29,7 @@ abstract class ArchiveDownloader extends FileDownloader { public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { - // 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 not upgrading/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); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index b965e62d4..3da51d39b 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -425,6 +425,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $output = ''; try { + if (is_dir($targetDir.'_compare')) { + $this->filesystem->removeDirectory($targetDir.'_compare'); + } + $this->download($package, $targetDir.'_compare', null, false); $this->httpDownloader->wait(); $this->install($package, $targetDir.'_compare', false); From e5fe35d5541d2081c447d2ea16b246186d5bfb5f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 09:24:25 +0200 Subject: [PATCH 29/52] Update test description --- .../Test/Fixtures/installer/update-with-all-dependencies.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e0c120714..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::allowListUpdateDependencies should update the dependencies of all allowed 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-- { From d1fedc3bd62cea10bc847a980fdb874af19b2b8d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 10:42:05 +0200 Subject: [PATCH 30/52] Restore old behavior of wiping dir contents before installing, fixes #8988 --- src/Composer/Downloader/ArchiveDownloader.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 14b434b74..a78aa95bc 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -29,11 +29,6 @@ abstract class ArchiveDownloader extends FileDownloader { public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { - // if not upgrading/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 parent::download($package, $path, $prevPackage, $output); } @@ -50,10 +45,7 @@ abstract class ArchiveDownloader extends FileDownloader $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); From aaef3ff5ff6827532f8ad9950c3971cc9236da93 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 10:42:37 +0200 Subject: [PATCH 31/52] Improve error reporting when unzip fails due to race condition in unhandled Promise, refs #8988 --- src/Composer/Downloader/ZipDownloader.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index d891fb33b..83b904a8a 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -104,9 +104,15 @@ class ZipDownloader extends ArchiveDownloader throw $processError; } - $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'); + 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); }; @@ -114,9 +120,12 @@ class ZipDownloader extends ArchiveDownloader try { $promise = $this->process->executeAsync($command); - return $promise->then(function ($process) use ($tryFallback, $command, $package) { + return $promise->then(function ($process) use ($tryFallback, $command, $package, $file) { if (!$process->isSuccessful()) { - return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$process->getErrorOutput())); + $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) { From 83c64a9d191b64689b408906d40943958a6a8eb0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 12:26:32 +0200 Subject: [PATCH 32/52] Reuse operation formatting logic in downloaders --- .../Operation/InstallOperation.php | 7 +++++- .../Operation/UninstallOperation.php | 7 +++++- .../Operation/UpdateOperation.php | 25 +++++++++++-------- src/Composer/Downloader/ArchiveDownloader.php | 3 ++- src/Composer/Downloader/FileDownloader.php | 14 +++++------ src/Composer/Downloader/PathDownloader.php | 19 ++++++-------- src/Composer/Downloader/VcsDownloader.php | 25 +++++-------------- .../Installer/MetapackageInstaller.php | 13 +++++----- src/Composer/Package/BasePackage.php | 2 +- .../Composer/Test/Package/BasePackageTest.php | 2 +- 10 files changed, 56 insertions(+), 61 deletions(-) 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/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index a78aa95bc..7cf19deee 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -17,6 +17,7 @@ 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 @@ -40,7 +41,7 @@ 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); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 3da51d39b..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; @@ -284,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); @@ -332,12 +335,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ public function update(PackageInterface $initial, PackageInterface $target, $path) { - $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 . "): ", false); + $this->io->writeError(" - " . UpdateOperation::format($initial, $target) . ": ", false); $promise = $this->remove($initial, $path, false); if (!$promise instanceof PromiseInterface) { @@ -360,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.'); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 2cb74d641..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. @@ -82,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); } @@ -124,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; @@ -187,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; @@ -200,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/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/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/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/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'])); From 472a62152d3386dce33df3935d647cb6e508bc81 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 16:09:38 +0200 Subject: [PATCH 33/52] Store default branch info inside metadata --- src/Composer/Package/AliasPackage.php | 5 +++++ src/Composer/Package/Dumper/ArrayDumper.php | 4 ++++ src/Composer/Package/Loader/ArrayLoader.php | 7 +++++++ src/Composer/Package/Package.php | 17 +++++++++++++++++ src/Composer/Package/PackageInterface.php | 5 +++++ src/Composer/Repository/VcsRepository.php | 20 +++++++++++++++++++- 6 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index ee93ec497..b0649ded0 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -392,6 +392,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/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index dece598f1..ce8d0c215 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -92,6 +92,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 228632b42..d24d34d36 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -53,6 +53,9 @@ class ArrayLoader implements LoaderInterface } else { $version = $this->versionParser->normalize($config['version']); } + if (isset($config['default_branch']) && $config['default_branch'] === true) { + $version = '9999999-dev'; + } $package = new $class($config['name'], $version, $config['version']); $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); @@ -75,6 +78,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( diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 6c7b426e7..7ce40a507 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -58,6 +58,7 @@ class Package extends BasePackage protected $devAutoload = array(); protected $includePaths = array(); protected $archiveExcludes = array(); + protected $isDefaultBranch = false; /** * Creates a new in memory package. @@ -569,6 +570,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; + } + /** * Replaces current version and pretty version with passed values. * It also sets stability. diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index cb16efa7e..c09c9e7ba 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -352,6 +352,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/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index ccc49396a..dd3aa83d1 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -149,8 +149,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; } @@ -211,6 +213,9 @@ 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) { @@ -251,6 +256,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) { @@ -280,6 +290,9 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $prefix = substr($branch, 0, 1) === 'v' ? 'v' : ''; $version = $prefix . preg_replace('{(\.9{7})+}', '.x', $parsedBranch); } + if ($driver->getRootIdentifier() === $branch) { + $parsedBranch = '9999999-dev'; + } $cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose); if ($cachedPackage) { @@ -305,6 +318,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'].')'); } From 93d4cf6f918a6743dfd60e764de633215a3d34fa Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 16:37:06 +0200 Subject: [PATCH 34/52] Add --no-show-signature where git supports it, fixes #8966 --- src/Composer/Downloader/GitDownloader.php | 4 +-- src/Composer/Package/Locker.php | 2 +- .../Package/Version/VersionGuesser.php | 4 +-- src/Composer/Repository/PathRepository.php | 3 +- src/Composer/Util/Git.php | 32 +++++++++++++------ .../Package/Version/VersionGuesserTest.php | 14 +++++++- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index edeaa7686..54e023354 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -48,7 +48,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $flag = Platform::isWindows() ? '/D ' : ''; // --dissociate option is only available since git 2.3.0-rc0 - $gitVersion = $this->gitUtil->getVersion(); + $gitVersion = GitUtil::getVersion($this->process); $msg = "Cloning ".$this->getShortHash($ref); $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer && git remote set-url origin %sanitizedUrl% && git remote set-url composer %sanitizedUrl%'; @@ -435,7 +435,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()); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 8a500d837..1adc45dc6 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -422,7 +422,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/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 721887787..fb1ee712b 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -86,7 +86,7 @@ class VersionGuesser if (null !== $versionData && null !== $versionData['version']) { return $this->postprocess($versionData); } - + return null; } @@ -169,7 +169,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; } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 4e860658f..a0a6835b8 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -21,6 +21,7 @@ use Composer\Package\Version\VersionParser; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; +use Composer\Util\Git as GitUtil; /** * This repository allows installing local packages that are not necessarily under their own VCS. @@ -176,7 +177,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/Util/Git.php b/src/Composer/Util/Git.php index 70f62bde2..cdb559712 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; @@ -291,6 +291,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) === '.') { @@ -398,16 +408,18 @@ class Git * * @return string|null The git version number. */ - public function getVersion() + public static function getVersion(ProcessExecutor $process = null) { - 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/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 31c47d72b..1ccea20ee 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\Package\Version\VersionGuesser; use Composer\Semver\VersionParser; use Composer\Test\TestCase; +use Composer\Util\Git as GitUtil; class VersionGuesserTest extends TestCase { @@ -66,7 +67,18 @@ class VersionGuesserTest extends TestCase ->expects($this->at($step)) ->method('execute') ->willReturnCallback(function ($command, &$output) use ($self) { - $self->assertEquals('git log --pretty="%H" -n1 HEAD', $command); + $self->assertEquals('git --version', $command); + + return 0; + }) + ; + + ++$step; + $executor + ->expects($this->at($step)) + ->method('execute') + ->willReturnCallback(function ($command, &$output) use ($self, $executor) { + $self->assertEquals('git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($executor), $command); return 128; }) From 8c0ecf733768ad651d4e50c1f58af51521971d6c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 16:41:33 +0200 Subject: [PATCH 35/52] Clarify why a dev tag was ignored, fixes #8951 --- src/Composer/Repository/VcsRepository.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index dd3aa83d1..be8ed7782 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -219,7 +219,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt // 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; } From fb7fc4a4ca47211651a9370c17cdaaf753cd825c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 16:59:43 +0200 Subject: [PATCH 36/52] Fix git tests --- .../Test/Downloader/GitDownloaderTest.php | 97 ++++++------------- .../Package/Version/VersionGuesserTest.php | 16 +-- 2 files changed, 36 insertions(+), 77 deletions(-) diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 393ecfc5f..5bf75d4f6 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -17,6 +17,8 @@ use Composer\Config; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; +use Composer\Util\Git as GitUtil; class GitDownloaderTest extends TestCase { @@ -29,6 +31,8 @@ class GitDownloaderTest extends TestCase { $this->skipIfNotExecutable('git'); + $this->initGitVersion('1.0.0'); + $this->fs = new Filesystem; $this->workingDir = $this->getUniqueTmpDirectory(); } @@ -39,10 +43,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) @@ -99,32 +108,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)); @@ -150,14 +150,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); @@ -167,7 +160,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) { @@ -175,7 +168,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) { @@ -183,28 +176,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)); @@ -231,50 +224,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)); @@ -315,28 +299,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)); @@ -362,14 +337,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)); diff --git a/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 1ccea20ee..5d8d4f5eb 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -17,6 +17,7 @@ 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 { @@ -31,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() @@ -41,6 +42,8 @@ class VersionGuesserTest extends TestCase $self = $this; $step = 0; + GitUtil::getVersion(new ProcessExecutor); + $executor ->expects($this->at($step)) ->method('execute') @@ -62,17 +65,6 @@ class VersionGuesserTest extends TestCase }) ; - ++$step; - $executor - ->expects($this->at($step)) - ->method('execute') - ->willReturnCallback(function ($command, &$output) use ($self) { - $self->assertEquals('git --version', $command); - - return 0; - }) - ; - ++$step; $executor ->expects($this->at($step)) From 0278e7453dc6026b20788b3ee28c0a754d59452b Mon Sep 17 00:00:00 2001 From: Michael Stucki Date: Mon, 15 Jun 2020 21:42:41 +0200 Subject: [PATCH 37/52] Clean Git repos during discard --- src/Composer/Downloader/GitDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 54e023354..ad8503cea 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -451,7 +451,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()); } From cb1f3899bbef0a0686b03a9702abaf980cff2d15 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 17:32:26 +0200 Subject: [PATCH 38/52] Revert "Store default branch info inside metadata" This reverts commit 472a62152d3386dce33df3935d647cb6e508bc81. --- src/Composer/Package/AliasPackage.php | 5 ----- src/Composer/Package/Dumper/ArrayDumper.php | 4 ---- src/Composer/Package/Loader/ArrayLoader.php | 7 ------- src/Composer/Package/Package.php | 17 ----------------- src/Composer/Package/PackageInterface.php | 5 ----- src/Composer/Repository/VcsRepository.php | 20 +------------------- 6 files changed, 1 insertion(+), 57 deletions(-) diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index b0649ded0..ee93ec497 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -392,11 +392,6 @@ 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/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index ce8d0c215..dece598f1 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -92,10 +92,6 @@ 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 d24d34d36..228632b42 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -53,9 +53,6 @@ class ArrayLoader implements LoaderInterface } else { $version = $this->versionParser->normalize($config['version']); } - if (isset($config['default_branch']) && $config['default_branch'] === true) { - $version = '9999999-dev'; - } $package = new $class($config['name'], $version, $config['version']); $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); @@ -78,10 +75,6 @@ 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( diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 7ce40a507..6c7b426e7 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -58,7 +58,6 @@ class Package extends BasePackage protected $devAutoload = array(); protected $includePaths = array(); protected $archiveExcludes = array(); - protected $isDefaultBranch = false; /** * Creates a new in memory package. @@ -570,22 +569,6 @@ class Package extends BasePackage return $this->archiveExcludes; } - /** - * @param bool $defaultBranch - */ - public function setIsDefaultBranch($defaultBranch) - { - $this->isDefaultBranch = $defaultBranch; - } - - /** - * {@inheritDoc} - */ - public function isDefaultBranch() - { - return $this->isDefaultBranch; - } - /** * Replaces current version and pretty version with passed values. * It also sets stability. diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index c09c9e7ba..cb16efa7e 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -352,11 +352,6 @@ 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/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index be8ed7782..f248420b0 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -149,10 +149,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->loader = new ArrayLoader($this->versionParser); } - $hasRootIdentifierComposerJson = false; try { - $hasRootIdentifierComposerJson = $driver->hasComposerFile($driver->getRootIdentifier()); - if ($hasRootIdentifierComposerJson) { + if ($driver->hasComposerFile($driver->getRootIdentifier())) { $data = $driver->getComposerInformation($driver->getRootIdentifier()); $this->packageName = !empty($data['name']) ? $data['name'] : null; } @@ -213,9 +211,6 @@ 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) { @@ -260,11 +255,6 @@ 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) { @@ -294,9 +284,6 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $prefix = substr($branch, 0, 1) === 'v' ? 'v' : ''; $version = $prefix . preg_replace('{(\.9{7})+}', '.x', $parsedBranch); } - if ($driver->getRootIdentifier() === $branch) { - $parsedBranch = '9999999-dev'; - } $cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose); if ($cachedPackage) { @@ -322,11 +309,6 @@ 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'].')'); } From ce368f8269f75b1382ad4e1488d9f598c47f6e97 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 16:09:38 +0200 Subject: [PATCH 39/52] Store default branch info inside metadata --- src/Composer/Package/AliasPackage.php | 5 +++++ src/Composer/Package/Dumper/ArrayDumper.php | 4 ++++ src/Composer/Package/Loader/ArrayLoader.php | 6 +++++- src/Composer/Package/Package.php | 17 +++++++++++++++++ src/Composer/Package/PackageInterface.php | 5 +++++ src/Composer/Repository/VcsRepository.php | 20 +++++++++++++++++++- 6 files changed, 55 insertions(+), 2 deletions(-) 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/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index 46bcf639a..84af84778 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..314019e17 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/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/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index b663f0f11..f498bdf8e 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,6 +231,9 @@ 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) { @@ -273,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) { @@ -302,6 +312,9 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $prefix = substr($branch, 0, 1) === 'v' ? 'v' : ''; $version = $prefix . preg_replace('{(\.9{7})+}', '.x', $parsedBranch); } + if ($driver->getRootIdentifier() === $branch) { + $parsedBranch = '9999999-dev'; + } $cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose); if ($cachedPackage) { @@ -327,6 +340,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'].')'); } From f70d527f4f9d03bd7d748c373db68dde56ada74f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 17:53:00 +0200 Subject: [PATCH 40/52] Fix git tests --- src/Composer/Util/Git.php | 2 +- .../Test/Downloader/GitDownloaderTest.php | 21 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index c53c0a11a..8a934cf30 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -403,7 +403,7 @@ class Git * * @return string|null The git version number. */ - public static function getVersion(ProcessExecutor $process = null) + public static function getVersion(ProcessExecutor $process) { if (false === self::$version) { self::$version = null; diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 933fa26b1..6618618e1 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -404,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); @@ -440,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) { @@ -479,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)); From 52afa5ef1603f56f3ba309920cd099d13563031d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 17:54:06 +0200 Subject: [PATCH 41/52] Fix handling of default branches --- src/Composer/Repository/VcsRepository.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index f498bdf8e..cbc7a7ec8 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -312,9 +312,6 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $prefix = substr($branch, 0, 1) === 'v' ? 'v' : ''; $version = $prefix . preg_replace('{(\.9{7})+}', '.x', $parsedBranch); } - if ($driver->getRootIdentifier() === $branch) { - $parsedBranch = '9999999-dev'; - } $cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose); if ($cachedPackage) { From 4682efcf77e562178cdcab98c92923204c4fd66d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 09:45:39 +0200 Subject: [PATCH 42/52] Support also default_branch flag in cached versions --- src/Composer/Repository/VcsRepository.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index cbc7a7ec8..d63623ccb 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -313,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); @@ -423,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; @@ -446,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'); From 6f0e5996dec07736c17001fba5be661804b4e32f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 14:12:44 +0200 Subject: [PATCH 43/52] Treat default branch name as a non-feature-branch no matter what it is called --- .../Package/Version/VersionGuesser.php | 27 +++-- .../Package/Version/VersionGuesserTest.php | 102 +++++++++++++++++- 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index b42fb6274..d3f78e5c0 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -154,8 +154,17 @@ class VersionGuesser if ($isFeatureBranch) { $featureVersion = $version; $featurePrettyVersion = $prettyVersion; + + // try to find name of default branch from git info + $defaultBranch = null; + if (0 === $this->process->execute('git remote show origin', $output) || 0 === $this->process->execute('git remote show upstream', $output)) { + if (preg_match('{^ HEAD branch: (.+)$}m', $output, $match)) { + $defaultBranch = trim($match[1]); + } + } + // try to find the best (nearest) version branch to assume this feature's version - $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path); + $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path, $defaultBranch); $version = $result['version']; $prettyVersion = $result['pretty_version']; } @@ -222,7 +231,7 @@ class VersionGuesser $branches = array_keys($driver->getBranches()); // try to find the best (nearest) version branch to assume this feature's version - $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path); + $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path, 'default'); $result['commit'] = ''; $result['feature_version'] = $version; $result['feature_pretty_version'] = $version; @@ -231,7 +240,7 @@ class VersionGuesser } } - private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path) + private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path, $defaultBranch) { $prettyVersion = $version; @@ -248,14 +257,14 @@ 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); + } + 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|trunk|default|develop|\d+\..+)$}', $candidate, $match)) { continue; } diff --git a/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 29619ea45..0b7486afd 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -135,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'; @@ -163,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"; @@ -206,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"; @@ -378,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"; @@ -419,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'; From 923f198a1f433d34e16f42fed104bc65a9272844 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 15:08:01 +0200 Subject: [PATCH 44/52] Fix default-branch attribute on package files, and add it to schema --- res/composer-schema.json | 4 ++++ src/Composer/Package/Dumper/ArrayDumper.php | 2 +- src/Composer/Package/Loader/ArrayLoader.php | 4 ++-- src/Composer/Repository/VcsRepository.php | 12 ++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) 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/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index 84af84778..903997601 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -96,7 +96,7 @@ class ArrayDumper } if ($package->isDefaultBranch()) { - $data['default_branch'] = true; + $data['default-branch'] = true; } $data = $this->dumpValues($package, $keys, $data); diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 314019e17..b2330e6e2 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -124,7 +124,7 @@ class ArrayLoader implements LoaderInterface $package->setInstallationSource($config['installation-source']); } - if (isset($config['default_branch']) && $config['default_branch'] === true) { + if (isset($config['default-branch']) && $config['default-branch'] === true) { $package->setIsDefaultBranch(true); } @@ -368,7 +368,7 @@ class ArrayLoader implements LoaderInterface } } - if (isset($config['default_branch']) && $config['default_branch'] === true) { + if (isset($config['default-branch']) && $config['default-branch'] === true) { return VersionParser::DEV_MASTER_ALIAS; } } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index d63623ccb..f3d7fe56b 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -231,8 +231,8 @@ 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']); + // 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) { @@ -337,9 +337,9 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $data['version'] = $version; $data['version_normalized'] = $parsedBranch; - unset($data['default_branch']); + unset($data['default-branch']); if ($driver->getRootIdentifier() === $branch) { - $data['default_branch'] = true; + $data['default-branch'] = true; } if ($isVeryVerbose) { @@ -446,9 +446,9 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->io->overwriteError($msg, false); } - unset($cachedPackage['default_branch']); + unset($cachedPackage['default-branch']); if ($isDefaultBranch) { - $cachedPackage['default_branch'] = true; + $cachedPackage['default-branch'] = true; } if ($existingPackage = $this->findPackage($cachedPackage['name'], new Constraint('=', $cachedPackage['version_normalized']))) { From 6349764c2db1bdaad3099547512c4acbafe3a2e0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 15:34:28 +0200 Subject: [PATCH 45/52] Fix root package handling of default-branch --- .../Package/Loader/RootPackageLoader.php | 5 +++ .../Package/Version/VersionGuesser.php | 45 ++++++++++++++----- .../Package/Loader/RootPackageLoaderTest.php | 35 +++++++++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) 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/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index d3f78e5c0..8b886fc25 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -110,6 +110,35 @@ 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) + { + GitUtil::cleanEnv(); + if (0 === $this->process->execute('git remote show origin', $output, $path) || 0 === $this->process->execute('git remote show upstream', $output, $path)) { + if (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(); @@ -155,16 +184,8 @@ class VersionGuesser $featureVersion = $version; $featurePrettyVersion = $prettyVersion; - // try to find name of default branch from git info - $defaultBranch = null; - if (0 === $this->process->execute('git remote show origin', $output) || 0 === $this->process->execute('git remote show upstream', $output)) { - if (preg_match('{^ HEAD branch: (.+)$}m', $output, $match)) { - $defaultBranch = trim($match[1]); - } - } - // 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, $defaultBranch); + $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path); $version = $result['version']; $prettyVersion = $result['pretty_version']; } @@ -231,7 +252,7 @@ class VersionGuesser $branches = array_keys($driver->getBranches()); // try to find the best (nearest) version branch to assume this feature's version - $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path, 'default'); + $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path); $result['commit'] = ''; $result['feature_version'] = $version; $result['feature_pretty_version'] = $version; @@ -240,7 +261,7 @@ class VersionGuesser } } - private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path, $defaultBranch) + private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path) { $prettyVersion = $version; @@ -262,6 +283,8 @@ class VersionGuesser 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 . ($defaultBranch ? '|'.preg_quote($defaultBranch) : '').'|master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) { 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 = ""; From d19f5db56810b58b15b5263ce4c1d4181de3f896 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 15:35:09 +0200 Subject: [PATCH 46/52] Fix tests to use default-branch prop --- .../installer/alias-solver-problems.test | 4 +- .../installer/alias-solver-problems2.test | 4 +- .../installer/alias-with-reference.test | 6 ++- .../aliased-priority-conflicting.test | 18 ++++--- .../installer/install-aliased-alias.test | 3 +- .../installer/install-dev-using-dist.test | 6 ++- .../Fixtures/installer/install-reference.test | 11 ++-- ...ed-conflict-does-not-match-dev-master.test | 2 +- .../installer/update-changes-url.test | 54 ++++++++++++------- ...pdate-dev-to-new-ref-picks-up-changes.test | 23 ++++---- .../update-downgrades-unstable-packages.test | 12 +++-- .../update-to-empty-from-locked.test | 6 ++- ...dating-dev-from-lock-removes-old-deps.test | 9 ++-- 13 files changed, 100 insertions(+), 58 deletions(-) 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/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..4cb2dbbb5 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 } ] } @@ -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-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/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-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 521b5611a..15600aefe 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -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/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-- From 7a37e78a30cb195e948e5a2cf2aaf64aa43c5294 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 17:56:13 +0200 Subject: [PATCH 47/52] Speed up installer tests by avoiding lots of bootstrapping and git processes --- src/Composer/Factory.php | 7 ++++- .../installer/install-dev-using-dist.test | 2 +- tests/Composer/Test/InstallerTest.php | 30 ++++++++++++++++--- tests/Composer/Test/Mock/FactoryMock.php | 7 +++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 509fa62b0..8eb2d24cf 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -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); @@ -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/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test index 4cb2dbbb5..3c171a4b0 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -26,7 +26,7 @@ Installs a dev package from lock using dist "minimum-stability": "dev" } --RUN-- -install --prefer-dist +install --EXPECT-LOCK-- { "packages": [ 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) { } From 6d2b5c1950bbf9b761dc84357490f381f7eccb74 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 19 Jun 2020 17:58:21 +0200 Subject: [PATCH 48/52] Add missing class --- .../Composer/Test/Mock/VersionGuesserMock.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/Composer/Test/Mock/VersionGuesserMock.php 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; + } + +} From e55a019b8d876c2babc80436f2828aa562ee5e8b Mon Sep 17 00:00:00 2001 From: Grummfy Date: Fri, 19 Jun 2020 13:32:31 +0200 Subject: [PATCH 49/52] replace toran with private packagist --- doc/articles/http-basic-authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From edd2aa27dbaccf522e4b13067a9914e7f8119126 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 19 Jun 2020 23:31:11 +0200 Subject: [PATCH 50/52] Add missing isVerbose argument to problem formatting call --- src/Composer/DependencyResolver/Rule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)'; } From e76fc2dc39952f1f598b3a05473d7296ca043d48 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 22 Jun 2020 10:48:15 +0200 Subject: [PATCH 51/52] Avoid blocking when guessing default branch name, fixes #9001 --- src/Composer/Package/Version/VersionGuesser.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 8b886fc25..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; @@ -117,9 +118,17 @@ class VersionGuesser */ public function getDefaultBranchName($path) { - GitUtil::cleanEnv(); - if (0 === $this->process->execute('git remote show origin', $output, $path) || 0 === $this->process->execute('git remote show upstream', $output, $path)) { - if (preg_match('{^ HEAD branch: (.+)$}m', $output, $match)) { + 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]); } } @@ -287,7 +296,7 @@ class VersionGuesser foreach ($branches as $candidate) { // do not compare against itself or other feature branches - if ($candidate === $branch || !preg_match('{^(' . $nonFeatureBranches . ($defaultBranch ? '|'.preg_quote($defaultBranch) : '').'|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; } From ae44a5963d18b61e0334295b005de7a4ee28aafc Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 22 Jun 2020 10:54:38 +0200 Subject: [PATCH 52/52] ZipArchive issues fixed in phpstan it seems --- phpstan/config.neon | 3 --- 1 file changed, 3 deletions(-) diff --git a/phpstan/config.neon b/phpstan/config.neon index c8c130a31..06d2ad838 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -26,9 +26,6 @@ parameters: # BC with older PHPUnit - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~' - # ZipArchive::* Class constants are already checked before use. - - '~^Access to undefined constant ZipArchive::~' - paths: - ../src - ../tests