diff --git a/.gitattributes b/.gitattributes index 51b431136..9cd1bb7da 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,4 @@ .travis.yml export-ignore appveyor.yml export-ignore phpunit.xml.dist export-ignore +/phpstan/ export-ignore diff --git a/.travis.yml b/.travis.yml index 64ee75f3a..8c0ee0bc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php -dist: trusty +dist: bionic git: depth: 5 @@ -9,27 +9,40 @@ cache: directories: - $HOME/.composer/cache -addons: - apt: - packages: - - parallel - matrix: include: - php: 5.3 dist: precise - php: 5.4 + dist: trusty - php: 5.5 + dist: trusty - php: 5.6 + dist: xenial - php: 7.0 + dist: xenial - php: 7.1 + dist: xenial - php: 7.2 + dist: xenial - php: 7.3 - - php: nightly + dist: xenial + # Regular 7.4 build with locked deps + - php: 7.4 + env: + - SYMFONY_PHPUNIT_VERSION=7.5 + # High deps check - php: 7.4 env: - deps=high - SYMFONY_PHPUNIT_VERSION=7.5 + # PHPStan checks + - php: 7.4 + env: + - deps=high + - PHPSTAN=1 + - SYMFONY_PHPUNIT_VERSION=7.5 + - php: nightly fast_finish: true allow_failures: - php: nightly @@ -44,9 +57,9 @@ before_install: install: # flags to pass to install - - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress" + - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-progress" # update deps to latest in case of high deps build - - if [ "$deps" == "high" ]; then composer config platform.php 7.2.4; composer update $flags; fi + - if [ "$deps" == "high" ]; then composer config platform.php 7.4.0; composer update $flags; fi # install dependencies using system provided composer binary - composer install $flags # install dependencies using composer from source @@ -58,9 +71,13 @@ before_script: - git config --global user.email travis@example.com script: - - ./vendor/bin/simple-phpunit - # run test suite directories in parallel using GNU parallel -# - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);' + - if [[ $PHPSTAN == "1" ]]; then + bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --no-update && + bin/composer update phpstan/* phpunit/* sebastian/* --with-dependencies && + vendor/bin/phpstan analyse --configuration=phpstan/config.neon; + else + vendor/bin/simple-phpunit; + fi before_deploy: - php -d phar.readonly=0 bin/compile @@ -73,4 +90,4 @@ deploy: on: tags: true repo: composer/composer - php: '7.2' + php: '7.3' diff --git a/appveyor.yml b/appveyor.yml index 922a20e75..15950da41 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ clone_depth: 5 environment: # This sets the PHP version (from Chocolatey) - PHPCI_CHOCO_VERSION: 7.3.1 + PHPCI_CHOCO_VERSION: 7.3.14 PHPCI_CACHE: C:\tools\phpci PHPCI_PHP: C:\tools\phpci\php PHPCI_COMPOSER: C:\tools\phpci\composer @@ -25,6 +25,15 @@ install: - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%" - php -v - IF %PHP%==0 (composer --version) ELSE (composer self-update) + - IF %PHP%==0 cd %PHPCI_PHP% + - IF %PHP%==0 copy php.ini-production php.ini /Y + - IF %PHP%==0 echo date.timezone="UTC" >> php.ini + - IF %PHP%==0 echo extension_dir=ext >> php.ini + - IF %PHP%==0 echo extension=php_openssl.dll >> php.ini + - IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini + - IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini + - IF %PHP%==0 echo extension=php_intl.dll >> php.ini + - IF %PHP%==0 echo extension=php_curl.dll >> php.ini - cd %APPVEYOR_BUILD_FOLDER% - composer install --prefer-dist --no-progress diff --git a/composer.json b/composer.json index 97b271de8..2204a5639 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require": { "php": "^5.3.2 || ^7.0", "composer/ca-bundle": "^1.0", - "composer/semver": "^1.0", + "composer/semver": "^2.0@dev", "composer/spdx-licenses": "^1.2", "composer/xdebug-handler": "^1.1", "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", @@ -34,7 +34,8 @@ "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0", "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0", "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0" + "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "react/promise": "^1.2 || ^2.7" }, "conflict": { "symfony/console": "2.8.38" @@ -55,7 +56,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -65,8 +66,13 @@ }, "autoload-dev": { "psr-4": { - "Composer\\Test\\": "tests/Composer/Test" - } + "Composer\\Test\\": "tests/Composer/Test", + "Composer\\PHPStanRules\\": "phpstan/Rules/src", + "Composer\\PHPStanRulesTests\\": "phpstan/Rules/tests" + }, + "classmap": [ + "phpstan/Rules/tests/data" + ] }, "bin": [ "bin/composer" diff --git a/composer.lock b/composer.lock index 583dff81e..4b5a5e636 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cc6f9640996dfad00a5b03a8be01a571", + "content-hash": "a0a9399315ac0b612d4296b8df745112", "packages": [ { "name": "composer/ca-bundle", @@ -60,20 +60,25 @@ "ssl", "tls" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/master" + }, "time": "2020-01-13T10:02:55+00:00" }, { "name": "composer/semver", - "version": "1.5.1", + "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" + "reference": "4df5ff3249f01018504939d66040d8d2b783d820" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "url": "https://api.github.com/repos/composer/semver/zipball/4df5ff3249f01018504939d66040d8d2b783d820", + "reference": "4df5ff3249f01018504939d66040d8d2b783d820", "shasum": "" }, "require": { @@ -85,7 +90,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -121,7 +126,22 @@ "validation", "versioning" ], - "time": "2020-01-13T12:06:48+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/2.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-03-11T13:41:23+00:00" }, { "name": "composer/spdx-licenses", @@ -181,6 +201,11 @@ "spdx", "validator" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.3" + }, "time": "2020-02-14T07:44:31+00:00" }, { @@ -225,6 +250,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/master" + }, "funding": [ { "url": "https://packagist.com", @@ -353,6 +383,44 @@ }, "time": "2019-11-01T11:05:21+00:00" }, + { + "name": "react/promise", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "eefff597e67ff66b719f8171480add3c91474a1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/eefff597e67ff66b719f8171480add3c91474a1e", + "reference": "eefff597e67ff66b719f8171480add3c91474a1e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "React\\Promise": "src/" + }, + "files": [ + "src/React/Promise/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "time": "2016-03-07T13:46:50+00:00" + }, { "name": "seld/jsonlint", "version": "1.7.2", @@ -448,6 +516,10 @@ "keywords": [ "phar" ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.1.0" + }, "time": "2020-02-14T15:25:33+00:00" }, { @@ -735,6 +807,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/master" + }, "time": "2020-01-13T11:15:53+00:00" }, { @@ -794,6 +869,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/master" + }, "time": "2020-01-13T11:15:53+00:00" }, { @@ -949,6 +1027,16 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/2.x" + }, "time": "2016-01-25T08:17:30+00:00" }, { @@ -1012,6 +1100,10 @@ "spy", "stub" ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, "time": "2020-03-05T15:02:03+00:00" }, { @@ -1076,6 +1168,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/1.2" + }, "time": "2017-01-29T09:50:25+00:00" }, { @@ -1128,6 +1224,10 @@ "keywords": [ "diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/1.4" + }, "time": "2017-05-22T07:24:03+00:00" }, { @@ -1195,6 +1295,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/master" + }, "time": "2016-11-19T08:54:04+00:00" }, { @@ -1248,6 +1352,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + }, "time": "2016-11-19T07:33:16+00:00" }, { @@ -1313,6 +1421,9 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1332,7 +1443,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "composer/semver": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -1342,5 +1455,5 @@ "platform-overrides": { "php": "5.3.9" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 8c634bcfd..db3c8c79c 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, upgrade or remove one dependency, you can explicitly list it as an argument: ```sh php composer.phar update monolog/monolog [...] diff --git a/doc/03-cli.md b/doc/03-cli.md index 29eeb398c..725be430b 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -106,7 +106,6 @@ resolution. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--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. * **--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. @@ -156,9 +155,8 @@ php composer.phar update "vendor/*" * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--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:** Update also dependencies of packages in the argument list, except those which are root requirements. +* **--with-all-dependencies:** Update also dependencies of packages in the argument list, including those which 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. @@ -198,11 +196,11 @@ If you do not specify a package, composer will prompt you to search for a packag ### Options * **--dev:** Add packages to `require-dev`. +* **--dry-run:** Simulate the command without actually doing anything. * **--prefer-source:** Install packages from `source` when available. * **--prefer-dist:** Install packages from `dist` when available. * **--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. * **--no-update:** Disables the automatic update of the dependencies. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--update-no-dev:** Run the dependency update with the `--no-dev` option. @@ -236,6 +234,7 @@ uninstalled. ### Options * **--dev:** Remove packages from `require-dev`. +* **--dry-run:** Simulate the command without actually doing anything. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. * **--no-update:** Disables the automatic update of the dependencies. @@ -408,16 +407,18 @@ Lists all packages suggested by currently installed set of packages. You can optionally pass one or multiple package names in the format of `vendor/package` to limit output to suggestions made by those packages only. -Use the `--by-package` or `--by-suggestion` flags to group the output by +Use the `--by-package` (default) or `--by-suggestion` flags to group the output by the package offering the suggestions or the suggested packages respectively. -Use the `--verbose (-v)` flag to display the suggesting package and the suggestion reason. -This implies `--by-package --by-suggestion`, showing both lists. +If you only want a list of suggested package names, use `--list`. ### Options -* **--by-package:** Groups output by suggesting package. +* **--by-package:** Groups output by suggesting package (default). * **--by-suggestion:** Groups output by suggested package. +* **--all:** Show suggestions from all dependencies, including transitive ones (by + default only direct dependencies' suggestions are shown). +* **--list:** Show only list of suggested package names. * **--no-dev:** Excludes suggestions from `require-dev` packages. ## fund @@ -952,4 +953,9 @@ The env var accepts domains, IP addresses, and IP address blocks in CIDR notation. You can restrict the filter to a particular port (e.g. `:80`). You can also set it to `*` to ignore the proxy for all HTTP requests. +### COMPOSER_DISABLE_NETWORK + +If set to `1`, disables network access (best effort). This can be used for debugging or +to run Composer on a plane or a starship with poor connectivity. + ← [Libraries](02-libraries.md) | [Schema](04-schema.md) → diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 86e24d87b..39974e469 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface if ($protocol === 's3') { $awsClient = new AwsClient($this->io, $this->composer->getConfig()); - $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); - $event->setRemoteFilesystem($s3RemoteFilesystem); + $s3Downloader = new S3Downloader($this->io, $event->getHttpDownloader()->getOptions(), $awsClient); + $event->setHttpdownloader($s3Downloader); } } } diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index 5524eba76..8977aa32d 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -43,8 +43,8 @@ Composer fires the following named events during its execution process: ### Installer Events -- **pre-dependencies-solving**: occurs before the dependencies are resolved. -- **post-dependencies-solving**: occurs after the dependencies have been resolved. +- **pre-operations-exec**: occurs before the install/upgrade/.. operations + are executed when installing a lock file. ### Package Events @@ -61,11 +61,13 @@ Composer fires the following named events during its execution process: - **command**: occurs before any Composer Command is executed on the CLI. It provides you with access to the input and output objects of the program. - **pre-file-download**: occurs before files are downloaded and allows - you to manipulate the `RemoteFilesystem` object prior to downloading files + you to manipulate the `HttpDownloader` object prior to downloading files based on the URL to be downloaded. - **pre-command-run**: occurs before a command is executed and allows you to manipulate the `InputInterface` object's options and arguments to tweak a command's behavior. +- **pre-pool-create**: occurs before the Pool of packages is created, and lets + you filter the list of packages which is going to enter the Solver. > **Note:** Composer makes no assumptions about the state of your dependencies > prior to `install` or `update`. Therefore, you should not specify scripts diff --git a/phpstan/Rules/src/AnonymousFunctionWithThisRule.php b/phpstan/Rules/src/AnonymousFunctionWithThisRule.php new file mode 100644 index 000000000..d9a44ecb5 --- /dev/null +++ b/phpstan/Rules/src/AnonymousFunctionWithThisRule.php @@ -0,0 +1,46 @@ + + */ +final class AnonymousFunctionWithThisRule implements Rule +{ + /** + * @inheritDoc + */ + public function getNodeType(): string + { + return \PhpParser\Node\Expr\Variable::class; + } + + /** + * @inheritDoc + */ + public function processNode(Node $node, Scope $scope): array + { + if (!\is_string($node->name) || $node->name !== 'this') { + return []; + } + + if ($scope->isInClosureBind()) { + return []; + } + + if (!$scope->isInClass()) { + // reported in other standard rule on level 0 + return []; + } + + if ($scope->isInAnonymousFunction()) { + return ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.']; + } + + return []; + } +} diff --git a/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php b/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php new file mode 100644 index 000000000..add285d17 --- /dev/null +++ b/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php @@ -0,0 +1,28 @@ + + */ +final class AnonymousFunctionWithThisRuleTest extends RuleTestCase +{ + /** + * @inheritDoc + */ + protected function getRule(): \PHPStan\Rules\Rule + { + return new AnonymousFunctionWithThisRule(); + } + + public function testWithThis(): void + { + $this->analyse([__DIR__ . '/data/method-with-this.php'], [ + ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 13], + ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 17], + ]); + } +} diff --git a/phpstan/Rules/tests/data/method-with-this.php b/phpstan/Rules/tests/data/method-with-this.php new file mode 100644 index 000000000..e0b15bffa --- /dev/null +++ b/phpstan/Rules/tests/data/method-with-this.php @@ -0,0 +1,34 @@ +firstProp; + }; + + call_user_func(function() { + $this->funMethod(); + }, $this); + + $bind = 'bind'; + function() use($bind) { + + }; + } +} + +function global_ok() { + $_SERVER['REMOTE_ADDR']; +} + +function global_this() { + // not checked by our rule, it is checked with standard phpstan rule on level 0 + $this['REMOTE_ADDR']; +} diff --git a/phpstan/autoload.php b/phpstan/autoload.php new file mode 100644 index 000000000..7d1ed7671 --- /dev/null +++ b/phpstan/autoload.php @@ -0,0 +1,5 @@ +writeError("Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. Skipping."); + } } - // TODO enable in Composer 2.0 - //return array(); + return array(); } - // TODO enable in Composer 2.0 & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses - //return $validClasses; - return array($classes, $validClasses); + return $validClasses; } /** diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 06c6a0996..2a4e7756f 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/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index bbe18a653..fd6d454f5 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -22,6 +22,7 @@ use Composer\Script\ScriptEvents; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\Filesystem; +use Composer\Util\Loop; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -111,8 +112,9 @@ EOT $archiveManager = $composer->getArchiveManager(); } else { $factory = new Factory; - $downloadManager = $factory->createDownloadManager($io, $config); - $archiveManager = $factory->createArchiveManager($config, $downloadManager); + $httpDownloader = $factory->createHttpDownloader($io, $config); + $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader); + $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader)); } if ($packageName) { diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index 888b2a7f2..56ee9f7f4 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -27,6 +27,8 @@ use Symfony\Component\Console\Command\Command; /** * Base class for Composer commands * + * @method Application getApplication() + * * @author Ryan Weaver * @author Konstantin Kudryashov */ @@ -46,7 +48,7 @@ abstract class BaseCommand extends Command * @param bool $required * @param bool|null $disablePlugins * @throws \RuntimeException - * @return Composer + * @return Composer|null */ public function getComposer($required = true, $disablePlugins = null) { @@ -173,7 +175,7 @@ abstract class BaseCommand extends Command if ($input->getOption('prefer-source') || $input->getOption('prefer-dist') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'))) { $preferSource = $input->getOption('prefer-source') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs')); - $preferDist = $input->getOption('prefer-dist'); + $preferDist = (bool) $input->getOption('prefer-dist'); } return array($preferSource, $preferDist); diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 78fe0551e..31975b5ac 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -12,11 +12,12 @@ namespace Composer\Command; -use Composer\DependencyResolver\Pool; use Composer\Package\Link; use Composer\Package\PackageInterface; -use Composer\Repository\ArrayRepository; +use Composer\Repository\InstalledArrayRepository; use Composer\Repository\CompositeRepository; +use Composer\Repository\RootPackageRepository; +use Composer\Repository\InstalledRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; @@ -71,15 +72,12 @@ class BaseDependencyCommand extends BaseCommand $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); - // Prepare repositories and set up a pool $platformOverrides = $composer->getConfig()->get('platform') ?: array(); - $repository = new CompositeRepository(array( - new ArrayRepository(array($composer->getPackage())), + $installedRepo = new InstalledRepository(array( + new RootPackageRepository($composer->getPackage()), $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); - $pool = new Pool(); - $pool->addRepository($repository); // Parse package name and constraint list($needle, $textConstraint) = array_pad( @@ -89,17 +87,17 @@ class BaseDependencyCommand extends BaseCommand ); // Find packages that are or provide the requested package first - $packages = $pool->whatProvides(strtolower($needle)); + $packages = $installedRepo->findPackagesWithReplacersAndProviders($needle); if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } // If the version we ask for is not installed then we need to locate it in remote repos and add it. // This is needed for why-not to resolve conflicts from an uninstalled version against installed packages. - if (!$repository->findPackage($needle, $textConstraint)) { + if (!$installedRepo->findPackage($needle, $textConstraint)) { $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO())); if ($match = $defaultRepos->findPackage($needle, $textConstraint)) { - $repository->addRepository(new ArrayRepository(array(clone $match))); + $installedRepo->addRepository(new InstalledArrayRepository(array(clone $match))); } } @@ -126,7 +124,7 @@ class BaseDependencyCommand extends BaseCommand $recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE); // Resolve dependencies - $results = $repository->getDependents($needles, $constraint, $inverted, $recursive); + $results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive); if (empty($results)) { $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : ''; $this->getIO()->writeError(sprintf( diff --git a/src/Composer/Command/CheckPlatformReqsCommand.php b/src/Composer/Command/CheckPlatformReqsCommand.php index 195a2c490..eba6d0946 100644 --- a/src/Composer/Command/CheckPlatformReqsCommand.php +++ b/src/Composer/Command/CheckPlatformReqsCommand.php @@ -20,6 +20,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\PlatformRepository; +use Composer\Repository\InstalledRepository; class CheckPlatformReqsCommand extends BaseCommand { @@ -48,12 +49,13 @@ EOT $requires = $composer->getPackage()->getRequires(); if ($input->getOption('no-dev')) { - $dependencies = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'))->getPackages(); + $installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev')); + $dependencies = $installedRepo->getPackages(); } else { - $dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages(); + $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); // fallback to lockfile if installed repo is empty - if (!$dependencies) { - $dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages(); + if (!$installedRepo->getPackages()) { + $installedRepo = $composer->getLocker()->getLockedRepository(true); } $requires += $composer->getPackage()->getDevRequires(); } @@ -61,7 +63,8 @@ EOT $requires[$require] = array($link); } - foreach ($dependencies as $package) { + $installedRepo = new InstalledRepository(array($installedRepo)); + foreach ($installedRepo->getPackages() as $package) { foreach ($package->getRequires() as $require => $link) { $requires[$require][] = $link; } @@ -69,19 +72,9 @@ EOT ksort($requires); - $platformRepo = new PlatformRepository(array(), array()); - $currentPlatformPackages = $platformRepo->getPackages(); - $currentPlatformPackageMap = array(); - - /** - * @var PackageInterface $currentPlatformPackage - */ - foreach ($currentPlatformPackages as $currentPlatformPackage) { - $currentPlatformPackageMap[$currentPlatformPackage->getName()] = $currentPlatformPackage; - } + $installedRepo->addRepository(new PlatformRepository(array(), array())); $results = array(); - $exitCode = 0; /** @@ -89,42 +82,62 @@ EOT */ foreach ($requires as $require => $links) { if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) { - if (isset($currentPlatformPackageMap[$require])) { - $pass = true; - $version = $currentPlatformPackageMap[$require]->getVersion(); - - foreach ($links as $link) { - if (!$link->getConstraint()->matches(new Constraint('=', $version))) { - $results[] = array( - $currentPlatformPackageMap[$require]->getPrettyName(), - $currentPlatformPackageMap[$require]->getPrettyVersion(), - $link, - 'failed', - ); - $pass = false; - - $exitCode = max($exitCode, 1); + $candidates = $installedRepo->findPackagesWithReplacersAndProviders($require); + if ($candidates) { + $reqResults = array(); + foreach ($candidates as $candidate) { + if ($candidate->getName() === $require) { + $candidateConstraint = new Constraint('=', $candidate->getVersion()); + $candidateConstraint->setPrettyString($candidate->getPrettyVersion()); + } else { + foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + if ($link->getTarget() === $require) { + $candidateConstraint = $link->getConstraint(); + break; + } + } + } + + foreach ($links as $link) { + if (!$link->getConstraint()->matches($candidateConstraint)) { + $reqResults[] = array( + $candidate->getName() === $require ? $candidate->getPrettyName() : $require, + $candidateConstraint->getPrettyString(), + $link, + 'failed'.($candidate->getName() === $require ? '' : ' provided by '.$candidate->getPrettyName().''), + ); + + // skip to next candidate + continue 2; + } } - } - if ($pass) { $results[] = array( - $currentPlatformPackageMap[$require]->getPrettyName(), - $currentPlatformPackageMap[$require]->getPrettyVersion(), + $candidate->getName() === $require ? $candidate->getPrettyName() : $require, + $candidateConstraint->getPrettyString(), null, - 'success', + 'success'.($candidate->getName() === $require ? '' : ' provided by '.$candidate->getPrettyName().''), ); - } - } else { - $results[] = array( - $require, - 'n/a', - $links[0], - 'missing', - ); - $exitCode = max($exitCode, 2); + // candidate matched, skip to next requirement + continue 2; + } + + // show the first error from every failed candidate + $results = array_merge($results, $reqResults); + $exitCode = max($exitCode, 1); + + continue; } + + $results[] = array( + $require, + 'n/a', + $links[0], + 'missing', + ); + + $exitCode = max($exitCode, 2); } } diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index be1743ba0..2695f0399 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -236,7 +236,7 @@ EOT } $settingKey = $input->getArgument('setting-key'); - if (!$settingKey) { + if (!$settingKey || !is_string($settingKey)) { return 0; } diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 1801583fd..9263ba7ee 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -20,7 +20,6 @@ use Composer\Installer\InstallationManager; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\BasePackage; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\Package\Version\VersionSelector; use Composer\Package\AliasPackage; @@ -28,6 +27,7 @@ use Composer\Repository\RepositoryFactory; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\InstalledFilesystemRepository; +use Composer\Repository\RepositorySet; use Composer\Script\ScriptEvents; use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputArgument; @@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder; use Composer\Json\JsonFile; use Composer\Config\JsonConfigSource; use Composer\Util\Filesystem; +use Composer\Util\Loop; use Composer\Package\Version\VersionParser; /** @@ -182,8 +183,6 @@ EOT $composer = Factory::create($io, null, $disablePlugins); } - $composer->getDownloadManager()->setOutputProgress(!$noProgress); - $fs = new Filesystem(); if ($noScripts === false) { @@ -334,8 +333,8 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $pool = new Pool($stability); - $pool->addRepository($sourceRepo); + $repositorySet = new RepositorySet($stability); + $repositorySet->addRepository($sourceRepo); $phpVersion = null; $prettyPhpVersion = null; @@ -349,7 +348,7 @@ EOT } // find the latest version if there are multiple - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability); if (!$package) { @@ -384,15 +383,17 @@ EOT $package = $package->getAliasOf(); } - $dm = $this->createDownloadManager($io, $config); + $factory = new Factory(); + + $httpDownloader = $factory->createHttpDownloader($io, $config); + $dm = $factory->createDownloadManager($io, $config, $httpDownloader); $dm->setPreferSource($preferSource) - ->setPreferDist($preferDist) - ->setOutputProgress(!$noProgress); + ->setPreferDist($preferDist); $projectInstaller = new ProjectInstaller($directory, $dm); - $im = $this->createInstallationManager(); + $im = $factory->createInstallationManager(new Loop($httpDownloader), $io); $im->addInstaller($projectInstaller); - $im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); + $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package))); $im->notifyInstalls($io); // collect suggestions @@ -408,16 +409,4 @@ EOT return $installedFromVcs; } - - protected function createDownloadManager(IOInterface $io, Config $config) - { - $factory = new Factory(); - - return $factory->createDownloadManager($io, $config); - } - - protected function createInstallationManager() - { - return new InstallationManager(); - } } diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 6c9158630..b2b08e6f0 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -22,7 +22,7 @@ use Composer\Plugin\PluginEvents; use Composer\Util\ConfigValidator; use Composer\Util\IniHelper; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\StreamContextFactory; use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Versions; @@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface; */ class DiagnoseCommand extends BaseCommand { - /** @var RemoteFilesystem */ - protected $rfs; + /** @var HttpDownloader */ + protected $httpDownloader; /** @var ProcessExecutor */ protected $process; @@ -86,7 +86,7 @@ EOT $config->merge(array('config' => array('secure-http' => false))); $config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO); - $this->rfs = Factory::createRemoteFilesystem($io, $config); + $this->httpDownloader = Factory::createHttpDownloader($io, $config); $this->process = new ProcessExecutor($io); $io->write('Checking platform settings: ', false); @@ -156,7 +156,7 @@ EOT $this->outputResult($this->checkVersion($config)); } - $io->write(sprintf('Composer version: %s', Composer::VERSION)); + $io->write(sprintf('Composer version: %s', Composer::getVersion())); $platformOverrides = $config->get('platform') ?: array(); $platformRepo = new PlatformRepository(array(), $platformOverrides); @@ -229,7 +229,7 @@ EOT } try { - $this->rfs->getContents('packagist.org', $proto . '://repo.packagist.org/packages.json', false); + $this->httpDownloader->get($proto . '://repo.packagist.org/packages.json'); } catch (TransportException $e) { if (false !== strpos($e->getMessage(), 'cafile')) { $result[] = '[' . get_class($e) . '] ' . $e->getMessage() . ''; @@ -256,11 +256,11 @@ EOT $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { - $json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/packages.json', false), true); + $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson(); $hash = reset($json['provider-includes']); $hash = $hash['sha256']; $path = str_replace('%hash%', $hash, key($json['provider-includes'])); - $provider = $this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/'.$path, false); + $provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody(); if (hash('sha256', $provider) !== $hash) { return 'It seems that your proxy is modifying http traffic on the fly'; @@ -288,10 +288,10 @@ EOT $url = 'http://repo.packagist.org/packages.json'; try { - $this->rfs->getContents('packagist.org', $url, false); + $this->httpDownloader->get($url); } catch (TransportException $e) { try { - $this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false))); + $this->httpDownloader->get($url, array('http' => array('request_fulluri' => false))); } catch (TransportException $e) { return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')'; } @@ -322,10 +322,10 @@ EOT $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0'; try { - $this->rfs->getContents('github.com', $url, false); + $this->httpDownloader->get($url); } catch (TransportException $e) { try { - $this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false))); + $this->httpDownloader->get($url, array('http' => array('request_fulluri' => false))); } catch (TransportException $e) { return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')'; } @@ -347,7 +347,7 @@ EOT try { $url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/'; - return $this->rfs->getContents($domain, $url, false, array( + return $this->httpDownloader->get($url, array( 'retry-auth-failure' => false, )) ? true : 'Unexpected error'; } catch (\Exception $e) { @@ -377,8 +377,7 @@ EOT } $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; - $json = $this->rfs->getContents($domain, $url, false, array('retry-auth-failure' => false)); - $data = json_decode($json, true); + $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson(); return $data['resources']['core']; } @@ -431,7 +430,7 @@ EOT return $result; } - $versionsUtil = new Versions($config, $this->rfs); + $versionsUtil = new Versions($config, $this->httpDownloader); $latest = $versionsUtil->getLatest(); if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { @@ -613,20 +612,6 @@ EOT $text .= "Install either of them or recompile php without --disable-iconv"; break; - case 'unicode': - $text = PHP_EOL."The detect_unicode setting must be disabled.".PHP_EOL; - $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; - $text .= " detect_unicode = Off"; - $displayIniMessage = true; - break; - - case 'suhosin': - $text = PHP_EOL."The suhosin.executor.include.whitelist setting is incorrect.".PHP_EOL; - $text .= "Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):".PHP_EOL; - $text .= " suhosin.executor.include.whitelist = phar ".$current; - $displayIniMessage = true; - break; - case 'php': $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; break; @@ -729,7 +714,7 @@ EOT /** * Check if allow_url_fopen is ON * - * @return bool|string + * @return true|string */ private function checkConnectivity() { diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index b7d907066..8e43f39a4 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -14,7 +14,7 @@ namespace Composer\Command; use Composer\Package\CompletePackageInterface; use Composer\Repository\RepositoryInterface; -use Composer\Repository\ArrayRepository; +use Composer\Repository\RootPackageRepository; use Composer\Repository\RepositoryFactory; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; @@ -157,7 +157,7 @@ EOT if ($composer) { return array_merge( - array(new ArrayRepository(array($composer->getPackage()))), // root package + array(new RootPackageRepository($composer->getPackage())), // root package array($composer->getRepositoryManager()->getLocalRepository()), // installed packages $composer->getRepositoryManager()->getRepositories() // remotes ); diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index d234a8cba..8f101515c 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -12,7 +12,6 @@ namespace Composer\Command; -use Composer\DependencyResolver\Pool; use Composer\Factory; use Composer\Json\JsonFile; use Composer\Package\BasePackage; @@ -22,6 +21,7 @@ use Composer\Package\Version\VersionSelector; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; +use Composer\Repository\RepositorySet; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; @@ -42,8 +42,8 @@ class InitCommand extends BaseCommand /** @var array */ private $gitConfig; - /** @var Pool[] */ - private $pools; + /** @var RepositorySet[] */ + private $repositorySets; /** * {@inheritdoc} @@ -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']); @@ -688,16 +688,16 @@ EOT return false !== filter_var($email, FILTER_VALIDATE_EMAIL); } - private function getPool(InputInterface $input, $minimumStability = null) + private function getRepositorySet(InputInterface $input, $minimumStability = null) { $key = $minimumStability ?: 'default'; - if (!isset($this->pools[$key])) { - $this->pools[$key] = $pool = new Pool($minimumStability ?: $this->getMinimumStability($input)); - $pool->addRepository($this->getRepos()); + if (!isset($this->repositorySets[$key])) { + $this->repositorySets[$key] = $repositorySet = new RepositorySet($minimumStability ?: $this->getMinimumStability($input)); + $repositorySet->addRepository($this->getRepos()); } - return $this->pools[$key]; + return $this->repositorySets[$key]; } private function getMinimumStability(InputInterface $input) @@ -733,8 +733,8 @@ EOT */ private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null) { - // find the latest version allowed in this pool - $versionSelector = new VersionSelector($this->getPool($input, $minimumStability)); + // find the latest version allowed in this repo set + $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability)); $ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs'); // ignore phpVersion if platform requirements are ignored diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 32fb1bdc6..d059928cc 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -44,7 +44,6 @@ class InstallCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), 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('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`.'), @@ -86,7 +85,6 @@ EOT } $composer = $this->getComposer(true, $input->getOption('no-plugins')); - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -108,7 +106,6 @@ EOT ->setDevMode(!$input->getOption('no-dev')) ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setRunScripts(!$input->getOption('no-scripts')) - ->setSkipSuggest($input->getOption('no-suggest')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index e4407d4cb..7ae8abbe2 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Config\JsonConfigSource; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; @@ -38,6 +39,7 @@ class RemoveCommand extends BaseCommand ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), @@ -92,26 +94,44 @@ EOT } } + $dryRun = $input->getOption('dry-run'); + $toRemove = array(); foreach ($packages as $package) { if (isset($composer[$type][$package])) { - $json->removeLink($type, $composer[$type][$package]); + if ($dryRun) { + $toRemove[$type][] = $composer[$type][$package]; + } else { + $json->removeLink($type, $composer[$type][$package]); + } } elseif (isset($composer[$altType][$package])) { $io->writeError('' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ', true)) { - $json->removeLink($altType, $composer[$altType][$package]); + if ($dryRun) { + $toRemove[$altType][] = $composer[$altType][$package]; + } else { + $json->removeLink($altType, $composer[$altType][$package]); + } } } } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) { foreach ($matches as $matchedPackage) { - $json->removeLink($type, $matchedPackage); + if ($dryRun) { + $toRemove[$type][] = $matchedPackage; + } else { + $json->removeLink($type, $matchedPackage); + } } } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) { foreach ($matches as $matchedPackage) { $io->writeError('' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ', true)) { - $json->removeLink($altType, $matchedPackage); + if ($dryRun) { + $toRemove[$altType][] = $matchedPackage; + } else { + $json->removeLink($altType, $matchedPackage); + } } } } @@ -127,7 +147,21 @@ EOT // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins')); - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); + + if ($dryRun) { + $rootPackage = $composer->getPackage(); + $links = array( + 'require' => $rootPackage->getRequires(), + 'require-dev' => $rootPackage->getDevRequires(), + ); + foreach ($toRemove as $type => $packages) { + foreach ($packages as $package) { + unset($links[$type][$package]); + } + } + $rootPackage->setRequires($links['require']); + $rootPackage->setDevRequires($links['require-dev']); + } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -146,10 +180,11 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($input->getOption('no-update-with-dependencies') ? Request::UPDATE_ONLY_LISTED : Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) + ->setDryRun($dryRun) ; $status = $install->run(); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index cbdfdaf9c..6fd54887e 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\DependencyResolver\Request; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -21,6 +22,8 @@ use Composer\Installer; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Package\Version\VersionParser; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\BasePackage; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Repository\CompositeRepository; @@ -35,9 +38,14 @@ use Composer\Util\Silencer; class RequireCommand extends InitCommand { private $newlyCreated; + private $firstRequire; private $json; private $file; private $composerBackup; + /** @var string file name */ + private $lock; + /** @var ?string contents before modification if the lock file exists */ + private $lockBackup; protected function configure() { @@ -47,16 +55,19 @@ class RequireCommand extends InitCommand ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), 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('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), new InputOption('update-with-dependencies', null, InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'), new InputOption('update-with-all-dependencies', null, InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'), + new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-dependencies'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), @@ -113,7 +124,9 @@ EOT } $this->json = new JsonFile($this->file); + $this->lock = Factory::getLockFile($this->file); $this->composerBackup = file_get_contents($this->json->getPath()); + $this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null; // check for writability by writing to the file as is_writable can not be trusted on network-mounts // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 @@ -186,7 +199,15 @@ EOT $sortPackages = $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages'); - if (!$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { + $this->firstRequire = $this->newlyCreated; + if (!$this->firstRequire) { + $composerDefinition = $this->json->read(); + if (empty($composerDefinition['require']) && empty($composerDefinition['require-dev'])) { + $this->firstRequire = true; + } + } + + if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { $composerDefinition = $this->json->read(); foreach ($requirements as $package => $version) { $composerDefinition[$requireKey][$package] = $version; @@ -202,51 +223,78 @@ EOT } try { - return $this->doUpdate($input, $output, $io, $requirements); + return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey); } catch (\Exception $e) { $this->revertComposerFile(false); throw $e; } } - private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements) + private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, $requireKey, $removeKey) { // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins')); - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); + + if ($input->getOption('dry-run')) { + $rootPackage = $composer->getPackage(); + $links = array( + 'require' => $rootPackage->getRequires(), + 'require-dev' => $rootPackage->getDevRequires(), + ); + $loader = new ArrayLoader(); + $newLinks = $loader->parseLinks($rootPackage->getName(), $rootPackage->getPrettyVersion(), BasePackage::$supportedLinkTypes[$requireKey]['description'], $requirements); + $links[$requireKey] = array_merge($links[$requireKey], $newLinks); + foreach ($requirements as $package => $constraint) { + unset($links[$removeKey][$package]); + } + $rootPackage->setRequires($links['require']); + $rootPackage->setDevRequires($links['require-dev']); + } $updateDevMode = !$input->getOption('update-no-dev'); $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } elseif ($input->getOption('update-with-dependencies') || $input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + } + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $install = Installer::create($io, $composer); $install + ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) ->setPreferSource($input->getOption('prefer-source')) ->setPreferDist($input->getOption('prefer-dist')) ->setDevMode($updateDevMode) ->setRunScripts(!$input->getOption('no-scripts')) - ->setSkipSuggest($input->getOption('no-suggest')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist(array_keys($requirements)) - ->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies')) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) + ->setDryRun($input->getOption('dry-run')) ; + // if no lock is present, or the file is brand new, we do not do a + // partial update as this is not supported by the Installer + if (!$this->firstRequire && $composer->getConfig()->get('lock')) { + $install->setUpdateAllowList(array_keys($requirements)); + } + $status = $install->run(); - if ($status !== 0) { + if ($status !== 0 || $input->getOption('dry-run')) { $this->revertComposerFile(false); } @@ -285,9 +333,19 @@ EOT if ($this->newlyCreated) { $io->writeError("\n".'Installation failed, deleting '.$this->file.'.'); unlink($this->json->getPath()); + if (file_exists($this->lock)) { + unlink($this->lock); + } } else { - $io->writeError("\n".'Installation failed, reverting '.$this->file.' to its original content.'); + $msg = ' to its '; + if ($this->lockBackup) { + $msg = ' and '.$this->lock.' to their '; + } + $io->writeError("\n".'Installation failed, reverting '.$this->file.$msg.'original content.'); file_put_contents($this->json->getPath(), $this->composerBackup); + if ($this->lockBackup) { + file_put_contents($this->lock, $this->lockBackup); + } } if ($hardExit) { diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 0dba48e28..356523492 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -77,9 +77,9 @@ EOT } $io = $this->getIO(); - $remoteFilesystem = Factory::createRemoteFilesystem($io, $config); + $httpDownloader = Factory::createHttpDownloader($io, $config); - $versionsUtil = new Versions($config, $remoteFilesystem); + $versionsUtil = new Versions($config, $httpDownloader); // switch channel if requested foreach (array('stable', 'preview', 'snapshot') as $channel) { @@ -154,11 +154,11 @@ EOT $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion); - $io->write(sprintf("Updating to version %s (%s channel).", $updateVersion, $versionsUtil->getChannel())); + $io->write(sprintf("Upgrading to version %s (%s channel).", $updateVersion, $versionsUtil->getChannel())); $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar'); - $signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false); + $signature = $httpDownloader->get($remoteFilename.'.sig')->getBody(); $io->writeError(' ', false); - $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress')); + $httpDownloader->copy($remoteFilename, $tempFilename); $io->writeError(''); if (!file_exists($tempFilename) || !$signature) { diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index b515cc6a9..6cf1905f5 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -14,7 +14,6 @@ namespace Composer\Command; use Composer\Composer; use Composer\DependencyResolver\DefaultPolicy; -use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; @@ -23,12 +22,14 @@ use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; -use Composer\Repository\ArrayRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; +use Composer\Repository\InstalledRepository; use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositorySet; +use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; @@ -52,8 +53,8 @@ class ShowCommand extends BaseCommand protected $versionParser; protected $colors; - /** @var Pool */ - private $pool; + /** @var RepositorySet */ + private $repositorySet; protected function configure() { @@ -152,13 +153,14 @@ EOT if ($input->getOption('self')) { $package = $this->getComposer()->getPackage(); - $repos = $installedRepo = new ArrayRepository(array($package)); + $repos = $installedRepo = new InstalledRepository(array(new RootPackageRepository($package))); } elseif ($input->getOption('platform')) { - $repos = $installedRepo = $platformRepo; + $repos = $installedRepo = new InstalledRepository(array($platformRepo)); } elseif ($input->getOption('available')) { - $installedRepo = $platformRepo; + $installedRepo = new InstalledRepository(array($platformRepo)); if ($composer) { $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); + $installedRepo->addRepository($composer->getRepositoryManager()->getLocalRepository()); } else { $defaultRepos = RepositoryFactory::defaultRepos($io); $repos = new CompositeRepository($defaultRepos); @@ -166,15 +168,15 @@ EOT } } elseif ($input->getOption('all') && $composer) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); - $installedRepo = new CompositeRepository(array($localRepo, $platformRepo)); + $installedRepo = new InstalledRepository(array($localRepo, $platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); } elseif ($input->getOption('all')) { $defaultRepos = RepositoryFactory::defaultRepos($io); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); - $installedRepo = $platformRepo; + $installedRepo = new InstalledRepository(array($platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } else { - $repos = $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); + $repos = $installedRepo = new InstalledRepository(array($this->getComposer()->getRepositoryManager()->getLocalRepository())); $rootPkg = $this->getComposer()->getPackage(); if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) { $io->writeError('No dependencies installed. Try running composer install or update.'); @@ -313,16 +315,13 @@ EOT foreach ($repos as $repo) { if ($repo === $platformRepo) { $type = 'platform'; - } elseif ( - $repo === $installedRepo - || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true)) - ) { + } elseif ($repo === $installedRepo || in_array($repo, $installedRepo->getRepositories(), true)) { $type = 'installed'; } else { $type = 'available'; } - if ($repo instanceof ComposerRepository && $repo->hasProviders()) { - foreach ($repo->getProviderNames() as $name) { + if ($repo instanceof ComposerRepository) { + foreach ($repo->getPackageNames() as $name) { if (!$packageFilter || preg_match($packageFilter, $name)) { $packages[$type][$name] = $name; } @@ -528,32 +527,27 @@ EOT /** * finds a package by name and version if provided * - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param RepositoryInterface $repos * @param string $name * @param ConstraintInterface|string $version * @throws \InvalidArgumentException * @return array array(CompletePackageInterface, array of versions) */ - protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null) + protected function getPackage(InstalledRepository $installedRepo, RepositoryInterface $repos, $name, $version = null) { $name = strtolower($name); $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); - $pool = new Pool('dev'); - $pool->addRepository($repos); + $repositorySet = new RepositorySet('dev'); + $repositorySet->allowInstalledRepositories(); + $repositorySet->addRepository($repos); $matchedPackage = null; $versions = array(); - $matches = $pool->whatProvides($name, $constraint); + $matches = $repositorySet->findPackages($name, $constraint); foreach ($matches as $index => $package) { - // skip providers/replacers - if ($package->getName() !== $name) { - unset($matches[$index]); - continue; - } - // select an exact match if it is in the installed repo and no specific version was required if (null === $version && $installedRepo->hasPackage($package)) { $matchedPackage = $package; @@ -563,8 +557,10 @@ EOT $matches[$index] = $package->getId(); } + $pool = $repositorySet->createPoolForPackage($name); + // select preferred package according to policy rules - if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) { + if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { $matchedPackage = $pool->literalToPackage($preferred[0]); } @@ -576,10 +572,10 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param PackageInterface|null $latestPackage */ - protected function printPackageInfo(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null) + protected function printPackageInfo(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $io = $this->getIO(); @@ -604,10 +600,10 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param PackageInterface|null $latestPackage */ - protected function printMeta(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null) + protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $io = $this->getIO(); $io->write('name : ' . $package->getPrettyName()); @@ -676,19 +672,21 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo */ - protected function printVersions(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo) + protected function printVersions(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo) { - uasort($versions, 'version_compare'); - $versions = array_keys(array_reverse($versions)); + $versions = array_keys($versions); + $versions = Semver::rsort($versions); // highlight installed version - if ($installedRepo->hasPackage($package)) { - $installedVersion = $package->getPrettyVersion(); - $key = array_search($installedVersion, $versions); - if (false !== $key) { - $versions[$key] = '* ' . $installedVersion . ''; + if ($installedPackages = $installedRepo->findPackages($package->getName())) { + foreach ($installedPackages as $installedPackage) { + $installedVersion = $installedPackage->getPrettyVersion(); + $key = array_search($installedVersion, $versions); + if (false !== $key) { + $versions[$key] = '* ' . $installedVersion . ''; + } } } @@ -752,10 +750,10 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param PackageInterface|null $latestPackage */ - protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null) + protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $json = array( 'name' => $package->getPrettyName(), @@ -975,15 +973,15 @@ EOT /** * Generate the package tree * - * @param PackageInterface $package - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $distantRepos + * @param PackageInterface $package + * @param InstalledRepository $installedRepo + * @param RepositoryInterface $remoteRepos * @return array */ protected function generatePackageTree( PackageInterface $package, - RepositoryInterface $installedRepo, - RepositoryInterface $distantRepos + InstalledRepository $installedRepo, + RepositoryInterface $remoteRepos ) { $requires = $package->getRequires(); ksort($requires); @@ -996,7 +994,7 @@ EOT 'version' => $require->getPrettyConstraint(), ); - $deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree); + $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $packagesInTree); if ($deepChildren) { $treeChildDesc['requires'] = $deepChildren; @@ -1020,10 +1018,10 @@ EOT /** * Display a package tree * - * @param PackageInterface|string $package - * @param array $packagesInTree - * @param string $previousTreeBar - * @param int $level + * @param array|string $package + * @param array $packagesInTree + * @param string $previousTreeBar + * @param int $level */ protected function displayTree( $package, @@ -1032,7 +1030,7 @@ EOT $level = 1 ) { $previousTreeBar = str_replace('├', '│', $previousTreeBar); - if (isset($package['requires'])) { + if (is_array($package) && isset($package['requires'])) { $requires = $package['requires']; $treeBar = $previousTreeBar . ' ├'; $i = 0; @@ -1075,22 +1073,22 @@ EOT * * @param string $name * @param PackageInterface|string $package - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $distantRepos + * @param InstalledRepository $installedRepo + * @param RepositoryInterface $remoteRepos * @param array $packagesInTree * @return array */ protected function addTree( $name, $package, - RepositoryInterface $installedRepo, - RepositoryInterface $distantRepos, + InstalledRepository $installedRepo, + RepositoryInterface $remoteRepos, array $packagesInTree ) { $children = array(); list($package, $versions) = $this->getPackage( $installedRepo, - $distantRepos, + $remoteRepos, $name, $package->getPrettyConstraint() === 'self.version' ? $package->getConstraint() : $package->getPrettyConstraint() ); @@ -1107,7 +1105,7 @@ EOT if (!in_array($requireName, $currentTree, true)) { $currentTree[] = $requireName; - $deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $currentTree); + $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $currentTree); if ($deepChildren) { $treeChildDesc['requires'] = $deepChildren; } @@ -1165,13 +1163,13 @@ EOT * @param string $phpVersion * @param bool $minorOnly * - * @return PackageInterface|null + * @return PackageInterface|false */ private function findLatestPackage(PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false) { - // find the latest version allowed in this pool + // find the latest version allowed in this repo set $name = $package->getName(); - $versionSelector = new VersionSelector($this->getPool($composer)); + $versionSelector = new VersionSelector($this->getRepositorySet($composer)); $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); if (isset($flags[$name])) { @@ -1195,13 +1193,13 @@ EOT return $versionSelector->findBestCandidate($name, $targetVersion, $phpVersion, $bestStability); } - private function getPool(Composer $composer) + private function getRepositorySet(Composer $composer) { - if (!$this->pool) { - $this->pool = new Pool($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); - $this->pool->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); + if (!$this->repositorySet) { + $this->repositorySet = new RepositorySet($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); + $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } - return $this->pool; + return $this->repositorySet; } } diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index 116d5b99e..b9708c3db 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -90,7 +90,7 @@ EOT // list packages foreach ($installedRepo->getCanonicalPackages() as $package) { - $downloader = $dm->getDownloaderForInstalledPackage($package); + $downloader = $dm->getDownloaderForPackage($package); $targetDir = $im->getInstallPath($package); if ($downloader instanceof ChangeReportInterface) { diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 0769f62f9..61875ba0b 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -13,6 +13,9 @@ namespace Composer\Command; use Composer\Repository\PlatformRepository; +use Composer\Repository\RootPackageRepository; +use Composer\Repository\CompositeRepository; +use Composer\Installer\SuggestedPackagesReporter; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,8 +29,10 @@ class SuggestsCommand extends BaseCommand ->setName('suggests') ->setDescription('Shows package suggestions.') ->setDefinition(array( - new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package'), + new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'), new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'), + new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'), + new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'), )) @@ -36,118 +41,66 @@ class SuggestsCommand extends BaseCommand The %command.name% command shows a sorted list of suggested packages. -Enabling -v implies --by-package --by-suggestion, showing both lists. - Read more at https://getcomposer.org/doc/03-cli.md#suggests EOT ) ; } + /** + * {@inheritDoc} + */ protected function execute(InputInterface $input, OutputInterface $output) { - $lock = $this->getComposer()->getLocker()->getLockData(); + $composer = $this->getComposer(); - if (empty($lock)) { - throw new \RuntimeException('Lockfile seems to be empty?'); + $installedRepos = array( + new RootPackageRepository(clone $composer->getPackage()), + ); + + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides()); + $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev')); + } else { + $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); + $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } - $packages = $lock['packages']; - - if (!$input->getOption('no-dev')) { - $packages += $lock['packages-dev']; - } + $installedRepo = new CompositeRepository($installedRepos); + $reporter = new SuggestedPackagesReporter($this->getIO()); $filter = $input->getArgument('packages'); - - // First assemble lookup list of packages that are installed, replaced or provided - $installed = array(); - foreach ($packages as $package) { - $installed[] = $package['name']; - - if (!empty($package['provide'])) { - $installed = array_merge($installed, array_keys($package['provide'])); - } - - if (!empty($package['replace'])) { - $installed = array_merge($installed, array_keys($package['replace'])); - } + if (empty($filter) && !$input->getOption('all')) { + $filter = array_map(function ($link) { + return $link->getTarget(); + }, array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires())); } - - // Undub and sort the install list into a sorted lookup array - $installed = array_flip($installed); - ksort($installed); - - // Init platform repo - $platform = new PlatformRepository(array(), $this->getComposer()->getConfig()->get('platform') ?: array()); - - // Next gather all suggestions that are not in that list - $suggesters = array(); - $suggested = array(); - foreach ($packages as $package) { - $packageName = $package['name']; - if ((!empty($filter) && !in_array($packageName, $filter)) || empty($package['suggest'])) { + foreach ($installedRepo->getPackages() as $package) { + if (!empty($filter) && !in_array($package->getName(), $filter)) { continue; } - foreach ($package['suggest'] as $suggestion => $reason) { - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $suggestion) && null !== $platform->findPackage($suggestion, '*')) { - continue; - } - if (!isset($installed[$suggestion])) { - $suggesters[$packageName][$suggestion] = $reason; - $suggested[$suggestion][$packageName] = $reason; - } - } - } - ksort($suggesters); - ksort($suggested); - // Determine output mode - $mode = 0; + $reporter->addSuggestionsFromPackage($package); + } + + // Determine output mode, default is by-package + $mode = SuggestedPackagesReporter::MODE_BY_PACKAGE; $io = $this->getIO(); - if ($input->getOption('by-package') || $io->isVerbose()) { - $mode |= 1; - } + // if by-suggestion is given we override the default if ($input->getOption('by-suggestion')) { - $mode |= 2; + $mode = SuggestedPackagesReporter::MODE_BY_SUGGESTION; + } + // unless by-package is also present then we enable both + if ($input->getOption('by-package')) { + $mode |= SuggestedPackagesReporter::MODE_BY_PACKAGE; + } + // list is exclusive and overrides everything else + if ($input->getOption('list')) { + $mode = SuggestedPackagesReporter::MODE_LIST; } - // Simple mode - if ($mode === 0) { - foreach (array_keys($suggested) as $suggestion) { - $io->write(sprintf('%s', $suggestion)); - } - - return 0; - } - - // Grouped by package - if ($mode & 1) { - foreach ($suggesters as $suggester => $suggestions) { - $io->write(sprintf('%s suggests:', $suggester)); - - foreach ($suggestions as $suggestion => $reason) { - $io->write(sprintf(' - %s: %s', $suggestion, $reason ?: '*')); - } - $io->write(''); - } - } - - // Grouped by suggestion - if ($mode & 2) { - // Improve readability in full mode - if ($mode & 1) { - $io->write(str_repeat('-', 78)); - } - foreach ($suggested as $suggestion => $suggesters) { - $io->write(sprintf('%s is suggested by:', $suggestion)); - - foreach ($suggesters as $suggester => $reason) { - $io->write(sprintf(' - %s: %s', $suggester, $reason ?: '*')); - } - $io->write(''); - } - } + $reporter->output($mode, $installedRepo); return 0; } diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index e68c265c0..840f7ae28 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Composer; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\IO\IOInterface; use Composer\Plugin\CommandEvent; @@ -48,9 +49,8 @@ class UpdateCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), 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, 'Update also dependencies of packages in the argument list, except those which are root requirements.'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, including those which are root requirements.'), 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`.'), @@ -121,7 +121,18 @@ EOT } } - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); + // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead + // they are further mutually exclusive with listing actual package names + $filteredPackages = array_filter($packages, function ($package) { + return !in_array($package, array('lock', 'nothing', 'mirrors'), true); + }); + $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); + $packages = $filteredPackages; + + if ($updateMirrors && !empty($packages)) { + $io->writeError('You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.'); + return -1; + } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -135,6 +146,13 @@ EOT $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } elseif ($input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + } + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) @@ -143,14 +161,13 @@ EOT ->setDevMode(!$input->getOption('no-dev')) ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setRunScripts(!$input->getOption('no-scripts')) - ->setSkipSuggest($input->getOption('no-suggest')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateMirrors($updateMirrors) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index a4b8c2a92..41e834993 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -124,6 +124,7 @@ class Compiler ->in(__DIR__.'/../../vendor/composer/ca-bundle/') ->in(__DIR__.'/../../vendor/composer/xdebug-handler/') ->in(__DIR__.'/../../vendor/psr/') + ->in(__DIR__.'/../../vendor/react/') ->sort($finderSort) ; diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 99f4756b0..b5bbb0a0b 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -53,7 +53,7 @@ class Composer const VERSION = '@package_version@'; const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; const RELEASE_DATE = '@release_date@'; - const SOURCE_VERSION = '1.10-dev+source'; + const SOURCE_VERSION = '2.0-dev+source'; public static function getVersion() { diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 6084d66e8..67634f0c5 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -265,7 +265,7 @@ class JsonConfigSource implements ConfigSourceInterface * * @param array $array * @param mixed $value - * @return array + * @return int */ private function arrayUnshiftRef(&$array, &$value) { diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index cc39c587b..9875b69c1 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -230,6 +230,12 @@ class Application extends BaseApplication if (function_exists('posix_getuid') && posix_getuid() === 0) { if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { $io->writeError('Do not run Composer as root/super user! See https://getcomposer.org/root for details'); + + if ($io->isInteractive()) { + if (!$io->askConfirmation('Continue as root/super user [yes]? ', true)) { + return 1; + } + } } if ($uid = (int) getenv('SUDO_UID')) { // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on @@ -292,7 +298,7 @@ class Application extends BaseApplication return $result; } catch (ScriptExecutionException $e) { - return $e->getCode(); + return (int) $e->getCode(); } catch (\Exception $e) { $this->hintCommonErrors($e); restore_error_handler(); diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 542c6e625..859410752 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -44,54 +44,33 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version, true); } - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false) + public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null) { - $packages = array(); + $packages = $this->groupLiteralsByName($pool, $literals); - foreach ($pool->whatProvides($package->getName(), null, $mustMatchName) as $candidate) { - if ($candidate !== $package) { - $packages[] = $candidate; - } - } - - return $packages; - } - - public function getPriority(Pool $pool, PackageInterface $package) - { - return $pool->getPriority($package->getRepository()); - } - - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null) - { - $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals); - - foreach ($packages as &$literals) { + foreach ($packages as &$nameLiterals) { $policy = $this; - usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); + usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) { + return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); }); } - foreach ($packages as &$literals) { - $literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals); - - $literals = $this->pruneToBestVersion($pool, $literals); - - $literals = $this->pruneRemoteAliases($pool, $literals); + foreach ($packages as &$sortedLiterals) { + $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals); + $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } $selected = call_user_func_array('array_merge', $packages); // now sort the result across all packages to respect replaces across packages - usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); + usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) { + return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); }); return $selected; } - protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals) + protected function groupLiteralsByName(Pool $pool, $literals) { $packages = array(); foreach ($literals as $literal) { @@ -100,12 +79,7 @@ class DefaultPolicy implements PolicyInterface if (!isset($packages[$packageName])) { $packages[$packageName] = array(); } - - if (isset($installedMap[abs($literal)])) { - array_unshift($packages[$packageName], $literal); - } else { - $packages[$packageName][] = $literal; - } + $packages[$packageName][] = $literal; } return $packages; @@ -114,61 +88,49 @@ class DefaultPolicy implements PolicyInterface /** * @protected */ - public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) + public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) { - if ($a->getRepository() === $b->getRepository()) { - // prefer aliases to the original package - if ($a->getName() === $b->getName()) { - $aAliased = $a instanceof AliasPackage; - $bAliased = $b instanceof AliasPackage; - if ($aAliased && !$bAliased) { - return -1; // use a - } - if (!$aAliased && $bAliased) { - return 1; // use b - } + // prefer aliases to the original package + if ($a->getName() === $b->getName()) { + $aAliased = $a instanceof AliasPackage; + $bAliased = $b instanceof AliasPackage; + if ($aAliased && !$bAliased) { + return -1; // use a } - - if (!$ignoreReplace) { - // return original, not replaced - if ($this->replaces($a, $b)) { - return 1; // use b - } - if ($this->replaces($b, $a)) { - return -1; // use a - } - - // for replacers not replacing each other, put a higher prio on replacing - // packages with the same vendor as the required package - if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { - $requiredVendor = substr($requiredPackage, 0, $pos); - - $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor; - $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor; - - if ($bIsSameVendor !== $aIsSameVendor) { - return $aIsSameVendor ? -1 : 1; - } - } + if (!$aAliased && $bAliased) { + return 1; // use b } - - // priority equal, sort by package id to make reproducible - if ($a->id === $b->id) { - return 0; - } - - return ($a->id < $b->id) ? -1 : 1; } - if (isset($installedMap[$a->id])) { - return -1; + if (!$ignoreReplace) { + // return original, not replaced + if ($this->replaces($a, $b)) { + return 1; // use b + } + if ($this->replaces($b, $a)) { + return -1; // use a + } + + // for replacers not replacing each other, put a higher prio on replacing + // packages with the same vendor as the required package + if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { + $requiredVendor = substr($requiredPackage, 0, $pos); + + $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor; + $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor; + + if ($bIsSameVendor !== $aIsSameVendor) { + return $aIsSameVendor ? -1 : 1; + } + } } - if (isset($installedMap[$b->id])) { - return 1; + // priority equal, sort by package id to make reproducible + if ($a->id === $b->id) { + return 0; } - return ($this->getPriority($pool, $a) > $this->getPriority($pool, $b)) ? -1 : 1; + return ($a->id < $b->id) ? -1 : 1; } /** @@ -218,37 +180,6 @@ class DefaultPolicy implements PolicyInterface return $bestLiterals; } - /** - * Assumes that installed packages come first and then all highest priority packages - */ - protected function pruneToHighestPriorityOrInstalled(Pool $pool, array $installedMap, array $literals) - { - $selected = array(); - - $priority = null; - - foreach ($literals as $literal) { - $package = $pool->literalToPackage($literal); - - if (isset($installedMap[$package->id])) { - $selected[] = $literal; - continue; - } - - if (null === $priority) { - $priority = $this->getPriority($pool, $package); - } - - if ($this->getPriority($pool, $package) != $priority) { - break; - } - - $selected[] = $literal; - } - - return $selected; - } - /** * Assumes that locally aliased (in root package requires) packages take priority over branch-alias ones * diff --git a/src/Composer/DependencyResolver/GenericRule.php b/src/Composer/DependencyResolver/GenericRule.php index df8a2a003..a07883872 100644 --- a/src/Composer/DependencyResolver/GenericRule.php +++ b/src/Composer/DependencyResolver/GenericRule.php @@ -23,14 +23,13 @@ class GenericRule extends Rule protected $literals; /** - * @param array $literals - * @param int $reason A RULE_* constant describing the reason for generating this rule - * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from + * @param array $literals + * @param int|null $reason A RULE_* constant describing the reason for generating this rule + * @param Link|PackageInterface|int|null $reasonData */ - public function __construct(array $literals, $reason, $reasonData, $job = null) + public function __construct(array $literals, $reason, $reasonData) { - parent::__construct($reason, $reasonData, $job); + parent::__construct($reason, $reasonData); // sort all packages ascending by id sort($literals); diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php new file mode 100644 index 000000000..b22f555e5 --- /dev/null +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -0,0 +1,36 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\Package\AliasPackage; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Semver\Constraint\Constraint; + +/** + * @author Nils Adermann + */ +class LocalRepoTransaction extends Transaction +{ + public function __construct(RepositoryInterface $lockedRepository, $localRepository) + { + parent::__construct( + $localRepository->getPackages(), + $lockedRepository->getPackages() + ); + } +} diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php new file mode 100644 index 000000000..dbbad01d6 --- /dev/null +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -0,0 +1,133 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\Package\AliasPackage; +use Composer\Package\RootAliasPackage; +use Composer\Package\RootPackageInterface; +use Composer\Repository\ArrayRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Test\Repository\ArrayRepositoryTest; + +/** + * @author Nils Adermann + */ +class LockTransaction extends Transaction +{ + /** + * packages in current lock file, platform repo or otherwise present + * @var array + */ + protected $presentMap; + + /** + * Packages which cannot be mapped, platform repo, root package, other fixed repos + * @var array + */ + protected $unlockableMap; + + /** + * @var array + */ + protected $resultPackages; + + public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions) + { + $this->presentMap = $presentMap; + $this->unlockableMap = $unlockableMap; + + $this->setResultPackages($pool, $decisions); + parent::__construct($this->presentMap, $this->resultPackages['all']); + + } + + // TODO make this a bit prettier instead of the two text indexes? + public function setResultPackages(Pool $pool, Decisions $decisions) + { + $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array()); + foreach ($decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + + if ($literal > 0) { + $package = $pool->literalToPackage($literal); + $this->resultPackages['all'][] = $package; + if (!isset($this->unlockableMap[$package->id])) { + $this->resultPackages['non-dev'][] = $package; + } + } + } + } + + public function setNonDevPackages(LockTransaction $extractionResult) + { + $packages = $extractionResult->getNewLockPackages(false); + + $this->resultPackages['dev'] = $this->resultPackages['non-dev']; + $this->resultPackages['non-dev'] = array(); + + foreach ($packages as $package) { + foreach ($this->resultPackages['dev'] as $i => $resultPackage) { + // TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible? + if ($package->getName() == $resultPackage->getName()) { + $this->resultPackages['non-dev'][] = $resultPackage; + unset($this->resultPackages['dev'][$i]); + } + } + } + } + + // TODO additionalFixedRepository needs to be looked at here as well? + public function getNewLockPackages($devMode, $updateMirrors = false) + { + $packages = array(); + foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { + if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is + // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed + if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { + foreach ($this->presentMap as $presentPackage) { + if ($package->getName() == $presentPackage->getName() && + $package->getVersion() == $presentPackage->getVersion() && + $presentPackage->getSourceReference() && + $presentPackage->getSourceType() === $package->getSourceType() + ) { + $package->setSourceDistReferences($presentPackage->getSourceReference()); + } + } + } + $packages[] = $package; + } + } + + return $packages; + } + + /** + * Checks which of the given aliases from composer.json are actually in use for the lock file + */ + public function getAliases($aliases) + { + $usedAliases = array(); + + foreach ($this->resultPackages['all'] as $package) { + if ($package instanceof AliasPackage) { + if (isset($aliases[$package->getName()])) { + $usedAliases[$package->getName()] = $aliases[$package->getName()]; + } + } + } + + return $usedAliases; + } +} diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php new file mode 100644 index 000000000..8de77a41b --- /dev/null +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -0,0 +1,105 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Package\PackageInterface; +use Composer\Package\Link; + +/** + * @author Nils Adermann + * + * MultiConflictRule([A, B, C]) acts as Rule([-A, -B]), Rule([-A, -C]), Rule([-B, -C]) + */ +class MultiConflictRule extends Rule +{ + protected $literals; + + /** + * @param array $literals + * @param int $reason A RULE_* constant describing the reason for generating this rule + * @param Link|PackageInterface $reasonData + */ + public function __construct(array $literals, $reason, $reasonData) + { + parent::__construct($reason, $reasonData); + + if (count($literals) < 3) { + throw new \RuntimeException("multi conflict rule requires at least 3 literals"); + } + + // sort all packages ascending by id + sort($literals); + + $this->literals = $literals; + } + + public function getLiterals() + { + return $this->literals; + } + + public function getHash() + { + $data = unpack('ihash', md5('c:'.implode(',', $this->literals), true)); + + return $data['hash']; + } + + /** + * Checks if this rule is equal to another one + * + * Ignores whether either of the rules is disabled. + * + * @param Rule $rule The rule to check against + * @return bool Whether the rules are equal + */ + public function equals(Rule $rule) + { + if ($rule instanceof MultiConflictRule) { + return $this->literals === $rule->getLiterals(); + } + return false; + } + + public function isAssertion() + { + return false; + } + + public function disable() + { + throw new \RuntimeException("Disabling multi conflict rules is not possible. Please contact composer at https://github.com/composer/composer to let us debug what lead to this situation."); + } + + /** + * Formats a rule as a string of the format (Literal1|Literal2|...) + * + * @return string + */ + public function __toString() + { + // TODO multi conflict? + $result = $this->isDisabled() ? 'disabled(multi(' : '(multi('; + + foreach ($this->literals as $i => $literal) { + if ($i != 0) { + $result .= '|'; + } + $result .= $literal; + } + + $result .= '))'; + + return $result; + } +} diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index 08c659c49..b719f49f9 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -47,20 +47,28 @@ class InstallOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'install'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + } + /** * {@inheritDoc} */ public function __toString() { - return 'Installing '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php index 920e54e67..e028b3079 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php @@ -48,20 +48,28 @@ class MarkAliasInstalledOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'markAliasInstalled'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')'; + } + /** * {@inheritDoc} */ public function __toString() { - return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php index 77f3aef8c..0468f2daf 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php @@ -48,20 +48,28 @@ class MarkAliasUninstalledOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'markAliasUninstalled'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')'; + } + /** * {@inheritDoc} */ public function __toString() { - return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/Operation/OperationInterface.php b/src/Composer/DependencyResolver/Operation/OperationInterface.php index 330cbceb1..be5e8f7af 100644 --- a/src/Composer/DependencyResolver/Operation/OperationInterface.php +++ b/src/Composer/DependencyResolver/Operation/OperationInterface.php @@ -20,11 +20,11 @@ namespace Composer\DependencyResolver\Operation; interface OperationInterface { /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType(); + public function getOperationType(); /** * Returns operation reason. @@ -33,6 +33,14 @@ interface OperationInterface */ public function getReason(); + /** + * Serializes the operation in a human readable format + * + * @param $lock bool Whether this is an operation on the lock file + * @return string + */ + public function show($lock); + /** * Serializes the operation in a human readable format * diff --git a/src/Composer/DependencyResolver/Operation/SolverOperation.php b/src/Composer/DependencyResolver/Operation/SolverOperation.php index e1a68585e..bbc077c31 100644 --- a/src/Composer/DependencyResolver/Operation/SolverOperation.php +++ b/src/Composer/DependencyResolver/Operation/SolverOperation.php @@ -43,8 +43,9 @@ abstract class SolverOperation implements OperationInterface return $this->reason; } - protected function formatVersion(PackageInterface $package) - { - return $package->getFullPrettyVersion(); - } + /** + * @param $lock bool Whether this is an operation on the lock file + * @return string + */ + abstract public function show($lock); } diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index b4a73811e..2ddc2f169 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -47,20 +47,28 @@ class UninstallOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'uninstall'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + } + /** * {@inheritDoc} */ public function __toString() { - return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index 8cfc6b700..61bcd7f5b 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -61,23 +61,41 @@ class UpdateOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'update'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + $fromVersion = $this->initialPackage->getFullPrettyVersion(); + $toVersion = $this->targetPackage->getFullPrettyVersion(); + + if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) { + $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); + $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); + } elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) { + $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); + $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); + } + + $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; + + return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; + } + /** * {@inheritDoc} */ public function __toString() { - $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Updating' : 'Downgrading'; - - return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '. - $this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index 3464bd594..49e23b041 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -20,8 +20,5 @@ use Composer\Package\PackageInterface; interface PolicyInterface { public function versionCompare(PackageInterface $a, PackageInterface $b, $operator); - - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package); - - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null); + public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null); } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 085aaa7bf..1e1100b15 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -12,143 +12,56 @@ namespace Composer\DependencyResolver; -use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\CompositeRepository; -use Composer\Repository\ComposerRepository; -use Composer\Repository\InstalledRepositoryInterface; -use Composer\Repository\PlatformRepository; use Composer\Package\PackageInterface; /** - * A package pool contains repositories that provide packages. + * A package pool contains all packages for dependency resolution * * @author Nils Adermann * @author Jordi Boggiano */ class Pool implements \Countable { - const MATCH_NAME = -1; const MATCH_NONE = 0; const MATCH = 1; const MATCH_PROVIDE = 2; const MATCH_REPLACE = 3; - const MATCH_FILTERED = 4; - protected $repositories = array(); - protected $providerRepos = array(); protected $packages = array(); protected $packageByName = array(); protected $packageByExactName = array(); - protected $acceptableStabilities; - protected $stabilityFlags; protected $versionParser; protected $providerCache = array(); - protected $filterRequires; - protected $whitelist = null; - protected $id = 1; + protected $unacceptableFixedPackages; - public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) + public function __construct(array $packages = array(), array $unacceptableFixedPackages = array()) { $this->versionParser = new VersionParser; - $this->acceptableStabilities = array(); - foreach (BasePackage::$stabilities as $stability => $value) { - if ($value <= BasePackage::$stabilities[$minimumStability]) { - $this->acceptableStabilities[$stability] = $value; - } - } - $this->stabilityFlags = $stabilityFlags; - $this->filterRequires = $filterRequires; - foreach ($filterRequires as $name => $constraint) { - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { - unset($this->filterRequires[$name]); - } - } + $this->setPackages($packages); + $this->unacceptableFixedPackages = $unacceptableFixedPackages; } - public function setWhitelist($whitelist) + private function setPackages(array $packages) { - $this->whitelist = $whitelist; - $this->providerCache = array(); - } + $id = 1; - /** - * Adds a repository and its packages to this package pool - * - * @param RepositoryInterface $repo A package repository - * @param array $rootAliases - */ - public function addRepository(RepositoryInterface $repo, $rootAliases = array()) - { - if ($repo instanceof CompositeRepository) { - $repos = $repo->getRepositories(); - } else { - $repos = array($repo); - } + foreach ($packages as $package) { + $this->packages[] = $package; - foreach ($repos as $repo) { - $this->repositories[] = $repo; + $package->id = $id++; + $this->packageByExactName[$package->getName()][$package->id] = $package; - $exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface; - - if ($repo instanceof ComposerRepository && $repo->hasProviders()) { - $this->providerRepos[] = $repo; - $repo->setRootAliases($rootAliases); - $repo->resetPackageIds(); - } else { - foreach ($repo->getPackages() as $package) { - $names = $package->getNames(); - $stability = $package->getStability(); - if ($exempt || $this->isPackageAcceptable($names, $stability)) { - $package->setId($this->id++); - $this->packages[] = $package; - $this->packageByExactName[$package->getName()][$package->id] = $package; - - foreach ($names as $provided) { - $this->packageByName[$provided][] = $package; - } - - // handle root package aliases - $name = $package->getName(); - if (isset($rootAliases[$name][$package->getVersion()])) { - $alias = $rootAliases[$name][$package->getVersion()]; - if ($package instanceof AliasPackage) { - $package = $package->getAliasOf(); - } - $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); - $aliasPackage->setRootPackageAlias(true); - $aliasPackage->setId($this->id++); - - $package->getRepository()->addPackage($aliasPackage); - $this->packages[] = $aliasPackage; - $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage; - - foreach ($aliasPackage->getNames() as $name) { - $this->packageByName[$name][] = $aliasPackage; - } - } - } - } + foreach ($package->getNames() as $provided) { + $this->packageByName[$provided][] = $package; } } } - public function getPriority(RepositoryInterface $repo) - { - $priority = array_search($repo, $this->repositories, true); - - if (false === $priority) { - throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool."); - } - - return -$priority; - } - /** * Retrieves the package object for a given package id. * @@ -176,104 +89,52 @@ class Pool implements \Countable * packages must match or null to return all * @param bool $mustMatchName Whether the name of returned packages * must match the given name - * @param bool $bypassFilters If enabled, filterRequires and stability matching is ignored * @return PackageInterface[] A set of packages */ - public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false, $bypassFilters = false) + public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false) { - if ($bypassFilters) { - return $this->computeWhatProvides($name, $constraint, $mustMatchName, true); - } - $key = ((int) $mustMatchName).$constraint; if (isset($this->providerCache[$name][$key])) { return $this->providerCache[$name][$key]; } - return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName, $bypassFilters); + return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName); } /** * @see whatProvides */ - private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false) + private function computeWhatProvides($name, $constraint, $mustMatchName = false) { $candidates = array(); - foreach ($this->providerRepos as $repo) { - foreach ($repo->whatProvides($this, $name, $bypassFilters) as $candidate) { - $candidates[] = $candidate; - if ($candidate->id < 1) { - $candidate->setId($this->id++); - $this->packages[$this->id - 2] = $candidate; - } - } - } - if ($mustMatchName) { - $candidates = array_filter($candidates, function ($candidate) use ($name) { - return $candidate->getName() == $name; - }); if (isset($this->packageByExactName[$name])) { - $candidates = array_merge($candidates, $this->packageByExactName[$name]); + $candidates = $this->packageByExactName[$name]; } } elseif (isset($this->packageByName[$name])) { - $candidates = array_merge($candidates, $this->packageByName[$name]); + $candidates = $this->packageByName[$name]; } - $matches = $provideMatches = array(); - $nameMatch = false; + $matches = array(); foreach ($candidates as $candidate) { - $aliasOfCandidate = null; - - // alias packages are not white listed, make sure that the package - // being aliased is white listed - if ($candidate instanceof AliasPackage) { - $aliasOfCandidate = $candidate->getAliasOf(); - } - - if ($this->whitelist !== null && !$bypassFilters && ( - (!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) || - ($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id])) - )) { - continue; - } - switch ($this->match($candidate, $name, $constraint, $bypassFilters)) { + switch ($this->match($candidate, $name, $constraint)) { case self::MATCH_NONE: break; - case self::MATCH_NAME: - $nameMatch = true; - break; - case self::MATCH: - $nameMatch = true; - $matches[] = $candidate; - break; - case self::MATCH_PROVIDE: - $provideMatches[] = $candidate; - break; - case self::MATCH_REPLACE: $matches[] = $candidate; break; - case self::MATCH_FILTERED: - break; - default: throw new \UnexpectedValueException('Unexpected match type'); } } - // if a package with the required name exists, we ignore providers - if ($nameMatch) { - return $matches; - } - - return array_merge($matches, $provideMatches); + return $matches; } public function literalToPackage($literal) @@ -296,23 +157,6 @@ class Pool implements \Countable return $prefix.' '.$package->getPrettyString(); } - public function isPackageAcceptable($name, $stability) - { - foreach ((array) $name as $n) { - // allow if package matches the global stability requirement and has no exception - if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) { - return true; - } - - // allow if package matches the package-specific stability flag - if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) { - return true; - } - } - - return false; - } - /** * Checks if the package matches the given constraint directly or through * provided or replaced packages @@ -322,27 +166,19 @@ class Pool implements \Countable * @param ConstraintInterface $constraint The constraint to verify * @return int One of the MATCH* constants of this class or 0 if there is no match */ - public function match($candidate, $name, ConstraintInterface $constraint = null, $bypassFilters) + public function match($candidate, $name, ConstraintInterface $constraint = null) { $candidateName = $candidate->getName(); $candidateVersion = $candidate->getVersion(); - $isDev = $candidate->getStability() === 'dev'; - $isAlias = $candidate instanceof AliasPackage; - - if (!$bypassFilters && !$isDev && !$isAlias && isset($this->filterRequires[$name])) { - $requireFilter = $this->filterRequires[$name]; - } else { - $requireFilter = new EmptyConstraint; - } if ($candidateName === $name) { $pkgConstraint = new Constraint('==', $candidateVersion); if ($constraint === null || $constraint->matches($pkgConstraint)) { - return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED; + return self::MATCH; } - return self::MATCH_NAME; + return self::MATCH_NONE; } $provides = $candidate->getProvides(); @@ -352,13 +188,13 @@ class Pool implements \Countable if (isset($replaces[0]) || isset($provides[0])) { foreach ($provides as $link) { if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { - return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; + return self::MATCH_PROVIDE; } } foreach ($replaces as $link) { if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { - return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; + return self::MATCH_REPLACE; } } @@ -366,13 +202,18 @@ class Pool implements \Countable } if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) { - return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; + return self::MATCH_PROVIDE; } if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) { - return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; + return self::MATCH_REPLACE; } return self::MATCH_NONE; } + + public function isUnacceptableFixedPackage(PackageInterface $package) + { + return in_array($package, $this->unacceptableFixedPackages, true); + } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php new file mode 100644 index 000000000..420e75786 --- /dev/null +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -0,0 +1,376 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +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\Repository\PlatformRepository; +use Composer\Repository\RootPackageRepository; +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Semver\Constraint\MultiConstraint; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Plugin\PrePoolCreateEvent; +use Composer\Plugin\PluginEvents; + +/** + * @author Nils Adermann + */ +class PoolBuilder +{ + private $acceptableStabilities; + private $stabilityFlags; + private $rootAliases; + private $rootReferences; + private $eventDispatcher; + private $io; + + private $aliasMap = array(); + private $nameConstraints = array(); + private $loadedNames = array(); + private $packages = array(); + private $unacceptableFixedPackages = array(); + private $updateAllowList = array(); + private $skippedLoad = array(); + private $updateAllowWarned = array(); + + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null) + { + $this->acceptableStabilities = $acceptableStabilities; + $this->stabilityFlags = $stabilityFlags; + $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; + $this->eventDispatcher = $eventDispatcher; + $this->io = $io; + } + + public function buildPool(array $repositories, Request $request) + { + if ($request->getUpdateAllowList()) { + $this->updateAllowList = $request->getUpdateAllowList(); + $this->warnAboutNonMatchingUpdateAllowList($request); + + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { + if (!$this->isUpdateAllowed($lockedPackage)) { + $request->fixPackage($lockedPackage); + $lockedName = $lockedPackage->getName(); + // remember which packages we skipped loading remote content for in this partial update + $this->skippedLoad[$lockedPackage->getName()] = $lockedName; + foreach ($lockedPackage->getReplaces() as $link) { + $this->skippedLoad[$link->getTarget()] = $lockedName; + } + } + } + } + + $loadNames = array(); + foreach ($request->getFixedPackages() as $package) { + $this->nameConstraints[$package->getName()] = null; + $this->loadedNames[$package->getName()] = true; + + // replace means conflict, so if a fixed package replaces a name, no need to load that one, packages would conflict anyways + foreach ($package->getReplaces() as $link) { + $this->nameConstraints[$package->getName()] = null; + $this->loadedNames[$link->getTarget()] = true; + } + + // TODO in how far can we do the above for conflicts? It's more tricky cause conflicts can be limited to + // specific versions while replace is a conflict with all versions of the name + + if ( + $package->getRepository() instanceof RootPackageRepository + || $package->getRepository() instanceof PlatformRepository + || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) + ) { + $loadNames += $this->loadPackage($request, $package, false); + } else { + $this->unacceptableFixedPackages[] = $package; + } + } + + foreach ($request->getRequires() as $packageName => $constraint) { + // fixed packages have already been added, so if a root require needs one of them, no need to do anything + if (isset($this->loadedNames[$packageName])) { + continue; + } + + $loadNames[$packageName] = $constraint; + $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; + } + + // clean up loadNames for anything we manually marked loaded above + foreach ($loadNames as $name => $void) { + if (isset($this->loadedNames[$name])) { + unset($loadNames[$name]); + } + } + + while (!empty($loadNames)) { + foreach ($loadNames as $name => $void) { + $this->loadedNames[$name] = true; + } + + $newLoadNames = array(); + foreach ($repositories as $repository) { + // these repos have their packages fixed if they need to be loaded so we + // never need to load anything else from them + if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) { + continue; + } + $result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags); + + foreach ($result['namesFound'] as $name) { + // avoid loading the same package again from other repositories once it has been found + unset($loadNames[$name]); + } + foreach ($result['packages'] as $package) { + $newLoadNames += $this->loadPackage($request, $package); + } + } + + $loadNames = $newLoadNames; + } + + // filter packages according to all the require statements collected for each package + foreach ($this->packages as $i => $package) { + // we check all alias related packages at once, so no need to check individual aliases + // isset also checks non-null value + if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) { + $constraint = $this->nameConstraints[$package->getName()]; + + $aliasedPackages = array($i => $package); + if (isset($this->aliasMap[spl_object_hash($package)])) { + $aliasedPackages += $this->aliasMap[spl_object_hash($package)]; + } + + $found = false; + foreach ($aliasedPackages as $packageOrAlias) { + if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) { + $found = true; + } + } + if (!$found) { + foreach ($aliasedPackages as $index => $packageOrAlias) { + unset($this->packages[$index]); + } + } + } + } + + if ($this->eventDispatcher) { + $prePoolCreateEvent = new PrePoolCreateEvent( + PluginEvents::PRE_POOL_CREATE, + $repositories, + $request, + $this->acceptableStabilities, + $this->stabilityFlags, + $this->rootAliases, + $this->rootReferences, + $this->packages, + $this->unacceptableFixedPackages + ); + $this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent); + $this->packages = $prePoolCreateEvent->getPackages(); + $this->unacceptableFixedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages(); + } + + $pool = new Pool($this->packages, $this->unacceptableFixedPackages); + + $this->aliasMap = array(); + $this->nameConstraints = array(); + $this->loadedNames = array(); + $this->packages = array(); + $this->unacceptableFixedPackages = array(); + + return $pool; + } + + private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true) + { + end($this->packages); + $index = key($this->packages) + 1; + $this->packages[] = $package; + + if ($package instanceof AliasPackage) { + $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; + } + + $name = $package->getName(); + + // we're simply setting the root references on all versions for a name here and rely on the solver to pick the + // right version. It'd be more work to figure out which versions and which aliases of those versions this may + // apply to + if (isset($this->rootReferences[$name])) { + // do not modify the references on already locked packages + if (!$request->isFixedPackage($package)) { + $package->setSourceDistReferences($this->rootReferences[$name]); + } + } + + // if propogateUpdate is false we are loading a fixed package, root aliases do not apply as they are manually + // loaded as separate packages in this case + if ($propagateUpdate && isset($this->rootAliases[$name][$package->getVersion()])) { + $alias = $this->rootAliases[$name][$package->getVersion()]; + if ($package instanceof AliasPackage) { + $basePackage = $package->getAliasOf(); + } else { + $basePackage = $package; + } + $aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']); + $aliasPackage->setRootPackageAlias(true); + + $this->packages[] = $aliasPackage; + $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage; + } + + $loadNames = array(); + foreach ($package->getRequires() as $link) { + $require = $link->getTarget(); + if (!isset($this->loadedNames[$require])) { + $loadNames[$require] = null; + // if this is a partial update with transitive dependencies we need to unfix the package we now know is a + // dependency of another package which we are trying to update, and then attempt to load it again + } elseif ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies() && isset($this->skippedLoad[$require])) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$require])) { + $this->unfixPackage($request, $require); + $loadNames[$require] = null; + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) { + $this->updateAllowWarned[$require] = true; + $this->io->writeError('Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.'); + } + } + + $linkConstraint = $link->getConstraint(); + if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) { + if (!array_key_exists($require, $this->nameConstraints)) { + $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false); + } elseif ($this->nameConstraints[$require]) { + // TODO addConstraint function? + $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false); + } + // else it is null and should stay null + } else { + $this->nameConstraints[$require] = null; + } + } + + // if we're doing a partial update with deps we also need to unfix packages which are being replaced in case they + // are currently locked and thus prevent this updateable package from being installable/updateable + if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { + foreach ($package->getReplaces() as $link) { + $replace = $link->getTarget(); + if (isset($this->loadedNames[$replace]) && isset($this->skippedLoad[$replace])) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$replace])) { + $this->unfixPackage($request, $replace); + $loadNames[$replace] = null; + // TODO should we try to merge constraints here? + $this->nameConstraints[$replace] = null; + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require])) { + $this->updateAllowWarned[$replace] = true; + $this->io->writeError('Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.'); + } + } + } + } + + return $loadNames; + } + + /** + * Checks if a particular name is required directly in the request + * + * @return bool + */ + private function isRootRequire(Request $request, $name) + { + $rootRequires = $request->getRequires(); + return isset($rootRequires[$name]); + } + + /** + * Checks whether the update allow list allows this package in the lock file to be updated + * @return bool + */ + private function isUpdateAllowed(PackageInterface $package) + { + foreach ($this->updateAllowList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + if (preg_match($patternRegexp, $package->getName())) { + return true; + } + } + + return false; + } + + private function warnAboutNonMatchingUpdateAllowList(Request $request) + { + foreach ($this->updateAllowList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + // update pattern matches a locked package? => all good + foreach ($request->getLockedRepository()->getPackages() as $package) { + if (preg_match($patternRegexp, $package->getName())) { + continue 2; + } + } + // update pattern matches a root require? => all good, probably a new package + foreach ($request->getRequires() as $packageName => $constraint) { + if (preg_match($patternRegexp, $packageName)) { + continue 2; + } + } + if (strpos($pattern, '*') !== false) { + $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); + } else { + $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); + } + } + } + + /** + * Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies + * found that this package actually needs to be updated + */ + private function unfixPackage(Request $request, $name) + { + // remove locked package by this name which was already initialized + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { + if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) { + if (false !== $index = array_search($lockedPackage, $this->packages, true)) { + $request->unfixPackage($lockedPackage); + unset($this->packages[$index]); + if (isset($this->aliasMap[spl_object_hash($lockedPackage)])) { + foreach ($this->aliasMap[spl_object_hash($lockedPackage)] as $aliasIndex => $aliasPackage) { + $request->unfixPackage($aliasPackage); + unset($this->packages[$aliasIndex]); + } + unset($this->aliasMap[spl_object_hash($lockedPackage)]); + } + } + } + } + + // if we unfixed a replaced package name, we also need to unfix the replacer itself + if ($this->skippedLoad[$name] !== $name) { + $this->unfixPackage($request, $this->skippedLoad[$name]); + } + + unset($this->skippedLoad[$name]); + unset($this->loadedNames[$name]); + } +} + diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index df289a4c1..13d0ffd4f 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -13,6 +13,8 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackageInterface; +use Composer\Repository\RepositorySet; +use Composer\Semver\Constraint\Constraint; /** * Represents a problem detected while solving dependencies @@ -28,20 +30,13 @@ class Problem protected $reasonSeen; /** - * A set of reasons for the problem, each is a rule or a job and a rule + * A set of reasons for the problem, each is a rule or a root require and a rule * @var array */ protected $reasons = array(); protected $section = 0; - protected $pool; - - public function __construct(Pool $pool) - { - $this->pool = $pool; - } - /** * Add a rule as a reason * @@ -49,10 +44,7 @@ class Problem */ public function addRule(Rule $rule) { - $this->addReason(spl_object_hash($rule), array( - 'rule' => $rule, - 'job' => $rule->getJob(), - )); + $this->addReason(spl_object_hash($rule), $rule); } /** @@ -68,124 +60,67 @@ class Problem /** * A human readable textual representation of the problem's reasons * - * @param array $installedMap A map of all installed packages + * @param array $installedMap A map of all present packages * @return string */ - public function getPrettyString(array $installedMap = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { + // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); if (count($reasons) === 1) { reset($reasons); - $reason = current($reasons); + $rule = current($reasons); - $job = $reason['job']; + if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) { + throw new \LogicException("Single reason problems must contain a request rule."); + } - $packageName = $job['packageName']; - $constraint = $job['constraint']; + $reasonData = $rule->getReasonData(); + $packageName = $reasonData['packageName']; + $constraint = $reasonData['constraint']; if (isset($constraint)) { - $packages = $this->pool->whatProvides($packageName, $constraint); + $packages = $pool->whatProvides($packageName, $constraint); } else { $packages = array(); } - if ($job && $job['cmd'] === 'install' && empty($packages)) { - - // handle php/hhvm - if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { - $version = phpversion(); - $available = $this->pool->whatProvides($packageName); - - if (count($available)) { - $firstAvailable = reset($available); - $version = $firstAvailable->getPrettyVersion(); - $extra = $firstAvailable->getExtra(); - if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { - $version .= '; ' . $firstAvailable->getDescription(); - } - } - - $msg = "\n - This package requires ".$packageName.$this->constraintToText($constraint).' but '; - - if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) { - return $msg . 'your HHVM version does not satisfy that requirement.'; - } - - if ($packageName === 'hhvm') { - return $msg . 'you are running this with PHP and not HHVM.'; - } - - return $msg . 'your PHP version ('. $version .') does not satisfy that requirement.'; - } - - // handle php extensions - if (0 === stripos($packageName, 'ext-')) { - if (false !== strpos($packageName, ' ')) { - return "\n - The requested PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'; - } - - $ext = substr($packageName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; - - return "\n - The requested PHP extension ".$packageName.$this->constraintToText($constraint).' '.$error.'. Install or enable PHP\'s '.$ext.' extension.'; - } - - // handle linked libs - if (0 === stripos($packageName, 'lib-')) { - if (strtolower($packageName) === 'lib-icu') { - $error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.'; - - return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' '.$error; - } - - return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.'; - } - - if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { - $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName); - - return "\n - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'; - } - - if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) { - return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.'; - } - - if ($providers = $this->pool->whatProvides($packageName, null, true, true)) { - return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.'; - } - - return "\n - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.'; + if (empty($packages)) { + return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint)); } } $messages = array(); - - foreach ($reasons as $reason) { - $rule = $reason['rule']; - $job = $reason['job']; - - if ($job) { - $messages[] = $this->jobToText($job); - } elseif ($rule) { - if ($rule instanceof Rule) { - $messages[] = $rule->getPrettyString($this->pool, $installedMap); - } - } + foreach ($reasons as $rule) { + $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } return "\n - ".implode("\n - ", $messages); } + public function isCausedByLock() + { + foreach ($this->reasons as $sectionRules) { + foreach ($sectionRules as $rule) { + if ($rule->isCausedByLock()) { + return true; + } + } + } + } + /** * Store a reason descriptor but ignore duplicates * * @param string $id A canonical identifier for the reason - * @param string $reason The reason descriptor + * @param Rule $reason The reason descriptor */ - protected function addReason($id, $reason) + protected function addReason($id, Rule $reason) { + // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message + // that is important to understand the issue? + if (!isset($this->reasonSeen[$id])) { $this->reasonSeen[$id] = true; $this->reasons[$this->section][] = $reason; @@ -198,39 +133,150 @@ class Problem } /** - * Turns a job into a human readable description - * - * @param array $job - * @return string + * @internal */ - protected function jobToText($job) + public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null) { - $packageName = $job['packageName']; - $constraint = $job['constraint']; - switch ($job['cmd']) { - case 'install': - $packages = $this->pool->whatProvides($packageName, $constraint); - if (!$packages) { - return 'No package found to satisfy install request for '.$packageName.$this->constraintToText($constraint); + // handle php/hhvm + if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { + $version = phpversion(); + $available = $pool->whatProvides($packageName); + + if (count($available)) { + $firstAvailable = reset($available); + $version = $firstAvailable->getPrettyVersion(); + $extra = $firstAvailable->getExtra(); + if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { + $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription()); + } + } + + $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but '; + + if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) { + return array($msg, 'your HHVM version does not satisfy that requirement.'); + } + + if ($packageName === 'hhvm') { + return array($msg, 'you are running this with PHP and not HHVM.'); + } + + return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.'); + } + + // handle php extensions + if (0 === stripos($packageName, 'ext-')) { + if (false !== strpos($packageName, ' ')) { + return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'); + } + + $ext = substr($packageName, 4); + $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system'; + + return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.'); + } + + // handle linked libs + if (0 === stripos($packageName, 'lib-')) { + if (strtolower($packageName) === 'lib-icu') { + $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.'; + + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error); + } + + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.'); + } + + $fixedPackage = null; + foreach ($request->getFixedPackages() as $package) { + if ($package->getName() === $packageName) { + $fixedPackage = $package; + if ($pool->isUnacceptableFixedPackage($package)) { + return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.'); + } + break; + } + } + + // first check if the actual requested package is found in normal conditions + // if so it must mean it is rejected by another constraint than the one given here + if ($packages = $repositorySet->findPackages($packageName, $constraint)) { + $rootReqs = $repositorySet->getRootRequires(); + if (isset($rootReqs[$packageName])) { + $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) { + return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion())); + }); + if (0 === count($filtered)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); + } + } + + if ($fixedPackage) { + $fixedConstraint = new Constraint('==', $fixedPackage->getVersion()); + $filtered = array_filter($packages, function ($p) use ($fixedConstraint) { + return $fixedConstraint->matches(new Constraint('==', $p->getVersion())); + }); + if (0 === count($filtered)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.'); + } + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.'); + } + + // check if the package is found when bypassing stability checks + if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); + } + + // check if the package is found when bypassing the constraint check + if ($packages = $repositorySet->findPackages($packageName, null)) { + // we must first verify if a valid package would be found in a lower priority repository + if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { + $higherRepoPackages = $repositorySet->findPackages($packageName, null); + $nextRepoPackages = array(); + $nextRepo = null; + + foreach ($allReposPackages as $package) { + if ($nextRepo === null || $nextRepo === $package->getRepository()) { + $nextRepoPackages[] = $package; + $nextRepo = $package->getRepository(); + } else { + break; + } } - return 'Installation request for '.$packageName.$this->constraintToText($constraint).' -> satisfiable by '.$this->getPackageList($packages).'.'; - case 'update': - return 'Update request for '.$packageName.$this->constraintToText($constraint).'.'; - case 'remove': - return 'Removal request for '.$packageName.$this->constraintToText($constraint).''; + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.'); + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.'); } - if (isset($constraint)) { - $packages = $this->pool->whatProvides($packageName, $constraint); - } else { - $packages = array(); + if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { + $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName); + + return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); } - return 'Job(cmd='.$job['cmd'].', target='.$packageName.', packages=['.$this->getPackageList($packages).'])'; + if ($providers = $repositorySet->getProviders($packageName)) { + $maxProviders = 20; + $providersStr = implode(array_map(function ($p) { + $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : ''; + return " - ${p['name']}".$description."\n"; + }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers)); + if (count($providers) > $maxProviders+1) { + $providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n"; + } + return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."); + } + + return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name."); } - protected function getPackageList($packages) + /** + * @internal + */ + public static function getPackageList(array $packages) { $prepared = array(); foreach ($packages as $package) { @@ -238,19 +284,37 @@ class Problem $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); } foreach ($prepared as $name => $package) { + // remove the implicit dev-master alias to avoid cruft in the display + if (isset($package['versions']['9999999-dev']) && isset($package['versions']['dev-master'])) { + unset($package['versions']['9999999-dev']); + } $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } return implode(', ', $prepared); } + private static function hasMultipleNames(array $packages) + { + $name = null; + foreach ($packages as $package) { + if ($name === null || $name === $package->getName()) { + $name = $package->getName(); + } else { + return true; + } + } + + return false; + } + /** - * Turns a constraint into text usable in a sentence describing a job + * Turns a constraint into text usable in a sentence describing a request * * @param \Composer\Semver\Constraint\ConstraintInterface $constraint * @return string */ - protected function constraintToText($constraint) + protected static function constraintToText($constraint) { return $constraint ? ' '.$constraint->getPrettyString() : ''; } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 85dc9c4d0..5782c3ff1 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -12,6 +12,10 @@ namespace Composer\DependencyResolver; +use Composer\Package\Package; +use Composer\Package\PackageInterface; +use Composer\Package\RootAliasPackage; +use Composer\Repository\LockArrayRepository; use Composer\Semver\Constraint\ConstraintInterface; /** @@ -19,60 +23,129 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class Request { - protected $jobs; + /** + * Identifies a partial update for listed packages only, all dependencies will remain at locked versions + */ + const UPDATE_ONLY_LISTED = 0; - public function __construct() + /** + * Identifies a partial update for listed packages and recursively all their dependencies, however dependencies + * also directly required by the root composer.json and their dependencies will remain at the locked version. + */ + const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE = 1; + + /** + * Identifies a partial update for listed packages and recursively all their dependencies, even dependencies + * also directly required by the root composer.json will be updated. + */ + const UPDATE_LISTED_WITH_TRANSITIVE_DEPS = 2; + + protected $lockedRepository; + protected $requires = array(); + protected $fixedPackages = array(); + protected $unlockables = array(); + protected $updateAllowList = array(); + protected $updateAllowTransitiveDependencies = false; + + public function __construct(LockArrayRepository $lockedRepository = null) { - $this->jobs = array(); + $this->lockedRepository = $lockedRepository; } - public function install($packageName, ConstraintInterface $constraint = null) + public function requireName($packageName, ConstraintInterface $constraint = null) { - $this->addJob($packageName, 'install', $constraint); - } - - public function update($packageName, ConstraintInterface $constraint = null) - { - $this->addJob($packageName, 'update', $constraint); - } - - public function remove($packageName, ConstraintInterface $constraint = null) - { - $this->addJob($packageName, 'remove', $constraint); + $packageName = strtolower($packageName); + $this->requires[$packageName] = $constraint; } /** * Mark an existing package as being installed and having to remain installed * - * These jobs will not be tempered with by the solver - * - * @param string $packageName - * @param ConstraintInterface|null $constraint + * @param bool $lockable if set to false, the package will not be written to the lock file */ - public function fix($packageName, ConstraintInterface $constraint = null) + public function fixPackage(PackageInterface $package, $lockable = true) { - $this->addJob($packageName, 'install', $constraint, true); + $this->fixedPackages[spl_object_hash($package)] = $package; + + if (!$lockable) { + $this->unlockables[spl_object_hash($package)] = $package; + } } - protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null, $fixed = false) + public function unfixPackage(PackageInterface $package) { - $packageName = strtolower($packageName); - - $this->jobs[] = array( - 'cmd' => $cmd, - 'packageName' => $packageName, - 'constraint' => $constraint, - 'fixed' => $fixed, - ); + unset($this->fixedPackages[spl_object_hash($package)]); + unset($this->unlockables[spl_object_hash($package)]); } - public function updateAll() + public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies) { - $this->jobs[] = array('cmd' => 'update-all'); + $this->updateAllowList = $updateAllowList; + $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; } - public function getJobs() + public function getUpdateAllowList() { - return $this->jobs; + return $this->updateAllowList; + } + + public function getUpdateAllowTransitiveDependencies() + { + return $this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED; + } + + public function getUpdateAllowTransitiveRootDependencies() + { + return $this->updateAllowTransitiveDependencies === self::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } + + public function getRequires() + { + return $this->requires; + } + + public function getFixedPackages() + { + return $this->fixedPackages; + } + + public function isFixedPackage(PackageInterface $package) + { + return isset($this->fixedPackages[spl_object_hash($package)]); + } + + // TODO look into removing the packageIds option, the only place true is used is for the installed map in the solver problems + // some locked packages may not be in the pool so they have a package->id of -1 + public function getPresentMap($packageIds = false) + { + $presentMap = array(); + + if ($this->lockedRepository) { + foreach ($this->lockedRepository->getPackages() as $package) { + $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package; + } + } + + foreach ($this->fixedPackages as $package) { + $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package; + } + + return $presentMap; + } + + public function getUnlockableMap() + { + $unlockableMap = array(); + + foreach ($this->unlockables as $package) { + $unlockableMap[$package->id] = $package; + } + + return $unlockableMap; + } + + public function getLockedRepository() + { + return $this->lockedRepository; } } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 82c9c499c..fe1afd12e 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\PackageInterface; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -24,8 +25,8 @@ abstract class Rule { // reason constants const RULE_INTERNAL_ALLOW_UPDATE = 1; - const RULE_JOB_INSTALL = 2; - const RULE_JOB_REMOVE = 3; + const RULE_ROOT_REQUIRE = 2; + const RULE_FIXED = 3; const RULE_PACKAGE_CONFLICT = 6; const RULE_PACKAGE_REQUIRES = 7; const RULE_PACKAGE_OBSOLETES = 8; @@ -41,22 +42,17 @@ abstract class Rule const BITFIELD_DISABLED = 16; protected $bitfield; - protected $job; + protected $request; protected $reasonData; /** * @param int $reason A RULE_* constant describing the reason for generating this rule * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from */ - public function __construct($reason, $reasonData, $job = null) + public function __construct($reason, $reasonData) { $this->reasonData = $reasonData; - if ($job) { - $this->job = $job; - } - $this->bitfield = (0 << self::BITFIELD_DISABLED) | ($reason << self::BITFIELD_REASON) | (255 << self::BITFIELD_TYPE); @@ -66,11 +62,6 @@ abstract class Rule abstract public function getHash(); - public function getJob() - { - return $this->job; - } - abstract public function equals(Rule $rule); public function getReason() @@ -85,11 +76,17 @@ abstract class Rule public function getRequiredPackage() { - if ($this->getReason() === self::RULE_JOB_INSTALL) { - return $this->reasonData; + $reason = $this->getReason(); + + if ($reason === self::RULE_ROOT_REQUIRE) { + return $this->reasonData['packageName']; } - if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) { + if ($reason === self::RULE_FIXED) { + return $this->reasonData['package']->getName(); + } + + if ($reason === self::RULE_PACKAGE_REQUIRES) { return $this->reasonData->getTarget(); } } @@ -126,7 +123,12 @@ abstract class Rule abstract public function isAssertion(); - public function getPrettyString(Pool $pool, array $installedMap = array()) + public function isCausedByLock() + { + return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']; + } + + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); @@ -142,17 +144,30 @@ abstract class Rule case self::RULE_INTERNAL_ALLOW_UPDATE: return $ruleText; - case self::RULE_JOB_INSTALL: - return "Install command rule ($ruleText)"; + case self::RULE_ROOT_REQUIRE: + $packageName = $this->reasonData['packageName']; + $constraint = $this->reasonData['constraint']; - case self::RULE_JOB_REMOVE: - return "Remove command rule ($ruleText)"; + $packages = $pool->whatProvides($packageName, $constraint); + if (!$packages) { + return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : ''); + } + + return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages).'.'; + + case self::RULE_FIXED: + $package = $this->reasonData['package']; + if ($this->reasonData['lockable']) { + return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.'; + } + + return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; case self::RULE_PACKAGE_CONFLICT: $package1 = $pool->literalToPackage($literals[0]); $package2 = $pool->literalToPackage($literals[1]); - return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.'; + return $package2->getPrettyString().' conflicts with '.$package1->getPrettyString().'.'; case self::RULE_PACKAGE_REQUIRES: $sourceLiteral = array_shift($literals); @@ -169,73 +184,103 @@ abstract class Rule } else { $targetName = $this->reasonData->getTarget(); - if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') { - // handle php/hhvm - if (defined('HHVM_VERSION')) { - return $text . ' -> your HHVM version does not satisfy that requirement.'; - } + $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint()); - $packages = $pool->whatProvides($targetName); - $package = count($packages) ? current($packages) : phpversion(); - - if ($targetName === 'hhvm') { - if ($package instanceof CompletePackage) { - return $text . ' -> your HHVM version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } else { - return $text . ' -> you are running this with PHP and not HHVM.'; - } - } - - - if (!($package instanceof CompletePackage)) { - return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.'; - } - - $extra = $package->getExtra(); - - if (!empty($extra['config.platform'])) { - $text .= ' -> your PHP version ('.phpversion().') overridden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } else { - $text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } - - return $text; - } - - if (0 === strpos($targetName, 'ext-')) { - // handle php extensions - $ext = substr($targetName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; - - return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.'; - } - - if (0 === strpos($targetName, 'lib-')) { - // handle linked libs - $lib = substr($targetName, 4); - - return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.'; - } - - if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) { - return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.'; - } - - return $text . ' -> no matching package found.'; + return $text . ' -> ' . $reason[1]; } return $text; case self::RULE_PACKAGE_OBSOLETES: + if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) { + $package1 = $pool->literalToPackage($literals[0]); + $package2 = $pool->literalToPackage($literals[1]); + + $replaces1 = $this->getReplacedNames($package1); + $replaces2 = $this->getReplacedNames($package2); + + $reason = null; + if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) { + $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and thus cannot coexist.'; + } elseif (in_array($package1->getName(), $replaces2, true)) { + $reason = $package2->getName().' replaces '.$package1->getName().' and thus cannot coexist with it.'; + } elseif (in_array($package2->getName(), $replaces1, true)) { + $reason = $package1->getName().' replaces '.$package2->getName().' and thus cannot coexist with it.'; + } + + if ($reason) { + if (isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + // swap vars so the if below passes + $tmp = $package2; + $package2 = $package1; + $package1 = $tmp; + } + if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) { + return $package1->getPrettyString().' cannot be installed as that would require removing '.$package2->getPrettyString().'. '.$reason; + } + + if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. '.$reason; + } + } + + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.'; + } + return $ruleText; case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + $replacedNames = null; + $packageNames = array(); + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + $pkgReplaces = $this->getReplacedNames($package); + if ($pkgReplaces) { + if ($replacedNames === null) { + $replacedNames = $this->getReplacedNames($package); + } else { + $replacedNames = array_intersect($replacedNames, $this->getReplacedNames($package)); + } + } + $packageNames[$package->getName()] = true; + } + + if ($replacedNames) { + $replacedNames = array_values(array_intersect(array_keys($packageNames), $replacedNames)); + } + if ($replacedNames && count($packageNames) > 1) { + $replacer = null; + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + if (array_intersect($replacedNames, $this->getReplacedNames($package))) { + $replacer = $package; + break; + } + } + $replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0]; + + if ($replacer) { + return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and thus cannot coexist with it.'; + } + } + + return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: - return 'Conclusion: '.$ruleText; + if (isset($learnedPool[$this->reasonData])) { + $learnedString = ', learned rules:'."\n - "; + $reasons = array(); + foreach ($learnedPool[$this->reasonData] as $learnedRule) { + $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); + } + $learnedString .= implode("\n - ", array_unique($reasons)); + } else { + $learnedString = ' (reasoning unavailable)'; + } + + return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: return $ruleText; default: @@ -252,17 +297,22 @@ abstract class Rule protected function formatPackagesUnique($pool, array $packages) { $prepared = array(); - foreach ($packages as $package) { + foreach ($packages as $index => $package) { if (!is_object($package)) { - $package = $pool->literalToPackage($package); + $packages[$index] = $pool->literalToPackage($package); } - $prepared[$package->getName()]['name'] = $package->getPrettyName(); - $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); - } - foreach ($prepared as $name => $package) { - $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } - return implode(', ', $prepared); + return Problem::getPackageList($packages); + } + + private function getReplacedNames(PackageInterface $package) + { + $names = array(); + foreach ($package->getReplaces() as $link) { + $names[] = $link->getTarget(); + } + + return $names; } } diff --git a/src/Composer/DependencyResolver/Rule2Literals.php b/src/Composer/DependencyResolver/Rule2Literals.php index 6bf47db34..2df95e09d 100644 --- a/src/Composer/DependencyResolver/Rule2Literals.php +++ b/src/Composer/DependencyResolver/Rule2Literals.php @@ -28,11 +28,10 @@ class Rule2Literals extends Rule * @param int $literal2 * @param int $reason A RULE_* constant describing the reason for generating this rule * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from */ - public function __construct($literal1, $literal2, $reason, $reasonData, $job = null) + public function __construct($literal1, $literal2, $reason, $reasonData) { - parent::__construct($reason, $reasonData, $job); + parent::__construct($reason, $reasonData); if ($literal1 < $literal2) { $this->literal1 = $literal1; diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index bf4de0d7c..d37ca1e9f 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -12,6 +12,8 @@ namespace Composer\DependencyResolver; +use Composer\Repository\RepositorySet; + /** * @author Nils Adermann */ @@ -19,7 +21,7 @@ class RuleSet implements \IteratorAggregate, \Countable { // highest priority => lowest number const TYPE_PACKAGE = 0; - const TYPE_JOB = 1; + const TYPE_REQUEST = 1; const TYPE_LEARNED = 4; /** @@ -32,7 +34,7 @@ class RuleSet implements \IteratorAggregate, \Countable protected static $types = array( 255 => 'UNKNOWN', self::TYPE_PACKAGE => 'PACKAGE', - self::TYPE_JOB => 'JOB', + self::TYPE_REQUEST => 'REQUEST', self::TYPE_LEARNED => 'LEARNED', ); @@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable return array_keys($types); } - public function getPrettyString(Pool $pool = null) + public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n"; + $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n"; } $string .= "\n\n"; } @@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable public function __toString() { - return $this->getPrettyString(null); + return $this->getPrettyString(null, null, null); } } diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index e8714a405..717a69d20 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -12,9 +12,11 @@ namespace Composer\DependencyResolver; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann @@ -24,13 +26,11 @@ class RuleSetGenerator protected $policy; protected $pool; protected $rules; - protected $jobs; - protected $installedMap; - protected $whitelistedMap; protected $addedMap; protected $conflictAddedMap; protected $addedPackages; protected $addedPackagesByNames; + protected $conflictsForName; public function __construct(PolicyInterface $policy, Pool $pool) { @@ -76,33 +76,17 @@ class RuleSetGenerator * @param array $packages The set of packages to choose from * @param int $reason A RULE_* constant describing the reason for * generating this rule - * @param array $job The job this rule was created from + * @param array $reasonData Additional data like the root require or fix request info * @return Rule The generated rule */ - protected function createInstallOneOfRule(array $packages, $reason, $job) + protected function createInstallOneOfRule(array $packages, $reason, $reasonData) { $literals = array(); foreach ($packages as $package) { $literals[] = $package->id; } - return new GenericRule($literals, $reason, $job['packageName'], $job); - } - - /** - * Creates a rule to remove a package - * - * The rule for a package A is (-A). - * - * @param PackageInterface $package The package to be removed - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param array $job The job this rule was created from - * @return Rule The generated rule - */ - protected function createRemoveRule(PackageInterface $package, $reason, $job) - { - return new GenericRule(array(-$package->id), $reason, $job['packageName'], $job); + return new GenericRule($literals, $reason, $reasonData); } /** @@ -129,6 +113,20 @@ class RuleSetGenerator return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData); } + protected function createMultiConflictRule(array $packages, $reason, $reasonData = null) + { + $literals = array(); + foreach ($packages as $package) { + $literals[] = -$package->id; + } + + if (count($literals) == 2) { + return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData); + } + + return new MultiConflictRule($literals, $reason, $reasonData); + } + /** * Adds a rule unless it duplicates an existing one of any type * @@ -147,41 +145,6 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } - protected function whitelistFromPackage(PackageInterface $package) - { - $workQueue = new \SplQueue; - $workQueue->enqueue($package); - - while (!$workQueue->isEmpty()) { - $package = $workQueue->dequeue(); - if (isset($this->whitelistedMap[$package->id])) { - continue; - } - - $this->whitelistedMap[$package->id] = true; - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true); - - foreach ($possibleRequires as $require) { - $workQueue->enqueue($require); - } - } - - $obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true); - - foreach ($obsoleteProviders as $provider) { - if ($provider === $package) { - continue; - } - - if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { - $workQueue->enqueue($provider); - } - } - } - } - protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs) { $workQueue = new \SplQueue; @@ -225,9 +188,16 @@ class RuleSetGenerator if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package)); - } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = ($packageName == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES; - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $package)); + } else { + if (!isset($this->conflictsForName[$packageName])) { + $this->conflictsForName[$packageName] = array(); + } + if (!$package instanceof AliasPackage) { + $this->conflictsForName[$packageName][$package->id] = $package; + } + if (!$provider instanceof AliasPackage) { + $this->conflictsForName[$packageName][$provider->id] = $provider; + } } } } @@ -248,7 +218,7 @@ class RuleSetGenerator /** @var PackageInterface $possibleConflict */ foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) { - $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true); + $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint()); if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) { $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link)); @@ -258,8 +228,6 @@ class RuleSetGenerator } // check obsoletes and implicit obsoletes of a package - $isInstalled = isset($this->installedMap[$package->id]); - foreach ($package->getReplaces() as $link) { if (!isset($this->addedPackagesByNames[$link->getTarget()])) { continue; @@ -272,12 +240,19 @@ class RuleSetGenerator } if (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES; + $reason = Rule::RULE_PACKAGE_OBSOLETES; $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link)); } } } } + + foreach ($this->conflictsForName as $name => $packages) { + if (count($packages) > 1) { + $reason = Rule::RULE_PACKAGE_SAME_NAME; + $this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, null)); + } + } } protected function obsoleteImpossibleForAlias($package, $provider) @@ -294,77 +269,61 @@ class RuleSetGenerator return $impossible; } - protected function whitelistFromJobs() + protected function addRulesForRequest(Request $request, $ignorePlatformReqs) { - foreach ($this->jobs as $job) { - switch ($job['cmd']) { - case 'install': - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true); - foreach ($packages as $package) { - $this->whitelistFromPackage($package); - } - break; + $unlockableMap = $request->getUnlockableMap(); + + foreach ($request->getFixedPackages() as $package) { + if ($package->id == -1) { + // fixed package was not added to the pool as it did not pass the stability requirements, this is fine + if ($this->pool->isUnacceptableFixedPackage($package)) { + continue; + } + + // otherwise, looks like a bug + throw new \LogicException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool."); + } + + $this->addRulesForPackage($package, $ignorePlatformReqs); + + $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array( + 'package' => $package, + 'lockable' => !isset($unlockableMap[$package->id]), + )); + $this->addRule(RuleSet::TYPE_REQUEST, $rule); + } + + foreach ($request->getRequires() as $packageName => $constraint) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { + continue; + } + + $packages = $this->pool->whatProvides($packageName, $constraint); + if ($packages) { + foreach ($packages as $package) { + $this->addRulesForPackage($package, $ignorePlatformReqs); + } + + $rule = $this->createInstallOneOfRule($packages, Rule::RULE_ROOT_REQUIRE, array( + 'packageName' => $packageName, + 'constraint' => $constraint, + )); + $this->addRule(RuleSet::TYPE_REQUEST, $rule); } } } - protected function addRulesForJobs($ignorePlatformReqs) + public function getRulesFor(Request $request, $ignorePlatformReqs = false) { - foreach ($this->jobs as $job) { - switch ($job['cmd']) { - case 'install': - if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { - break; - } - - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - if ($packages) { - foreach ($packages as $package) { - if (!isset($this->installedMap[$package->id])) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } - } - - $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job); - $this->addRule(RuleSet::TYPE_JOB, $rule); - } - break; - case 'remove': - // remove all packages with this name including uninstalled - // ones to make sure none of them are picked as replacements - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - foreach ($packages as $package) { - $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job); - $this->addRule(RuleSet::TYPE_JOB, $rule); - } - break; - } - } - } - - public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false) - { - $this->jobs = $jobs; $this->rules = new RuleSet; - $this->installedMap = $installedMap; - - $this->whitelistedMap = array(); - foreach ($this->installedMap as $package) { - $this->whitelistFromPackage($package); - } - $this->whitelistFromJobs(); - - $this->pool->setWhitelist($this->whitelistedMap); $this->addedMap = array(); $this->conflictAddedMap = array(); $this->addedPackages = array(); $this->addedPackagesByNames = array(); - foreach ($this->installedMap as $package) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } + $this->conflictsForName = array(); - $this->addRulesForJobs($ignorePlatformReqs); + $this->addRulesForRequest($request, $ignorePlatformReqs); $this->addConflictRules($ignorePlatformReqs); diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php index 31a22414d..e7fae0c61 100644 --- a/src/Composer/DependencyResolver/RuleWatchGraph.php +++ b/src/Composer/DependencyResolver/RuleWatchGraph.php @@ -44,13 +44,24 @@ class RuleWatchGraph return; } - foreach (array($node->watch1, $node->watch2) as $literal) { - if (!isset($this->watchChains[$literal])) { - $this->watchChains[$literal] = new RuleWatchChain; - } + if (!$node->getRule() instanceof MultiConflictRule) { + foreach (array($node->watch1, $node->watch2) as $literal) { + if (!isset($this->watchChains[$literal])) { + $this->watchChains[$literal] = new RuleWatchChain; + } - $this->watchChains[$literal]->unshift($node); + $this->watchChains[$literal]->unshift($node); + } + } else { + foreach ($node->getRule()->getLiterals() as $literal) { + if (!isset($this->watchChains[$literal])) { + $this->watchChains[$literal] = new RuleWatchChain; + } + + $this->watchChains[$literal]->unshift($node); + } } + } /** @@ -92,28 +103,40 @@ class RuleWatchGraph $chain->rewind(); while ($chain->valid()) { $node = $chain->current(); - $otherWatch = $node->getOtherWatch($literal); + if (!$node->getRule() instanceof MultiConflictRule) { + $otherWatch = $node->getOtherWatch($literal); - if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) { - $ruleLiterals = $node->getRule()->getLiterals(); + if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) { + $ruleLiterals = $node->getRule()->getLiterals(); - $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { - return $literal !== $ruleLiteral && - $otherWatch !== $ruleLiteral && - !$decisions->conflict($ruleLiteral); - }); + $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { + return $literal !== $ruleLiteral && + $otherWatch !== $ruleLiteral && + !$decisions->conflict($ruleLiteral); + }); - if ($alternativeLiterals) { - reset($alternativeLiterals); - $this->moveWatch($literal, current($alternativeLiterals), $node); - continue; + if ($alternativeLiterals) { + reset($alternativeLiterals); + $this->moveWatch($literal, current($alternativeLiterals), $node); + continue; + } + + if ($decisions->conflict($otherWatch)) { + return $node->getRule(); + } + + $decisions->decide($otherWatch, $level, $node->getRule()); } + } else { + foreach ($node->getRule()->getLiterals() as $otherLiteral) { + if ($literal !== $otherLiteral && !$decisions->satisfy($otherLiteral)) { + if ($decisions->conflict($otherLiteral)) { + return $node->getRule(); + } - if ($decisions->conflict($otherWatch)) { - return $node->getRule(); + $decisions->decide($otherLiteral, $level, $node->getRule()); + } } - - $decisions->decide($otherWatch, $level, $node->getRule()); } $chain->next(); diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php index eeaa54162..926c144b4 100644 --- a/src/Composer/DependencyResolver/RuleWatchNode.php +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -55,7 +55,7 @@ class RuleWatchNode $literals = $this->rule->getLiterals(); // if there are only 2 elements, both are being watched anyway - if (count($literals) < 3) { + if (count($literals) < 3 || $this->rule instanceof MultiConflictRule) { return; } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index a046a7a92..873f9d778 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -13,7 +13,7 @@ namespace Composer\DependencyResolver; use Composer\IO\IOInterface; -use Composer\Repository\RepositoryInterface; +use Composer\Package\PackageInterface; use Composer\Repository\PlatformRepository; /** @@ -28,23 +28,18 @@ class Solver protected $policy; /** @var Pool */ protected $pool; - /** @var RepositoryInterface */ - protected $installed; + /** @var RuleSet */ protected $rules; /** @var RuleSetGenerator */ protected $ruleSetGenerator; - /** @var array */ - protected $jobs; - /** @var int[] */ - protected $updateMap = array(); /** @var RuleWatchGraph */ protected $watchGraph; /** @var Decisions */ protected $decisions; - /** @var int[] */ - protected $installedMap; + /** @var PackageInterface[] */ + protected $fixedMap; /** @var int */ protected $propagateIndex; @@ -66,16 +61,13 @@ class Solver /** * @param PolicyInterface $policy * @param Pool $pool - * @param RepositoryInterface $installed * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io) + public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io) { $this->io = $io; $this->policy = $policy; $this->pool = $pool; - $this->installed = $installed; - $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool); } /** @@ -86,6 +78,11 @@ class Solver return count($this->rules); } + public function getPool() + { + return $this->pool; + } + // aka solver_makeruledecisions private function makeAssertionRuleDecisions() @@ -121,23 +118,23 @@ class Solver $conflict = $this->decisions->decisionRule($literal); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); - $this->disableProblem($rule); + $rule->disable(); $this->problems[] = $problem; continue; } - // conflict with another job - $problem = new Problem($this->pool); + // conflict with another root require/fixed package + $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); - // push all of our rules (can only be job rules) + // push all of our rules (can only be root require/fixed package rules) // asserting this literal on the problem stack - foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) { + foreach ($this->rules->getIteratorFor(RuleSet::TYPE_REQUEST) as $assertRule) { if ($assertRule->isDisabled() || !$assertRule->isAssertion()) { continue; } @@ -148,9 +145,8 @@ class Solver if (abs($literal) !== abs($assertRuleLiteral)) { continue; } - $problem->addRule($assertRule); - $this->disableProblem($assertRule); + $assertRule->disable(); } $this->problems[] = $problem; @@ -159,47 +155,29 @@ class Solver } } - protected function setupInstalledMap() + protected function setupFixedMap(Request $request) { - $this->installedMap = array(); - foreach ($this->installed->getPackages() as $package) { - $this->installedMap[$package->id] = $package; + $this->fixedMap = array(); + foreach ($request->getFixedPackages() as $package) { + $this->fixedMap[$package->id] = $package; } } /** + * @param Request $request * @param bool $ignorePlatformReqs */ - protected function checkForRootRequireProblems($ignorePlatformReqs) + protected function checkForRootRequireProblems($request, $ignorePlatformReqs) { - foreach ($this->jobs as $job) { - switch ($job['cmd']) { - case 'update': - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - foreach ($packages as $package) { - if (isset($this->installedMap[$package->id])) { - $this->updateMap[$package->id] = true; - } - } - break; + foreach ($request->getRequires() as $packageName => $constraint) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { + continue; + } - case 'update-all': - foreach ($this->installedMap as $package) { - $this->updateMap[$package->id] = true; - } - break; - - case 'install': - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { - break; - } - - if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) { - $problem = new Problem($this->pool); - $problem->addRule(new GenericRule(array(), null, null, $job)); - $this->problems[] = $problem; - } - break; + if (!$this->pool->whatProvides($packageName, $constraint)) { + $problem = new Problem(); + $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint))); + $this->problems[] = $problem; } } } @@ -207,15 +185,16 @@ class Solver /** * @param Request $request * @param bool $ignorePlatformReqs - * @return array + * @return LockTransaction */ public function solve(Request $request, $ignorePlatformReqs = false) { - $this->jobs = $request->getJobs(); + $this->setupFixedMap($request); - $this->setupInstalledMap(); - $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); - $this->checkForRootRequireProblems($ignorePlatformReqs); + $this->io->writeError('Generating rules', true, IOInterface::DEBUG); + $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); + $this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs); + $this->checkForRootRequireProblems($request, $ignorePlatformReqs); $this->decisions = new Decisions($this->pool); $this->watchGraph = new RuleWatchGraph; @@ -223,29 +202,20 @@ class Solver $this->watchGraph->insert(new RuleWatchNode($rule)); } - /* make decisions based on job/update assertions */ + /* make decisions based on root require/fix assertions */ $this->makeAssertionRuleDecisions(); $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG); $before = microtime(true); - $this->runSat(true); + $this->runSat(); $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); - // decide to remove everything that's installed and undecided - foreach ($this->installedMap as $packageId => $void) { - if ($this->decisions->undecided($packageId)) { - $this->decisions->decide(-$packageId, 1, null); - } - } - if ($this->problems) { - throw new SolverProblemsException($this->problems, $this->installedMap); + throw new SolverProblemsException($this->problems, $this->learnedPool); } - $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions); - - return $transaction->getOperations(); + return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); } /** @@ -322,11 +292,10 @@ class Solver * * @param int $level * @param string|int $literal - * @param bool $disableRules * @param Rule $rule * @return int */ - private function setPropagateLearn($level, $literal, $disableRules, Rule $rule) + private function setPropagateLearn($level, $literal, Rule $rule) { $level++; @@ -340,7 +309,7 @@ class Solver } if ($level == 1) { - return $this->analyzeUnsolvable($rule, $disableRules); + return $this->analyzeUnsolvable($rule); } // conflict @@ -377,14 +346,13 @@ class Solver /** * @param int $level * @param array $decisionQueue - * @param bool $disableRules * @param Rule $rule * @return int */ - private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule) + private function selectAndInstall($level, array $decisionQueue, Rule $rule) { // choose best package to install from decisionQueue - $literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage()); + $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage()); $selectedLiteral = array_shift($literals); @@ -393,7 +361,7 @@ class Solver $this->branches[] = array($literals, $level); } - return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule); + return $this->setPropagateLearn($level, $selectedLiteral, $rule); } /** @@ -539,12 +507,11 @@ class Solver /** * @param Rule $conflictRule - * @param bool $disableRules * @return int */ - private function analyzeUnsolvable(Rule $conflictRule, $disableRules) + private function analyzeUnsolvable(Rule $conflictRule) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($conflictRule); $this->analyzeUnsolvableRule($problem, $conflictRule); @@ -586,41 +553,9 @@ class Solver } } - if ($disableRules) { - foreach ($this->problems[count($this->problems) - 1] as $reason) { - $this->disableProblem($reason['rule']); - } - - $this->resetSolver(); - - return 1; - } - return 0; } - /** - * @param Rule $why - */ - private function disableProblem(Rule $why) - { - $job = $why->getJob(); - - if (!$job) { - $why->disable(); - - return; - } - - // disable all rules of this job - foreach ($this->rules as $rule) { - /** @var Rule $rule */ - if ($job === $rule->getJob()) { - $rule->disable(); - } - } - } - private function resetSolver() { $this->decisions->reset(); @@ -661,17 +596,14 @@ class Solver } } - /** - * @param bool $disableRules - */ - private function runSat($disableRules = true) + private function runSat() { $this->propagateIndex = 0; /* * here's the main loop: * 1) propagate new decisions (only needed once) - * 2) fulfill jobs + * 2) fulfill root requires/fixed packages * 3) fulfill all unresolved rules * 4) minimalize solution if we had choices * if we encounter a problem, we rewind to a safe level and restart @@ -679,10 +611,7 @@ class Solver */ $decisionQueue = array(); - /** - * @todo this makes $disableRules always false; determine the rationale and possibly remove dead code? - */ - $disableRules = array(); + $decisionSupplementQueue = array(); $level = 1; $systemLevel = $level + 1; @@ -691,7 +620,7 @@ class Solver if (1 === $level) { $conflictRule = $this->propagate($level); if (null !== $conflictRule) { - if ($this->analyzeUnsolvable($conflictRule, $disableRules)) { + if ($this->analyzeUnsolvable($conflictRule)) { continue; } @@ -699,9 +628,9 @@ class Solver } } - // handle job rules + // handle root require/fixed package rules if ($level < $systemLevel) { - $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB); + $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_REQUEST); foreach ($iterator as $rule) { if ($rule->isEnabled()) { $decisionQueue = array(); @@ -718,26 +647,21 @@ class Solver } if ($noneSatisfied && count($decisionQueue)) { - // prune all update packages until installed version - // except for requested updates - if (count($this->installed) != count($this->updateMap)) { - $prunedQueue = array(); - foreach ($decisionQueue as $literal) { - if (isset($this->installedMap[abs($literal)])) { - $prunedQueue[] = $literal; - if (isset($this->updateMap[abs($literal)])) { - $prunedQueue = $decisionQueue; - break; - } - } + // if any of the options in the decision queue are fixed, only use those + $prunedQueue = array(); + foreach ($decisionQueue as $literal) { + if (isset($this->fixedMap[abs($literal)])) { + $prunedQueue[] = $literal; } + } + if (!empty($prunedQueue)) { $decisionQueue = $prunedQueue; } } if ($noneSatisfied && count($decisionQueue)) { $oLevel = $level; - $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule); + $level = $this->selectAndInstall($level, $decisionQueue, $rule); if (0 === $level) { return; @@ -751,7 +675,7 @@ class Solver $systemLevel = $level + 1; - // jobs left + // root requires/fixed packages left $iterator->next(); if ($iterator->valid()) { continue; @@ -813,7 +737,7 @@ class Solver continue; } - $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule); + $level = $this->selectAndInstall($level, $decisionQueue, $rule); if (0 === $level) { return; @@ -856,7 +780,7 @@ class Solver $why = $this->decisions->lastReason(); - $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why); + $level = $this->setPropagateLearn($level, $lastLiteral, $why); if ($level == 0) { return; diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 142895697..cfaa110c6 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -13,6 +13,7 @@ namespace Composer\DependencyResolver; use Composer\Util\IniHelper; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -20,29 +21,33 @@ use Composer\Util\IniHelper; class SolverProblemsException extends \RuntimeException { protected $problems; - protected $installedMap; + protected $learnedPool; - public function __construct(array $problems, array $installedMap) + public function __construct(array $problems, array $learnedPool) { $this->problems = $problems; - $this->installedMap = $installedMap; + $this->learnedPool = $learnedPool; - parent::__construct($this->createMessage(), 2); + parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2); } - protected function createMessage() + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isDevExtraction = false) { + $installedMap = $request->getPresentMap(true); $text = "\n"; $hasExtensionProblems = false; + $isCausedByLock = false; foreach ($this->problems as $i => $problem) { - $text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n"; + $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; } + + $isCausedByLock |= $problem->isCausedByLock(); } - if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) { + if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead for further common problems."; } @@ -50,6 +55,10 @@ class SolverProblemsException extends \RuntimeException $text .= $this->createExtensionHint(); } + if ($isCausedByLock && !$isDevExtraction) { + $text .= "\nUse the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions."; + } + return $text; } @@ -76,8 +85,8 @@ class SolverProblemsException extends \RuntimeException private function hasExtensionProblems(array $reasonSets) { foreach ($reasonSets as $reasonSet) { - foreach ($reasonSet as $reason) { - if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) { + foreach ($reasonSet as $rule) { + if (0 === strpos($rule->getRequiredPackage(), 'ext-')) { return true; } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index c8d3bbe53..6f7cc79fb 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -13,161 +13,205 @@ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Composer\Repository\PlatformRepository; /** * @author Nils Adermann */ class Transaction { - protected $policy; - protected $pool; - protected $installedMap; - protected $decisions; - protected $transaction; + /** + * @var array + */ + protected $operations; - public function __construct($policy, $pool, $installedMap, $decisions) + /** + * Packages present at the beginning of the transaction + * @var array + */ + protected $presentPackages; + + /** + * Package set resulting from this transaction + * @var array + */ + protected $resultPackageMap; + + /** + * @var array + */ + protected $resultPackagesByName = array(); + + public function __construct($presentPackages, $resultPackages) { - $this->policy = $policy; - $this->pool = $pool; - $this->installedMap = $installedMap; - $this->decisions = $decisions; - $this->transaction = array(); + $this->presentPackages = $presentPackages; + $this->setResultPackageMaps($resultPackages); + $this->operations = $this->calculateOperations(); } public function getOperations() { - $installMeansUpdateMap = $this->findUpdates(); + return $this->operations; + } - $updateMap = array(); - $installMap = array(); - $uninstallMap = array(); + private function setResultPackageMaps($resultPackages) + { + $packageSort = function (PackageInterface $a, PackageInterface $b) { + // sort alias packages by the same name behind their non alias version + if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { + return $a instanceof AliasPackage ? -1 : 1; + } + return strcmp($b->getName(), $a->getName()); + }; - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; + $this->resultPackageMap = array(); + foreach ($resultPackages as $package) { + $this->resultPackageMap[spl_object_hash($package)] = $package; + foreach ($package->getNames() as $name) { + $this->resultPackagesByName[$name][] = $package; + } + } - $package = $this->pool->literalToPackage($literal); + uasort($this->resultPackageMap, $packageSort); + foreach ($this->resultPackagesByName as $name => $packages) { + uasort($this->resultPackagesByName[$name], $packageSort); + } + } - // wanted & installed || !wanted & !installed - if (($literal > 0) == isset($this->installedMap[$package->id])) { + protected function calculateOperations() + { + $operations = array(); + + $presentPackageMap = array(); + $removeMap = array(); + $presentAliasMap = array(); + $removeAliasMap = array(); + foreach ($this->presentPackages as $package) { + if ($package instanceof AliasPackage) { + $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + } else { + $presentPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; + } + } + + $stack = $this->getRootPackages(); + + $visited = array(); + $processed = array(); + + while (!empty($stack)) { + $package = array_pop($stack); + + if (isset($processed[spl_object_hash($package)])) { continue; } - if ($literal > 0) { - if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { - $source = $installMeansUpdateMap[abs($literal)]; - - $updateMap[$package->id] = array( - 'package' => $package, - 'source' => $source, - 'reason' => $reason, - ); - - // avoid updates to one package from multiple origins - unset($installMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->id] = true; - } else { - $installMap[$package->id] = array( - 'package' => $package, - 'reason' => $reason, - ); - } - } - } - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && - isset($this->installedMap[$package->id]) && - !isset($ignoreRemove[$package->id])) { - $uninstallMap[$package->id] = array( - 'package' => $package, - 'reason' => $reason, - ); - } - } - - $this->transactionFromMaps($installMap, $updateMap, $uninstallMap); - - return $this->transaction; - } - - protected function transactionFromMaps($installMap, $updateMap, $uninstallMap) - { - $queue = array_map( - function ($operation) { - return $operation['package']; - }, - $this->findRootPackages($installMap, $updateMap) - ); - - $visited = array(); - - while (!empty($queue)) { - $package = array_pop($queue); - $packageId = $package->id; - - if (!isset($visited[$packageId])) { - $queue[] = $package; + if (!isset($visited[spl_object_hash($package)])) { + $visited[spl_object_hash($package)] = true; + $stack[] = $package; if ($package instanceof AliasPackage) { - $queue[] = $package->getAliasOf(); + $stack[] = $package->getAliasOf(); } else { foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + $possibleRequires = $this->getProvidersInResult($link); foreach ($possibleRequires as $require) { - $queue[] = $require; + $stack[] = $require; } } } + } elseif (!isset($processed[spl_object_hash($package)])) { + $processed[spl_object_hash($package)] = true; - $visited[$package->id] = true; - } else { - if (isset($installMap[$packageId])) { - $this->install( - $installMap[$packageId]['package'], - $installMap[$packageId]['reason'] - ); - unset($installMap[$packageId]); - } - if (isset($updateMap[$packageId])) { - $this->update( - $updateMap[$packageId]['source'], - $updateMap[$packageId]['package'], - $updateMap[$packageId]['reason'] - ); - unset($updateMap[$packageId]); + if ($package instanceof AliasPackage) { + $aliasKey = $package->getName().'::'.$package->getVersion(); + if (isset($presentAliasMap[$aliasKey])) { + unset($removeAliasMap[$aliasKey]); + } else { + $operations[] = new Operation\MarkAliasInstalledOperation($package); + } + } else { + if (isset($presentPackageMap[$package->getName()])) { + $source = $presentPackageMap[$package->getName()]; + + // do we need to update? + // TODO different for lock? + if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() || + $package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() || + $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference() + ) { + $operations[] = new Operation\UpdateOperation($source, $package); + } + unset($removeMap[$package->getName()]); + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } } } } - foreach ($uninstallMap as $uninstall) { - $this->uninstall($uninstall['package'], $uninstall['reason']); + foreach ($removeMap as $name => $package) { + array_unshift($operations, new Operation\UninstallOperation($package, null)); } + foreach ($removeAliasMap as $nameVersion => $package) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); + } + + $operations = $this->movePluginsToFront($operations); + // TODO fix this: + // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls + $operations = $this->moveUninstallsToFront($operations); + + // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? + /* + if ('update' === $opType) { + $targetPackage = $operation->getTargetPackage(); + if ($targetPackage->isDev()) { + $initialPackage = $operation->getInitialPackage(); + if ($targetPackage->getVersion() === $initialPackage->getVersion() + && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) + && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) + ) { + $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); + $this->io->writeError('', true, IOInterface::DEBUG); + + continue; + } + } + }*/ + + return $this->operations = $operations; } - protected function findRootPackages($installMap, $updateMap) + /** + * Determine which packages in the result are not required by any other packages in it. + * + * These serve as a starting point to enumerate packages in a topological order despite potential cycles. + * If there are packages with a cycle on the top level the package with the lowest name gets picked + * + * @return array + */ + protected function getRootPackages() { - $packages = $installMap + $updateMap; - $roots = $packages; + $roots = $this->resultPackageMap; - foreach ($packages as $packageId => $operation) { - $package = $operation['package']; - - if (!isset($roots[$packageId])) { + foreach ($this->resultPackageMap as $packageHash => $package) { + if (!isset($roots[$packageHash])) { continue; } foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + $possibleRequires = $this->getProvidersInResult($link); foreach ($possibleRequires as $require) { if ($require !== $package) { - unset($roots[$require->id]); + unset($roots[spl_object_hash($require)]); } } } @@ -176,69 +220,87 @@ class Transaction return $roots; } - protected function findUpdates() + protected function getProvidersInResult(Link $link) { - $installMeansUpdateMap = array(); + if (!isset($this->resultPackagesByName[$link->getTarget()])) { + return array(); + } + return $this->resultPackagesByName[$link->getTarget()]; + } - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $package = $this->pool->literalToPackage($literal); + /** + * Workaround: if your packages depend on plugins, we must be sure + * that those are installed / updated first; else it would lead to packages + * being installed multiple times in different folders, when running Composer + * twice. + * + * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, + * it at least fixes the symptoms and makes usage of composer possible (again) + * in such scenarios. + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function movePluginsToFront(array $operations) + { + $pluginsNoDeps = array(); + $pluginsWithDeps = array(); + $pluginRequires = array(); - if ($package instanceof AliasPackage) { + foreach (array_reverse($operations, true) as $idx => $op) { + if ($op instanceof Operation\InstallOperation) { + $package = $op->getPackage(); + } elseif ($op instanceof Operation\UpdateOperation) { + $package = $op->getTargetPackage(); + } else { continue; } - // !wanted & installed - if ($literal <= 0 && isset($this->installedMap[$package->id])) { - $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); + // is this package a plugin? + $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; - $literals = array($package->id); + // is this a plugin or a dependency of a plugin? + if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { + // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); - foreach ($updates as $update) { - $literals[] = $update->id; + // is this a plugin with no meaningful dependencies? + if ($isPlugin && !count($requires)) { + // plugins with no dependencies go to the very front + array_unshift($pluginsNoDeps, $op); + } else { + // capture the requirements for this package so those packages will be moved up as well + $pluginRequires = array_merge($pluginRequires, $requires); + // move the operation to the front + array_unshift($pluginsWithDeps, $op); } - foreach ($literals as $updateLiteral) { - if ($updateLiteral !== $literal) { - $installMeansUpdateMap[abs($updateLiteral)] = $package; - } - } + unset($operations[$idx]); } } - return $installMeansUpdateMap; + return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); } - protected function install($package, $reason) + /** + * Removals of packages should be executed before installations in + * case two packages resolve to the same path (due to custom installers) + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function moveUninstallsToFront(array $operations) { - if ($package instanceof AliasPackage) { - return $this->markAliasInstalled($package, $reason); + $uninstOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof Operation\UninstallOperation) { + $uninstOps[] = $op; + unset($operations[$idx]); + } } - $this->transaction[] = new Operation\InstallOperation($package, $reason); - } - - protected function update($from, $to, $reason) - { - $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason); - } - - protected function uninstall($package, $reason) - { - if ($package instanceof AliasPackage) { - return $this->markAliasUninstalled($package, $reason); - } - - $this->transaction[] = new Operation\UninstallOperation($package, $reason); - } - - protected function markAliasInstalled($package, $reason) - { - $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason); - } - - protected function markAliasUninstalled($package, $reason) - { - $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason); + return array_merge($uninstOps, $operations); } } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index d041a7f88..318e73c6f 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -30,33 +30,56 @@ abstract class ArchiveDownloader extends FileDownloader * @throws \RuntimeException * @throws \UnexpectedValueException */ - public function download(PackageInterface $package, $path, $output = true) + public function install(PackageInterface $package, $path, $output = true) { - $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); - $retries = 3; - while ($retries--) { - $fileName = parent::download($package, $path, $output); + if ($output) { + $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): Extracting archive"); + } else { + $this->io->writeError('Extracting archive', false); + } - if ($output) { - $this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE); + $this->filesystem->ensureDirectoryExists($path); + if (!$this->filesystem->isDirEmpty($path)) { + throw new \RuntimeException('Expected empty path to extract '.$package.' into but directory exists: '.$path); + } + + do { + $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); + } while (is_dir($temporaryDir)); + + $fileName = $this->getFileName($package, $path); + + 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; } - try { - $this->filesystem->ensureDirectoryExists($temporaryDir); - try { - $this->extract($fileName, $temporaryDir); - } catch (\Exception $e) { - // remove cache if the file was corrupted - parent::clearLastCacheWrite($package); - throw $e; + $this->filesystem->unlink($fileName); + + $renameAsOne = false; + if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) { + $renameAsOne = true; + } + + $contentDir = $this->getFolderContent($temporaryDir); + $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir)); + + if ($renameAsOne) { + // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents + if ($singleDirAtTopLevel) { + $extractedDir = (string) reset($contentDir); + } else { + $extractedDir = $temporaryDir; } - - $this->filesystem->unlink($fileName); - - $contentDir = $this->getFolderContent($temporaryDir); - + $this->filesystem->rename($extractedDir, $path); + } else { // only one dir in the archive, extract its contents out of it - if (1 === count($contentDir) && is_dir(reset($contentDir))) { + if ($singleDirAtTopLevel) { $contentDir = $this->getFolderContent((string) reset($contentDir)); } @@ -65,44 +88,16 @@ abstract class ArchiveDownloader extends FileDownloader $file = (string) $file; $this->filesystem->rename($file, $path . '/' . basename($file)); } - - $this->filesystem->removeDirectory($temporaryDir); - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { - $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); - } - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { - $this->filesystem->removeDirectory($this->config->get('vendor-dir')); - } - } catch (\Exception $e) { - // clean up - $this->filesystem->removeDirectory($path); - $this->filesystem->removeDirectory($temporaryDir); - - // retry downloading if we have an invalid zip file - if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) { - $this->io->writeError(''); - if ($this->io->isDebug()) { - $this->io->writeError(' Invalid zip file ('.$e->getMessage().'), retrying...'); - } else { - $this->io->writeError(' Invalid zip file, retrying...'); - } - usleep(500000); - continue; - } - - throw $e; } - break; - } - } + $this->filesystem->removeDirectory($temporaryDir); + } catch (\Exception $e) { + // clean up + $this->filesystem->removeDirectory($path); + $this->filesystem->removeDirectory($temporaryDir); - /** - * {@inheritdoc} - */ - protected function getFileName(PackageInterface $package, $path) - { - return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); + throw $e; + } } /** @@ -113,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader * * @throws \UnexpectedValueException If can not extract downloaded file to path */ - abstract protected function extract($file, $path); + abstract protected function extract(PackageInterface $package, $file, $path); /** * Returns the folder content, excluding dotfiles diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 15c00a6e6..3ee786c17 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\IO\IOInterface; use Composer\Util\Filesystem; +use React\Promise\PromiseInterface; /** * Downloaders manager. @@ -24,6 +25,7 @@ use Composer\Util\Filesystem; class DownloadManager { private $io; + private $httpDownloader; private $preferDist = false; private $preferSource = false; private $packagePreferences = array(); @@ -33,9 +35,9 @@ class DownloadManager /** * Initializes download manager. * - * @param IOInterface $io The Input Output Interface - * @param bool $preferSource prefer downloading from source - * @param Filesystem|null $filesystem custom Filesystem object + * @param IOInterface $io The Input Output Interface + * @param bool $preferSource prefer downloading from source + * @param Filesystem|null $filesystem custom Filesystem object */ public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null) { @@ -83,22 +85,6 @@ class DownloadManager return $this; } - /** - * Sets whether to output download progress information for all registered - * downloaders - * - * @param bool $outputProgress - * @return DownloadManager - */ - public function setOutputProgress($outputProgress) - { - foreach ($this->downloaders as $downloader) { - $downloader->setOutputProgress($outputProgress); - } - - return $this; - } - /** * Sets installer downloader for a specific installation type. * @@ -140,7 +126,7 @@ class DownloadManager * wrong type * @return DownloaderInterface|null */ - public function getDownloaderForInstalledPackage(PackageInterface $package) + public function getDownloaderForPackage(PackageInterface $package) { $installationSource = $package->getInstallationSource(); @@ -154,7 +140,7 @@ class DownloadManager $downloader = $this->getDownloader($package->getSourceType()); } else { throw new \InvalidArgumentException( - 'Package '.$package.' seems not been installed properly' + 'Package '.$package.' does not have an installation source set' ); } @@ -171,63 +157,117 @@ class DownloadManager return $downloader; } + public function getDownloaderType(DownloaderInterface $downloader) + { + return array_search($downloader, $this->downloaders); + } + /** * Downloads package into target dir. * - * @param PackageInterface $package package instance - * @param string $targetDir target dir - * @param bool $preferSource prefer installation from source + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param PackageInterface|null $prevPackage previous package instance in case of updates * + * @return PromiseInterface * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException */ - public function download(PackageInterface $package, $targetDir, $preferSource = null) + public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { - $preferSource = null !== $preferSource ? $preferSource : $this->preferSource; - $sourceType = $package->getSourceType(); - $distType = $package->getDistType(); + $targetDir = $this->normalizeTargetDir($targetDir); + $this->filesystem->ensureDirectoryExists(dirname($targetDir)); - $sources = array(); - if ($sourceType) { - $sources[] = 'source'; - } - if ($distType) { - $sources[] = 'dist'; - } + $sources = $this->getAvailableSources($package, $prevPackage); - if (empty($sources)) { - throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); - } + $io = $this->io; + $self = $this; - if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) { - $sources = array_reverse($sources); - } - - $this->filesystem->ensureDirectoryExists($targetDir); - - foreach ($sources as $i => $source) { - if (isset($e)) { - $this->io->writeError(' Now trying to download from ' . $source . ''); + $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) { + $source = array_shift($sources); + if ($retry) { + $io->writeError(' Now trying to download from ' . $source . ''); } $package->setInstallationSource($source); - try { - $downloader = $this->getDownloaderForInstalledPackage($package); - if ($downloader) { - $downloader->download($package, $targetDir); - } - break; - } catch (\RuntimeException $e) { - if ($i === count($sources) - 1) { - throw $e; + + $downloader = $self->getDownloaderForPackage($package); + if (!$downloader) { + return \React\Promise\resolve(); + } + + $handleError = function ($e) use ($sources, $source, $package, $io, $download) { + if ($e instanceof \RuntimeException) { + if (!$sources) { + throw $e; + } + + $io->writeError( + ' Failed to download '. + $package->getPrettyName(). + ' from ' . $source . ': '. + $e->getMessage().'' + ); + + return $download(true); } - $this->io->writeError( - ' Failed to download '. - $package->getPrettyName(). - ' from ' . $source . ': '. - $e->getMessage().'' - ); + throw $e; + }; + + try { + $result = $downloader->download($package, $targetDir, $prevPackage); + } catch (\Exception $e) { + return $handleError($e); } + if (!$result instanceof PromiseInterface) { + return \React\Promise\resolve($result); + } + + $res = $result->then(function ($res) { + return $res; + }, $handleError); + + return $res; + }; + + return $download(); + } + + /** + * Prepares an operation execution + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param PackageInterface|null $prevPackage previous package instance in case of updates + * + * @return PromiseInterface|null + */ + public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) + { + $targetDir = $this->normalizeTargetDir($targetDir); + $downloader = $this->getDownloaderForPackage($package); + if ($downloader) { + return $downloader->prepare($type, $package, $targetDir, $prevPackage); + } + } + + /** + * Installs package into target dir. + * + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * + * @return PromiseInterface|null + * @throws \InvalidArgumentException if package have no urls to download from + * @throws \RuntimeException + */ + public function install(PackageInterface $package, $targetDir) + { + $targetDir = $this->normalizeTargetDir($targetDir); + $downloader = $this->getDownloaderForPackage($package); + if ($downloader) { + return $downloader->install($package, $targetDir); } } @@ -238,39 +278,30 @@ class DownloadManager * @param PackageInterface $target target package version * @param string $targetDir target dir * + * @return PromiseInterface|null * @throws \InvalidArgumentException if initial package is not installed */ public function update(PackageInterface $initial, PackageInterface $target, $targetDir) { - $downloader = $this->getDownloaderForInstalledPackage($initial); + $targetDir = $this->normalizeTargetDir($targetDir); + $downloader = $this->getDownloaderForPackage($target); + $initialDownloader = $this->getDownloaderForPackage($initial); + + // no downloaders present means update from metapackage to metapackage, nothing to do + if (!$initialDownloader && !$downloader) { + return; + } + + // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed if (!$downloader) { - return; - } - - $installationSource = $initial->getInstallationSource(); - - if ('dist' === $installationSource) { - $initialType = $initial->getDistType(); - $targetType = $target->getDistType(); - } else { - $initialType = $initial->getSourceType(); - $targetType = $target->getSourceType(); - } - - // upgrading from a dist stable package to a dev package, force source reinstall - if ($target->isDev() && 'dist' === $installationSource) { - $downloader->remove($initial, $targetDir); - $this->download($target, $targetDir); - - return; + return $initialDownloader->remove($initial, $targetDir); } + $initialType = $this->getDownloaderType($initialDownloader); + $targetType = $this->getDownloaderType($downloader); if ($initialType === $targetType) { - $target->setInstallationSource($installationSource); try { - $downloader->update($initial, $target, $targetDir); - - return; + return $downloader->update($initial, $target, $targetDir); } catch (\RuntimeException $e) { if (!$this->io->isInteractive()) { throw $e; @@ -282,8 +313,17 @@ class DownloadManager } } - $downloader->remove($initial, $targetDir); - $this->download($target, $targetDir, 'source' === $installationSource); + // if downloader type changed, or update failed and user asks for reinstall, + // we wipe the dir and do a new install instead of updating it + $promise = $initialDownloader->remove($initial, $targetDir); + if ($promise) { + $self = $this; + return $promise->then(function ($res) use ($self, $target, $targetDir) { + return $self->install($target, $targetDir); + }); + } + + return $this->install($target, $targetDir); } /** @@ -291,12 +331,34 @@ class DownloadManager * * @param PackageInterface $package package instance * @param string $targetDir target dir + * + * @return PromiseInterface|null */ public function remove(PackageInterface $package, $targetDir) { - $downloader = $this->getDownloaderForInstalledPackage($package); + $targetDir = $this->normalizeTargetDir($targetDir); + $downloader = $this->getDownloaderForPackage($package); if ($downloader) { - $downloader->remove($package, $targetDir); + return $downloader->remove($package, $targetDir); + } + } + + /** + * Cleans up a failed operation + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param PackageInterface|null $prevPackage previous package instance in case of updates + * + * @return PromiseInterface|null + */ + public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) + { + $targetDir = $this->normalizeTargetDir($targetDir); + $downloader = $this->getDownloaderForPackage($package); + if ($downloader) { + return $downloader->cleanup($type, $package, $targetDir, $prevPackage); } } @@ -322,4 +384,64 @@ class DownloadManager return $package->isDev() ? 'source' : 'dist'; } + + /** + * @return string[] + */ + private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null) + { + $sourceType = $package->getSourceType(); + $distType = $package->getDistType(); + + // add source before dist by default + $sources = array(); + if ($sourceType) { + $sources[] = 'source'; + } + if ($distType) { + $sources[] = 'dist'; + } + + if (empty($sources)) { + throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); + } + + if ( + $prevPackage + // if we are updating, we want to keep the same source as the previously installed package (if available in the new one) + && in_array($prevPackage->getInstallationSource(), $sources, true) + // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over + && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev()) + ) { + $prevSource = $prevPackage->getInstallationSource(); + usort($sources, function ($a, $b) use ($prevSource) { + return $a === $prevSource ? -1 : 1; + }); + + return $sources; + } + + // reverse sources in case dist is the preferred source for this package + if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) { + $sources = array_reverse($sources); + } + + return $sources; + } + + /** + * Downloaders expect a /path/to/dir without trailing slash + * + * If any Installer provides a path with a trailing slash, this can cause bugs so make sure we remove them + * + * @return string + */ + private function normalizeTargetDir($dir) + { + if ($dir === '\\' || $dir === '/') { + return $dir; + } + + return rtrim($dir, '\\/'); + } } diff --git a/src/Composer/Downloader/DownloaderInterface.php b/src/Composer/Downloader/DownloaderInterface.php index 713bf36dc..01e7f95c8 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -13,6 +13,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; +use React\Promise\PromiseInterface; /** * Downloader interface. @@ -30,12 +31,35 @@ interface DownloaderInterface public function getInstallationSource(); /** - * Downloads specific package into specific folder. + * This should do any network-related tasks to prepare for an upcoming install/update + * + * @return PromiseInterface|null + */ + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null); + + /** + * Do anything that needs to be done between all downloads have been completed and the actual operation is executed + * + * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore + * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or + * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can + * be undone as much as possible. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $path download path + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null); + + /** + * Installs specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path */ - public function download(PackageInterface $package, $path); + public function install(PackageInterface $package, $path); /** * Updates specific package in specific folder from initial to target version. @@ -55,10 +79,17 @@ interface DownloaderInterface public function remove(PackageInterface $package, $path); /** - * Sets whether to output download progress information or not + * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps * - * @param bool $outputProgress - * @return DownloaderInterface + * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give + * all installers a change to cleanup things they did previously, so you need to keep track of changes + * applied in the installer/downloader themselves. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $path download path + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null */ - public function setOutputProgress($outputProgress); + public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 819fbcefb..bce1b7133 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -24,8 +24,9 @@ use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\Url as UrlUtil; +use Composer\Downloader\TransportException; /** * Base downloader for files @@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface { protected $io; protected $config; - protected $rfs; + protected $httpDownloader; protected $filesystem; protected $cache; - protected $outputProgress = true; - private $lastCacheWrites = array(); + /** + * @private this is only public for php 5.3 support in closures + */ + public $lastCacheWrites = array(); private $eventDispatcher; /** @@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface * * @param IOInterface $io The IO instance * @param Config $config The config + * @param HttpDownloader $httpDownloader The remote filesystem * @param EventDispatcher $eventDispatcher The event dispatcher - * @param Cache $cache Optional cache instance - * @param RemoteFilesystem $rfs The remote filesystem + * @param Cache $cache Cache instance * @param Filesystem $filesystem The filesystem */ - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null) { $this->io = $io; $this->config = $config; $this->eventDispatcher = $eventDispatcher; - $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader; $this->filesystem = $filesystem ?: new Filesystem(); $this->cache = $cache; @@ -81,127 +84,191 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface /** * {@inheritDoc} */ - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { if (!$package->getDistUrl()) { throw new \InvalidArgumentException('The given package is missing url information'); } - if ($output) { - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): ", false); - } - + $retries = 3; $urls = $package->getDistUrls(); - while ($url = array_shift($urls)) { - try { - $fileName = $this->doDownload($package, $path, $url); - break; - } catch (\Exception $e) { - if ($this->io->isDebug()) { - $this->io->writeError(''); - $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); - } elseif (count($urls)) { - $this->io->writeError(''); - $this->io->writeError(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')', false); - } - - if (!count($urls)) { - throw $e; - } - } + foreach ($urls as $index => $url) { + $processedUrl = $this->processUrl($package, $url); + $urls[$index] = array( + 'base' => $url, + 'processed' => $processedUrl, + 'cacheKey' => $this->getCacheKey($package, $processedUrl) + ); } - if ($output) { - $this->io->writeError(''); - } - - return $fileName; - } - - protected function doDownload(PackageInterface $package, $path, $url) - { - $this->filesystem->emptyDirectory($path); - $fileName = $this->getFileName($package, $path); + $this->filesystem->ensureDirectoryExists($path); + $this->filesystem->ensureDirectoryExists(dirname($fileName)); - $processedUrl = $this->processUrl($package, $url); - $origin = RemoteFilesystem::getOrigin($processedUrl); + $io = $this->io; + $cache = $this->cache; + $httpDownloader = $this->httpDownloader; + $eventDispatcher = $this->eventDispatcher; + $filesystem = $this->filesystem; + $self = $this; - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl); - if ($this->eventDispatcher) { - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - } - $rfs = $preFileDownloadEvent->getRemoteFilesystem(); + $accept = null; + $reject = null; + $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) { + $url = reset($urls); + + if ($eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); + $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + } - try { $checksum = $package->getDistSha1Checksum(); - $cacheKey = $this->getCacheKey($package, $processedUrl); + $cacheKey = $url['cacheKey']; // use from cache if it is present and has a valid checksum or we have no checksum to check against - if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) { - $this->io->writeError('Loading from cache', false); + if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) { + if ($output) { + $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); + } + $result = \React\Promise\resolve($fileName); } else { - // download if cache restore failed - if (!$this->outputProgress) { - $this->io->writeError('Downloading', false); + if ($output) { + $io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } - // try to download 3 times then fail hard - $retries = 3; - while ($retries--) { - try { - $rfs->copy($origin, $processedUrl, $fileName, $this->outputProgress, $package->getTransportOptions()); - break; - } catch (TransportException $e) { - // if we got an http response with a proper code, then requesting again will probably not help, abort - if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) { - throw $e; - } - $this->io->writeError(''); - $this->io->writeError(' Download failed, retrying...', true, IOInterface::VERBOSE); - usleep(500000); - } - } - - if (!$this->outputProgress) { - $this->io->writeError(' (100%)', false); - } - - if ($this->cache) { - $this->lastCacheWrites[$package->getName()] = $cacheKey; - $this->cache->copyFrom($cacheKey, $fileName); - } + $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) + ->then($accept, $reject); } - if (!file_exists($fileName)) { - throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the' - .' directory is writable and you have internet connectivity'); + return $result->then(function ($result) use ($fileName, $checksum, $url) { + // in case of retry, the first call's Promise chain finally calls this twice at the end, + // once with $result being the returned $fileName from $accept, and then once for every + // failed request with a null result, which can be skipped. + if (null === $result) { + return $fileName; + } + + if (!file_exists($fileName)) { + throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the' + .' directory is writable and you have internet connectivity'); + } + + if ($checksum && hash_file('sha1', $fileName) !== $checksum) { + throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')'); + } + + return $fileName; + }); + }; + + $accept = function ($response) use ($cache, $package, $fileName, $self, &$urls) { + $url = reset($urls); + $cacheKey = $url['cacheKey']; + + if ($cache) { + $self->lastCacheWrites[$package->getName()] = $cacheKey; + $cache->copyFrom($cacheKey, $fileName); } - if ($checksum && hash_file('sha1', $fileName) !== $checksum) { - throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')'); - } - } catch (\Exception $e) { + $response->collect(); + + return $fileName; + }; + + $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) { // clean up - $this->filesystem->removeDirectory($path); - $this->clearLastCacheWrite($package); - throw $e; - } + if (file_exists($fileName)) { + $filesystem->unlink($fileName); + } + $self->clearLastCacheWrite($package); - return $fileName; + if ($e instanceof TransportException) { + // if we got an http response with a proper code, then requesting again will probably not help, abort + if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) { + $retries = 0; + } + } + + // special error code returned when network is being artificially disabled + if ($e instanceof TransportException && $e->getStatusCode() === 499) { + $retries = 0; + $urls = array(); + } + + if ($retries) { + usleep(500000); + $retries--; + + return $download(); + } + + array_shift($urls); + if ($urls) { + if ($io->isDebug()) { + $io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); + $io->writeError(' Trying the next URL for '.$package->getName()); + } elseif (count($urls)) { + $io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); + } + + $retries = 3; + usleep(100000); + + return $download(); + } + + throw $e; + }; + + return $download(); } /** * {@inheritDoc} */ - public function setOutputProgress($outputProgress) + public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { - $this->outputProgress = $outputProgress; - - return $this; } - protected function clearLastCacheWrite(PackageInterface $package) + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + $fileName = $this->getFileName($package, $path); + if (file_exists($fileName)) { + $this->filesystem->unlink($fileName); + } + if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); + } + if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir')); + } + if (is_dir($path) && $this->filesystem->isDirEmpty($path)) { + $this->filesystem->removeDirectory($path); + } + } + + /** + * {@inheritDoc} + */ + public function install(PackageInterface $package, $path, $output = true) + { + if ($output) { + $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + } + + $this->filesystem->emptyDirectory($path); + $this->filesystem->ensureDirectoryExists($path); + $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME)); + } + + /** + * TODO mark private in v3 + * @protected This is public due to PHP 5.3 + */ + public function clearLastCacheWrite(PackageInterface $package) { if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) { $this->cache->remove($this->lastCacheWrites[$package->getName()]); @@ -218,11 +285,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $from = $initial->getFullPrettyVersion(); $to = $target->getFullPrettyVersion(); - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); $this->remove($initial, $path, false); - $this->download($target, $path, false); + $this->install($target, $path, false); $this->io->writeError(''); } @@ -249,7 +316,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ protected function getFileName(PackageInterface $package, $path) { - return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); + return rtrim($this->config->get('vendor-dir').'/composer/'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); } /** @@ -291,15 +358,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface public function getLocalChanges(PackageInterface $package, $targetDir) { $prevIO = $this->io; - $prevProgress = $this->outputProgress; $this->io = new NullIO; $this->io->loadConfiguration($this->config); - $this->outputProgress = false; $e = null; try { - $this->download($package, $targetDir.'_compare', false); + $res = $this->download($package, $targetDir.'_compare', null, false); + $this->httpDownloader->wait(); + $res = $this->install($package, $targetDir.'_compare', false); $comparer = new Comparer(); $comparer->setSource($targetDir.'_compare'); @@ -311,7 +378,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface } $this->io = $prevIO; - $this->outputProgress = $prevProgress; if ($e) { throw $e; diff --git a/src/Composer/Downloader/FossilDownloader.php b/src/Composer/Downloader/FossilDownloader.php index 135e973e0..be7c987b3 100644 --- a/src/Composer/Downloader/FossilDownloader.php +++ b/src/Composer/Downloader/FossilDownloader.php @@ -23,7 +23,15 @@ class FossilDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); @@ -49,7 +57,7 @@ class FossilDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index edeaa7686..483a3d364 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\Git as GitUtil; +use Composer\Util\Url; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Cache; @@ -29,6 +30,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface private $hasStashedChanges = false; private $hasDiscardedChanges = false; private $gitUtil; + private $cachedPackages = array(); public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) { @@ -39,7 +41,28 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + GitUtil::cleanEnv(); + + $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/'; + $gitVersion = $this->gitUtil->getVersion(); + + // --dissociate option is only available since git 2.3.0-rc0 + if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { + $this->io->writeError(" - Syncing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") into cache"); + $this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); + $ref = $package->getSourceReference(); + if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref) && is_dir($cachePath)) { + $this->cachedPackages[$package->getId()][$ref] = true; + } + } + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); @@ -47,31 +70,20 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $ref = $package->getSourceReference(); $flag = Platform::isWindows() ? '/D ' : ''; - // --dissociate option is only available since git 2.3.0-rc0 - $gitVersion = $this->gitUtil->getVersion(); - $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%'; - if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { - $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); - try { - if (!$this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref)) { - $this->io->writeError('Failed to update '.$url.' in cache, package installation for '.$package->getPrettyName().' might fail.'); - } - if (is_dir($cachePath)) { - $command = - 'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% ' - . '&& cd '.$flag.'%path% ' - . '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%'; - $msg = "Cloning ".$this->getShortHash($ref).' from cache'; - } - } catch (\RuntimeException $e) { - if (0 === strpos(get_class($e), 'PHPUnit')) { - throw $e; - } + if (!empty($this->cachedPackages[$package->getId()][$ref])) { + $msg = "Cloning ".$this->getShortHash($ref).' from cache'; + $command = + 'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% ' + . '&& cd '.$flag.'%path% ' + . '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%'; + } else { + $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%'; + if (getenv('COMPOSER_DISABLE_NETWORK')) { + throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); } } + $this->io->writeError($msg); $commandCallable = function ($url) use ($path, $command, $cachePath) { @@ -105,13 +117,52 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { GitUtil::cleanEnv(); + $path = $this->normalizePath($path); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } + $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/'; + $ref = $target->getSourceReference(); + $flag = Platform::isWindows() ? '/D ' : ''; + + if (!empty($this->cachedPackages[$target->getId()][$ref])) { + $msg = "Checking out ".$this->getShortHash($ref).' from cache'; + $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%'; + } else { + $msg = "Checking out ".$this->getShortHash($ref); + $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%'; + if (getenv('COMPOSER_DISABLE_NETWORK')) { + throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); + } + } + + $this->io->writeError($msg); + + $commandCallable = function ($url) use ($ref, $command, $cachePath) { + return str_replace( + array('%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'), + array( + ProcessExecutor::escape($url), + ProcessExecutor::escape($ref.'^{commit}'), + ProcessExecutor::escape($cachePath), + ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url)), + ), + $command + ); + }; + + $this->gitUtil->runCommand($commandCallable, $url, $path); + if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { + if ($target->getDistReference() === $target->getSourceReference()) { + $target->setDistReference($newRef); + } + $target->setSourceReference($newRef); + } + $updateOriginUrl = false; if ( 0 === $this->process->execute('git remote -v', $output, $path) @@ -122,28 +173,6 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $updateOriginUrl = true; } } - - $ref = $target->getSourceReference(); - $this->io->writeError(" Checking out ".$this->getShortHash($ref)); - $command = '(git remote set-url composer %s && git rev-parse --quiet --verify %s || (git fetch composer && git fetch --tags composer)) && git remote set-url composer %s'; - - $commandCallable = function ($url) use ($command, $ref) { - return sprintf( - $command, - ProcessExecutor::escape($url), - ProcessExecutor::escape($ref.'^{commit}'), - ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url)) - ); - }; - - $this->gitUtil->runCommand($commandCallable, $url, $path); - if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { - if ($target->getDistReference() === $target->getSourceReference()) { - $target->setDistReference($newRef); - } - $target->setSourceReference($newRef); - } - if ($updateOriginUrl) { $this->updateOriginUrl($path, $target->getSourceUrl()); } @@ -272,7 +301,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $changes = array_map(function ($elem) { return ' '.$elem; }, preg_split('{\s*\r?\n\s*}', $changes)); - $this->io->writeError(' The package has modified files:'); + $this->io->writeError(' '.$package->getPrettyName().' has modified files:'); $this->io->writeError(array_slice($changes, 0, 10)); if (count($changes) > 10) { $this->io->writeError(' ' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list'); @@ -373,7 +402,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface ) { $command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference)); if (0 === $this->process->execute($command, $output, $path)) { - return; + return null; } } @@ -391,14 +420,14 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface ) { $command = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference)); if (0 === $this->process->execute($command, $output, $path)) { - return; + return null; } } } $command = sprintf($template, ProcessExecutor::escape($gitRef)); if (0 === $this->process->execute($command, $output, $path)) { - return; + return null; } // reference was not found (prints "fatal: reference is not a tree: $ref") @@ -406,7 +435,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $this->io->writeError(' '.$reference.' is gone (history was rewritten?)'); } - throw new \RuntimeException(GitUtil::sanitizeUrl('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); } protected function updateOriginUrl($path, $url) diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 19e4a45e1..91be4593d 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; /** @@ -28,17 +28,19 @@ use Composer\IO\IOInterface; */ class GzipDownloader extends ArchiveDownloader { + /** @var ProcessExecutor */ protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { - $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3)); + $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME); + $targetFilepath = $path . DIRECTORY_SEPARATOR . $filename; // Try to use gunzip on *nix if (!Platform::isWindows()) { @@ -63,14 +65,6 @@ class GzipDownloader extends ArchiveDownloader $this->extractUsingExt($file, $targetFilepath); } - /** - * {@inheritdoc} - */ - protected function getFileName(PackageInterface $package, $path) - { - return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); - } - private function extractUsingExt($file, $targetFilepath) { $archiveFile = gzopen($file, 'rb'); diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index 2921cc4b7..91144a13d 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -24,7 +24,15 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); @@ -44,7 +52,7 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index d7f0c06ee..81a93ac9a 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -37,7 +37,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter /** * {@inheritdoc} */ - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { $url = $package->getDistUrl(); $realUrl = realpath($url); @@ -50,14 +50,6 @@ 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() - )); - } - return; } @@ -73,6 +65,29 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $realUrl )); } + } + + /** + * {@inheritdoc} + */ + public function install(PackageInterface $package, $path, $output = true) + { + $url = $package->getDistUrl(); + $realUrl = realpath($url); + + if (realpath($path) === $realUrl) { + if ($output) { + $this->io->writeError(sprintf( + ' - Installing %s (%s): Source already present', + $package->getName(), + $package->getFullPrettyVersion() + )); + } else { + $this->io->writeError('Source already present', false); + } + + return; + } // Get the transport options with default values $transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true); @@ -154,7 +169,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $fileSystem->mirror($realUrl, $path, $iterator); } - $this->io->writeError(''); + if ($output) { + $this->io->writeError(''); + } } /** @@ -164,7 +181,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter { $realUrl = realpath($package->getDistUrl()); - if (realpath($path) === $realUrl) { + if ($path === $realUrl) { if ($output) { $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "), source is still present in $path"); } diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php index 92091e2c9..8be866929 100644 --- a/src/Composer/Downloader/PerforceDownloader.php +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -27,7 +27,15 @@ class PerforceDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + public function doInstall(PackageInterface $package, $path, $url) { $ref = $package->getSourceReference(); $label = $this->getLabelFromSourceReference($ref); @@ -76,9 +84,9 @@ class PerforceDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - $this->doDownload($target, $path, $url); + $this->doInstall($target, $path, $url); } /** diff --git a/src/Composer/Downloader/PharDownloader.php b/src/Composer/Downloader/PharDownloader.php index 13fec244b..62741ee0e 100644 --- a/src/Composer/Downloader/PharDownloader.php +++ b/src/Composer/Downloader/PharDownloader.php @@ -12,6 +12,8 @@ namespace Composer\Downloader; +use Composer\Package\PackageInterface; + /** * Downloader for phar files * @@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader /** * {@inheritDoc} */ - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { // Can throw an UnexpectedValueException $archive = new \Phar($file); diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 40cd09896..d0fbadcc6 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -18,8 +18,9 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; +use Composer\Package\PackageInterface; use RarArchive; /** @@ -31,15 +32,16 @@ use RarArchive; */ class RarDownloader extends ArchiveDownloader { + /** @var ProcessExecutor */ protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { $processError = null; diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index f7e14e6ce..0281862b5 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { SvnUtil::cleanEnv(); $ref = $package->getSourceReference(); @@ -42,13 +50,13 @@ class SvnDownloader extends VcsDownloader } $this->io->writeError(" Checking out ".$package->getSourceReference()); - $this->execute($url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); + $this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); } /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { SvnUtil::cleanEnv(); $ref = $target->getSourceReference(); @@ -64,7 +72,7 @@ class SvnDownloader extends VcsDownloader } $this->io->writeError(" Checking out " . $ref); - $this->execute($url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); + $this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); } /** @@ -93,7 +101,7 @@ class SvnDownloader extends VcsDownloader * @throws \RuntimeException * @return string */ - protected function execute($baseUrl, $command, $url, $cwd = null, $path = null) + protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null) { $util = new SvnUtil($baseUrl, $this->io, $this->config); $util->setCacheCredentials($this->cacheCredentials); @@ -101,7 +109,7 @@ class SvnDownloader extends VcsDownloader return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); } catch (\RuntimeException $e) { throw new \RuntimeException( - 'Package could not be downloaded, '.$e->getMessage() + $package->getPrettyName().' could not be downloaded, '.$e->getMessage() ); } } @@ -127,7 +135,7 @@ class SvnDownloader extends VcsDownloader return ' '.$elem; }, preg_split('{\s*\r?\n\s*}', $changes)); $countChanges = count($changes); - $this->io->writeError(sprintf(' The package has modified file%s:', $countChanges === 1 ? '' : 's')); + $this->io->writeError(sprintf(' '.$package->getPrettyName().' has modified file%s:', $countChanges === 1 ? '' : 's')); $this->io->writeError(array_slice($changes, 0, 10)); if ($countChanges > 10) { $remainingChanges = $countChanges - 10; diff --git a/src/Composer/Downloader/TarDownloader.php b/src/Composer/Downloader/TarDownloader.php index 34c43da5f..e48407230 100644 --- a/src/Composer/Downloader/TarDownloader.php +++ b/src/Composer/Downloader/TarDownloader.php @@ -12,6 +12,8 @@ namespace Composer\Downloader; +use Composer\Package\PackageInterface; + /** * Downloader for tar files: tar, tar.gz or tar.bz2 * @@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader /** * {@inheritDoc} */ - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { // Can throw an UnexpectedValueException $archive = new \PharData($file); diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index aa666058e..2236c4d88 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; +use React\Promise\PromiseInterface; /** * @author Jordi Boggiano @@ -54,44 +55,78 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa /** * {@inheritDoc} */ - public function download(PackageInterface $package, $path) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + if (!$package->getSourceReference()) { + throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); + } + + $urls = $this->prepareUrls($package->getSourceUrls()); + + while ($url = array_shift($urls)) { + try { + return $this->doDownload($package, $path, $url, $prevPackage); + } catch (\Exception $e) { + // rethrow phpunit exceptions to avoid hard to debug bug failures + if ($e instanceof \PHPUnit\Framework\Exception) { + throw $e; + } + if ($this->io->isDebug()) { + $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); + } elseif (count($urls)) { + $this->io->writeError(' Failed, trying the next URL'); + } + if (!count($urls)) { + throw $e; + } + } + } + } + + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + if ($type === 'update') { + $this->cleanChanges($prevPackage, $path, true); + } elseif ($type === 'install') { + $this->filesystem->emptyDirectory($path); + } elseif ($type === 'uninstall') { + $this->cleanChanges($package, $path, false); + } + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + if ($type === 'update') { + // TODO keep track of whether prepare was called for this package + $this->reapplyChanges($path); + } + } + + /** + * {@inheritDoc} + */ + public function install(PackageInterface $package, $path) { if (!$package->getSourceReference()) { throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): ", false); - $this->filesystem->emptyDirectory($path); - $urls = $package->getSourceUrls(); + $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { try { - if (Filesystem::isLocalPath($url)) { - // realpath() below will not understand - // url that starts with "file://" - $needle = 'file://'; - $isFileProtocol = false; - if (0 === strpos($url, $needle)) { - $url = substr($url, strlen($needle)); - $isFileProtocol = true; - } - - // realpath() below will not understand %20 spaces etc. - if (false !== strpos($url, '%')) { - $url = rawurldecode($url); - } - - $url = realpath($url); - - if ($isFileProtocol) { - $url = $needle . $url; - } - } - $this->doDownload($package, $path, $url); + $this->doInstall($package, $path, $url); break; } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures - if ($e instanceof \PHPUnit_Framework_Exception) { + if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { @@ -130,25 +165,21 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa $to = $target->getFullPrettyVersion(); } - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); - $this->cleanChanges($initial, $path, true); - $urls = $target->getSourceUrls(); + $urls = $this->prepareUrls($target->getSourceUrls()); $exception = null; while ($url = array_shift($urls)) { try { - if (Filesystem::isLocalPath($url)) { - $url = realpath($url); - } $this->doUpdate($initial, $target, $path, $url); $exception = null; break; } catch (\Exception $exception) { // rethrow phpunit exceptions to avoid hard to debug bug failures - if ($exception instanceof \PHPUnit_Framework_Exception) { + if ($exception instanceof \PHPUnit\Framework\Exception) { throw $exception; } if ($this->io->isDebug()) { @@ -159,8 +190,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } } - $this->reapplyChanges($path); - // print the commit logs if in verbose mode and VCS metadata is present // because in case of missing metadata code would trigger another exception if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) { @@ -196,21 +225,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa public function remove(PackageInterface $package, $path) { $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); - $this->cleanChanges($package, $path, false); if (!$this->filesystem->removeDirectory($path)) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } } - /** - * Download progress information is not available for all VCS downloaders. - * {@inheritDoc} - */ - public function setOutputProgress($outputProgress) - { - return $this; - } - /** * {@inheritDoc} */ @@ -244,7 +263,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } /** - * Guarantee that no changes have been made to the local copy + * Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not) * * @param string $path * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly @@ -253,14 +272,28 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa { } + /** + * Downloads data needed to run an install/update later + * + * @param PackageInterface $package package instance + * @param string $path download path + * @param string $url package url + * @param PackageInterface|null $prevPackage previous package (in case of an update) + * + * @return PromiseInterface|null + */ + abstract protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null); + /** * Downloads specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url + * + * @return PromiseInterface|null */ - abstract protected function doDownload(PackageInterface $package, $path, $url); + abstract protected function doInstall(PackageInterface $package, $path, $url); /** * Updates specific package in specific folder from initial to target version. @@ -269,6 +302,8 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param PackageInterface $target updated package * @param string $path download path * @param string $url package url + * + * @return PromiseInterface|null */ abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url); @@ -290,4 +325,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @return bool */ abstract protected function hasMetadataRepository($path); + + private function prepareUrls(array $urls) + { + foreach ($urls as $index => $url) { + if (Filesystem::isLocalPath($url)) { + // realpath() below will not understand + // url that starts with "file://" + $fileProtocol = 'file://'; + $isFileProtocol = false; + if (0 === strpos($url, $fileProtocol)) { + $url = substr($url, strlen($fileProtocol)); + $isFileProtocol = true; + } + + // realpath() below will not understand %20 spaces etc. + if (false !== strpos($url, '%')) { + $url = rawurldecode($url); + } + + $urls[$index] = realpath($url); + + if ($isFileProtocol) { + $urls[$index] = $fileProtocol . $urls[$index]; + } + } + } + + return $urls; + } } diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 4a9b854d3..371ceda1b 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -17,7 +17,7 @@ use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; /** @@ -28,16 +28,17 @@ use Composer\IO\IOInterface; */ class XzDownloader extends ArchiveDownloader { + /** @var ProcessExecutor */ protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); @@ -49,12 +50,4 @@ class XzDownloader extends ArchiveDownloader throw new \RuntimeException($processError); } - - /** - * {@inheritdoc} - */ - protected function getFileName(PackageInterface $package, $path) - { - return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); - } } diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 8aca21e59..29c7fd82a 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -19,7 +19,7 @@ use Composer\Package\PackageInterface; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Symfony\Component\Process\ExecutableFinder; use ZipArchive; @@ -33,19 +33,21 @@ 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, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } /** * {@inheritDoc} */ - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { if (null === self::$hasSystemUnzip) { $finder = new ExecutableFinder; @@ -74,7 +76,7 @@ class ZipDownloader extends ArchiveDownloader } } - return parent::download($package, $path, $output); + return parent::download($package, $path, $prevPackage, $output); } /** @@ -185,7 +187,7 @@ class ZipDownloader extends ArchiveDownloader * @param string $file File to extract * @param string $path Path where to extract file */ - public function extract($file, $path) + public function extract(PackageInterface $package, $file, $path) { // Each extract calls its alternative if not available or fails if (self::$isWindows) { diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index a5797fdae..bcd95b91b 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -13,13 +13,16 @@ namespace Composer\EventDispatcher; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\Transaction; use Composer\Installer\InstallerEvent; use Composer\IO\IOInterface; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositorySet; use Composer\Script; use Composer\Installer\PackageEvent; use Composer\Installer\BinaryInstaller; @@ -46,7 +49,7 @@ class EventDispatcher protected $io; protected $loader; protected $process; - protected $listeners; + protected $listeners = array(); private $eventStack; /** @@ -99,40 +102,34 @@ class EventDispatcher /** * Dispatch a package event. * - * @param string $eventName The constant in PackageEvents - * @param bool $devMode Whether or not we are in dev mode - * @param PolicyInterface $policy The policy - * @param Pool $pool The pool - * @param CompositeRepository $installedRepo The installed repository - * @param Request $request The request - * @param array $operations The list of operations - * @param OperationInterface $operation The package being installed/updated/removed + * @param string $eventName The constant in PackageEvents + * @param bool $devMode Whether or not we are in dev mode + * @param RepositoryInterface $localRepo The installed repository + * @param array $operations The list of operations + * @param OperationInterface $operation The package being installed/updated/removed * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchPackageEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) + public function dispatchPackageEvent($eventName, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation) { - return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations, $operation)); + return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation)); } /** * Dispatch a installer event. * - * @param string $eventName The constant in InstallerEvents - * @param bool $devMode Whether or not we are in dev mode - * @param PolicyInterface $policy The policy - * @param Pool $pool The pool - * @param CompositeRepository $installedRepo The installed repository - * @param Request $request The request - * @param array $operations The list of operations + * @param string $eventName The constant in InstallerEvents + * @param bool $devMode Whether or not we are in dev mode + * @param bool $executeOperations True if operations will be executed, false in --dry-run + * @param Transaction $transaction The transaction contains the list of operations * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function dispatchInstallerEvent($eventName, $devMode, $executeOperations, Transaction $transaction) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction)); } /** @@ -160,6 +157,9 @@ class EventDispatcher throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public'); } + if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) { + $this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1] ), true, IOInterface::VERBOSE); + } $event = $this->checkListenerExpectedEvent($callable, $event); $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isComposerScript($callable)) { @@ -172,8 +172,8 @@ class EventDispatcher $args = array_merge($script, $event->getArguments()); $flags = $event->getFlags(); if (substr($callable, 0, 10) === '@composer ') { - $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($callable, 9); - if (0 !== ($exitCode = $this->process->execute($exec))) { + $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args); + if (0 !== ($exitCode = $this->executeTty($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); @@ -184,6 +184,7 @@ class EventDispatcher } try { + /** @var InstallerEvent $event */ $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags); $scriptEvent->setOriginatingEvent($event); $return = $this->dispatch($scriptName, $scriptEvent); @@ -247,7 +248,7 @@ class EventDispatcher } } - if (0 !== ($exitCode = $this->process->execute($exec))) { + if (0 !== ($exitCode = $this->executeTty($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); @@ -264,6 +265,15 @@ class EventDispatcher return $return; } + protected function executeTty($exec) + { + if ($this->io->isInteractive()) { + return $this->process->executeTty($exec); + } + + return $this->process->execute($exec); + } + protected function getPhpExecCommand() { $finder = new PhpExecutableFinder(); @@ -327,44 +337,6 @@ class EventDispatcher $expected = $typehint->getName(); - // BC support - if (!$event instanceof $expected && $expected === 'Composer\Script\CommandEvent') { - trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED); - $event = new \Composer\Script\CommandEvent( - $event->getName(), - $event->getComposer(), - $event->getIO(), - $event->isDevMode(), - $event->getArguments() - ); - } - if (!$event instanceof $expected && $expected === 'Composer\Script\PackageEvent') { - trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED); - $event = new \Composer\Script\PackageEvent( - $event->getName(), - $event->getComposer(), - $event->getIO(), - $event->isDevMode(), - $event->getPolicy(), - $event->getPool(), - $event->getInstalledRepo(), - $event->getRequest(), - $event->getOperations(), - $event->getOperation() - ); - } - if (!$event instanceof $expected && $expected === 'Composer\Script\Event') { - trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED); - $event = new \Composer\Script\Event( - $event->getName(), - $event->getComposer(), - $event->getIO(), - $event->isDevMode(), - $event->getArguments(), - $event->getFlags() - ); - } - return $event; } @@ -397,6 +369,22 @@ class EventDispatcher $this->listeners[$eventName][$priority][] = $listener; } + /** + * @param callable|object $listener A callable or an object instance for which all listeners should be removed + */ + public function removeListener($listener) + { + foreach ($this->listeners as $eventName => $priorities) { + foreach ($priorities as $priority => $listeners) { + foreach ($listeners as $index => $candidate) { + if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) { + unset($this->listeners[$eventName][$priority][$index]); + } + } + } + } + } + /** * Adds object methods as listeners for the events in getSubscribedEvents * @@ -513,7 +501,7 @@ class EventDispatcher * * @param Event $event * @throws \RuntimeException - * @return number + * @return int */ protected function pushEvent(Event $event) { diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 8a0ff1e2d..f50050604 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -23,7 +23,8 @@ use Composer\Repository\WritableRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; +use Composer\Util\Loop; use Composer\Util\Silencer; use Composer\Plugin\PluginEvents; use Composer\EventDispatcher\Event; @@ -222,6 +223,13 @@ class Factory return trim(getenv('COMPOSER')) ?: './composer.json'; } + public static function getLockFile($composerFile) + { + return "json" === pathinfo($composerFile, PATHINFO_EXTENSION) + ? substr($composerFile, 0, -4).'lock' + : $composerFile . '.lock'; + } + public static function createAdditionalStyles() { return array( @@ -325,14 +333,15 @@ class Factory $io->loadConfiguration($config); } - $rfs = self::createRemoteFilesystem($io, $config); + $httpDownloader = self::createHttpDownloader($io, $config); + $loop = new Loop($httpDownloader); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); $composer->setEventDispatcher($dispatcher); // initialize repository manager - $rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs); + $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher); $composer->setRepositoryManager($rm); // load local repository @@ -352,12 +361,12 @@ class Factory $composer->setPackage($package); // initialize installation manager - $im = $this->createInstallationManager(); + $im = $this->createInstallationManager($loop, $io, $dispatcher); $composer->setInstallationManager($im); if ($fullLoad) { // initialize download manager - $dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs); + $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher); $composer->setDownloadManager($dm); // initialize autoload generator @@ -365,7 +374,7 @@ class Factory $composer->setAutoloadGenerator($generator); // initialize archive manager - $am = $this->createArchiveManager($config, $dm); + $am = $this->createArchiveManager($config, $dm, $loop); $composer->setArchiveManager($am); } @@ -386,11 +395,9 @@ class Factory // init locker if possible if ($fullLoad && isset($composerFile)) { - $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) - ? substr($composerFile, 0, -4).'lock' - : $composerFile . '.lock'; + $lockFile = self::getLockFile($composerFile); - $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $rm, $im, file_get_contents($composerFile)); + $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)); $composer->setLocker($locker); } @@ -411,7 +418,7 @@ class Factory /** * @param IOInterface $io IO instance * @param bool $disablePlugins Whether plugins should not be loaded - * @return Composer + * @return Composer|null */ public static function createGlobal(IOInterface $io, $disablePlugins = false) { @@ -451,7 +458,7 @@ class Factory * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ - public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { @@ -484,14 +491,14 @@ class Factory $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); - $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs)); - $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs)); - $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs)); - $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs)); + $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); return $dm; } @@ -501,15 +508,9 @@ class Factory * @param Downloader\DownloadManager $dm Manager use to download sources * @return Archiver\ArchiveManager */ - public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null) + public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop) { - if (null === $dm) { - $io = new IO\NullIO(); - $io->loadConfiguration($config); - $dm = $this->createDownloadManager($io, $config); - } - - $am = new Archiver\ArchiveManager($dm); + $am = new Archiver\ArchiveManager($dm, $loop); $am->addArchiver(new Archiver\ZipArchiver); $am->addArchiver(new Archiver\PharArchiver); @@ -531,9 +532,9 @@ class Factory /** * @return Installer\InstallationManager */ - protected function createInstallationManager() + public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { - return new Installer\InstallationManager(); + return new Installer\InstallationManager($loop, $io, $eventDispatcher); } /** @@ -579,10 +580,10 @@ class Factory /** * @param IOInterface $io IO instance * @param Config $config Config instance - * @param array $options Array of options passed directly to RemoteFilesystem constructor - * @return RemoteFilesystem + * @param array $options Array of options passed directly to HttpDownloader constructor + * @return HttpDownloader */ - public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array()) + public static function createHttpDownloader(IOInterface $io, Config $config = null, $options = array()) { static $warned = false; $disableTls = false; @@ -596,18 +597,18 @@ class Factory throw new Exception\NoSslException('The openssl extension is required for SSL/TLS protection but is not available. ' . 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.'); } - $remoteFilesystemOptions = array(); + $httpDownloaderOptions = array(); if ($disableTls === false) { if ($config && $config->get('cafile')) { - $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile'); + $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile'); } if ($config && $config->get('capath')) { - $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath'); + $httpDownloaderOptions['ssl']['capath'] = $config->get('capath'); } - $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options); + $httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options); } try { - $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); + $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls); } catch (TransportException $e) { if (false !== strpos($e->getMessage(), 'cafile')) { $io->write('Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.'); @@ -620,7 +621,7 @@ class Factory throw $e; } - return $remoteFilesystem; + return $httpDownloader; } /** diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index d9dbc2d6f..e2d916a15 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -14,10 +14,9 @@ namespace Composer\IO; use Composer\Config; use Composer\Util\ProcessExecutor; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; -abstract class BaseIO implements IOInterface, LoggerInterface +abstract class BaseIO implements IOInterface { protected $authentications = array(); diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php index 5766ba479..0ba1cf932 100644 --- a/src/Composer/IO/IOInterface.php +++ b/src/Composer/IO/IOInterface.php @@ -13,13 +13,14 @@ namespace Composer\IO; use Composer\Config; +use Psr\Log\LoggerInterface; /** * The Input/Output helper interface. * * @author François Pluchino */ -interface IOInterface +interface IOInterface extends LoggerInterface { const QUIET = 1; const NORMAL = 2; @@ -80,6 +81,24 @@ interface IOInterface */ public function writeError($messages, $newline = true, $verbosity = self::NORMAL); + /** + * Writes a message to the output, without formatting it. + * + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $verbosity Verbosity level from the VERBOSITY_* constants + */ + public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL); + + /** + * Writes a message to the error output, without formatting it. + * + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $verbosity Verbosity level from the VERBOSITY_* constants + */ + public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL); + /** * Overwrites a previous message to the output. * @@ -107,7 +126,7 @@ interface IOInterface * @param string $default The default answer if none is given by the user * * @throws \RuntimeException If there is no data to read in the input stream - * @return string The user answer + * @return string|null The user answer */ public function ask($question, $default = null); @@ -145,7 +164,7 @@ interface IOInterface * * @param string $question The question to ask * - * @return string The answer + * @return string|null The answer */ public function askAndHideAnswer($question); @@ -160,7 +179,7 @@ interface IOInterface * @param bool $multiselect Select more than one value separated by comma * * @throws \InvalidArgumentException - * @return int|string|array The selected value or values (the key of the choices array) + * @return int|string|array|bool The selected value or values (the key of the choices array) */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 5fcc6efa2..15cfb78e1 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -14,6 +14,8 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\LocalRepoTransaction; +use Composer\DependencyResolver\LockTransaction; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -33,18 +35,25 @@ use Composer\Installer\NoopInstaller; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; +use Composer\Package\RootAliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Link; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Package\Package; +use Composer\Repository\ArrayRepository; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\InstalledRepository; +use Composer\Repository\RootPackageRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryManager; @@ -74,6 +83,12 @@ class Installer */ protected $package; + // TODO can we get rid of the below and just use the package itself? + /** + * @var RootPackageInterface + */ + protected $fixedRootPackage; + /** * @var DownloadManager */ @@ -118,7 +133,6 @@ class Installer protected $ignorePlatformReqs = false; protected $preferStable = false; protected $preferLowest = false; - protected $skipSuggest = false; protected $writeLock; protected $executeOperations = true; @@ -127,9 +141,9 @@ class Installer * * @var array|null */ - protected $updateWhitelist = null; - protected $whitelistDependencies = false; // TODO 2.0 rename to whitelistTransitiveDependencies - protected $whitelistAllDependencies = false; + protected $updateMirrors = false; + protected $updateAllowList = null; + protected $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; /** * @var SuggestedPackagesReporter @@ -139,7 +153,7 @@ class Installer /** * @var RepositoryInterface */ - protected $additionalInstalledRepository; + protected $additionalFixedRepository; /** * Constructor @@ -184,8 +198,13 @@ class Installer gc_collect_cycles(); gc_disable(); + if ($this->updateAllowList && $this->updateMirrors) { + throw new \RuntimeException("The installer options updateMirrors and updateAllowList are mutually exclusive."); + } + // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { + $this->io->writeError('No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.'); $this->update = true; } @@ -195,7 +214,6 @@ class Installer $this->executeOperations = false; $this->writeLock = false; $this->dumpAutoloader = false; - $this->installationManager->addInstaller(new NoopInstaller); $this->mockLocalRepositories($this->repositoryManager); } @@ -204,6 +222,7 @@ class Installer putenv('COMPOSER_DEV_MODE='.$_SERVER['COMPOSER_DEV_MODE']); // dispatch pre event + // should we treat this more strictly as running an update and then running an install, triggering events multiple times? $eventName = $this->update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; $this->eventDispatcher->dispatchScript($eventName, $this->devMode); } @@ -211,25 +230,19 @@ class Installer $this->downloadManager->setPreferSource($this->preferSource); $this->downloadManager->setPreferDist($this->preferDist); - // create installed repo, this contains all local packages + platform packages (php & extensions) $localRepo = $this->repositoryManager->getLocalRepository(); - if ($this->update) { - $platformOverrides = $this->config->get('platform') ?: array(); - } else { - $platformOverrides = $this->locker->getPlatformOverrides(); - } - $platformRepo = new PlatformRepository(array(), $platformOverrides); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - - $aliases = $this->getRootAliases(); - $this->aliasPlatformPackages($platformRepo, $aliases); if (!$this->suggestedPackagesReporter) { $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); } try { - list($res, $devPackages) = $this->doInstall($localRepo, $installedRepo, $platformRepo, $aliases); + if ($this->update) { + // TODO introduce option to set doInstall to false (update lock file without vendor install) + $res = $this->doUpdate($localRepo, true); + } else { + $res = $this->doInstall($localRepo); + } if ($res !== 0) { return $res; } @@ -244,13 +257,18 @@ class Installer $this->installationManager->notifyInstalls($this->io); } - // output suggestions if we're in dev mode - if ($this->devMode && !$this->skipSuggest) { - $this->suggestedPackagesReporter->output($installedRepo); + if ($this->update) { + $installedRepo = new InstalledRepository(array( + $this->locker->getLockedRepository($this->devMode), + $this->createPlatformRepo(false), + new RootPackageRepository(clone $this->package), + )); + $this->suggestedPackagesReporter->outputMinimalistic($installedRepo); } - # Find abandoned packages and warn user - foreach ($localRepo->getPackages() as $package) { + // Find abandoned packages and warn user + $lockedRepository = $this->locker->getLockedRepository(true); + foreach ($lockedRepository->getPackages() as $package) { if (!$package instanceof CompletePackage || !$package->isAbandoned()) { continue; } @@ -268,30 +286,6 @@ class Installer ); } - // write lock - if ($this->update && $this->writeLock) { - $localRepo->reload(); - - $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); - $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); - - $updatedLock = $this->locker->setLockData( - array_diff($localRepo->getCanonicalPackages(), $devPackages), - $devPackages, - $platformReqs, - $platformDevReqs, - $aliases, - $this->package->getMinimumStability(), - $this->package->getStabilityFlags(), - $this->preferStable || $this->package->getPreferStable(), - $this->preferLowest, - $this->config->get('platform') ?: array() - ); - if ($updatedLock) { - $this->io->writeError('Writing lock file'); - } - } - if ($this->dumpAutoloader) { // write autoloader if ($this->optimizeAutoloader) { @@ -346,189 +340,311 @@ class Installer return 0; } - /** - * @param RepositoryInterface $localRepo - * @param RepositoryInterface $installedRepo - * @param PlatformRepository $platformRepo - * @param array $aliases - * @return array [int, PackageInterfaces[]|null] with the exit code and an array of dev packages on update, or null on install - */ - protected function doInstall($localRepo, $installedRepo, $platformRepo, $aliases) + protected function doUpdate(RepositoryInterface $localRepo, $doInstall) { - // init vars - $lockedRepository = null; - $repositories = null; + $platformRepo = $this->createPlatformRepo(true); + $aliases = $this->getRootAliases(true); - // 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 - // packages in that case - if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { - try { - $lockedRepository = $this->locker->getLockedRepository($this->devMode); - } catch (\RuntimeException $e) { - // if there are dev requires, then we really can not install - if ($this->package->getDevRequires()) { - throw $e; - } - // no require-dev in composer.json and the lock file was created with no dev info, so skip them - $lockedRepository = $this->locker->getLockedRepository(); - } + $lockedRepository = null; + + if ($this->locker->isLocked()) { + $lockedRepository = $this->locker->getLockedRepository(true); } - $this->whitelistUpdateDependencies( - $lockedRepository ?: $localRepo, - $this->package->getRequires(), - $this->package->getDevRequires() - ); + if ($this->updateAllowList) { + if (!$lockedRepository) { + $this->io->writeError('Cannot update only a partial set of packages without a lock file present.', true, IOInterface::QUIET); + return 1; + } + } $this->io->writeError('Loading composer repositories with package information'); - // creating repository pool - $policy = $this->createPolicy(); - $pool = $this->createPool($this->update ? null : $lockedRepository); - $pool->addRepository($installedRepo, $aliases); - if ($this->update) { - $repositories = $this->repositoryManager->getRepositories(); - foreach ($repositories as $repository) { - $pool->addRepository($repository, $aliases); - } + // creating repository set + $policy = $this->createPolicy(true); + $repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases); + $repositories = $this->repositoryManager->getRepositories(); + foreach ($repositories as $repository) { + $repositorySet->addRepository($repository); } - // Add the locked repository after the others in case we are doing a - // partial update so missing packages can be found there still. - // For installs from lock it's the only one added so it is first if ($lockedRepository) { - $pool->addRepository($lockedRepository, $aliases); + $repositorySet->addRepository($lockedRepository); } - // creating requirements request - $request = $this->createRequest($this->package, $platformRepo); + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); - if ($this->update) { - // remove unstable packages from the localRepo if they don't match the current stability settings - $removedUnstablePackages = array(); - foreach ($localRepo->getPackages() as $package) { - if ( - !$pool->isPackageAcceptable($package->getNames(), $package->getStability()) - && $this->installationManager->isPackageInstalled($localRepo, $package) - ) { - $removedUnstablePackages[$package->getName()] = true; - $request->remove($package->getName(), new Constraint('=', $package->getVersion())); - } - } + $this->io->writeError('Updating dependencies'); - $this->io->writeError('Updating dependencies'.($this->devMode ? ' (including require-dev)' : '').''); + $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); - $request->updateAll(); - - $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); - - foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - - // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock, or their currently installed version - if ($this->updateWhitelist) { - $currentPackages = $this->getCurrentPackages($installedRepo); - - // collect packages to fixate from root requirements as well as installed packages - $candidates = array(); - foreach ($links as $link) { - $candidates[$link->getTarget()] = true; - $rootRequires[$link->getTarget()] = $link; - } - foreach ($currentPackages as $package) { - $candidates[$package->getName()] = true; - } - - // fix them to the version in lock (or currently installed) if they are not updateable - foreach ($candidates as $candidate => $dummy) { - foreach ($currentPackages as $curPackage) { - if ($curPackage->getName() === $candidate) { - if (!$this->isUpdateable($curPackage) && !isset($removedUnstablePackages[$curPackage->getName()])) { - $constraint = new Constraint('=', $curPackage->getVersion()); - $description = $this->locker->isLocked() ? '(locked at' : '(installed at'; - $requiredAt = isset($rootRequires[$candidate]) ? ', required as ' . $rootRequires[$candidate]->getPrettyConstraint() : ''; - $constraint->setPrettyString($description . ' ' . $curPackage->getPrettyVersion() . $requiredAt . ')'); - $request->install($curPackage->getName(), $constraint); - } - break; - } - } - } + // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata + if ($this->updateMirrors) { + foreach ($lockedRepository->getPackages() as $lockedPackage) { + $request->requireName($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); } } else { - $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); + foreach ($links as $link) { + $request->requireName($link->getTarget(), $link->getConstraint()); + } + } + + // pass the allow list into the request, so the pool builder can apply it + if ($this->updateAllowList) { + $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); + } + + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); + + // solve dependencies + $solver = new Solver($policy, $pool, $this->io, $repositorySet); + try { + $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + $ruleSetSize = $solver->getRuleSetSize(); + $solver = null; + } catch (SolverProblemsException $e) { + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + if (!$this->devMode) { + $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); + } + + return max(1, $e->getCode()); + } + + $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); + $this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE); + + if (!$lockTransaction->getOperations()) { + $this->io->writeError('Nothing to modify in lock file'); + } + + $exitCode = $this->extractDevPackages($lockTransaction, $platformRepo, $aliases, $policy); + if ($exitCode !== 0) { + return $exitCode; + } + + // write lock + $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); + $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); + + $installsUpdates = $uninstalls = array(); + if ($lockTransaction->getOperations()) { + $installNames = $updateNames = $uninstallNames = array(); + foreach ($lockTransaction->getOperations() as $operation) { + if ($operation instanceof InstallOperation) { + $installsUpdates[] = $operation; + $installNames[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); + } elseif ($operation instanceof UpdateOperation) { + $installsUpdates[] = $operation; + $updateNames[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); + } elseif ($operation instanceof UninstallOperation) { + $uninstalls[] = $operation; + $uninstallNames[] = $operation->getPackage()->getPrettyName(); + } + } + + $this->io->writeError(sprintf( + "Lock file operations: %d install%s, %d update%s, %d removal%s", + count($installNames), + 1 === count($installNames) ? '' : 's', + count($updateNames), + 1 === count($updateNames) ? '' : 's', + count($uninstalls), + 1 === count($uninstalls) ? '' : 's' + )); + if ($installNames) { + $this->io->writeError("Installs: ".implode(', ', $installNames), true, IOInterface::VERBOSE); + } + if ($updateNames) { + $this->io->writeError("Updates: ".implode(', ', $updateNames), true, IOInterface::VERBOSE); + } + if ($uninstalls) { + $this->io->writeError("Removals: ".implode(', ', $uninstallNames), true, IOInterface::VERBOSE); + } + } + + $sortByName = function ($a, $b) { + if ($a instanceof UpdateOperation) { + $a = $a->getTargetPackage()->getName(); + } else { + $a = $a->getPackage()->getName(); + } + if ($b instanceof UpdateOperation) { + $b = $b->getTargetPackage()->getName(); + } else { + $b = $b->getPackage()->getName(); + } + + return strcmp($a, $b); + }; + usort($uninstalls, $sortByName); + usort($installsUpdates, $sortByName); + + foreach (array_merge($uninstalls, $installsUpdates) as $operation) { + // collect suggestions + if ($operation instanceof InstallOperation) { + $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); + } + + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug()) { + $this->io->writeError(' - ' . $operation->show(true)); + } + } + + $updatedLock = $this->locker->setLockData( + $lockTransaction->getNewLockPackages(false, $this->updateMirrors), + $lockTransaction->getNewLockPackages(true, $this->updateMirrors), + $platformReqs, + $platformDevReqs, + $lockTransaction->getAliases($aliases), + $this->package->getMinimumStability(), + $this->package->getStabilityFlags(), + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest, + $this->config->get('platform') ?: array(), + $this->writeLock && $this->executeOperations + ); + if ($updatedLock && $this->writeLock && $this->executeOperations) { + $this->io->writeError('Writing lock file'); + } + + // see https://github.com/composer/composer/issues/2764 + if ($this->executeOperations && count($lockTransaction->getOperations()) > 0) { + $vendorDir = $this->config->get('vendor-dir'); + if (is_dir($vendorDir)) { + // suppress errors as this fails sometimes on OSX for no apparent reason + // see https://github.com/composer/composer/issues/4070#issuecomment-129792748 + @touch($vendorDir); + } + } + + if ($doInstall) { + // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false + return $this->doInstall($localRepo, true); + } + + return 0; + } + + /** + * Run the solver a second time on top of the existing update result with only the current result set in the pool + * and see what packages would get removed if we only had the non-dev packages in the solver request + */ + protected function extractDevPackages(LockTransaction $lockTransaction, $platformRepo, $aliases, $policy) + { + if (!$this->package->getDevRequires()) { + return 0; + } + + $resultRepo = new ArrayRepository(array()); + $loader = new ArrayLoader(null, true); + $dumper = new ArrayDumper(); + foreach ($lockTransaction->getNewLockPackages(false) as $pkg) { + $resultRepo->addPackage($loader->load($dumper->dump($pkg))); + } + + $repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases); + $repositorySet->addRepository($resultRepo); + + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, null); + + $links = $this->package->getRequires(); + foreach ($links as $link) { + $request->requireName($link->getTarget(), $link->getConstraint()); + } + + $pool = $repositorySet->createPoolWithAllPackages(); + + $solver = new Solver($policy, $pool, $this->io, $repositorySet); + try { + $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + $solver = null; + } catch (SolverProblemsException $e) { + $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); + $this->io->writeError('Your requirements can be resolved successfully when require-dev packages are present.'); + $this->io->writeError('You may need to move packages from require-dev or some of their dependencies to require.'); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, true)); + + return max(1, $e->getCode()); + } + + $lockTransaction->setNonDevPackages($nonDevLockTransaction); + + return 0; + } + + /** + * @param RepositoryInterface $localRepo + * @param bool $alreadySolved Whether the function is called as part of an update command or independently + * @return int exit code + */ + protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) + { + $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); + + $lockedRepository = $this->locker->getLockedRepository($this->devMode); + + // verify that the lock file works with the current platform repository + // we can skip this part if we're doing this as the second step after an update + if (!$alreadySolved) { + $this->io->writeError('Verifying lock file contents can be installed on current platform.'); + + $platformRepo = $this->createPlatformRepo(false); + // creating repository set + $policy = $this->createPolicy(false); + // use aliases from lock file only, so empty root aliases here + $repositorySet = $this->createRepositorySet(false, $platformRepo, array(), $lockedRepository); + $repositorySet->addRepository($lockedRepository); + + // creating requirements request + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); if (!$this->locker->isFresh()) { $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update `.', true, IOInterface::QUIET); } foreach ($lockedRepository->getPackages() as $package) { - $version = $package->getVersion(); - if (isset($aliases[$package->getName()][$version])) { - $version = $aliases[$package->getName()][$version]['alias_normalized']; - } - $constraint = new Constraint('=', $version); - $constraint->setPrettyString($package->getPrettyVersion()); - $request->install($package->getName(), $constraint); + $request->fixPackage($package); } foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { - $request->install($link->getTarget(), $link->getConstraint()); + $request->requireName($link->getTarget(), $link->getConstraint()); + } + + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); + + // solve dependencies + $solver = new Solver($policy, $pool, $this->io, $repositorySet); + try { + $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + $solver = null; + + // installing the locked packages on this platform resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system + if (0 !== count($lockTransaction->getOperations())) { + $this->io->writeError('Your lock file cannot be installed on this system without changes. Please run composer update.', true, IOInterface::QUIET); + // TODO actually display operations to explain what happened? + return 1; + } + } catch (SolverProblemsException $e) { + $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + + return max(1, $e->getCode()); } } - // force dev packages to have the latest links if we update or install from a (potentially new) lock - $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); + // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? + $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_OPERATIONS_EXEC, $this->devMode, $this->executeOperations, $localRepoTransaction); - // solve dependencies - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $installedRepo, $this->io); - try { - $operations = $solver->solve($request, $this->ignorePlatformReqs); - $ruleSetSize = $solver->getRuleSetSize(); - $solver = null; - } catch (SolverProblemsException $e) { - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); - if ($this->update && !$this->devMode) { - $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); - } - - return array(max(1, $e->getCode()), array()); + if (!$localRepoTransaction->getOperations()) { + $this->io->writeError('Nothing to install, update or remove'); } - // force dev packages to be updated if we update or install from a (potentially new) lock - $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); - - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $pool, $installedRepo, $request, $operations); - - $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); - $this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE); - - // execute operations - if (!$operations) { - $this->io->writeError('Nothing to install or update'); - } - - $operations = $this->movePluginsToFront($operations); - $operations = $this->moveUninstallsToFront($operations); - - // extract dev packages and mark them to be skipped if it's a --no-dev install or update - // we also force them to be uninstalled if they are present in the local repo - if ($this->update) { - $devPackages = $this->extractDevPackages($operations, $localRepo, $platformRepo, $aliases); - if (!$this->devMode) { - $operations = $this->filterDevPackageOperations($devPackages, $operations, $localRepo); - } - } else { - $devPackages = null; - } - - if ($operations) { + if ($localRepoTransaction->getOperations()) { $installs = $updates = $uninstalls = array(); - foreach ($operations as $operation) { + foreach ($localRepoTransaction->getOperations() as $operation) { if ($operation instanceof InstallOperation) { $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UpdateOperation) { @@ -558,326 +674,41 @@ class Installer } } - foreach ($operations as $operation) { - // collect suggestions - $jobType = $operation->getJobType(); - if ('install' === $jobType) { - $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); - } - - // updating, force dev packages' references if they're in root package refs - if ($this->update) { - $package = null; - if ('update' === $jobType) { - $package = $operation->getTargetPackage(); - } elseif ('install' === $jobType) { - $package = $operation->getPackage(); - } - if ($package && $package->isDev()) { - $references = $this->package->getReferences(); - if (isset($references[$package->getName()])) { - $this->updateInstallReferences($package, $references[$package->getName()]); - } - } - if ('update' === $jobType) { - $targetPackage = $operation->getTargetPackage(); - if ($targetPackage->isDev()) { - $initialPackage = $operation->getInitialPackage(); - if ($targetPackage->getVersion() === $initialPackage->getVersion() - && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) - && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) - ) { - $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); - $this->io->writeError('', true, IOInterface::DEBUG); - - continue; - } - } - } - } - - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); - if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $pool, $installedRepo, $request, $operations, $operation); - } - - // output non-alias ops when not executing operations (i.e. dry run), output alias ops in debug verbosity - if (!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) { - $this->io->writeError(' - ' . $operation); - } elseif ($this->io->isDebug() && false !== strpos($operation->getJobType(), 'Alias')) { - $this->io->writeError(' - ' . $operation); - } - - $this->installationManager->execute($localRepo, $operation); - - // output reasons why the operation was ran, only for install/update operations - if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { - $reason = $operation->getReason(); - if ($reason instanceof Rule) { - switch ($reason->getReason()) { - case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - } - } - } - - if ($this->executeOperations || $this->writeLock) { - $localRepo->write(); - } - - $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); - if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $pool, $installedRepo, $request, $operations, $operation); - } - } - if ($this->executeOperations) { - // force source/dist urls to be updated for all packages - $this->processPackageUrls($pool, $policy, $localRepo, $repositories); - $localRepo->write(); - } - - // see https://github.com/composer/composer/issues/2764 - if ($operations) { - $vendorDir = $this->config->get('vendor-dir'); - if (is_dir($vendorDir)) { - // suppress errors as this fails sometimes on OSX for no apparent reason - // see https://github.com/composer/composer/issues/4070#issuecomment-129792748 - @touch($vendorDir); - } - } - - return array(0, $devPackages); - } - - /** - * Extracts the dev packages out of the localRepo - * - * This works by faking the operations so we can see what the dev packages - * would be at the end of the operation execution. This lets us then remove - * the dev packages from the list of operations accordingly if we are in a - * --no-dev install or update. - * - * @return array - */ - private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) - { - if (!$this->package->getDevRequires()) { - return array(); - } - - // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with - $tempLocalRepo = clone $localRepo; - foreach ($operations as $operation) { - switch ($operation->getJobType()) { - case 'install': - case 'markAliasInstalled': - if (!$tempLocalRepo->hasPackage($operation->getPackage())) { - $tempLocalRepo->addPackage(clone $operation->getPackage()); - } - break; - - case 'uninstall': - case 'markAliasUninstalled': - $tempLocalRepo->removePackage($operation->getPackage()); - break; - - case 'update': - $tempLocalRepo->removePackage($operation->getInitialPackage()); - if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { - $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); - } - break; - - default: - throw new \LogicException('Unknown type: '.$operation->getJobType()); - } - } - - // we have to reload the local repo to handle aliases properly - // but as it is not persisted on disk we use a loader/dumper - // to reload it in memory - $localRepo = new InstalledArrayRepository(array()); - $loader = new ArrayLoader(null, true); - $dumper = new ArrayDumper(); - foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { - $localRepo->addPackage($loader->load($dumper->dump($pkg))); - } - unset($tempLocalRepo, $loader, $dumper); - - $policy = $this->createPolicy(); - $pool = $this->createPool(); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $pool->addRepository($installedRepo, $aliases); - - // creating requirements request without dev requirements - $request = $this->createRequest($this->package, $platformRepo); - $request->updateAll(); - foreach ($this->package->getRequires() as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - - // solve deps to see which get removed - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $installedRepo, $this->io); - $ops = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); - - $devPackages = array(); - foreach ($ops as $op) { - if ($op->getJobType() === 'uninstall') { - $devPackages[] = $op->getPackage(); - } - } - - return $devPackages; - } - - /** - * @return OperationInterface[] filtered operations, dev packages are uninstalled and all operations on them ignored - */ - private function filterDevPackageOperations(array $devPackages, array $operations, RepositoryInterface $localRepo) - { - $finalOps = array(); - $packagesToSkip = array(); - foreach ($devPackages as $pkg) { - $packagesToSkip[$pkg->getName()] = true; - if ($installedDevPkg = $localRepo->findPackage($pkg->getName(), '*')) { - if ($installedDevPkg instanceof AliasPackage) { - $finalOps[] = new MarkAliasUninstalledOperation($installedDevPkg, 'non-dev install removing it'); - $installedDevPkg = $installedDevPkg->getAliasOf(); + $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode); + } else { + foreach ($localRepoTransaction->getOperations() as $operation) { + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug()) { + $this->io->writeError(' - ' . $operation->show(false)); } - $finalOps[] = new UninstallOperation($installedDevPkg, 'non-dev install removing it'); } } - // skip operations applied on dev packages - foreach ($operations as $op) { - $package = $op->getJobType() === 'update' ? $op->getTargetPackage() : $op->getPackage(); - if (isset($packagesToSkip[$package->getName()])) { - continue; - } + return 0; + } - $finalOps[] = $op; + private function createPlatformRepo($forUpdate) + { + if ($forUpdate) { + $platformOverrides = $this->config->get('platform') ?: array(); + } else { + $platformOverrides = $this->locker->getPlatformOverrides(); } - return $finalOps; + return new PlatformRepository(array(), $platformOverrides); } /** - * Workaround: if your packages depend on plugins, we must be sure - * that those are installed / updated first; else it would lead to packages - * being installed multiple times in different folders, when running Composer - * twice. - * - * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, - * it at least fixes the symptoms and makes usage of composer possible (again) - * in such scenarios. - * - * @param OperationInterface[] $operations - * @return OperationInterface[] reordered operation list + * @param bool $forUpdate + * @param PlatformRepository $platformRepo + * @param array $rootAliases + * @param RepositoryInterface|null $lockedRepository + * @return RepositorySet */ - private function movePluginsToFront(array $operations) + private function createRepositorySet($forUpdate, PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { - $pluginsNoDeps = array(); - $pluginsWithDeps = array(); - $pluginRequires = array(); - - foreach (array_reverse($operations, true) as $idx => $op) { - if ($op instanceof InstallOperation) { - $package = $op->getPackage(); - } elseif ($op instanceof UpdateOperation) { - $package = $op->getTargetPackage(); - } else { - continue; - } - - // is this package a plugin? - $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; - - // is this a plugin or a dependency of a plugin? - if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { - // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - // is this a plugin with no meaningful dependencies? - if ($isPlugin && !count($requires)) { - // plugins with no dependencies go to the very front - array_unshift($pluginsNoDeps, $op); - } else { - // capture the requirements for this package so those packages will be moved up as well - $pluginRequires = array_merge($pluginRequires, $requires); - // move the operation to the front - array_unshift($pluginsWithDeps, $op); - } - - unset($operations[$idx]); - } - } - - return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); - } - - /** - * Removals of packages should be executed before installations in - * case two packages resolve to the same path (due to custom installers) - * - * @param OperationInterface[] $operations - * @return OperationInterface[] reordered operation list - */ - private function moveUninstallsToFront(array $operations) - { - $uninstOps = array(); - foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { - $uninstOps[] = $op; - unset($operations[$idx]); - } - } - - return array_merge($uninstOps, $operations); - } - - /** - * @return RepositoryInterface - */ - private function createInstalledRepo(RepositoryInterface $localRepo, PlatformRepository $platformRepo) - { - // clone root package to have one in the installed repo that does not require anything - // we don't want it to be uninstallable, but its requirements should not conflict - // with the lock file for example - $installedRootPackage = clone $this->package; - $installedRootPackage->setRequires(array()); - $installedRootPackage->setDevRequires(array()); - - $repos = array( - $localRepo, - new InstalledArrayRepository(array($installedRootPackage)), - $platformRepo, - ); - $installedRepo = new CompositeRepository($repos); - if ($this->additionalInstalledRepository) { - $installedRepo->addRepository($this->additionalInstalledRepository); - } - - return $installedRepo; - } - - /** - * @param RepositoryInterface|null $lockedRepository - * @return Pool - */ - private function createPool(RepositoryInterface $lockedRepository = null) - { - if ($this->update) { + if ($forUpdate) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); @@ -894,30 +725,41 @@ class Installer } } - $rootConstraints = array(); + $rootRequires = array(); foreach ($requires as $req => $constraint) { // skip platform requirements from the root package to avoid filtering out existing platform packages if ($this->ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) { continue; } if ($constraint instanceof Link) { - $rootConstraints[$req] = $constraint->getConstraint(); + $rootRequires[$req] = $constraint->getConstraint(); } else { - $rootConstraints[$req] = $constraint; + $rootRequires[$req] = $constraint; } } - return new Pool($minimumStability, $stabilityFlags, $rootConstraints); + $this->fixedRootPackage = clone $this->package; + $this->fixedRootPackage->setRequires(array()); + $this->fixedRootPackage->setDevRequires(array()); + + $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires); + $repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage)); + $repositorySet->addRepository($platformRepo); + if ($this->additionalFixedRepository) { + $repositorySet->addRepository($this->additionalFixedRepository); + } + + return $repositorySet; } /** * @return DefaultPolicy */ - private function createPolicy() + private function createPolicy($forUpdate) { $preferStable = null; $preferLowest = null; - if (!$this->update) { + if (!$forUpdate) { $preferStable = $this->locker->getPreferStable(); $preferLowest = $this->locker->getPreferLowest(); } @@ -934,37 +776,36 @@ class Installer } /** - * @param RootPackageInterface $rootPackage - * @param PlatformRepository $platformRepo + * @param RootPackageInterface $rootPackage + * @param PlatformRepository $platformRepo + * @param RepositoryInterface|null $lockedRepository * @return Request */ - private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo) + private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo, $lockedRepository = null) { - $request = new Request(); + $request = new Request($lockedRepository); - $constraint = new Constraint('=', $rootPackage->getVersion()); - $constraint->setPrettyString($rootPackage->getPrettyVersion()); - $request->install($rootPackage->getName(), $constraint); + $request->fixPackage($rootPackage, false); + if ($rootPackage instanceof RootAliasPackage) { + $request->fixPackage($rootPackage->getAliasOf(), false); + } $fixedPackages = $platformRepo->getPackages(); - if ($this->additionalInstalledRepository) { - $additionalFixedPackages = $this->additionalInstalledRepository->getPackages(); - $fixedPackages = array_merge($fixedPackages, $additionalFixedPackages); + if ($this->additionalFixedRepository) { + $fixedPackages = array_merge($fixedPackages, $this->additionalFixedRepository->getPackages()); } // fix the version of all platform packages + additionally installed packages // to prevent the solver trying to remove or update those + // TODO why not replaces? $provided = $rootPackage->getProvides(); foreach ($fixedPackages as $package) { - $constraint = new Constraint('=', $package->getVersion()); - $constraint->setPrettyString($package->getPrettyVersion()); - // skip platform packages that are provided by the root package if ($package->getRepository() !== $platformRepo || !isset($provided[$package->getName()]) - || !$provided[$package->getName()]->getConstraint()->matches($constraint) + || !$provided[$package->getName()]->getConstraint()->matches(new Constraint('=', $package->getVersion())) ) { - $request->fix($package->getName(), $constraint); + $request->fixPackage($package, false); } } @@ -972,173 +813,12 @@ class Installer } /** - * @param WritableRepositoryInterface $localRepo - * @param Pool $pool - * @param PolicyInterface $policy - * @param array $repositories - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $lockedRepository - * @param string $task - * @param array|null $operations + * @param bool $forUpdate * @return array */ - private function processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) + private function getRootAliases($forUpdate) { - if ($task === 'force-updates' && null === $operations) { - throw new \InvalidArgumentException('Missing operations argument'); - } - if ($task === 'force-links') { - $operations = array(); - } - - if ($this->update && $this->updateWhitelist) { - $currentPackages = $this->getCurrentPackages($installedRepo); - } - - foreach ($localRepo->getCanonicalPackages() as $package) { - // skip non-dev packages - if (!$package->isDev()) { - continue; - } - - // skip packages that will be updated/uninstalled - foreach ($operations as $operation) { - if (('update' === $operation->getJobType() && $operation->getInitialPackage()->equals($package)) - || ('uninstall' === $operation->getJobType() && $operation->getPackage()->equals($package)) - ) { - continue 2; - } - } - - if ($this->update) { - // skip package if the whitelist 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) { - if ($curPackage->isDev() && $curPackage->getName() === $package->getName() && $curPackage->getVersion() === $package->getVersion()) { - if ($task === 'force-links') { - $package->setRequires($curPackage->getRequires()); - $package->setConflicts($curPackage->getConflicts()); - $package->setProvides($curPackage->getProvides()); - $package->setReplaces($curPackage->getReplaces()); - } elseif ($task === 'force-updates') { - if (($curPackage->getSourceReference() && $curPackage->getSourceReference() !== $package->getSourceReference()) - || ($curPackage->getDistReference() && $curPackage->getDistReference() !== $package->getDistReference()) - ) { - $operations[] = new UpdateOperation($package, $curPackage); - } - } - - break; - } - } - - continue; - } - - // find similar packages (name/version) in all repositories - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion())); - foreach ($matches as $index => $match) { - // skip local packages - if (!in_array($match->getRepository(), $repositories, true)) { - unset($matches[$index]); - continue; - } - - // skip providers/replacers - if ($match->getName() !== $package->getName()) { - unset($matches[$index]); - continue; - } - - $matches[$index] = $match->getId(); - } - - // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); - - if ($task === 'force-links' && $newPackage) { - $package->setRequires($newPackage->getRequires()); - $package->setConflicts($newPackage->getConflicts()); - $package->setProvides($newPackage->getProvides()); - $package->setReplaces($newPackage->getReplaces()); - } - - if ( - $task === 'force-updates' - && $newPackage - && ( - ($newPackage->getSourceReference() && $newPackage->getSourceReference() !== $package->getSourceReference()) - || ($newPackage->getDistReference() && $newPackage->getDistReference() !== $package->getDistReference()) - ) - ) { - $operations[] = new UpdateOperation($package, $newPackage); - - continue; - } - } - - if ($task === 'force-updates') { - // force installed package to update to referenced version in root package if it does not match the installed version - $references = $this->package->getReferences(); - - if (isset($references[$package->getName()]) && $references[$package->getName()] !== $package->getSourceReference()) { - // changing the source ref to update to will be handled in the operations loop - $operations[] = new UpdateOperation($package, clone $package); - } - } - } else { - // force update to locked version if it does not match the installed version - foreach ($lockedRepository->findPackages($package->getName()) as $lockedPackage) { - if ($lockedPackage->isDev() && $lockedPackage->getVersion() === $package->getVersion()) { - if ($task === 'force-links') { - $package->setRequires($lockedPackage->getRequires()); - $package->setConflicts($lockedPackage->getConflicts()); - $package->setProvides($lockedPackage->getProvides()); - $package->setReplaces($lockedPackage->getReplaces()); - } elseif ($task === 'force-updates') { - if (($lockedPackage->getSourceReference() && $lockedPackage->getSourceReference() !== $package->getSourceReference()) - || ($lockedPackage->getDistReference() && $lockedPackage->getDistReference() !== $package->getDistReference()) - ) { - $operations[] = new UpdateOperation($package, $lockedPackage); - } - } - - break; - } - } - } - } - - return $operations; - } - - /** - * Loads the most "current" list of packages that are installed meaning from lock ideally or from installed repo as fallback - * @param RepositoryInterface $installedRepo - * @return array - */ - private function getCurrentPackages($installedRepo) - { - if ($this->locker->isLocked()) { - try { - return $this->locker->getLockedRepository(true)->getPackages(); - } catch (\RuntimeException $e) { - // fetch only non-dev packages from lock if doing a dev update fails due to a previously incomplete lock file - return $this->locker->getLockedRepository()->getPackages(); - } - } - - return $installedRepo->getPackages(); - } - - /** - * @return array - */ - private function getRootAliases() - { - if ($this->update) { + if ($forUpdate) { $aliases = $this->package->getAliases(); } else { $aliases = $this->locker->getAliases(); @@ -1156,143 +836,6 @@ class Installer return $normalizedAliases; } - /** - * @param Pool $pool - * @param PolicyInterface $policy - * @param WritableRepositoryInterface $localRepo - * @param array $repositories - */ - private function processPackageUrls($pool, $policy, $localRepo, $repositories) - { - if (!$this->update) { - return; - } - - $rootRefs = $this->package->getReferences(); - - foreach ($localRepo->getCanonicalPackages() as $package) { - // find similar packages (name/version) in all repositories - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion())); - foreach ($matches as $index => $match) { - // skip local packages - if (!in_array($match->getRepository(), $repositories, true)) { - unset($matches[$index]); - continue; - } - - // skip providers/replacers - if ($match->getName() !== $package->getName()) { - unset($matches[$index]); - continue; - } - - $matches[$index] = $match->getId(); - } - - // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); - - // update the dist and source URLs - $sourceUrl = $package->getSourceUrl(); - $newSourceUrl = $newPackage->getSourceUrl(); - $newReference = $newPackage->getSourceReference(); - - if ($package->isDev() && isset($rootRefs[$package->getName()]) && $package->getSourceReference() === $rootRefs[$package->getName()]) { - $newReference = $rootRefs[$package->getName()]; - } - - $this->updatePackageUrl($package, $newSourceUrl, $newPackage->getSourceType(), $newReference, $newPackage->getDistUrl(), $newPackage->getDistType(), $newPackage->getDistSha1Checksum()); - - if ($package instanceof CompletePackage && $newPackage instanceof CompletePackage) { - $package->setAbandoned($newPackage->getReplacementPackage() ?: $newPackage->isAbandoned()); - } - - $package->setDistMirrors($newPackage->getDistMirrors()); - $package->setSourceMirrors($newPackage->getSourceMirrors()); - $package->setTransportOptions($newPackage->getTransportOptions()); - } - } - } - - private function updatePackageUrl(PackageInterface $package, $sourceUrl, $sourceType, $sourceReference, $distUrl, $distType, $distShaSum) - { - $oldSourceRef = $package->getSourceReference(); - - if ($package->getSourceUrl() !== $sourceUrl) { - $package->setSourceType($sourceType); - $package->setSourceUrl($sourceUrl); - $package->setSourceReference($sourceReference); - } - - // only update dist url for github/bitbucket/gitlab dists as they use a combination of dist url + dist reference to install - // but for other urls this is ambiguous and could result in bad outcomes - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $distUrl)) { - $package->setDistUrl($distUrl); - $package->setDistType($distType); - $package->setDistSha1Checksum($distShaSum); - $this->updateInstallReferences($package, $sourceReference); - } - - if ($this->updateWhitelist && !$this->isUpdateable($package)) { - $this->updateInstallReferences($package, $oldSourceRef); - } - } - - private function updateInstallReferences(PackageInterface $package, $reference) - { - if (!$reference) { - return; - } - - $package->setSourceReference($reference); - - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistReference($reference); - $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); - } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it - $package->setDistReference($reference); - } - } - - /** - * @param PlatformRepository $platformRepo - * @param array $aliases - */ - private function aliasPlatformPackages(PlatformRepository $platformRepo, $aliases) - { - foreach ($aliases as $package => $versions) { - foreach ($versions as $version => $alias) { - $packages = $platformRepo->findPackages($package, $version); - foreach ($packages as $package) { - $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); - $aliasPackage->setRootPackageAlias(true); - $platformRepo->addPackage($aliasPackage); - } - } - } - } - - /** - * @param PackageInterface $package - * @return bool - */ - private function isUpdateable(PackageInterface $package) - { - if (!$this->updateWhitelist) { - throw new \LogicException('isUpdateable should only be called when a whitelist is present'); - } - - foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $patternRegexp = BasePackage::packageNameToRegexp($whiteListedPattern); - if (preg_match($patternRegexp, $package->getName())) { - return true; - } - } - - return false; - } - /** * @param array $links * @return array @@ -1309,112 +852,6 @@ class Installer return $platformReqs; } - /** - * Adds all dependencies of the update whitelist to the whitelist, 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. - * - * @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 - * repo might be empty but locked repo will always be current. - * @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) - { - if (!$this->updateWhitelist) { - return; - } - - $rootRequires = array_merge($rootRequires, $rootDevRequires); - - $skipPackages = array(); - if (!$this->whitelistAllDependencies) { - foreach ($rootRequires as $require) { - $skipPackages[$require->getTarget()] = true; - } - } - - $pool = new Pool('dev'); - $pool->addRepository($localOrLockRepo); - - $seen = array(); - - $rootRequiredPackageNames = array_keys($rootRequires); - - foreach ($this->updateWhitelist as $packageName => $void) { - $packageQueue = new \SplQueue; - $nameMatchesRequiredPackage = false; - - $depPackages = $pool->whatProvides($packageName); - $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) { - $matchesByPattern[] = $pool->whatProvides($installedPackage['name']); - } - - // add root requirements which match the whitelisted name/pattern - $whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName); - foreach ($rootRequiredPackageNames as $rootRequiredPackageName) { - if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) { - $nameMatchesRequiredPackage = true; - break; - } - } - } - - if (!empty($matchesByPattern)) { - $depPackages = array_merge($depPackages, call_user_func_array('array_merge', $matchesByPattern)); - } - - if (count($depPackages) == 0 && !$nameMatchesRequiredPackage && !in_array($packageName, array('nothing', 'lock', 'mirrors'))) { - $this->io->writeError('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); - } - - foreach ($depPackages as $depPackage) { - $packageQueue->enqueue($depPackage); - } - - while (!$packageQueue->isEmpty()) { - $package = $packageQueue->dequeue(); - if (isset($seen[$package->getId()])) { - continue; - } - - $seen[$package->getId()] = true; - $this->updateWhitelist[$package->getName()] = true; - - if (!$this->whitelistDependencies && !$this->whitelistAllDependencies) { - continue; - } - - $requires = $package->getRequires(); - - foreach ($requires as $require) { - $requirePackages = $pool->whatProvides($require->getTarget()); - - foreach ($requirePackages as $requirePackage) { - if (isset($this->updateWhitelist[$requirePackage->getName()])) { - continue; - } - - 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.'); - continue; - } - - $packageQueue->enqueue($requirePackage); - } - } - } - } - } - /** * Replace local repositories with InstalledArrayRepository instances * @@ -1462,12 +899,12 @@ class Installer } /** - * @param RepositoryInterface $additionalInstalledRepository + * @param RepositoryInterface $additionalFixedRepository * @return $this */ - public function setAdditionalInstalledRepository(RepositoryInterface $additionalInstalledRepository) + public function setAdditionalFixedRepository(RepositoryInterface $additionalFixedRepository) { - $this->additionalInstalledRepository = $additionalInstalledRepository; + $this->additionalFixedRepository = $additionalFixedRepository; return $this; } @@ -1675,6 +1112,19 @@ class Installer return $this; } + /** + * Update the lock file to the exact same versions and references but use current remote metadata like URLs and mirror info + * + * @param bool $updateMirrors + * @return Installer + */ + public function setUpdateMirrors($updateMirrors) + { + $this->updateMirrors = $updateMirrors; + + return $this; + } + /** * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version @@ -1682,49 +1132,29 @@ class Installer * @param array $packages * @return Installer */ - public function setUpdateWhitelist(array $packages) + public function setUpdateAllowList(array $packages) { - $this->updateWhitelist = array_flip(array_map('strtolower', $packages)); + $this->updateAllowList = array_flip(array_map('strtolower', $packages)); return $this; } /** - * @deprecated use setWhitelistTransitiveDependencies instead - */ - public function setWhitelistDependencies($updateDependencies = true) - { - return $this->setWhitelistTransitiveDependencies($updateDependencies); - } - - /** - * Should dependencies of whitelisted packages (but not direct dependencies) be updated? + * Should dependencies of packages marked for update be updated? * - * This will NOT whitelist any dependencies that are also directly defined - * in the root package. + * Depending on the chosen constant this will either only update the directly named packages, all transitive + * dependencies which are not root requirement or all transitive dependencies including root requirements * - * @param bool $updateTransitiveDependencies + * @param int $updateAllowTransitiveDependencies One of the UPDATE_ constants on the Request class * @return Installer */ - public function setWhitelistTransitiveDependencies($updateTransitiveDependencies = true) + public function setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) { - $this->whitelistDependencies = (bool) $updateTransitiveDependencies; + if (!in_array($updateAllowTransitiveDependencies, array(Request::UPDATE_ONLY_LISTED, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS), true)) { + throw new \RuntimeException("Invalid value for updateAllowTransitiveDependencies supplied"); + } - return $this; - } - - /** - * Should all dependencies of whitelisted packages be updated recursively? - * - * This will whitelist any dependencies of the whitelisted packages, including - * those defined in the root package. - * - * @param bool $updateAllDependencies - * @return Installer - */ - public function setWhitelistAllDependencies($updateAllDependencies = true) - { - $this->whitelistAllDependencies = (bool) $updateAllDependencies; + $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; return $this; } @@ -1785,19 +1215,6 @@ class Installer return $this; } - /** - * Should suggestions be skipped? - * - * @param bool $skipSuggest - * @return Installer - */ - public function setSkipSuggest($skipSuggest = true) - { - $this->skipSuggest = (bool) $skipSuggest; - - return $this; - } - /** * Disables plugins. * diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 9f50b5980..40d2a7f83 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -23,7 +23,9 @@ use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\EventDispatcher\EventDispatcher; use Composer\Util\StreamContextFactory; +use Composer\Util\Loop; /** * Package operation manager. @@ -37,6 +39,16 @@ class InstallationManager private $installers = array(); private $cache = array(); private $notifiablePackages = array(); + private $loop; + private $io; + private $eventDispatcher; + + public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) + { + $this->loop = $loop; + $this->io = $io; + $this->eventDispatcher = $eventDispatcher; + } public function reset() { @@ -151,13 +163,105 @@ class InstallationManager /** * Executes solver operation. * - * @param RepositoryInterface $repo repository in which to check - * @param OperationInterface $operation operation instance + * @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 */ - public function execute(RepositoryInterface $repo, OperationInterface $operation) + public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { - $method = $operation->getJobType(); - $this->$method($repo, $operation); + $promises = array(); + + foreach ($operations as $operation) { + $opType = $operation->getOperationType(); + $promise = null; + + if ($opType === 'install') { + $package = $operation->getPackage(); + $installer = $this->getInstaller($package->getType()); + $promise = $installer->download($package); + } elseif ($opType === 'update') { + $target = $operation->getTargetPackage(); + $targetType = $target->getType(); + $installer = $this->getInstaller($targetType); + $promise = $installer->download($target, $operation->getInitialPackage()); + } + + if ($promise) { + $promises[] = $promise; + } + } + + if (!empty($promises)) { + $this->loop->wait($promises); + } + + foreach ($operations as $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 === 'install' || $opType === 'uninstall') { + $package = $operation->getPackage(); + $initialPackage = null; + } elseif ($opType === 'update') { + $package = $operation->getTargetPackage(); + $initialPackage = $operation->getInitialPackage(); + } + $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 (null === $promise) { + $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); + } + + $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { + return $installManager->$opType($repo, $operation); + })->then(function () use ($opType, $installer, $package, $initialPackage) { + return $installer->cleanup($opType, $package, $initialPackage); + })->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, $installer, $package, $initialPackage, $loop, $io) { + $io->writeError(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); + + $promise = $installer->cleanup($opType, $package, $initialPackage); + if ($promise) { + $loop->wait(array($promise)); + } + + throw $e; + }); + + $promises[] = $promise; + } + + if (!empty($promises)) { + $this->loop->wait($promises); + } } /** @@ -170,8 +274,10 @@ class InstallationManager { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); - $installer->install($repo, $package); + $promise = $installer->install($repo, $package); $this->markForNotification($package); + + return $promise; } /** @@ -190,12 +296,15 @@ class InstallationManager if ($initialType === $targetType) { $installer = $this->getInstaller($initialType); - $installer->update($repo, $initial, $target); + $promise = $installer->update($repo, $initial, $target); $this->markForNotification($target); } else { $this->getInstaller($initialType)->uninstall($repo, $initial); - $this->getInstaller($targetType)->install($repo, $target); + $installer = $this->getInstaller($targetType); + $promise = $installer->install($repo, $target); } + + return $promise; } /** @@ -208,7 +317,8 @@ class InstallationManager { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); - $installer->uninstall($repo, $package); + + return $installer->uninstall($repo, $package); } /** diff --git a/src/Composer/Installer/InstallerEvent.php b/src/Composer/Installer/InstallerEvent.php index 87153bd51..1b8f03de4 100644 --- a/src/Composer/Installer/InstallerEvent.php +++ b/src/Composer/Installer/InstallerEvent.php @@ -14,18 +14,13 @@ namespace Composer\Installer; use Composer\Composer; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Operation\OperationInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\Transaction; use Composer\EventDispatcher\Event; use Composer\IO\IOInterface; -use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositorySet; -/** - * An event for all installer. - * - * @author François Pluchino - */ class InstallerEvent extends Event { /** @@ -44,29 +39,14 @@ class InstallerEvent extends Event private $devMode; /** - * @var PolicyInterface + * @var bool */ - private $policy; + private $executeOperations; /** - * @var Pool + * @var Transaction */ - private $pool; - - /** - * @var CompositeRepository - */ - private $installedRepo; - - /** - * @var Request - */ - private $request; - - /** - * @var OperationInterface[] - */ - private $operations; + private $transaction; /** * Constructor. @@ -75,24 +55,18 @@ class InstallerEvent extends Event * @param Composer $composer * @param IOInterface $io * @param bool $devMode - * @param PolicyInterface $policy - * @param Pool $pool - * @param CompositeRepository $installedRepo - * @param Request $request - * @param OperationInterface[] $operations + * @param bool $executeOperations + * @param Transaction $transaction */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, $executeOperations, Transaction $transaction) { parent::__construct($eventName); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; - $this->policy = $policy; - $this->pool = $pool; - $this->installedRepo = $installedRepo; - $this->request = $request; - $this->operations = $operations; + $this->executeOperations = $executeOperations; + $this->transaction = $transaction; } /** @@ -120,42 +94,18 @@ class InstallerEvent extends Event } /** - * @return PolicyInterface + * @return bool */ - public function getPolicy() + public function isExecutingOperations() { - return $this->policy; + return $this->executeOperations; } /** - * @return Pool + * @return Transaction|null */ - public function getPool() + public function getTransaction() { - return $this->pool; - } - - /** - * @return CompositeRepository - */ - public function getInstalledRepo() - { - return $this->installedRepo; - } - - /** - * @return Request - */ - public function getRequest() - { - return $this->request; - } - - /** - * @return OperationInterface[] - */ - public function getOperations() - { - return $this->operations; + return $this->transaction; } } diff --git a/src/Composer/Installer/InstallerEvents.php b/src/Composer/Installer/InstallerEvents.php index e05c92587..f75b8bdd4 100644 --- a/src/Composer/Installer/InstallerEvents.php +++ b/src/Composer/Installer/InstallerEvents.php @@ -12,32 +12,15 @@ namespace Composer\Installer; -/** - * The Installer Events. - * - * @author François Pluchino - */ class InstallerEvents { /** - * The PRE_DEPENDENCIES_SOLVING event occurs as a installer begins - * resolve operations. + * The PRE_OPERATIONS_EXEC event occurs before the lock file gets + * installed and operations are executed. * - * The event listener method receives a - * Composer\Installer\InstallerEvent instance. + * The event listener method receives an Composer\Installer\InstallerEvent instance. * * @var string */ - const PRE_DEPENDENCIES_SOLVING = 'pre-dependencies-solving'; - - /** - * The POST_DEPENDENCIES_SOLVING event occurs as a installer after - * resolve operations. - * - * The event listener method receives a - * Composer\Installer\InstallerEvent instance. - * - * @var string - */ - const POST_DEPENDENCIES_SOLVING = 'post-dependencies-solving'; + const PRE_OPERATIONS_EXEC = 'pre-operations-exec'; } diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index e64dfadd2..cc4bef7e9 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -15,6 +15,7 @@ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use InvalidArgumentException; +use React\Promise\PromiseInterface; /** * Interface for the package installation manager. @@ -42,20 +43,46 @@ interface InstallerInterface */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package); + /** + * Downloads the files needed to later install the given package. + * + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null); + + /** + * Do anything that needs to be done between all downloads have been completed and the actual operation is executed + * + * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore + * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or + * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can + * be undone as much as possible. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null); + /** * Installs specific package. * - * @param InstalledRepositoryInterface $repo repository in which to check - * @param PackageInterface $package package instance + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $package package instance + * @return PromiseInterface|null */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package); /** * Updates specific package. * - * @param InstalledRepositoryInterface $repo repository in which to check - * @param PackageInterface $initial already installed package version - * @param PackageInterface $target updated version + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $initial already installed package version + * @param PackageInterface $target updated version + * @return PromiseInterface|null * * @throws InvalidArgumentException if $initial package is not installed */ @@ -64,11 +91,26 @@ interface InstallerInterface /** * Uninstalls specific package. * - * @param InstalledRepositoryInterface $repo repository in which to check - * @param PackageInterface $package package instance + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $package package instance + * @return PromiseInterface|null */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package); + /** + * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps + * + * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give + * all installers a change to cleanup things they did previously, so you need to keep track of changes + * applied in the installer/downloader themselves. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null); + /** * Returns the installation path of a package * diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 34fbbbee4..5e99e1f47 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -43,7 +43,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface * * @param IOInterface $io * @param Composer $composer - * @param string $type + * @param string|null $type * @param Filesystem $filesystem * @param BinaryInstaller $binaryInstaller */ @@ -85,6 +85,39 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath); } + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->initializeVendorDir(); + $downloadPath = $this->getInstallPath($package); + + return $this->downloadManager->download($package, $downloadPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->initializeVendorDir(); + $downloadPath = $this->getInstallPath($package); + + return $this->downloadManager->prepare($type, $package, $downloadPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->initializeVendorDir(); + $downloadPath = $this->getInstallPath($package); + + return $this->downloadManager->cleanup($type, $package, $downloadPath, $prevPackage); + } + /** * {@inheritDoc} */ @@ -194,7 +227,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface protected function installCode(PackageInterface $package) { $downloadPath = $this->getInstallPath($package); - $this->downloadManager->download($package, $downloadPath); + $this->downloadManager->install($package, $downloadPath); } protected function updateCode(PackageInterface $initial, PackageInterface $target) diff --git a/src/Composer/Installer/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index e1f31c1bf..fbd983fa3 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -47,6 +47,30 @@ class MetapackageInstaller implements InstallerInterface return $repo->hasPackage($package); } + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null) + { + // noop + } + + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + // noop + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + // noop + } + /** * {@inheritDoc} */ @@ -69,7 +93,7 @@ class MetapackageInstaller implements InstallerInterface $name = $target->getName(); $from = $initial->getFullPrettyVersion(); $to = $target->getFullPrettyVersion(); - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . ")"); $repo->removePackage($initial); diff --git a/src/Composer/Installer/NoopInstaller.php b/src/Composer/Installer/NoopInstaller.php index 72cf17d22..4fe581ff5 100644 --- a/src/Composer/Installer/NoopInstaller.php +++ b/src/Composer/Installer/NoopInstaller.php @@ -40,6 +40,27 @@ class NoopInstaller implements InstallerInterface return $repo->hasPackage($package); } + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null) + { + } + + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer/PackageEvent.php b/src/Composer/Installer/PackageEvent.php index f5cf0ed6e..881820c91 100644 --- a/src/Composer/Installer/PackageEvent.php +++ b/src/Composer/Installer/PackageEvent.php @@ -16,19 +16,45 @@ use Composer\Composer; use Composer\IO\IOInterface; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; -use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositorySet; +use Composer\EventDispatcher\Event; /** * The Package Event. * * @author Jordi Boggiano */ -class PackageEvent extends InstallerEvent +class PackageEvent extends Event { /** - * @var OperationInterface The package instance + * @var Composer + */ + private $composer; + + /** + * @var IOInterface + */ + private $io; + + /** + * @var bool + */ + private $devMode; + + /** + * @var RepositoryInterface + */ + private $localRepo; + + /** + * @var OperationInterface[] + */ + private $operations; + + /** + * @var OperationInterface The operation instance which is being executed */ private $operation; @@ -39,20 +65,63 @@ class PackageEvent extends InstallerEvent * @param Composer $composer * @param IOInterface $io * @param bool $devMode - * @param PolicyInterface $policy - * @param Pool $pool - * @param CompositeRepository $installedRepo + * @param RepositoryInterface $localRepo * @param Request $request * @param OperationInterface[] $operations * @param OperationInterface $operation */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositoryInterface $localRepo, array $operations = array(), OperationInterface $operation) { - parent::__construct($eventName, $composer, $io, $devMode, $policy, $pool, $installedRepo, $request, $operations); + parent::__construct($eventName); + $this->composer = $composer; + $this->io = $io; + $this->devMode = $devMode; + $this->localRepo = $localRepo; + $this->operations = $operations; $this->operation = $operation; } + /** + * @return Composer + */ + public function getComposer() + { + return $this->composer; + } + + /** + * @return IOInterface + */ + public function getIO() + { + return $this->io; + } + + /** + * @return bool + */ + public function isDevMode() + { + return $this->devMode; + } + + /** + * @return RepositoryInterface + */ + public function getLocalRepo() + { + return $this->localRepo; + } + + /** + * @return OperationInterface[] + */ + public function getOperations() + { + return $this->operations; + } + /** * Returns the package instance. * diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index c400ca4a6..a52e1937e 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -50,19 +50,27 @@ class PluginInstaller extends LibraryInstaller /** * {@inheritDoc} */ - public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $extra = $package->getExtra(); if (empty($extra['class'])) { throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); } + return parent::download($package, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { parent::install($repo, $package); try { $this->composer->getPluginManager()->registerPackage($package, true); } catch (\Exception $e) { // Rollback installation - $this->io->writeError('Plugin installation failed, rolling back'); + $this->io->writeError('Plugin initialization failed, uninstalling plugin'); parent::uninstall($repo, $package); throw $e; } @@ -73,12 +81,22 @@ class PluginInstaller extends LibraryInstaller */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { - $extra = $target->getExtra(); - if (empty($extra['class'])) { - throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); - } - parent::update($repo, $initial, $target); - $this->composer->getPluginManager()->registerPackage($target, true); + + try { + $this->composer->getPluginManager()->deactivatePackage($initial, true); + $this->composer->getPluginManager()->registerPackage($target, true); + } catch (\Exception $e) { + // Rollback installation + $this->io->writeError('Plugin initialization failed, uninstalling plugin'); + parent::uninstall($repo, $target); + throw $e; + } + } + + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $this->composer->getPluginManager()->uninstallPackage($package, true); + parent::uninstall($repo, $package); } } diff --git a/src/Composer/Installer/ProjectInstaller.php b/src/Composer/Installer/ProjectInstaller.php index c79238b36..069c741ec 100644 --- a/src/Composer/Installer/ProjectInstaller.php +++ b/src/Composer/Installer/ProjectInstaller.php @@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface /** * {@inheritDoc} */ - public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $installPath = $this->installPath; if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) { @@ -67,7 +67,32 @@ class ProjectInstaller implements InstallerInterface if (!is_dir($installPath)) { mkdir($installPath, 0777, true); } - $this->downloadManager->download($package, $installPath); + + return $this->downloadManager->download($package, $installPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $this->downloadManager->install($package, $this->installPath); } /** diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php index 25788e547..e70422d14 100644 --- a/src/Composer/Installer/SuggestedPackagesReporter.php +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -24,6 +24,10 @@ use Symfony\Component\Console\Formatter\OutputFormatter; */ class SuggestedPackagesReporter { + const MODE_LIST = 1; + const MODE_BY_PACKAGE = 2; + const MODE_BY_SUGGESTION = 4; + /** * @var array */ @@ -91,38 +95,105 @@ class SuggestedPackagesReporter /** * Output suggested packages. + * * Do not list the ones already installed if installed repository provided. * - * @param RepositoryInterface $installedRepo Installed packages + * @param int $mode One of the MODE_* constants from this class * @return SuggestedPackagesReporter */ - public function output(RepositoryInterface $installedRepo = null) + public function output($mode, RepositoryInterface $installedRepo = null) + { + $suggestedPackages = $this->getFilteredSuggestions($installedRepo); + + $suggesters = array(); + $suggested = array(); + foreach ($suggestedPackages as $suggestion) { + $suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason']; + $suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason']; + } + ksort($suggesters); + ksort($suggested); + + // Simple mode + if ($mode & self::MODE_LIST) { + foreach (array_keys($suggested) as $name) { + $this->io->write(sprintf('%s', $name)); + } + + return 0; + } + + // Grouped by package + if ($mode & self::MODE_BY_PACKAGE) { + foreach ($suggesters as $suggester => $suggestions) { + $this->io->write(sprintf('%s suggests:', $suggester)); + + foreach ($suggestions as $suggestion => $reason) { + $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason))); + } + $this->io->write(''); + } + } + + // Grouped by suggestion + if ($mode & self::MODE_BY_SUGGESTION) { + // Improve readability in full mode + if ($mode & self::MODE_BY_PACKAGE) { + $this->io->write(str_repeat('-', 78)); + } + foreach ($suggested as $suggestion => $suggesters) { + $this->io->write(sprintf('%s is suggested by:', $suggestion)); + + foreach ($suggesters as $suggester => $reason) { + $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason))); + } + $this->io->write(''); + } + } + + return $this; + } + + /** + * Output number of new suggested packages and a hint to use suggest command. + ** + * Do not list the ones already installed if installed repository provided. + * + * @return SuggestedPackagesReporter + */ + public function outputMinimalistic(RepositoryInterface $installedRepo = null) + { + $suggestedPackages = $this->getFilteredSuggestions($installedRepo); + if ($suggestedPackages) { + $this->io->writeError(''.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.'); + } + + return $this; + } + + private function getFilteredSuggestions(RepositoryInterface $installedRepo = null) { $suggestedPackages = $this->getPackages(); - $installedPackages = array(); - if (null !== $installedRepo && ! empty($suggestedPackages)) { + $installedNames = array(); + if (null !== $installedRepo && !empty($suggestedPackages)) { foreach ($installedRepo->getPackages() as $package) { - $installedPackages = array_merge( - $installedPackages, + $installedNames = array_merge( + $installedNames, $package->getNames() ); } } + $suggestions = array(); foreach ($suggestedPackages as $suggestion) { - if (in_array($suggestion['target'], $installedPackages)) { + if (in_array($suggestion['target'], $installedNames)) { continue; } - $this->io->writeError(sprintf( - '%s suggests installing %s%s', - $suggestion['source'], - $this->escapeOutput($suggestion['target']), - $this->escapeOutput('' !== $suggestion['reason'] ? ' ('.$suggestion['reason'].')' : '') - )); + $suggestions[] = $suggestion; } - return $this; + return $suggestions; } /** diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 89524df39..d1d189287 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -15,7 +15,7 @@ namespace Composer\Json; use JsonSchema\Validator; use Seld\JsonLint\JsonParser; use Seld\JsonLint\ParsingException; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; @@ -37,25 +37,25 @@ class JsonFile const COMPOSER_SCHEMA_PATH = '/../../../res/composer-schema.json'; private $path; - private $rfs; + private $httpDownloader; private $io; /** * Initializes json file reader/parser. * - * @param string $path path to a lockfile - * @param RemoteFilesystem $rfs required for loading http/https json files + * @param string $path path to a lockfile + * @param HttpDownloader $httpDownloader required for loading http/https json files * @param IOInterface $io * @throws \InvalidArgumentException */ - public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null) + public function __construct($path, HttpDownloader $httpDownloader = null, IOInterface $io = null) { $this->path = $path; - if (null === $rfs && preg_match('{^https?://}i', $path)) { - throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed'); + if (null === $httpDownloader && preg_match('{^https?://}i', $path)) { + throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed'); } - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; $this->io = $io; } @@ -86,8 +86,8 @@ class JsonFile public function read() { try { - if ($this->rfs) { - $json = $this->rfs->getContents($this->path, $this->path, false); + if ($this->httpDownloader) { + $json = $this->httpDownloader->get($this->path)->getBody(); } else { if ($this->io && $this->io->isDebug()) { $this->io->writeError('Reading ' . $this->path); diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index ee93ec497..1debf3e30 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -416,4 +416,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->setDistType($type); } + + public function setSourceDistReferences($reference) + { + return $this->aliasOf->setSourceDistReferences($reference); + } } diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 6f8fa8a01..e354e4454 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Util\Filesystem; +use Composer\Util\Loop; use Composer\Json\JsonFile; /** @@ -25,6 +26,7 @@ use Composer\Json\JsonFile; class ArchiveManager { protected $downloadManager; + protected $loop; protected $archivers = array(); @@ -36,9 +38,10 @@ class ArchiveManager /** * @param DownloadManager $downloadManager A manager used to download package sources */ - public function __construct(DownloadManager $downloadManager) + public function __construct(DownloadManager $downloadManager, Loop $loop) { $this->downloadManager = $downloadManager; + $this->loop = $loop; } /** @@ -149,7 +152,9 @@ class ArchiveManager try { // Download sources - $this->downloadManager->download($package, $sourcePath); + $promise = $this->downloadManager->download($package, $sourcePath); + $this->loop->wait(array($promise)); + $this->downloadManager->install($package, $sourcePath); } catch (\Exception $e) { $filesystem->removeDirectory($sourcePath); throw $e; diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 9630e7ef0..9ecbcb332 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -210,18 +210,30 @@ abstract class BasePackage implements PackageInterface /** * {@inheritDoc} */ - public function getFullPrettyVersion($truncate = true) + public function getFullPrettyVersion($truncate = true, $displayMode = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV) { - if (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) { + if ($displayMode === PackageInterface::DISPLAY_SOURCE_REF_IF_DEV && + (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) + ) { return $this->getPrettyVersion(); } - // if source reference is a sha1 hash -- truncate - if ($truncate && strlen($this->getSourceReference()) === 40) { - return $this->getPrettyVersion() . ' ' . substr($this->getSourceReference(), 0, 7); + switch ($displayMode) { + case PackageInterface::DISPLAY_SOURCE_REF_IF_DEV: + case PackageInterface::DISPLAY_SOURCE_REF: + $reference = $this->getSourceReference(); + break; + case PackageInterface::DISPLAY_DIST_REF: + $reference = $this->getDistReference(); + break; } - return $this->getPrettyVersion() . ' ' . $this->getSourceReference(); + // if source reference is a sha1 hash -- truncate + if ($truncate && strlen($reference) === 40) { + return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7); + } + + return $this->getPrettyVersion() . ' ' . $reference; } public function getStabilityPriority() @@ -238,14 +250,14 @@ abstract class BasePackage implements PackageInterface /** * Build a regexp from a package name, expanding * globs as required * - * @param string $whiteListedPattern + * @param string $allowPattern * @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($allowPattern, $wrap = '{^%s$}i') { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + $cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern)); - return sprintf($wrap, $cleanedWhiteListedPattern); + return sprintf($wrap, $cleanedAllowPattern); } } diff --git a/src/Composer/Package/Link.php b/src/Composer/Package/Link.php index 217da0713..5a6c683cc 100644 --- a/src/Composer/Package/Link.php +++ b/src/Composer/Package/Link.php @@ -123,6 +123,6 @@ class Link */ public function getPrettyString(PackageInterface $sourcePackage) { - return $sourcePackage->getPrettyString().' '.$this->description.' '.$this->target.' '.$this->constraint->getPrettyString().''; + return $sourcePackage->getPrettyString().' '.$this->description.' '.$this->target.($this->constraint ? ' '.$this->constraint->getPrettyString() : ''); } } diff --git a/src/Composer/Package/LinkConstraint/EmptyConstraint.php b/src/Composer/Package/LinkConstraint/EmptyConstraint.php deleted file mode 100644 index 33f9e2e82..000000000 --- a/src/Composer/Package/LinkConstraint/EmptyConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\EmptyConstraint as SemverEmptyConstraint; - -trigger_error('The ' . __NAMESPACE__ . '\EmptyConstraint class is deprecated, use Composer\Semver\Constraint\EmptyConstraint instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\EmptyConstraint instead - */ -class EmptyConstraint extends SemverEmptyConstraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/LinkConstraintInterface.php b/src/Composer/Package/LinkConstraint/LinkConstraintInterface.php deleted file mode 100644 index b8903ea8f..000000000 --- a/src/Composer/Package/LinkConstraint/LinkConstraintInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\ConstraintInterface; - -trigger_error('The ' . __NAMESPACE__ . '\LinkConstraintInterface interface is deprecated, use Composer\Semver\Constraint\ConstraintInterface instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\ConstraintInterface instead - */ -interface LinkConstraintInterface extends ConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/MultiConstraint.php b/src/Composer/Package/LinkConstraint/MultiConstraint.php deleted file mode 100644 index 10a996568..000000000 --- a/src/Composer/Package/LinkConstraint/MultiConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\MultiConstraint as SemverMultiConstraint; - -trigger_error('The ' . __NAMESPACE__ . '\MultiConstraint class is deprecated, use Composer\Semver\Constraint\MultiConstraint instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\MultiConstraint instead - */ -class MultiConstraint extends SemverMultiConstraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/SpecificConstraint.php b/src/Composer/Package/LinkConstraint/SpecificConstraint.php deleted file mode 100644 index 12d3194b6..000000000 --- a/src/Composer/Package/LinkConstraint/SpecificConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\AbstractConstraint; - -trigger_error('The ' . __NAMESPACE__ . '\SpecificConstraint abstract class is deprecated, there is no replacement for it.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\AbstractConstraint instead - */ -abstract class SpecificConstraint extends AbstractConstraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/VersionConstraint.php b/src/Composer/Package/LinkConstraint/VersionConstraint.php deleted file mode 100644 index d6b1cbe1d..000000000 --- a/src/Composer/Package/LinkConstraint/VersionConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\Constraint; - -trigger_error('The ' . __NAMESPACE__ . '\VersionConstraint class is deprecated, use Composer\Semver\Constraint\Constraint instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\Constraint instead - */ -class VersionConstraint extends Constraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 228632b42..a9ddabf7d 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -18,7 +18,6 @@ use Composer\Package\Link; use Composer\Package\RootAliasPackage; use Composer\Package\RootPackageInterface; use Composer\Package\Version\VersionParser; -use Composer\Semver\VersionParser as SemverVersionParser; /** * @author Konstantin Kudryashiv @@ -29,7 +28,7 @@ class ArrayLoader implements LoaderInterface protected $versionParser; protected $loadOptions; - public function __construct(SemverVersionParser $parser = null, $loadOptions = false) + public function __construct(VersionParser $parser = null, $loadOptions = false) { if (!$parser) { $parser = new VersionParser; @@ -39,6 +38,46 @@ class ArrayLoader implements LoaderInterface } public function load(array $config, $class = 'Composer\Package\CompletePackage') + { + $package = $this->createObject($config, $class); + + foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + $package->{$method}( + $this->parseLinks( + $package->getName(), + $package->getPrettyVersion(), + $opts['description'], + $config[$type] + ) + ); + } + } + + $package = $this->configureObject($package, $config); + + return $package; + } + + public function loadPackages(array $versions, $class) + { + $packages = array(); + $linkCache = array(); + + foreach ($versions as $version) { + $package = $this->createObject($version, $class); + + $this->configureCachedLinks($linkCache, $package, $version); + $package = $this->configureObject($package, $version); + + $packages[] = $package; + } + + return $packages; + } + + private function createObject(array $config, $class) { if (!isset($config['name'])) { throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').'); @@ -50,10 +89,20 @@ class ArrayLoader implements LoaderInterface // handle already normalized versions if (isset($config['version_normalized'])) { $version = $config['version_normalized']; + + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it + if ($version === '9999999-dev') { + $version = $this->versionParser->normalize($config['version']); + } } else { $version = $this->versionParser->normalize($config['version']); } - $package = new $class($config['name'], $version, $config['version']); + + return new $class($config['name'], $version, $config['version']); + } + + private function configureObject($package, array $config) + { $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); if (isset($config['target-dir'])) { @@ -110,20 +159,6 @@ class ArrayLoader implements LoaderInterface } } - foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { - if (isset($config[$type])) { - $method = 'set'.ucfirst($opts['method']); - $package->{$method}( - $this->parseLinks( - $package->getName(), - $package->getPrettyVersion(), - $opts['description'], - $config[$type] - ) - ); - } - } - if (isset($config['suggest']) && is_array($config['suggest'])) { foreach ($config['suggest'] as $target => $reason) { if ('self.version' === trim($reason)) { @@ -207,21 +242,50 @@ class ArrayLoader implements LoaderInterface } } - if ($aliasNormalized = $this->getBranchAlias($config)) { - if ($package instanceof RootPackageInterface) { - $package = new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); - } else { - $package = new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); - } - } - if ($this->loadOptions && isset($config['transport-options'])) { $package->setTransportOptions($config['transport-options']); } + if ($aliasNormalized = $this->getBranchAlias($config)) { + if ($package instanceof RootPackageInterface) { + return new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + + return new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + return $package; } + private function configureCachedLinks(&$linkCache, $package, array $config) + { + $name = $package->getName(); + $prettyVersion = $package->getPrettyVersion(); + + foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + + $links = array(); + foreach ($config[$type] as $prettyTarget => $constraint) { + $target = strtolower($prettyTarget); + if ($constraint === 'self.version') { + $links[$target] = $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint); + } else { + if (!isset($linkCache[$name][$type][$target][$constraint])) { + $linkCache[$name][$type][$target][$constraint] = array($target, $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint)); + } + + list($target, $link) = $linkCache[$name][$type][$target][$constraint]; + $links[$target] = $link; + } + } + + $package->{$method}($links); + } + } + } + /** * @param string $source source package name * @param string $sourceVersion source package version (pretty version ideally) @@ -233,21 +297,26 @@ class ArrayLoader implements LoaderInterface { $res = array(); foreach ($links as $target => $constraint) { - if (!is_string($constraint)) { - throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($constraint) . ' (' . var_export($constraint, true) . ')'); - } - if ('self.version' === $constraint) { - $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); - } else { - $parsedConstraint = $this->versionParser->parseConstraints($constraint); - } - - $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint); + $res[strtolower($target)] = $this->createLink($source, $sourceVersion, $description, $target, $constraint); } return $res; } + private function createLink($source, $sourceVersion, $description, $target, $prettyConstraint) + { + if (!is_string($prettyConstraint)) { + throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')'); + } + if ('self.version' === $prettyConstraint) { + $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); + } else { + $parsedConstraint = $this->versionParser->parseConstraints($prettyConstraint); + } + + return new Link($source, $target, $parsedConstraint, $description, $prettyConstraint); + } + /** * Retrieves a branch alias (dev-master => 1.0.x-dev for example) if it exists * @@ -256,39 +325,42 @@ class ArrayLoader implements LoaderInterface */ public function getBranchAlias(array $config) { - if (('dev-' !== substr($config['version'], 0, 4) && '-dev' !== substr($config['version'], -4)) - || !isset($config['extra']['branch-alias']) - || !is_array($config['extra']['branch-alias']) - ) { + if ('dev-' !== substr($config['version'], 0, 4) && '-dev' !== substr($config['version'], -4)) { return; } - foreach ($config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { - // ensure it is an alias to a -dev package - if ('-dev' !== substr($targetBranch, -4)) { - continue; - } + if (isset($config['extra']['branch-alias']) && is_array($config['extra']['branch-alias'])) { + foreach ($config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { + // ensure it is an alias to a -dev package + if ('-dev' !== substr($targetBranch, -4)) { + continue; + } - // normalize without -dev and ensure it's a numeric branch that is parseable - $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4)); - if ('-dev' !== substr($validatedTargetBranch, -4)) { - continue; - } + // normalize without -dev and ensure it's a numeric branch that is parseable + $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4)); + if ('-dev' !== substr($validatedTargetBranch, -4)) { + continue; + } - // ensure that it is the current branch aliasing itself - if (strtolower($config['version']) !== strtolower($sourceBranch)) { - continue; - } + // ensure that it is the current branch aliasing itself + if (strtolower($config['version']) !== strtolower($sourceBranch)) { + continue; + } - // If using numeric aliases ensure the alias is a valid subversion - if (($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) - && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) - && (stripos($targetPrefix, $sourcePrefix) !== 0) - ) { - continue; - } + // If using numeric aliases ensure the alias is a valid subversion + if (($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) + && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) + && (stripos($targetPrefix, $sourcePrefix) !== 0) + ) { + continue; + } - return $validatedTargetBranch; + return $validatedTargetBranch; + } + } + + if (in_array($config['version'], array('dev-master', 'dev-default', 'dev-trunk'), true)) { + return '9999999-dev'; } } } diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 2115d7378..3dfe3b7d2 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -72,10 +72,8 @@ class RootPackageLoader extends ArrayLoader { if (!isset($config['name'])) { $config['name'] = '__root__'; - } elseif ($this->io) { - if ($err = ValidatingArrayLoader::hasPackageNamingError($config['name'])) { - $this->io->writeError('Deprecation warning: Your package name '.$err.' Make sure you fix this as Composer 2.0 will error.'); - } + } elseif ($err = ValidatingArrayLoader::hasPackageNamingError($config['name'])) { + throw new \RuntimeException('Your package name '.$err); } $autoVersioned = false; if (!isset($config['version'])) { @@ -147,13 +145,11 @@ class RootPackageLoader extends ArrayLoader } } - if ($this->io) { - foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { - if (isset($config[$linkType])) { - foreach ($config[$linkType] as $linkName => $constraint) { - if ($err = ValidatingArrayLoader::hasPackageNamingError($linkName, true)) { - $this->io->writeError('Deprecation warning: '.$linkType.'.'.$err.' Make sure you fix this as Composer 2.0 will error.'); - } + foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { + if (isset($config[$linkType])) { + foreach ($config[$linkType] as $linkName => $constraint) { + if ($err = ValidatingArrayLoader::hasPackageNamingError($linkName, true)) { + throw new \RuntimeException($linkType.'.'.$err); } } } diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index f02f6b165..5efc7ffb5 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -219,7 +219,7 @@ class ValidatingArrayLoader implements LoaderInterface } } - $unboundConstraint = new Constraint('=', $this->versionParser->normalize('dev-master')); + $unboundConstraint = new Constraint('=', '10000000-dev'); $stableConstraint = new Constraint('=', '1.0.0'); foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 8a500d837..04210bd7f 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -14,9 +14,9 @@ namespace Composer\Package; use Composer\Json\JsonFile; use Composer\Installer\InstallationManager; +use Composer\Repository\LockArrayRepository; use Composer\Repository\RepositoryManager; use Composer\Util\ProcessExecutor; -use Composer\Repository\ArrayRepository; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; use Composer\Plugin\PluginInterface; @@ -32,29 +32,34 @@ use Seld\JsonLint\ParsingException; */ class Locker { + /** @var JsonFile */ private $lockFile; - private $repositoryManager; + /** @var InstallationManager */ private $installationManager; + /** @var string */ private $hash; + /** @var string */ private $contentHash; + /** @var ArrayLoader */ private $loader; + /** @var ArrayDumper */ private $dumper; + /** @var ProcessExecutor */ private $process; private $lockDataCache; + private $virtualFileWritten; /** * Initializes packages locker. * * @param IOInterface $io * @param JsonFile $lockFile lockfile loader - * @param RepositoryManager $repositoryManager repository manager instance * @param InstallationManager $installationManager installation manager instance * @param string $composerFileContents The contents of the composer file */ - public function __construct(IOInterface $io, JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $composerFileContents) + public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents) { $this->lockFile = $lockFile; - $this->repositoryManager = $repositoryManager; $this->installationManager = $installationManager; $this->hash = md5($composerFileContents); $this->contentHash = self::getContentHash($composerFileContents); @@ -109,7 +114,7 @@ class Locker */ public function isLocked() { - if (!$this->lockFile->exists()) { + if (!$this->virtualFileWritten && !$this->lockFile->exists()) { return false; } @@ -146,19 +151,19 @@ class Locker * * @param bool $withDevReqs true to retrieve the locked dev packages * @throws \RuntimeException - * @return \Composer\Repository\RepositoryInterface + * @return \Composer\Repository\LockArrayRepository */ public function getLockedRepository($withDevReqs = false) { $lockData = $this->getLockData(); - $packages = new ArrayRepository(); + $packages = new LockArrayRepository(); $lockedPackages = $lockData['packages']; if ($withDevReqs) { if (isset($lockData['packages-dev'])) { $lockedPackages = array_merge($lockedPackages, $lockData['packages-dev']); } else { - throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or run update to install those packages.'); + throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or delete it and run composer update to generate a new lock file.'); } } @@ -167,14 +172,30 @@ class Locker } if (isset($lockedPackages[0]['name'])) { + $packageByName = array(); foreach ($lockedPackages as $info) { - $packages->addPackage($this->loader->load($info)); + $package = $this->loader->load($info); + $packages->addPackage($package); + $packageByName[$package->getName()] = $package; + + if ($package instanceof AliasPackage) { + $packages->addPackage($package->getAliasOf()); + $packageByName[$package->getAliasOf()->getName()] = $package->getAliasOf(); + } + } + + if (isset($lockData['aliases'])) { + foreach ($lockData['aliases'] as $alias) { + if (isset($packageByName[$alias['package']])) { + $packages->addPackage(new AliasPackage($packageByName[$alias['package']], $alias['alias_normalized'], $alias['alias'])); + } + } } return $packages; } - throw new \RuntimeException('Your composer.lock was created before 2012-09-15, and is not supported anymore. Run "composer update" to generate a new one.'); + throw new \RuntimeException('Your composer.lock is invalid. Run "composer update" to generate a new one.'); } /** @@ -190,7 +211,7 @@ class Locker if (!empty($lockData['platform'])) { $requirements = $this->loader->parseLinks( - '__ROOT__', + '__root__', '1.0.0', 'requires', isset($lockData['platform']) ? $lockData['platform'] : array() @@ -199,7 +220,7 @@ class Locker if ($withDevReqs && !empty($lockData['platform-dev'])) { $devRequirements = $this->loader->parseLinks( - '__ROOT__', + '__root__', '1.0.0', 'requires', isset($lockData['platform-dev']) ? $lockData['platform-dev'] : array() @@ -283,10 +304,11 @@ class Locker * @param bool $preferStable * @param bool $preferLowest * @param array $platformOverrides + * @param bool $write Whether to actually write data to disk, useful in tests and for --dry-run * * @return bool */ - public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides, $write = true) { $lock = array( '_readme' => array('This file locks the dependencies of your project to a known state', @@ -325,22 +347,21 @@ class Locker } $lock['plugin-api-version'] = PluginInterface::PLUGIN_API_VERSION; - if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) { - if ($this->lockFile->exists()) { - unlink($this->lockFile->getPath()); - } - - return false; - } - try { $isLocked = $this->isLocked(); } catch (ParsingException $e) { $isLocked = false; } if (!$isLocked || $lock !== $this->getLockData()) { - $this->lockFile->write($lock); - $this->lockDataCache = null; + if ($write) { + $this->lockFile->write($lock); +// $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); + $this->lockDataCache = null; + $this->virtualFileWritten = false; + } else { + $this->virtualFileWritten = true; + $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); + } return true; } diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 6c7b426e7..c633e1856 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -569,6 +569,23 @@ class Package extends BasePackage return $this->archiveExcludes; } + /** + * {@inheritDoc} + */ + public function setSourceDistReferences($reference) + { + $this->setSourceReference($reference); + + // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL + // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? + if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $this->getDistUrl())) { + $this->setDistReference($reference); + $this->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $this->getDistUrl())); + } elseif ($this->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it + $this->setDistReference($reference); + } + } + /** * 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..9db188358 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -21,6 +21,10 @@ use Composer\Repository\RepositoryInterface; */ interface PackageInterface { + const DISPLAY_SOURCE_REF_IF_DEV = 0; + const DISPLAY_SOURCE_REF = 1; + const DISPLAY_DIST_REF = 2; + /** * Returns the package's name without version info, thus not a unique identifier * @@ -76,7 +80,7 @@ interface PackageInterface /** * Returns the package targetDir property * - * @return string The package targetDir + * @return string|null The package targetDir */ public function getTargetDir(); @@ -198,9 +202,10 @@ interface PackageInterface * @see getPrettyVersion * * @param bool $truncate If the source reference is a sha1 hash, truncate it + * @param int $displayMode One of the DISPLAY_ constants on this interface determining display of references * @return string version */ - public function getFullPrettyVersion($truncate = true); + public function getFullPrettyVersion($truncate = true, $displayMode = self::DISPLAY_SOURCE_REF_IF_DEV); /** * Returns the release date of the package @@ -386,4 +391,13 @@ interface PackageInterface * @return void */ public function setDistReference($reference); + + /** + * Set dist and source references and update dist URL for ones that contain a reference + * + * @param string $reference + * + * @return void + */ + public function setSourceDistReferences($reference); } diff --git a/src/Composer/Package/Version/StabilityFilter.php b/src/Composer/Package/Version/StabilityFilter.php new file mode 100644 index 000000000..ed27af080 --- /dev/null +++ b/src/Composer/Package/Version/StabilityFilter.php @@ -0,0 +1,43 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Version; + +use Composer\Package\BasePackage; + +/** + * @author Jordi Boggiano + */ +class StabilityFilter +{ + /** + * Checks if any of the provided package names in the given stability match the configured acceptable stability and flags + * + * @return bool true if any package name is acceptable + */ + public static function isPackageAcceptable(array $acceptableStabilities, array $stabilityFlags, $names, $stability) + { + foreach ($names as $name) { + // allow if package matches the package-specific stability flag + if (isset($stabilityFlags[$name])) { + if (BasePackage::$stabilities[$stability] <= $stabilityFlags[$name]) { + return true; + } + } elseif (isset($acceptableStabilities[$stability])) { + // allow if package matches the global stability requirement and has no exception + return true; + } + } + + return false; + } +} diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 721887787..5bd52b218 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -17,6 +17,7 @@ use Composer\Repository\Vcs\HgDriver; use Composer\IO\NullIO; use Composer\Semver\VersionParser as SemverVersionParser; use Composer\Util\Git as GitUtil; +use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; use Composer\Util\Svn as SvnUtil; @@ -214,7 +215,8 @@ class VersionGuesser } // re-use the HgDriver to fetch branches (this properly includes bookmarks) - $driver = new HgDriver(array('url' => $path), new NullIO(), $this->config, $this->process); + $io = new NullIO(); + $driver = new HgDriver(array('url' => $path), $io, $this->config, new HttpDownloader($io, $this->config), $this->process); $branches = array_keys($driver->getBranches()); // try to find the best (nearest) version branch to assume this feature's version @@ -233,7 +235,7 @@ class VersionGuesser // ignore feature branches if they have no branch-alias or self.version is used // and find the branch they came from to use as a version instead - if ((isset($packageConfig['extra']['branch-alias']) && !isset($packageConfig['extra']['branch-alias'][$version])) + if (!isset($packageConfig['extra']['branch-alias'][$version]) || strpos(json_encode($packageConfig), '"self.version"') ) { $branch = preg_replace('{^dev-}', '', $version); diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 831c61d5f..3c0a3aff5 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -70,6 +70,9 @@ class VersionParser extends SemverVersionParser */ public static function isUpgrade($normalizedFrom, $normalizedTo) { + $normalizedFrom = str_replace(array('dev-master', 'dev-trunk', 'dev-default'), '9999999-dev', $normalizedFrom); + $normalizedTo = str_replace(array('dev-master', 'dev-trunk', 'dev-default'), '9999999-dev', $normalizedTo); + if (substr($normalizedFrom, 0, 4) === 'dev-' || substr($normalizedTo, 0, 4) === 'dev-') { return true; } diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index 8e225d803..a8b4ae17b 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -17,6 +17,7 @@ use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; /** @@ -27,13 +28,13 @@ use Composer\Semver\Constraint\Constraint; */ class VersionSelector { - private $pool; + private $repositorySet; private $parser; - public function __construct(Pool $pool) + public function __construct(RepositorySet $repositorySet) { - $this->pool = $pool; + $this->repositorySet = $repositorySet; } /** @@ -44,12 +45,12 @@ class VersionSelector * @param string $targetPackageVersion * @param string $targetPhpVersion * @param string $preferredStability - * @return PackageInterface|bool + * @return PackageInterface|false */ public function findBestCandidate($packageName, $targetPackageVersion = null, $targetPhpVersion = null, $preferredStability = 'stable') { $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; - $candidates = $this->pool->whatProvides(strtolower($packageName), $constraint, true); + $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint); if ($targetPhpVersion) { $phpConstraint = new Constraint('==', $this->getParser()->normalize($targetPhpVersion)); diff --git a/src/Composer/Plugin/PluginEvents.php b/src/Composer/Plugin/PluginEvents.php index 1fb368baf..39cac6798 100644 --- a/src/Composer/Plugin/PluginEvents.php +++ b/src/Composer/Plugin/PluginEvents.php @@ -58,4 +58,15 @@ class PluginEvents * @var string */ const PRE_COMMAND_RUN = 'pre-command-run'; + + /** + * The PRE_POOL_CREATE event occurs before the Pool of packages is created, and lets + * you filter the list of packages which is going to enter the Solver + * + * The event listener method receives a + * Composer\Plugin\PrePoolCreateEvent instance. + * + * @var string + */ + const PRE_POOL_CREATE = 'pre-pool-create'; } diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index 6eaca4e90..4390764ff 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -27,7 +27,7 @@ interface PluginInterface * * @var string */ - const PLUGIN_API_VERSION = '1.1.0'; + const PLUGIN_API_VERSION = '2.0.0'; /** * Apply plugin modifications to Composer @@ -36,4 +36,26 @@ interface PluginInterface * @param IOInterface $io */ public function activate(Composer $composer, IOInterface $io); + + /** + * Remove any hooks from Composer + * + * This will be called when a plugin is deactivated before being + * uninstalled, but also before it gets upgraded to a new version + * so the old one can be deactivated and the new one activated. + * + * @param Composer $composer + * @param IOInterface $io + */ + public function deactivate(Composer $composer, IOInterface $io); + + /** + * Prepare the plugin to be uninstalled + * + * This will be called after deactivate. + * + * @param Composer $composer + * @param IOInterface $io + */ + public function uninstall(Composer $composer, IOInterface $io); } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index bb9b66d83..97757f4fa 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -19,10 +19,10 @@ use Composer\Package\CompletePackage; use Composer\Package\Package; use Composer\Package\Version\VersionParser; use Composer\Repository\RepositoryInterface; +use Composer\Repository\InstalledRepository; use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; -use Composer\DependencyResolver\Pool; use Composer\Plugin\Capability\Capability; use Composer\Util\PackageSorter; @@ -145,7 +145,7 @@ class PluginManager $oldInstallerPlugin = ($package->getType() === 'composer-installer'); - if (in_array($package->getName(), $this->registeredPlugins)) { + if (isset($this->registeredPlugins[$package->getName()])) { return; } @@ -158,14 +158,13 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $pool = new Pool('dev'); - $pool->addRepository($localRepo); + $installedRepo = new InstalledRepository(array($localRepo)); if ($globalRepo) { - $pool->addRepository($globalRepo); + $installedRepo->addRepository($globalRepo); } $autoloadPackages = array($package->getName() => $package); - $autoloadPackages = $this->collectDependencies($pool, $autoloadPackages, $package); + $autoloadPackages = $this->collectDependencies($installedRepo, $autoloadPackages, $package); $generator = $this->composer->getAutoloadGenerator(); $autoloads = array(); @@ -201,16 +200,82 @@ class PluginManager if ($oldInstallerPlugin) { $installer = new $class($this->io, $this->composer); $this->composer->getInstallationManager()->addInstaller($installer); + $this->registeredPlugins[$package->getName()] = $installer; } elseif (class_exists($class)) { $plugin = new $class(); $this->addPlugin($plugin); - $this->registeredPlugins[] = $package->getName(); + $this->registeredPlugins[$package->getName()] = $plugin; } elseif ($failOnMissingClasses) { throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class); } } } + /** + * Deactivates a plugin package + * + * If it's of type composer-installer it is unregistered from the installers + * instead for BC + * + * @param PackageInterface $package + * + * @throws \UnexpectedValueException + */ + public function deactivatePackage(PackageInterface $package) + { + if ($this->disablePlugins) { + return; + } + + $oldInstallerPlugin = ($package->getType() === 'composer-installer'); + + if (!isset($this->registeredPlugins[$package->getName()])) { + return; + } + + if ($oldInstallerPlugin) { + $installer = $this->registeredPlugins[$package->getName()]; + unset($this->registeredPlugins[$package->getName()]); + $this->composer->getInstallationManager()->removeInstaller($installer); + } else { + $plugin = $this->registeredPlugins[$package->getName()]; + unset($this->registeredPlugins[$package->getName()]); + $this->removePlugin($plugin); + } + } + + /** + * Uninstall a plugin package + * + * If it's of type composer-installer it is unregistered from the installers + * instead for BC + * + * @param PackageInterface $package + * + * @throws \UnexpectedValueException + */ + public function uninstallPackage(PackageInterface $package) + { + if ($this->disablePlugins) { + return; + } + + $oldInstallerPlugin = ($package->getType() === 'composer-installer'); + + if (!isset($this->registeredPlugins[$package->getName()])) { + return; + } + + if ($oldInstallerPlugin) { + $this->deactivatePackage($package); + } else { + $plugin = $this->registeredPlugins[$package->getName()]; + unset($this->registeredPlugins[$package->getName()]); + $this->removePlugin($plugin); + $this->uninstallPlugin($plugin); + } + } + /** * Returns the version of the internal composer-plugin-api package. * @@ -241,6 +306,44 @@ class PluginManager } } + /** + * Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance + * + * Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer + * programmatically and want to deregister a plugin class directly this is a valid way + * to do it. + * + * @param PluginInterface $plugin plugin instance + */ + public function removePlugin(PluginInterface $plugin) + { + $index = array_search($plugin, $this->plugins, true); + if ($index === false) { + return; + } + + $this->io->writeError('Unloading plugin '.get_class($plugin), true, IOInterface::DEBUG); + unset($this->plugins[$index]); + $plugin->deactivate($this->composer, $this->io); + + $this->composer->getEventDispatcher()->removeListener($plugin); + } + + /** + * Notifies a plugin it is being uninstalled and should clean up + * + * Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer + * programmatically and want to deregister a plugin class directly this is a valid way + * to do it. + * + * @param PluginInterface $plugin plugin instance + */ + public function uninstallPlugin(PluginInterface $plugin) + { + $this->io->writeError('Uninstalling plugin '.get_class($plugin), true, IOInterface::DEBUG); + $plugin->uninstall($this->composer, $this->io); + } + /** * Load all plugins and installers from a repository * @@ -272,13 +375,13 @@ class PluginManager /** * Recursively generates a map of package names to packages for all deps * - * @param Pool $pool Package pool of installed packages - * @param array $collected Current state of the map for recursion - * @param PackageInterface $package The package to analyze + * @param InstalledRepository $installedRepo Set of local repos + * @param array $collected Current state of the map for recursion + * @param PackageInterface $package The package to analyze * * @return array Map of package names to packages */ - private function collectDependencies(Pool $pool, array $collected, PackageInterface $package) + private function collectDependencies(InstalledRepository $installedRepo, array $collected, PackageInterface $package) { $requires = array_merge( $package->getRequires(), @@ -286,33 +389,17 @@ class PluginManager ); foreach ($requires as $requireLink) { - $requiredPackage = $this->lookupInstalledPackage($pool, $requireLink); - if ($requiredPackage && !isset($collected[$requiredPackage->getName()])) { - $collected[$requiredPackage->getName()] = $requiredPackage; - $collected = $this->collectDependencies($pool, $collected, $requiredPackage); + foreach ($installedRepo->findPackagesWithReplacersAndProviders($requireLink->getTarget(), $requireLink->getConstraint()) as $requiredPackage) { + if (!isset($collected[$requiredPackage->getName()])) { + $collected[$requiredPackage->getName()] = $requiredPackage; + $collected = $this->collectDependencies($installedRepo, $collected, $requiredPackage); + } } } return $collected; } - /** - * Resolves a package link to a package in the installed pool - * - * Since dependencies are already installed this should always find one. - * - * @param Pool $pool Pool of installed packages only - * @param Link $link Package link to look up - * - * @return PackageInterface|null The found package - */ - private function lookupInstalledPackage(Pool $pool, Link $link) - { - $packages = $pool->whatProvides($link->getTarget(), $link->getConstraint()); - - return !empty($packages) ? $packages[0] : null; - } - /** * Retrieves the path a package is installed to. * diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 7ae6821ce..c2751da02 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -13,7 +13,7 @@ namespace Composer\Plugin; use Composer\EventDispatcher\Event; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * The pre file download event. @@ -23,9 +23,9 @@ use Composer\Util\RemoteFilesystem; class PreFileDownloadEvent extends Event { /** - * @var RemoteFilesystem + * @var HttpDownloader */ - private $rfs; + private $httpDownloader; /** * @var string @@ -36,34 +36,22 @@ class PreFileDownloadEvent extends Event * Constructor. * * @param string $name The event name - * @param RemoteFilesystem $rfs + * @param HttpDownloader $httpDownloader * @param string $processedUrl */ - public function __construct($name, RemoteFilesystem $rfs, $processedUrl) + public function __construct($name, HttpDownloader $httpDownloader, $processedUrl) { parent::__construct($name); - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; $this->processedUrl = $processedUrl; } /** - * Returns the remote filesystem - * - * @return RemoteFilesystem + * @return HttpDownloader */ - public function getRemoteFilesystem() + public function getHttpDownloader() { - return $this->rfs; - } - - /** - * Sets the remote filesystem - * - * @param RemoteFilesystem $rfs - */ - public function setRemoteFilesystem(RemoteFilesystem $rfs) - { - $this->rfs = $rfs; + return $this->httpDownloader; } /** diff --git a/src/Composer/Plugin/PrePoolCreateEvent.php b/src/Composer/Plugin/PrePoolCreateEvent.php new file mode 100644 index 000000000..0e6617739 --- /dev/null +++ b/src/Composer/Plugin/PrePoolCreateEvent.php @@ -0,0 +1,158 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +use Composer\EventDispatcher\Event; +use Symfony\Component\Console\Input\InputInterface; +use Composer\Repository\RepositoryInterface; +use Composer\DependencyResolver\Request; +use Composer\Package\PackageInterface; + +/** + * The pre command run event. + * + * @author Jordi Boggiano + */ +class PrePoolCreateEvent extends Event +{ + /** + * @var RepositoryInterface[] + */ + private $repositories; + /** + * @var Request + */ + private $request; + /** + * @var array + */ + private $acceptableStabilities; + /** + * @var array + */ + private $stabilityFlags; + /** + * @var array + */ + private $rootAliases; + /** + * @var array + */ + private $rootReferences; + /** + * @var PackageInterface[] + */ + private $packages; + /** + * @var PackageInterface[] + */ + private $unacceptableFixedPackages; + + /** + * @param string $name The event name + * @param RepositoryInterface[] $repositories + */ + public function __construct($name, array $repositories, Request $request, array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $packages, array $unacceptableFixedPackages) + { + parent::__construct($name); + + $this->repositories = $repositories; + $this->request = $request; + $this->acceptableStabilities = $acceptableStabilities; + $this->stabilityFlags = $stabilityFlags; + $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; + $this->packages = $packages; + $this->unacceptableFixedPackages = $unacceptableFixedPackages; + } + + /** + * @return RepositoryInterface[] + */ + public function getRepositories() + { + return $this->repositories; + } + + /** + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return array + */ + public function getAcceptableStabilities() + { + return $this->acceptableStabilities; + } + + /** + * @return array + */ + public function getStabilityFlags() + { + return $this->stabilityFlags; + } + + /** + * @return array + */ + public function getRootAliases() + { + return $this->rootAliases; + } + + /** + * @return array + */ + public function getRootReferences() + { + return $this->rootReferences; + } + + /** + * @return PackageInterface[] + */ + public function getPackages() + { + return $this->packages; + } + + /** + * @return PackageInterface[] + */ + public function getUnacceptableFixedPackages() + { + return $this->unacceptableFixedPackages; + } + + /** + * @param PackageInterface[] $packages + */ + public function setPackages(array $packages) + { + $this->packages = $packages; + } + + /** + * @param PackageInterface[] $packages + */ + public function setUnacceptableFixedPackages(array $packages) + { + $this->unacceptableFixedPackages = $packages; + } +} diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index c11c7fe68..37a241cbc 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -16,6 +16,7 @@ use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Package\Version\StabilityFilter; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; @@ -24,12 +25,12 @@ use Composer\Semver\Constraint\Constraint; * * @author Nils Adermann */ -class ArrayRepository extends BaseRepository +class ArrayRepository implements RepositoryInterface { /** @var PackageInterface[] */ protected $packages; - - /** + + /** * @var PackageInterface[] indexed by package unique name and used to cache hasPackage calls */ protected $packageMap; @@ -41,6 +42,50 @@ class ArrayRepository extends BaseRepository } } + public function getRepoName() + { + return 'array repo (defining '.$this->count().' package'.($this->count() > 1 ? 's' : '').')'; + } + + /** + * {@inheritDoc} + */ + public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags) + { + $packages = $this->getPackages(); + + $result = array(); + $namesFound = array(); + foreach ($packages as $package) { + if (array_key_exists($package->getName(), $packageMap)) { + if ( + (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) + && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability()) + ) { + // add selected packages which match stability requirements + $result[spl_object_hash($package)] = $package; + // add the aliased package for packages where the alias matches + if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } + } + + $namesFound[$package->getName()] = true; + } + } + + // add aliases of packages that were selected, even if the aliases did not match + foreach ($packages as $package) { + if ($package instanceof AliasPackage) { + if (isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package)] = $package; + } + } + } + + return array('namesFound' => array_keys($namesFound), 'packages' => $result); + } + /** * {@inheritDoc} */ @@ -81,8 +126,7 @@ class ArrayRepository extends BaseRepository foreach ($this->getPackages() as $package) { if ($name === $package->getName()) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if (null === $constraint || $constraint->matches($pkgConstraint)) { + if (null === $constraint || $constraint->matches(new Constraint('==', $package->getVersion()))) { $packages[] = $package; } } @@ -160,6 +204,32 @@ class ArrayRepository extends BaseRepository $this->packageMap = null; } + /** + * {@inheritDoc} + */ + public function getProviders($packageName) + { + $result = array(); + + foreach ($this->getPackages() as $candidate) { + if (isset($result[$candidate->getName()])) { + continue; + } + foreach ($candidate->getProvides() as $link) { + if ($packageName === $link->getTarget()) { + $result[$candidate->getName()] = array( + 'name' => $candidate->getName(), + 'description' => $candidate->getDescription(), + 'type' => $candidate->getType(), + ); + continue 2; + } + } + } + + return $result; + } + protected function createAliasPackage(PackageInterface $package, $alias, $prettyAlias) { return new AliasPackage($package instanceof AliasPackage ? $package->getAliasOf() : $package, $alias, $prettyAlias); @@ -205,6 +275,10 @@ class ArrayRepository extends BaseRepository */ public function count() { + if (null === $this->packages) { + $this->initialize(); + } + return count($this->packages); } diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index aff80e4cd..a0acb7a61 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -43,6 +43,11 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $this->repoConfig = $repoConfig; } + public function getRepoName() + { + return 'artifact repo ('.$this->lookup.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 649c2f115..e705e6852 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -16,54 +16,74 @@ use Composer\Package\Loader\ArrayLoader; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; -use Composer\DependencyResolver\Pool; +use Composer\Package\Version\StabilityFilter; use Composer\Json\JsonFile; use Composer\Cache; use Composer\Config; use Composer\Composer; use Composer\Factory; use Composer\IO\IOInterface; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; +use Composer\Util\Loop; use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Downloader\TransportException; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Util\Http\Response; +use Composer\Util\MetadataMinifier; +use Composer\Util\Url; +use React\Promise\Promise; /** * @author Jordi Boggiano */ class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface { - protected $config; - protected $repoConfig; - protected $options; - protected $url; - protected $baseUrl; - protected $io; - protected $rfs; + private $config; + private $repoConfig; + private $options; + private $url; + private $baseUrl; + private $io; + private $httpDownloader; + private $loop; protected $cache; protected $notifyUrl; protected $searchUrl; + /** @var string|null a URL containing %package% which can be queried to get providers of a given name */ + protected $providersApiUrl; protected $hasProviders = false; protected $providersUrl; + protected $availablePackages; protected $lazyProvidersUrl; protected $providerListing; - protected $providers = array(); - protected $providersByUid = array(); protected $loader; - protected $rootAliases; - protected $allowSslDowngrade = false; - protected $eventDispatcher; - protected $sourceMirrors; - protected $distMirrors; + private $allowSslDowngrade = false; + private $eventDispatcher; + private $sourceMirrors; + private $distMirrors; private $degradedMode = false; private $rootData; private $hasPartialPackages; private $partialPackagesByName; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private + * @var array list of package names which returned a 404 and should not be re-fetched in case loadPackage is called several times + * useful for v2 metadata repositories with lazy providers + */ + public $packagesNotFoundCache = array(); + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private + */ + public $versionParser; + + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { parent::__construct(); if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) { @@ -99,15 +119,18 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->baseUrl = rtrim(preg_replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/'); $this->io = $io; - $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); - $this->loader = new ArrayLoader(); - if ($rfs && $this->options) { - $rfs = clone $rfs; - $rfs->setOptions($this->options); - } - $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $this->config, $this->options); + $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$~'); + $this->versionParser = new VersionParser(); + $this->loader = new ArrayLoader($this->versionParser); + $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; $this->repoConfig = $repoConfig; + $this->loop = new Loop($this->httpDownloader); + } + + public function getRepoName() + { + return 'composer repo ('.Url::sanitize($this->url).')'; } public function getRepoConfig() @@ -115,40 +138,44 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $this->repoConfig; } - public function setRootAliases(array $rootAliases) - { - $this->rootAliases = $rootAliases; - } - /** * {@inheritDoc} */ public function findPackage($name, $constraint) { - if (!$this->hasProviders()) { - return parent::findPackage($name, $constraint); - } + // this call initializes loadRootServerFile which is needed for the rest below to work + $hasProviders = $this->hasProviders(); $name = strtolower($name); if (!$constraint instanceof ConstraintInterface) { - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($constraint); + $constraint = $this->versionParser->parseConstraints($constraint); } - foreach ($this->getProviderNames() as $providerName) { - if ($name === $providerName) { - $packages = $this->whatProvides(new Pool('dev'), $providerName); - foreach ($packages as $package) { - if ($name === $package->getName()) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if ($constraint->matches($pkgConstraint)) { - return $package; - } - } - } - break; + if ($this->lazyProvidersUrl) { + if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { + return $this->filterPackages($this->whatProvides($name), $constraint, true); } + + if (is_array($this->availablePackages) && !isset($this->availablePackages[$name])) { + return; + } + + $packages = $this->loadAsyncPackages(array($name => $constraint)); + + return reset($packages['packages']); } + + if ($hasProviders) { + foreach ($this->getProviderNames() as $providerName) { + if ($name === $providerName) { + return $this->filterPackages($this->whatProvides($providerName), $constraint, true); + } + } + + return; + } + + return parent::findPackage($name, $constraint); } /** @@ -156,46 +183,199 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito */ public function findPackages($name, $constraint = null) { - if (!$this->hasProviders()) { - return parent::findPackages($name, $constraint); - } - // normalize name + // this call initializes loadRootServerFile which is needed for the rest below to work + $hasProviders = $this->hasProviders(); + $name = strtolower($name); - if (null !== $constraint && !$constraint instanceof ConstraintInterface) { - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($constraint); + $constraint = $this->versionParser->parseConstraints($constraint); } - $packages = array(); + if ($this->lazyProvidersUrl) { + if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { + return $this->filterPackages($this->whatProvides($name), $constraint); + } - foreach ($this->getProviderNames() as $providerName) { - if ($name === $providerName) { - $candidates = $this->whatProvides(new Pool('dev'), $providerName); - foreach ($candidates as $package) { - if ($name === $package->getName()) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if (null === $constraint || $constraint->matches($pkgConstraint)) { - $packages[] = $package; - } - } + if (is_array($this->availablePackages) && !isset($this->availablePackages[$name])) { + return array(); + } + + $result = $this->loadAsyncPackages(array($name => $constraint)); + + return $result['packages']; + } + + if ($hasProviders) { + foreach ($this->getProviderNames() as $providerName) { + if ($name === $providerName) { + return $this->filterPackages($this->whatProvides($providerName), $constraint); } - break; + } + + return array(); + } + + return parent::findPackages($name, $constraint); + } + + private function filterPackages(array $packages, $constraint = null, $returnFirstMatch = false) + { + if (null === $constraint) { + if ($returnFirstMatch) { + return reset($packages); + } + + return $packages; + } + + $filteredPackages = array(); + + foreach ($packages as $package) { + $pkgConstraint = new Constraint('==', $package->getVersion()); + + if ($constraint->matches($pkgConstraint)) { + if ($returnFirstMatch) { + return $package; + } + + $filteredPackages[] = $package; } } - return $packages; + if ($returnFirstMatch) { + return null; + } + + return $filteredPackages; } public function getPackages() { - if ($this->hasProviders()) { - throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getProviderNames instead.'); + $hasProviders = $this->hasProviders(); + + if ($this->lazyProvidersUrl) { + if (is_array($this->availablePackages)) { + $packageMap = array(); + foreach ($this->availablePackages as $name) { + $packageMap[$name] = new EmptyConstraint(); + } + + $result = $this->loadAsyncPackages($packageMap); + + return array_values($result['packages']); + } + + if ($this->hasPartialPackages()) { + return array_values($this->partialPackagesByName); + } + + throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getPackageNames instead.'); + } + + if ($hasProviders) { + throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getPackageNames instead.'); } return parent::getPackages(); } + public function getPackageNames() + { + // TODO add getPackageNames to the RepositoryInterface perhaps? With filtering capability embedded? + $hasProviders = $this->hasProviders(); + + if ($this->lazyProvidersUrl) { + if (is_array($this->availablePackages)) { + return array_keys($this->availablePackages); + } + + // TODO implement new list API endpoint for those repos somehow? + + if ($this->hasPartialPackages()) { + return array_keys($this->partialPackagesByName); + } + + return array(); + } + + if ($hasProviders) { + return $this->getProviderNames(); + } + + $names = array(); + foreach ($this->getPackages() as $package) { + $names[] = $package->getPrettyName(); + } + + return $names; + } + + public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags) + { + // this call initializes loadRootServerFile which is needed for the rest below to work + $hasProviders = $this->hasProviders(); + + if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) { + return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags); + } + + $packages = array(); + $namesFound = array(); + + if ($hasProviders || $this->hasPartialPackages()) { + foreach ($packageNameMap as $name => $constraint) { + $matches = array(); + + // if a repo has no providers but only partial packages and the partial packages are missing + // then we don't want to call whatProvides as it would try to load from the providers and fail + if (!$hasProviders && !isset($this->partialPackagesByName[$name])) { + continue; + } + + $candidates = $this->whatProvides($name, $acceptableStabilities, $stabilityFlags); + foreach ($candidates as $candidate) { + if ($candidate->getName() !== $name) { + throw new \LogicException('whatProvides should never return a package with a different name than the requested one'); + } + $namesFound[$name] = true; + if (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { + $matches[spl_object_hash($candidate)] = $candidate; + if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { + $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); + } + } + } + + // add aliases of matched packages even if they did not match the constraint + foreach ($candidates as $candidate) { + if ($candidate instanceof AliasPackage) { + if (isset($matches[spl_object_hash($candidate->getAliasOf())])) { + $matches[spl_object_hash($candidate)] = $candidate; + } + } + } + $packages = array_merge($packages, $matches); + + unset($packageNameMap[$name]); + } + } + + if ($this->lazyProvidersUrl && count($packageNameMap)) { + if (is_array($this->availablePackages)) { + $availPackages = $this->availablePackages; + $packageNameMap = array_filter($packageNameMap, function ($name) use ($availPackages) { + return isset($availPackages[strtolower($name)]); + }, ARRAY_FILTER_USE_KEY); + } + + $result = $this->loadAsyncPackages($packageNameMap, $acceptableStabilities, $stabilityFlags); + $packages = array_merge($packages, $result['packages']); + $namesFound = array_merge($namesFound, $result['namesFound']); + } + + return array('namesFound' => array_keys($namesFound), 'packages' => $packages); + } + /** * {@inheritDoc} */ @@ -206,9 +386,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { $url = str_replace(array('%query%', '%type%'), array($query, $type), $this->searchUrl); - $origin = RemoteFilesystem::getOrigin($url); - $json = $this->rfs->getContents($origin, $url, false); - $search = JsonFile::parseJson($json, $url); + $search = $this->httpDownloader->get($url, $this->options)->decodeJson(); if (empty($search['results'])) { return array(); @@ -225,11 +403,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $results; } - if ($this->hasProviders()) { + if ($this->hasProviders() || $this->lazyProvidersUrl) { $results = array(); $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i'; - foreach ($this->getProviderNames() as $name) { + foreach ($this->getPackageNames() as $name) { if (preg_match($regex, $name)) { $results[] = array('name' => $name); } @@ -241,7 +419,44 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return parent::search($query, $mode); } - public function getProviderNames() + public function getProviders($packageName) + { + $this->loadRootServerFile(); + $result = array(); + + if ($this->providersApiUrl) { + $apiResult = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); + + foreach ($apiResult['providers'] as $provider) { + $result[$provider['name']] = $provider; + } + + return $result; + } + + if ($this->hasPartialPackages()) { + foreach ($this->partialPackagesByName as $versions) { + foreach ($versions as $candidate) { + if (isset($result[$candidate['name']]) || !isset($candidate['provide'][$packageName])) { + continue; + } + $result[$candidate['name']] = array( + 'name' => $candidate['name'], + 'description' => isset($candidate['description']) ? $candidate['description'] : '', + 'type' => isset($candidate['type']) ? $candidate['type'] : '', + ); + } + } + } + + if ($this->packages) { + $result = array_merge($result, parent::getProviders($packageName)); + } + + return $result; + } + + private function getProviderNames() { $this->loadRootServerFile(); @@ -249,14 +464,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loadProviderListings($this->loadRootServerFile()); } - if ($this->hasPartialPackages) { - if (null === $this->partialPackagesByName) { - $this->initializePartialPackages(); - } - - return array_keys($this->partialPackagesByName); - } - if ($this->lazyProvidersUrl) { // Can not determine list of provided packages for lazy repositories return array(); @@ -269,7 +476,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return array(); } - protected function configurePackageTransportOptions(PackageInterface $package) + private function configurePackageTransportOptions(PackageInterface $package) { foreach ($package->getDistUrls() as $url) { if (strpos($url, $this->baseUrl) === 0) { @@ -280,40 +487,21 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } - public function hasProviders() + private function hasProviders() { $this->loadRootServerFile(); return $this->hasProviders; } - public function resetPackageIds() - { - foreach ($this->providersByUid as $package) { - if ($package instanceof AliasPackage) { - $package->getAliasOf()->setId(-1); - } - $package->setId(-1); - } - } - /** - * @param Pool $pool - * @param string $name package name - * @param bool $bypassFilters If set to true, this bypasses the stability filtering, and forces a recompute without cache + * @param string $name package name + * @param callable $isPackageAcceptableCallable * @return array|mixed */ - public function whatProvides(Pool $pool, $name, $bypassFilters = false) + private function whatProvides($name, array $acceptableStabilities = null, array $stabilityFlags = null) { - if (isset($this->providers[$name]) && !$bypassFilters) { - return $this->providers[$name]; - } - - if ($this->hasPartialPackages && null === $this->partialPackagesByName) { - $this->initializePartialPackages(); - } - - if (!$this->hasPartialPackages || !isset($this->partialPackagesByName[$name])) { + if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) { // skip platform packages, root package and composer-plugin-api if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { return array(); @@ -380,81 +568,53 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $loadingPartialPackage = true; } - $this->providers[$name] = array(); + $result = array(); + $versionsToLoad = array(); foreach ($packages['packages'] as $versions) { foreach ($versions as $version) { - if (!$loadingPartialPackage && $this->hasPartialPackages && isset($this->partialPackagesByName[$version['name']])) { + $normalizedName = strtolower($version['name']); + + // only load the actual named package, not other packages that might find themselves in the same file + if ($normalizedName !== $name) { continue; } - // avoid loading the same objects twice - if (isset($this->providersByUid[$version['uid']])) { - // skip if already assigned - if (!isset($this->providers[$name][$version['uid']])) { - // expand alias in two packages - if ($this->providersByUid[$version['uid']] instanceof AliasPackage) { - $this->providers[$name][$version['uid']] = $this->providersByUid[$version['uid']]->getAliasOf(); - $this->providers[$name][$version['uid'].'-alias'] = $this->providersByUid[$version['uid']]; - } else { - $this->providers[$name][$version['uid']] = $this->providersByUid[$version['uid']]; - } - // check for root aliases - if (isset($this->providersByUid[$version['uid'].'-root'])) { - $this->providers[$name][$version['uid'].'-root'] = $this->providersByUid[$version['uid'].'-root']; - } - } - } else { - if (!$bypassFilters && !$pool->isPackageAcceptable(strtolower($version['name']), VersionParser::parseStability($version['version']))) { - continue; + if (!$loadingPartialPackage && $this->hasPartialPackages() && isset($this->partialPackagesByName[$normalizedName])) { + continue; + } + + if (!isset($versionsToLoad[$version['uid']])) { + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $this->versionParser->normalize($version['version']); + } elseif ($version['version_normalized'] === '9999999-dev') { + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it + $version['version_normalized'] = $this->versionParser->normalize($version['version']); } - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); - $package->setRepository($this); - - if ($package instanceof AliasPackage) { - $aliased = $package->getAliasOf(); - $aliased->setRepository($this); - - $this->providers[$name][$version['uid']] = $aliased; - $this->providers[$name][$version['uid'].'-alias'] = $package; - - // override provider with its alias so it can be expanded in the if block above - $this->providersByUid[$version['uid']] = $package; - } else { - $this->providers[$name][$version['uid']] = $package; - $this->providersByUid[$version['uid']] = $package; - } - - // handle root package aliases - unset($rootAliasData); - - if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; - } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; - } - - if (isset($rootAliasData)) { - $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); - $alias->setRepository($this); - - $this->providers[$name][$version['uid'].'-root'] = $alias; - $this->providersByUid[$version['uid'].'-root'] = $alias; + if ($this->isVersionAcceptable($acceptableStabilities, $stabilityFlags, null, $normalizedName, $version)) { + $versionsToLoad[$version['uid']] = $version; } } } } - $result = $this->providers[$name]; + // load acceptable packages in the providers + $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + $uids = array_keys($versionsToLoad); - // clean up the cache because otherwise using this puts the repo in an inconsistent state with a polluted unfiltered cache - // which is likely not an issue but might cause hard to track behaviors depending on how the repo is used - if ($bypassFilters) { - foreach ($this->providers[$name] as $uid => $provider) { - unset($this->providersByUid[$uid]); + foreach ($loadedPackages as $index => $package) { + $package->setRepository($this); + $uid = $uids[$index]; + + if ($package instanceof AliasPackage) { + $aliased = $package->getAliasOf(); + $aliased->setRepository($this); + + $result[$uid] = $aliased; + $result[$uid.'-alias'] = $package; + } else { + $result[$uid] = $package; } - unset($this->providers[$name]); } return $result; @@ -469,8 +629,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $repoData = $this->loadDataFromServer(); - foreach ($repoData as $package) { - $this->addPackage($this->createPackage($package, 'Composer\Package\CompletePackage')); + foreach ($this->createPackages($repoData, 'Composer\Package\CompletePackage') as $package) { + $this->addPackage($package); } } @@ -485,6 +645,126 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->configurePackageTransportOptions($package); } + /** + * @param array $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded + */ + private function loadAsyncPackages(array $packageNames, array $acceptableStabilities = null, array $stabilityFlags = null) + { + $this->loadRootServerFile(); + + $packages = array(); + $namesFound = array(); + $promises = array(); + $repo = $this; + + if (!$this->lazyProvidersUrl) { + throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url'); + } + + // load ~dev versions of the packages as well if needed + foreach ($packageNames as $name => $constraint) { + if ($acceptableStabilities && $stabilityFlags && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), 'dev')) { + $packageNames[$name.'~dev'] = $constraint; + } + } + + foreach ($packageNames as $name => $constraint) { + $name = strtolower($name); + + $realName = preg_replace('{~dev$}', '', $name); + // skip platform packages, root package and composer-plugin-api + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $realName) || '__root__' === $realName || 'composer-plugin-api' === $realName) { + continue; + } + + $url = str_replace('%package%', $name, $this->lazyProvidersUrl); + $cacheKey = 'provider-'.strtr($name, '/', '~').'.json'; + + $lastModified = null; + if ($contents = $this->cache->read($cacheKey)) { + $contents = json_decode($contents, true); + $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null; + } + + $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) + ->then(function ($response) use (&$packages, &$namesFound, $contents, $realName, $constraint, $repo, $acceptableStabilities, $stabilityFlags) { + if (true === $response) { + $response = $contents; + } + + if (!isset($response['packages'][$realName])) { + return; + } + + $versions = $response['packages'][$realName]; + + if (isset($response['minified']) && $response['minified'] === 'composer/2.0') { + $versions = MetadataMinifier::expand($versions); + } + + $namesFound[$realName] = true; + $versionsToLoad = array(); + foreach ($versions as $version) { + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } elseif ($version['version_normalized'] === '9999999-dev') { + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } + + if ($repo->isVersionAcceptable($acceptableStabilities, $stabilityFlags, $constraint, $realName, $version)) { + $versionsToLoad[] = $version; + } + } + + $loadedPackages = $repo->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + foreach ($loadedPackages as $package) { + $package->setRepository($repo); + $packages[spl_object_hash($package)] = $package; + + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $package->getAliasOf()->setRepository($repo); + $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } + } + }); + } + + $this->loop->wait($promises); + + return array('namesFound' => $namesFound, 'packages' => $packages); + // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed + } + + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * + * @param string $name package name (must be lowercased already) + * @private + */ + public function isVersionAcceptable(array $acceptableStabilities = null, array $stabilityFlags = null, $constraint = null, $name, $versionData) + { + $versions = array($versionData['version_normalized']); + + if ($alias = $this->loader->getBranchAlias($versionData)) { + $versions[] = $alias; + } + + foreach ($versions as $version) { + if (null !== $acceptableStabilities && null !== $stabilityFlags && !StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), VersionParser::parseStability($version))) { + continue; + } + + if ($constraint && !$constraint->matches(new Constraint('==', $version))) { + continue; + } + + return true; + } + + return false; + } + protected function loadRootServerFile() { if (null !== $this->rootData) { @@ -539,6 +819,29 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); } + // metadata-url indiates V2 repo protocol so it takes over from all the V1 types + // V2 only has lazyProviders and possibly partial packages, but no ability to process anything else, + // V2 also supports async loading + if (!empty($data['metadata-url'])) { + $this->lazyProvidersUrl = $this->canonicalizeUrl($data['metadata-url']); + $this->providersUrl = null; + $this->hasProviders = false; + $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); + $this->allowSslDowngrade = false; + + // provides a list of package names that are available in this repo + // this disables lazy-provider behavior in the sense that if a list is available we assume it is finite and won't search for other packages in that repo + // while if no list is there lazyProvidersUrl is used when looking for any package name to see if the repo knows it + if (!empty($data['available-packages'])) { + $availPackages = array_map('strtolower', $data['available-packages']); + $this->availablePackages = array_combine($availPackages, $availPackages); + } + + // Remove legacy keys as most repos need to be compatible with Composer v1 + // as well but we are not interested in the old format anymore at this point + unset($data['providers-url'], $data['providers'], $data['providers-includes']); + } + if ($this->allowSslDowngrade) { $this->url = str_replace('https://', 'http://', $this->url); $this->baseUrl = str_replace('https://', 'http://', $this->baseUrl); @@ -553,21 +856,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasProviders = true; } - // force values for packagist - if (preg_match('{^https?://repo\.packagist\.org/?$}i', $this->url) && !empty($this->repoConfig['force-lazy-providers'])) { - $this->url = 'https://repo.packagist.org'; - $this->baseUrl = 'https://repo.packagist.org'; - $this->lazyProvidersUrl = $this->canonicalizeUrl('https://repo.packagist.org/p/%package%.json'); - $this->providersUrl = null; - } elseif (!empty($this->repoConfig['force-lazy-providers'])) { - $this->lazyProvidersUrl = $this->canonicalizeUrl('/p/%package%.json'); - $this->providersUrl = null; + if (!empty($data['providers-api'])) { + $this->providersApiUrl = $data['providers-api']; } return $this->rootData = $data; } - protected function canonicalizeUrl($url) + private function canonicalizeUrl($url) { if ('/' === $url[0]) { if (preg_match('{^[^:]++://[^/]*+}', $this->url, $matches)) { @@ -580,14 +876,23 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $url; } - protected function loadDataFromServer() + private function loadDataFromServer() { $data = $this->loadRootServerFile(); return $this->loadIncludes($data); } - protected function loadProviderListings($data) + private function hasPartialPackages() + { + if ($this->hasPartialPackages && null === $this->partialPackagesByName) { + $this->initializePartialPackages(); + } + + return $this->hasPartialPackages; + } + + private function loadProviderListings($data) { if (isset($data['providers'])) { if (!is_array($this->providerListing)) { @@ -612,7 +917,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } - protected function loadIncludes($data) + private function loadIncludes($data) { $packages = array(); @@ -649,23 +954,37 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $packages; } - protected function createPackage(array $data, $class = 'Composer\Package\CompletePackage') + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * + * @private + */ + public function createPackages(array $packages, $class = 'Composer\Package\CompletePackage') { + if (!$packages) { + return array(); + } + try { - if (!isset($data['notification-url'])) { - $data['notification-url'] = $this->notifyUrl; + foreach ($packages as &$data) { + if (!isset($data['notification-url'])) { + $data['notification-url'] = $this->notifyUrl; + } } - $package = $this->loader->load($data, $class); - if (isset($this->sourceMirrors[$package->getSourceType()])) { - $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); - } - $package->setDistMirrors($this->distMirrors); - $this->configurePackageTransportOptions($package); + $packages = $this->loader->loadPackages($packages, $class); - return $package; + foreach ($packages as $package) { + if (isset($this->sourceMirrors[$package->getSourceType()])) { + $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); + } + $package->setDistMirrors($this->distMirrors); + $this->configurePackageTransportOptions($package); + } + + return $packages; } catch (\Exception $e) { - throw new \RuntimeException('Could not load package '.(isset($data['name']) ? $data['name'] : json_encode($data)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); + throw new \RuntimeException('Could not load packages '.(isset($packages[0]['name']) ? $packages[0]['name'] : json_encode($packages)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); } } @@ -684,15 +1003,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $filename); if ($this->eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); } - $origin = RemoteFilesystem::getOrigin($filename); - $rfs = $preFileDownloadEvent->getRemoteFilesystem(); - - $json = $rfs->getContents($origin, $filename, false); + $response = $this->httpDownloader->get($filename, $this->options); + $json = $response->getBody(); if ($sha256 && $sha256 !== hash('sha256', $json)) { // undo downgrade before trying again if http seems to be hijacked or modifying content somehow if ($this->allowSslDowngrade) { @@ -711,12 +1028,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito throw new RepositorySecurityException('The contents of '.$filename.' do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.'); } - $data = JsonFile::parseJson($json, $filename); - RemoteFilesystem::outputWarnings($this->io, $this->url, $data); + $data = $response->decodeJson(); + HttpDownloader::outputWarnings($this->io, $this->url, $data); if ($cacheKey) { if ($storeLastModifiedTime) { - $lastModifiedDate = $rfs->findHeaderValue($rfs->getLastHeaders(), 'last-modified'); + $lastModifiedDate = $response->getHeader('last-modified'); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = json_encode($data); @@ -725,8 +1042,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->cache->write($cacheKey, $json); } + $response->collect(); + break; } catch (\Exception $e) { + if ($e instanceof \LogicException) { + throw $e; + } + if ($e instanceof TransportException && $e->getStatusCode() === 404) { throw $e; } @@ -758,28 +1081,32 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; } - protected function fetchFileIfLastModified($filename, $cacheKey, $lastModifiedTime) + private function fetchFileIfLastModified($filename, $cacheKey, $lastModifiedTime) { $retries = 3; while ($retries--) { try { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $filename); if ($this->eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); } - $origin = RemoteFilesystem::getOrigin($filename); - $rfs = $preFileDownloadEvent->getRemoteFilesystem(); - $options = array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))); - $json = $rfs->getContents($origin, $filename, false, $options); - if ($json === '' && $rfs->findStatusCode($rfs->getLastHeaders()) === 304) { + $options = $this->options; + if (isset($options['http']['header'])) { + $options['http']['header'] = (array) $options['http']['header']; + } + $options['http']['header'][] = array('If-Modified-Since: '.$lastModifiedTime); + $response = $this->httpDownloader->get($filename, $options); + $json = $response->getBody(); + if ($json === '' && $response->getStatusCode() === 304) { return true; } - $data = JsonFile::parseJson($json, $filename); - RemoteFilesystem::outputWarnings($this->io, $this->url, $data); + $data = $response->decodeJson(); + HttpDownloader::outputWarnings($this->io, $this->url, $data); - $lastModifiedDate = $rfs->findHeaderValue($rfs->getLastHeaders(), 'last-modified'); + $lastModifiedDate = $response->getHeader('last-modified'); + $response->collect(); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = json_encode($data); @@ -788,6 +1115,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; } catch (\Exception $e) { + if ($e instanceof \LogicException) { + throw $e; + } + if ($e instanceof TransportException && $e->getStatusCode() === 404) { throw $e; } @@ -808,6 +1139,88 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } + private function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) + { + $retries = 3; + + if (isset($this->packagesNotFoundCache[$filename])) { + return new Promise(function ($resolve, $reject) { $resolve(array('packages' => array())); }); + } + + $httpDownloader = $this->httpDownloader; + if ($this->eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + } + + $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); + + $io = $this->io; + $url = $this->url; + $cache = $this->cache; + $degradedMode =& $this->degradedMode; + $repo = $this; + + $accept = function ($response) use ($io, $url, $filename, $cache, $cacheKey, $repo) { + // package not found is acceptable for a v2 protocol repository + if ($response->getStatusCode() === 404) { + $repo->packagesNotFoundCache[$filename] = true; + return array('packages' => array()); + } + + $json = $response->getBody(); + if ($json === '' && $response->getStatusCode() === 304) { + return true; + } + + $data = $response->decodeJson(); + HttpDownloader::outputWarnings($io, $url, $data); + + $lastModifiedDate = $response->getHeader('last-modified'); + $response->collect(); + if ($lastModifiedDate) { + $data['last-modified'] = $lastModifiedDate; + $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE); + } + $cache->write($cacheKey, $json); + + return $data; + }; + + $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode, $repo) { + if ($e instanceof TransportException && $e->getStatusCode() === 404) { + $repo->packagesNotFoundCache[$filename] = true; + return false; + } + + // special error code returned when network is being artificially disabled + if ($e instanceof TransportException && $e->getStatusCode() === 499) { + $retries = 0; + } + + if (--$retries > 0) { + usleep(100000); + + return $httpDownloader->add($filename, $options)->then($accept, $reject); + } + + if (!$degradedMode) { + $io->writeError(''.$e->getMessage().''); + $io->writeError(''.$url.' could not be fully loaded, package information was loaded from the local cache and may be out of date'); + } + $degradedMode = true; + + // special error code returned when network is being artificially disabled + if ($e instanceof TransportException && $e->getStatusCode() === 499) { + return $accept(new Response(array('url' => $url), 404, array(), '')); + } + + throw $e; + }; + + return $httpDownloader->add($filename, $options)->then($accept, $reject); + } + /** * This initializes the packages key of a partial packages.json that contain some packages inlined + a providers-lazy-url * @@ -819,19 +1232,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->partialPackagesByName = array(); foreach ($rootData['packages'] as $package => $versions) { - $package = strtolower($package); foreach ($versions as $version) { - $this->partialPackagesByName[$package][] = $version; - if (!empty($version['provide']) && is_array($version['provide'])) { - foreach ($version['provide'] as $provided => $providedVersion) { - $this->partialPackagesByName[strtolower($provided)][] = $version; - } - } - if (!empty($version['replace']) && is_array($version['replace'])) { - foreach ($version['replace'] as $provided => $providedVersion) { - $this->partialPackagesByName[strtolower($provided)][] = $version; - } - } + $this->partialPackagesByName[strtolower($version['name'])][] = $version; } } diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index ce57504f0..acabaffb0 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -19,7 +19,7 @@ use Composer\Package\PackageInterface; * * @author Beau Simensen */ -class CompositeRepository extends BaseRepository +class CompositeRepository implements RepositoryInterface { /** * List of repositories @@ -39,6 +39,11 @@ class CompositeRepository extends BaseRepository } } + public function getRepoName() + { + return 'composite repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')'; + } + /** * Returns all the wrapped repositories * @@ -94,6 +99,26 @@ class CompositeRepository extends BaseRepository return $packages ? call_user_func_array('array_merge', $packages) : array(); } + /** + * {@inheritDoc} + */ + public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags) + { + $packages = array(); + $namesFound = array(); + foreach ($this->repositories as $repository) { + /* @var $repository RepositoryInterface */ + $result = $repository->loadPackages($packageMap, $acceptableStabilities, $stabilityFlags); + $packages[] = $result['packages']; + $namesFound[] = $result['namesFound']; + } + + return array( + 'packages' => $packages ? call_user_func_array('array_merge', $packages) : array(), + 'namesFound' => $namesFound ? array_unique(call_user_func_array('array_merge', $namesFound)) : array(), + ); + } + /** * {@inheritdoc} */ @@ -122,6 +147,20 @@ class CompositeRepository extends BaseRepository return $packages ? call_user_func_array('array_merge', $packages) : array(); } + /** + * {@inheritdoc} + */ + public function getProviders($packageName) + { + $results = array(); + foreach ($this->repositories as $repository) { + /* @var $repository RepositoryInterface */ + $results[] = $repository->getProviders($packageName); + } + + return $results ? call_user_func_array('array_merge', $results) : array(); + } + /** * {@inheritdoc} */ diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 204aa095d..3bebfbdb3 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -15,6 +15,8 @@ namespace Composer\Repository; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Installer\InstallationManager; +use Composer\Util\Filesystem; /** * Filesystem repository. @@ -49,7 +51,12 @@ class FilesystemRepository extends WritableArrayRepository } try { - $packages = $this->file->read(); + $data = $this->file->read(); + if (isset($data['packages'])) { + $packages = $data['packages']; + } else { + $packages = $data; + } // forward compatibility for composer v2 installed.json if (isset($packages['packages'])) { @@ -79,16 +86,21 @@ class FilesystemRepository extends WritableArrayRepository /** * Writes writable repository. */ - public function write() + public function write($devMode, InstallationManager $installationManager) { - $data = array(); + $data = array('packages' => array(), 'dev' => $devMode); $dumper = new ArrayDumper(); + $fs = new Filesystem(); + $repoDir = dirname($fs->normalizePath($this->file->getPath())); foreach ($this->getCanonicalPackages() as $package) { - $data[] = $dumper->dump($package); + $pkgArray = $dumper->dump($package); + $path = $installationManager->getInstallPath($package); + $pkgArray['install-path'] = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $fs->isAbsolutePath($path) ? $path : getcwd() . '/' . $path, true) : null; + $data['packages'][] = $pkgArray; } - usort($data, function ($a, $b) { + usort($data['packages'], function ($a, $b) { return strcmp($a['name'], $b['name']); }); diff --git a/src/Composer/Repository/InstalledArrayRepository.php b/src/Composer/Repository/InstalledArrayRepository.php index c801d49ea..de1dd67d8 100644 --- a/src/Composer/Repository/InstalledArrayRepository.php +++ b/src/Composer/Repository/InstalledArrayRepository.php @@ -15,10 +15,14 @@ namespace Composer\Repository; /** * Installed array repository. * - * This is used for serving the RootPackage inside an in-memory InstalledRepository + * This is used as an in-memory InstalledRepository mostly for testing purposes * * @author Jordi Boggiano */ class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface { + public function getRepoName() + { + return 'installed '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/InstalledFilesystemRepository.php b/src/Composer/Repository/InstalledFilesystemRepository.php index 1ff8a0a06..bf81734d4 100644 --- a/src/Composer/Repository/InstalledFilesystemRepository.php +++ b/src/Composer/Repository/InstalledFilesystemRepository.php @@ -19,4 +19,8 @@ namespace Composer\Repository; */ class InstalledFilesystemRepository extends FilesystemRepository implements InstalledRepositoryInterface { + public function getRepoName() + { + return 'installed '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/InstalledRepository.php similarity index 77% rename from src/Composer/Repository/BaseRepository.php rename to src/Composer/Repository/InstalledRepository.php index d668f43cb..49640fb90 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -12,18 +12,59 @@ namespace Composer\Repository; -use Composer\Package\RootPackageInterface; +use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; +use Composer\Package\AliasPackage; +use Composer\Package\RootPackageInterface; use Composer\Package\Link; + /** - * Common ancestor class for generic repository functionality. + * Installed repository is a composite of all installed repo types. * - * @author Niels Keurentjes + * The main use case is tagging a repo as an "installed" repository, and offering a way to get providers/replacers easily. + * + * Installed repos are LockArrayRepository, InstalledRepositoryInterface, RootPackageRepository and PlatformRepository + * + * @author Jordi Boggiano */ -abstract class BaseRepository implements RepositoryInterface +class InstalledRepository extends CompositeRepository { + public function findPackagesWithReplacersAndProviders($name, $constraint = null) + { + $name = strtolower($name); + + if (null !== $constraint && !$constraint instanceof ConstraintInterface) { + $versionParser = new VersionParser(); + $constraint = $versionParser->parseConstraints($constraint); + } + + $matches = array(); + foreach ($this->getRepositories() as $repo) { + foreach ($repo->getPackages() as $candidate) { + if ($name === $candidate->getName()) { + if (null === $constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { + $matches[] = $candidate; + } + continue; + } + + foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + if ( + $name === $link->getTarget() + && ($constraint === null || $link->getConstraint() === null || $constraint->matches($link->getConstraint())) + ) { + $matches[] = $candidate; + continue 2; + } + } + } + } + + return $matches; + } + /** * Returns a list of links causing the requested needle packages to be installed, as an associative array with the * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship @@ -175,4 +216,27 @@ abstract class BaseRepository implements RepositoryInterface return $results; } + + public function getRepoName() + { + return 'installed repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->getRepositories())).')'; + } + + /** + * Add a repository. + * @param RepositoryInterface $repository + */ + public function addRepository(RepositoryInterface $repository) + { + if ( + $repository instanceof LockArrayRepository + || $repository instanceof InstalledRepositoryInterface + || $repository instanceof RootPackageRepository + || $repository instanceof PlatformRepository + ) { + return parent::addRepository($repository); + } + + throw new \LogicException('An InstalledRepository can not contain a repository of type '.get_class($repository).' ('.$repository->getRepoName().')'); + } } diff --git a/src/Composer/Repository/LockArrayRepository.php b/src/Composer/Repository/LockArrayRepository.php new file mode 100644 index 000000000..b485e79ec --- /dev/null +++ b/src/Composer/Repository/LockArrayRepository.php @@ -0,0 +1,29 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +/** + * Lock array repository. + * + * Regular array repository, only uses a different type to identify the lock file as the source of info + * + * @author Nils Adermann + */ +class LockArrayRepository extends ArrayRepository +{ + public function getRepoName() + { + return 'lock repo'; + } +} + diff --git a/src/Composer/Repository/PackageRepository.php b/src/Composer/Repository/PackageRepository.php index 52b9a0f6b..de6d31d4d 100644 --- a/src/Composer/Repository/PackageRepository.php +++ b/src/Composer/Repository/PackageRepository.php @@ -58,4 +58,9 @@ class PackageRepository extends ArrayRepository $this->addPackage($package); } } + + public function getRepoName() + { + return preg_replace('{^array }', 'package ', parent::getRepoName()); + } } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 4e860658f..27803455f 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\Url; /** * This repository allows installing local packages that are not necessarily under their own VCS. @@ -116,6 +117,11 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn parent::__construct(); } + public function getRepoName() + { + return 'path repo ('.Url::sanitize($this->repoConfig['url']).')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/Pear/BaseChannelReader.php b/src/Composer/Repository/Pear/BaseChannelReader.php index 9b26eb9db..9b9acf2f2 100644 --- a/src/Composer/Repository/Pear/BaseChannelReader.php +++ b/src/Composer/Repository/Pear/BaseChannelReader.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * Base PEAR Channel reader. @@ -33,12 +33,12 @@ abstract class BaseChannelReader const ALL_RELEASES_NS = 'http://pear.php.net/dtd/rest.allreleases'; const PACKAGE_INFO_NS = 'http://pear.php.net/dtd/rest.package'; - /** @var RemoteFilesystem */ - private $rfs; + /** @var HttpDownloader */ + private $httpDownloader; - protected function __construct(RemoteFilesystem $rfs) + protected function __construct(HttpDownloader $httpDownloader) { - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; } /** @@ -47,12 +47,16 @@ abstract class BaseChannelReader * @param string $origin server * @param string $path relative path to content * @throws \UnexpectedValueException - * @return \SimpleXMLElement + * @return string */ protected function requestContent($origin, $path) { $url = rtrim($origin, '/') . '/' . ltrim($path, '/'); - $content = $this->rfs->getContents($origin, $url, false); + try { + $content = $this->httpDownloader->get($url)->getBody(); + } catch (\Exception $e) { + throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.', 0, $e); + } if (!$content) { throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.'); } diff --git a/src/Composer/Repository/Pear/ChannelReader.php b/src/Composer/Repository/Pear/ChannelReader.php index 73cc9152e..14d48ad86 100644 --- a/src/Composer/Repository/Pear/ChannelReader.php +++ b/src/Composer/Repository/Pear/ChannelReader.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * PEAR Channel package reader. @@ -26,12 +26,12 @@ class ChannelReader extends BaseChannelReader /** @var array of ('xpath test' => 'rest implementation') */ private $readerMap; - public function __construct(RemoteFilesystem $rfs) + public function __construct(HttpDownloader $httpDownloader) { - parent::__construct($rfs); + parent::__construct($httpDownloader); - $rest10reader = new ChannelRest10Reader($rfs); - $rest11reader = new ChannelRest11Reader($rfs); + $rest10reader = new ChannelRest10Reader($httpDownloader); + $rest11reader = new ChannelRest11Reader($httpDownloader); $this->readerMap = array( 'REST1.3' => $rest11reader, diff --git a/src/Composer/Repository/Pear/ChannelRest10Reader.php b/src/Composer/Repository/Pear/ChannelRest10Reader.php index 489914d5d..9d14b71ea 100644 --- a/src/Composer/Repository/Pear/ChannelRest10Reader.php +++ b/src/Composer/Repository/Pear/ChannelRest10Reader.php @@ -13,6 +13,7 @@ namespace Composer\Repository\Pear; use Composer\Downloader\TransportException; +use Composer\Util\HttpDownloader; /** * Read PEAR packages using REST 1.0 interface @@ -29,9 +30,9 @@ class ChannelRest10Reader extends BaseChannelReader { private $dependencyReader; - public function __construct($rfs) + public function __construct(HttpDownloader $httpDownloader) { - parent::__construct($rfs); + parent::__construct($httpDownloader); $this->dependencyReader = new PackageDependencyParser(); } @@ -149,7 +150,7 @@ class ChannelRest10Reader extends BaseChannelReader * @param string $baseUrl * @param string $packageName * @param string $version - * @return DependencyInfo[] + * @return DependencyInfo */ private function readPackageReleaseDependencies($baseUrl, $packageName, $version) { diff --git a/src/Composer/Repository/Pear/ChannelRest11Reader.php b/src/Composer/Repository/Pear/ChannelRest11Reader.php index f9e05f5be..18b1b10f3 100644 --- a/src/Composer/Repository/Pear/ChannelRest11Reader.php +++ b/src/Composer/Repository/Pear/ChannelRest11Reader.php @@ -12,6 +12,8 @@ namespace Composer\Repository\Pear; +use Composer\Util\HttpDownloader; + /** * Read PEAR packages using REST 1.1 interface * @@ -25,9 +27,9 @@ class ChannelRest11Reader extends BaseChannelReader { private $dependencyReader; - public function __construct($rfs) + public function __construct(HttpDownloader $httpDownloader) { - parent::__construct($rfs); + parent::__construct($httpDownloader); $this->dependencyReader = new PackageDependencyParser(); } diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index c4f0b83e7..97e131afb 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -21,7 +21,7 @@ use Composer\Repository\Pear\ChannelInfo; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Config; use Composer\Factory; @@ -38,7 +38,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn { private $url; private $io; - private $rfs; + private $httpDownloader; private $versionParser; private $repoConfig; @@ -47,7 +47,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn */ private $vendorAlias; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, RemoteFilesystem $rfs = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null) { parent::__construct(); if (!preg_match('{^https?://}', $repoConfig['url'])) { @@ -61,12 +61,17 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn $this->url = rtrim($repoConfig['url'], '/'); $this->io = $io; - $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader; $this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null; $this->versionParser = new VersionParser(); $this->repoConfig = $repoConfig; } + public function getRepoName() + { + return 'pear repo ('.$this->url.')'; + } + public function getRepoConfig() { return $this->repoConfig; @@ -78,7 +83,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn $this->io->writeError('Initializing PEAR repository '.$this->url); - $reader = new ChannelReader($this->rfs); + $reader = new ChannelReader($this->httpDownloader); try { $channelInfo = $reader->read($this->url); } catch (\Exception $e) { @@ -97,7 +102,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn * * @param ChannelInfo $channelInfo * @param SemverVersionParser $versionParser - * @return CompletePackage + * @return CompletePackage[] */ private function buildComposerPackages(ChannelInfo $channelInfo, SemverVersionParser $versionParser) { diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 265cdbf4a..84f3d4b66 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -51,6 +51,11 @@ class PlatformRepository extends ArrayRepository parent::__construct($packages); } + public function getRepoName() + { + return 'platform repo'; + } + protected function initialize() { parent::initialize(); @@ -216,6 +221,13 @@ class PlatformRepository extends ArrayRepository $prettyVersion = LIBXSLT_DOTTED_VERSION; break; + case 'zip': + if (defined('ZipArchive::LIBZIP_VERSION')) { + $prettyVersion = \ZipArchive::LIBZIP_VERSION; + } else { + continue 2; + } + default: // None handled extensions have no special cases, skip continue 2; @@ -275,7 +287,7 @@ class PlatformRepository extends ArrayRepository } else { $actualText = 'actual: '.$package->getPrettyVersion(); } - $overrider->setDescription($overrider->getDescription().' ('.$actualText.')'); + $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } @@ -288,7 +300,7 @@ class PlatformRepository extends ArrayRepository } else { $actualText = 'actual: '.$package->getPrettyVersion(); } - $overrider->setDescription($overrider->getDescription().' ('.$actualText.')'); + $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } @@ -335,6 +347,10 @@ class PlatformRepository extends ArrayRepository $this->addPackage($ext); } + /** + * @param string $name + * @return string + */ private function buildPackageName($name) { return 'ext-' . str_replace(' ', '-', $name); diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index 9bca0bd07..97a8ee957 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -16,7 +16,7 @@ use Composer\Factory; use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Json\JsonFile; /** @@ -36,7 +36,7 @@ class RepositoryFactory if (0 === strpos($repository, 'http')) { $repoConfig = array('type' => 'composer', 'url' => $repository); } elseif ("json" === pathinfo($repository, PATHINFO_EXTENSION)) { - $json = new JsonFile($repository, Factory::createRemoteFilesystem($io, $config)); + $json = new JsonFile($repository, Factory::createHttpDownloader($io, $config)); $data = $json->read(); if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) { $repoConfig = array('type' => 'composer', 'url' => 'file://' . strtr(realpath($repository), '\\', '/')); @@ -77,7 +77,7 @@ class RepositoryFactory */ public static function createRepo(IOInterface $io, Config $config, array $repoConfig) { - $rm = static::manager($io, $config, null, Factory::createRemoteFilesystem($io, $config)); + $rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config)); $repos = static::createRepos($rm, array($repoConfig)); return reset($repos); @@ -101,7 +101,7 @@ class RepositoryFactory if (!$io) { throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager'); } - $rm = static::manager($io, $config, null, Factory::createRemoteFilesystem($io, $config)); + $rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config)); } return static::createRepos($rm, $config->getRepositories()); @@ -111,12 +111,12 @@ class RepositoryFactory * @param IOInterface $io * @param Config $config * @param EventDispatcher $eventDispatcher - * @param RemoteFilesystem $rfs + * @param HttpDownloader $httpDownloader * @return RepositoryManager */ - public static function manager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { - $rm = new RepositoryManager($io, $config, $eventDispatcher, $rfs); + $rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 9a2aaf3b5..98b616fa0 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\PackageInterface; +use Composer\Semver\Constraint\ConstraintInterface; /** * Repository interface. @@ -38,8 +39,8 @@ interface RepositoryInterface extends \Countable /** * Searches for the first match of a package by name and version. * - * @param string $name package name - * @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against + * @param string $name package name + * @param string|ConstraintInterface $constraint package version or version constraint to match against * * @return PackageInterface|null */ @@ -48,8 +49,8 @@ interface RepositoryInterface extends \Countable /** * Searches for all packages matching a name and optionally a version. * - * @param string $name package name - * @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against + * @param string $name package name + * @param string|ConstraintInterface $constraint package version or version constraint to match against * * @return PackageInterface[] */ @@ -62,13 +63,44 @@ interface RepositoryInterface extends \Countable */ public function getPackages(); + /** + * Returns list of registered packages with the supplied name + * + * @param ConstraintInterface[] $packageNameMap package names pointing to constraints + * @param array $acceptableStabilities + * @param array $stabilityFlags + * @return array [namesFound => string[], packages => PackageInterface[]] + */ + public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags); + /** * Searches the repository for packages containing the query * * @param string $query search query * @param int $mode a set of SEARCH_* constants to search on, implementations should do a best effort only + * @param string $type The type of package to search for. Defaults to all types of packages * * @return array[] an array of array('name' => '...', 'description' => '...') */ - public function search($query, $mode = 0); + public function search($query, $mode = 0, $type = null); + + /** + * Returns a list of packages providing a given package name + * + * Packages which have the same name as $packageName should not be returned, only those that have a "provide" on it. + * + * @param string $packageName package name which must be provided + * + * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...') + */ + public function getProviders($packageName); + + /** + * Returns a name representing this repository to the user + * + * This is best effort and definitely can not always be very precise + * + * @return string + */ + public function getRepoName(); } diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 87b82d14d..2dca57099 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -16,7 +16,7 @@ use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * Repositories manager. @@ -33,14 +33,14 @@ class RepositoryManager private $io; private $config; private $eventDispatcher; - private $rfs; + private $httpDownloader; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { $this->io = $io; $this->config = $config; + $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; - $this->rfs = $rfs; } /** @@ -125,13 +125,7 @@ class RepositoryManager $class = $this->repositoryClasses[$type]; - $reflMethod = new \ReflectionMethod($class, '__construct'); - $params = $reflMethod->getParameters(); - if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') { - return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->rfs); - } - - return new $class($config, $this->io, $this->config, $this->eventDispatcher); + return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher); } /** diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php new file mode 100644 index 000000000..b0489e99b --- /dev/null +++ b/src/Composer/Repository/RepositorySet.php @@ -0,0 +1,243 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\PoolBuilder; +use Composer\DependencyResolver\Request; +use Composer\EventDispatcher\EventDispatcher; +use Composer\IO\IOInterface; +use Composer\IO\NullIO; +use Composer\Package\BasePackage; +use Composer\Package\Version\VersionParser; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\LockArrayRepository; +use Composer\Repository\InstalledRepositoryInterface; +use Composer\Repository\InstalledRepository; +use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Package\Version\StabilityFilter; + +/** + * @author Nils Adermann + */ +class RepositorySet +{ + /** + * Packages are returned even though their stability does not match the required stability + */ + const ALLOW_UNACCEPTABLE_STABILITIES = 1; + /** + * Packages will be looked up in all repositories, even after they have been found in a higher prio one + */ + const ALLOW_SHADOWED_REPOSITORIES = 2; + + /** @var array */ + private $rootAliases; + /** @var array */ + private $rootReferences; + + /** @var RepositoryInterface[] */ + private $repositories = array(); + + private $acceptableStabilities; + private $stabilityFlags; + private $rootRequires; + + /** @var bool */ + private $locked = false; + /** @var bool */ + private $allowInstalledRepositories = false; + + public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) + { + $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; + + $this->acceptableStabilities = array(); + foreach (BasePackage::$stabilities as $stability => $value) { + if ($value <= BasePackage::$stabilities[$minimumStability]) { + $this->acceptableStabilities[$stability] = $value; + } + } + $this->stabilityFlags = $stabilityFlags; + $this->rootRequires = $rootRequires; + foreach ($rootRequires as $name => $constraint) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { + unset($this->rootRequires[$name]); + } + } + } + + public function allowInstalledRepositories($allow = true) + { + $this->allowInstalledRepositories = $allow; + } + + public function getRootRequires() + { + return $this->rootRequires; + } + + /** + * Adds a repository to this repository set + * + * The first repos added have a higher priority. As soon as a package is found in any + * repository the search for that package ends, and following repos will not be consulted. + * + * @param RepositoryInterface $repo A package repository + */ + public function addRepository(RepositoryInterface $repo) + { + if ($this->locked) { + throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore."); + } + + if ($repo instanceof CompositeRepository) { + $repos = $repo->getRepositories(); + } else { + $repos = array($repo); + } + + foreach ($repos as $repo) { + $this->repositories[] = $repo; + } + } + + /** + * Find packages providing or matching a name and optionally meeting a constraint in all repositories + * + * Returned in the order of repositories, matching priority + * + * @param string $name + * @param ConstraintInterface|null $constraint + * @param int $flags any of the ALLOW_* constants from this class to tweak what is returned + * @return array + */ + public function findPackages($name, ConstraintInterface $constraint = null, $flags = 0) + { + $ignoreStability = ($flags & self::ALLOW_UNACCEPTABLE_STABILITIES) !== 0; + $loadFromAllRepos = ($flags & self::ALLOW_SHADOWED_REPOSITORIES) !== 0; + + $packages = array(); + if ($loadFromAllRepos) { + foreach ($this->repositories as $repository) { + $packages[] = $repository->findPackages($name, $constraint) ?: array(); + } + } else { + foreach ($this->repositories as $repository) { + $result = $repository->loadPackages(array($name => $constraint), $ignoreStability ? BasePackage::$stabilities : $this->acceptableStabilities, $ignoreStability ? array() : $this->stabilityFlags); + + $packages[] = $result['packages']; + foreach ($result['namesFound'] as $nameFound) { + // avoid loading the same package again from other repositories once it has been found + if ($name === $nameFound) { + break 2; + } + } + } + } + + $candidates = $packages ? call_user_func_array('array_merge', $packages) : array(); + + // when using loadPackages above (!$loadFromAllRepos) the repos already filter for stability so no need to do it again + if ($ignoreStability || !$loadFromAllRepos) { + return $candidates; + } + + $result = array(); + foreach ($candidates as $candidate) { + if ($this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { + $result[] = $candidate; + } + } + + return $candidates; + } + + public function getProviders($packageName) + { + $providers = array(); + foreach ($this->repositories as $repository) { + if ($repoProviders = $repository->getProviders($packageName)) { + $providers = array_merge($providers, $repoProviders); + } + } + + return $providers; + } + + public function isPackageAcceptable($names, $stability) + { + return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability); + } + + /** + * Create a pool for dependency resolution from the packages in this repository set. + * + * @return Pool + */ + public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null) + { + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher); + + foreach ($this->repositories as $repo) { + if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { + throw new \LogicException('The pool can not accept packages from an installed repository'); + } + } + + $this->locked = true; + + return $poolBuilder->buildPool($this->repositories, $request); + } + + /** + * Create a pool for dependency resolution from the packages in this repository set. + * + * @return Pool + */ + public function createPoolWithAllPackages() + { + foreach ($this->repositories as $repo) { + if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { + throw new \LogicException('The pool can not accept packages from an installed repository'); + } + } + + $this->locked = true; + + $packages = array(); + foreach ($this->repositories as $repository) { + $packages = array_merge($packages, $repository->getPackages()); + } + return new Pool($packages); + } + + // TODO unify this with above in some simpler version without "request"? + public function createPoolForPackage($packageName, LockArrayRepository $lockedRepo = null) + { + return $this->createPoolForPackages(array($packageName), $lockedRepo); + } + + public function createPoolForPackages($packageNames, LockArrayRepository $lockedRepo = null) + { + $request = new Request($lockedRepo); + + foreach ($packageNames as $packageName) { + $request->requireName($packageName); + } + + return $this->createPool($request, new NullIO()); + } +} diff --git a/src/Composer/Repository/RootPackageRepository.php b/src/Composer/Repository/RootPackageRepository.php new file mode 100644 index 000000000..9652282f7 --- /dev/null +++ b/src/Composer/Repository/RootPackageRepository.php @@ -0,0 +1,35 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Package\RootPackageInterface; + +/** + * Root package repository. + * + * This is used for serving the RootPackage inside an in-memory InstalledRepository + * + * @author Jordi Boggiano + */ +class RootPackageRepository extends ArrayRepository +{ + public function __construct(RootPackageInterface $package) + { + parent::__construct(array($package)); + } + + public function getRepoName() + { + return 'root package repo'; + } +} diff --git a/src/Composer/Repository/Vcs/BitbucketDriver.php b/src/Composer/Repository/Vcs/BitbucketDriver.php index c360bad10..22180b55d 100644 --- a/src/Composer/Repository/Vcs/BitbucketDriver.php +++ b/src/Composer/Repository/Vcs/BitbucketDriver.php @@ -16,6 +16,7 @@ use Composer\Cache; use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\Util\Bitbucket; +use Composer\Util\Http\Response; abstract class BitbucketDriver extends VcsDriver { @@ -92,7 +93,7 @@ abstract class BitbucketDriver extends VcsDriver ) ); - $repoData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource, true), $resource); + $repoData = $this->fetchWithOAuthCredentials($resource, true)->decodeJson(); if ($this->fallbackDriver) { return false; } @@ -206,7 +207,7 @@ abstract class BitbucketDriver extends VcsDriver $file ); - return $this->getContentsWithOAuthCredentials($resource); + return $this->fetchWithOAuthCredentials($resource)->getBody(); } /** @@ -231,7 +232,7 @@ abstract class BitbucketDriver extends VcsDriver $this->repository, $identifier ); - $commit = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $commit = $this->fetchWithOAuthCredentials($resource)->decodeJson(); return new \DateTime($commit['date']); } @@ -293,7 +294,7 @@ abstract class BitbucketDriver extends VcsDriver ); $hasNext = true; while ($hasNext) { - $tagsData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $tagsData = $this->fetchWithOAuthCredentials($resource)->decodeJson(); foreach ($tagsData['values'] as $data) { $this->tags[$data['name']] = $data['target']['hash']; } @@ -337,7 +338,7 @@ abstract class BitbucketDriver extends VcsDriver ); $hasNext = true; while ($hasNext) { - $branchData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $branchData = $this->fetchWithOAuthCredentials($resource)->decodeJson(); foreach ($branchData['values'] as $data) { // skip headless branches which seem to be deleted branches that bitbucket nevertheless returns in the API if ($this->vcsType === 'hg' && empty($data['heads'])) { @@ -363,14 +364,14 @@ abstract class BitbucketDriver extends VcsDriver * @param string $url The URL of content * @param bool $fetchingRepoData * - * @return mixed The result + * @return Response The result */ - protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false) + protected function fetchWithOAuthCredentials($url, $fetchingRepoData = false) { try { return parent::getContents($url); } catch (TransportException $e) { - $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem); + $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->httpDownloader); if (403 === $e->getCode() || (401 === $e->getCode() && strpos($e->getMessage(), 'Could not authenticate against') === 0)) { if (!$this->io->hasAuthentication($this->originUrl) @@ -380,7 +381,9 @@ abstract class BitbucketDriver extends VcsDriver } if (!$this->io->isInteractive() && $fetchingRepoData) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } } @@ -399,6 +402,8 @@ abstract class BitbucketDriver extends VcsDriver { try { $this->setupFallbackDriver($this->generateSshUrl()); + + return true; } catch (\RuntimeException $e) { $this->fallbackDriver = null; @@ -442,7 +447,7 @@ abstract class BitbucketDriver extends VcsDriver $this->repository ); - $data = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $data = $this->fetchWithOAuthCredentials($resource)->decodeJson(); if (isset($data['mainbranch'])) { return $data['mainbranch']; } diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 82d934b5b..08dbe9233 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -75,8 +75,8 @@ class GitBitbucketDriver extends BitbucketDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->remoteFilesystem + $this->httpDownloader, + $this->process ); $this->fallbackDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 2fe7e872e..c79b91e6b 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -18,6 +18,7 @@ use Composer\Json\JsonFile; use Composer\Cache; use Composer\IO\IOInterface; use Composer\Util\GitHub; +use Composer\Util\Http\Response; /** * @author Jordi Boggiano @@ -195,10 +196,9 @@ class GitHubDriver extends VcsDriver foreach (array($this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/.github/FUNDING.yml', $this->getApiUrl() . '/repos/'.$this->owner.'/.github/contents/FUNDING.yml') as $file) { try { - $result = $this->remoteFilesystem->getContents($this->originUrl, $file, false, array( + $response = $this->httpDownloader->get($file, array( 'retry-auth-failure' => false, - )); - $response = json_decode($result, true); + ))->decodeJson(); } catch (TransportException $e) { continue; } @@ -277,7 +277,7 @@ class GitHubDriver extends VcsDriver } $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier); - $resource = JsonFile::parseJson($this->getContents($resource)); + $resource = $this->getContents($resource)->decodeJson(); if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) { throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier); } @@ -295,7 +295,7 @@ class GitHubDriver extends VcsDriver } $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); - $commit = JsonFile::parseJson($this->getContents($resource), $resource); + $commit = $this->getContents($resource)->decodeJson(); return new \DateTime($commit['commit']['committer']['date']); } @@ -313,12 +313,13 @@ class GitHubDriver extends VcsDriver $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100'; do { - $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); + $response = $this->getContents($resource); + $tagsData = $response->decodeJson(); foreach ($tagsData as $tag) { $this->tags[$tag['name']] = $tag['commit']['sha']; } - $resource = $this->getNextPage(); + $resource = $this->getNextPage($response); } while ($resource); } @@ -340,7 +341,8 @@ class GitHubDriver extends VcsDriver $branchBlacklist = array('gh-pages'); do { - $branchData = JsonFile::parseJson($this->getContents($resource), $resource); + $response = $this->getContents($resource); + $branchData = $response->decodeJson(); foreach ($branchData as $branch) { $name = substr($branch['ref'], 11); if (!in_array($name, $branchBlacklist)) { @@ -348,7 +350,7 @@ class GitHubDriver extends VcsDriver } } - $resource = $this->getNextPage(); + $resource = $this->getNextPage($response); } while ($resource); } @@ -412,7 +414,7 @@ class GitHubDriver extends VcsDriver try { return parent::getContents($url); } catch (TransportException $e) { - $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem); + $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->httpDownloader); switch ($e->getCode()) { case 401: @@ -427,16 +429,18 @@ class GitHubDriver extends VcsDriver } if (!$this->io->isInteractive()) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } $scopesIssued = array(); $scopesNeeded = array(); if ($headers = $e->getHeaders()) { - if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-OAuth-Scopes')) { + if ($scopes = Response::findHeaderValue($headers, 'X-OAuth-Scopes')) { $scopesIssued = explode(' ', $scopes); } - if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { + if ($scopes = Response::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { $scopesNeeded = explode(' ', $scopes); } } @@ -455,7 +459,9 @@ class GitHubDriver extends VcsDriver } if (!$this->io->isInteractive() && $fetchingRepoData) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } $rateLimited = $gitHubUtil->isRateLimited($e->getHeaders()); @@ -501,7 +507,7 @@ class GitHubDriver extends VcsDriver $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository; - $this->repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl); + $this->repoData = $this->getContents($repoDataUrl, true)->decodeJson(); if (null === $this->repoData && null !== $this->gitDriver) { return; } @@ -532,7 +538,7 @@ class GitHubDriver extends VcsDriver // are not interactive) then we fallback to GitDriver. $this->setupGitDriver($this->generateSshUrl()); - return; + return true; } catch (\RuntimeException $e) { $this->gitDriver = null; @@ -547,23 +553,20 @@ class GitHubDriver extends VcsDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->remoteFilesystem + $this->httpDownloader, + $this->process ); $this->gitDriver->initialize(); } - protected function getNextPage() + protected function getNextPage(Response $response) { - $headers = $this->remoteFilesystem->getLastHeaders(); - foreach ($headers as $header) { - if (preg_match('{^link:\s*(.+?)\s*$}i', $header, $match)) { - $links = explode(',', $match[1]); - foreach ($links as $link) { - if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { - return $match[1]; - } - } + $header = $response->getHeader('link'); + + $links = explode(',', $header); + foreach ($links as $link) { + if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { + return $match[1]; } } } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 2878f3f74..6e98c838f 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -17,8 +17,9 @@ use Composer\Cache; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Downloader\TransportException; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\GitLab; +use Composer\Util\Http\Response; /** * Driver for GitLab API, use the Git driver for local checkouts. @@ -109,14 +110,14 @@ class GitLabDriver extends VcsDriver } /** - * Updates the RemoteFilesystem instance. + * Updates the HttpDownloader instance. * Mainly useful for tests. * * @internal */ - public function setRemoteFilesystem(RemoteFilesystem $remoteFilesystem) + public function setHttpDownloader(HttpDownloader $httpDownloader) { - $this->remoteFilesystem = $remoteFilesystem; + $this->httpDownloader = $httpDownloader; } /** @@ -175,7 +176,7 @@ class GitLabDriver extends VcsDriver $resource = $this->getApiUrl().'/repository/files/'.$this->urlEncodeAll($file).'/raw?ref='.$identifier; try { - $content = $this->getContents($resource); + $content = $this->getContents($resource)->getBody(); } catch (TransportException $e) { if ($e->getCode() !== 404) { throw $e; @@ -329,7 +330,8 @@ class GitLabDriver extends VcsDriver $references = array(); do { - $data = JsonFile::parseJson($this->getContents($resource), $resource); + $response = $this->getContents($resource); + $data = $response->decodeJson(); foreach ($data as $datum) { $references[$datum['name']] = $datum['commit']['id']; @@ -340,7 +342,7 @@ class GitLabDriver extends VcsDriver } if (count($data) >= $perPage) { - $resource = $this->getNextPage(); + $resource = $this->getNextPage($response); } else { $resource = false; } @@ -353,7 +355,7 @@ class GitLabDriver extends VcsDriver { // we need to fetch the default branch from the api $resource = $this->getApiUrl(); - $this->project = JsonFile::parseJson($this->getContents($resource, true), $resource); + $this->project = $this->getContents($resource, true)->decodeJson(); if (isset($this->project['visibility'])) { $this->isPrivate = $this->project['visibility'] !== 'public'; } else { @@ -376,7 +378,7 @@ class GitLabDriver extends VcsDriver // are not interactive) then we fallback to GitDriver. $this->setupGitDriver($url); - return; + return true; } catch (\RuntimeException $e) { $this->gitDriver = null; @@ -410,8 +412,8 @@ class GitLabDriver extends VcsDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->remoteFilesystem + $this->httpDownloader, + $this->process ); $this->gitDriver->initialize(); } @@ -422,10 +424,10 @@ class GitLabDriver extends VcsDriver protected function getContents($url, $fetchingRepoData = false) { try { - $res = parent::getContents($url); + $response = parent::getContents($url); if ($fetchingRepoData) { - $json = JsonFile::parseJson($res, $url); + $json = $response->decodeJson(); // Accessing the API with a token with Guest (10) access will return // more data than unauthenticated access but no default_branch data @@ -460,9 +462,9 @@ class GitLabDriver extends VcsDriver } } - return $res; + return $response; } catch (TransportException $e) { - $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->remoteFilesystem); + $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->httpDownloader); switch ($e->getCode()) { case 401: @@ -477,7 +479,9 @@ class GitLabDriver extends VcsDriver } if (!$this->io->isInteractive()) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } $this->io->writeError('Failed to download ' . $this->namespace . '/' . $this->repository . ':' . $e->getMessage() . ''); $gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, 'Your credentials are required to fetch private repository metadata ('.$this->url.')'); @@ -490,7 +494,9 @@ class GitLabDriver extends VcsDriver } if (!$this->io->isInteractive() && $fetchingRepoData) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } throw $e; @@ -530,17 +536,14 @@ class GitLabDriver extends VcsDriver return true; } - private function getNextPage() + protected function getNextPage(Response $response) { - $headers = $this->remoteFilesystem->getLastHeaders(); - foreach ($headers as $header) { - if (preg_match('{^link:\s*(.+?)\s*$}i', $header, $match)) { - $links = explode(',', $match[1]); - foreach ($links as $link) { - if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { - return $match[1]; - } - } + $header = $response->getHeader('link'); + + $links = explode(',', $header); + foreach ($links as $link) { + if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { + return $match[1]; } } } diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index 1cf630da9..2d0596467 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -75,8 +75,8 @@ class HgBitbucketDriver extends BitbucketDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->remoteFilesystem + $this->httpDownloader, + $this->process ); $this->fallbackDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index 5227630f6..37946da23 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -19,8 +19,9 @@ use Composer\Factory; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; /** * A driver implementation for driver with authentication interaction. @@ -41,8 +42,8 @@ abstract class VcsDriver implements VcsDriverInterface protected $config; /** @var ProcessExecutor */ protected $process; - /** @var RemoteFilesystem */ - protected $remoteFilesystem; + /** @var HttpDownloader */ + protected $httpDownloader; /** @var array */ protected $infoCache = array(); /** @var Cache */ @@ -54,10 +55,10 @@ abstract class VcsDriver implements VcsDriverInterface * @param array $repoConfig The repository configuration * @param IOInterface $io The IO instance * @param Config $config The composer configuration + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking */ - final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + final public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process) { if (Filesystem::isLocalPath($repoConfig['url'])) { $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); @@ -68,8 +69,8 @@ abstract class VcsDriver implements VcsDriverInterface $this->repoConfig = $repoConfig; $this->io = $io; $this->config = $config; - $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader; + $this->process = $process; } /** @@ -156,13 +157,13 @@ abstract class VcsDriver implements VcsDriverInterface * * @param string $url The URL of content * - * @return mixed The result + * @return Response */ protected function getContents($url) { $options = isset($this->repoConfig['options']) ? $this->repoConfig['options'] : array(); - return $this->remoteFilesystem->getContents($this->originUrl, $url, false, $options); + return $this->httpDownloader->get($url, $options); } /** diff --git a/src/Composer/Repository/Vcs/VcsDriverInterface.php b/src/Composer/Repository/Vcs/VcsDriverInterface.php index 5e3bcec68..e59bcf647 100644 --- a/src/Composer/Repository/Vcs/VcsDriverInterface.php +++ b/src/Composer/Repository/Vcs/VcsDriverInterface.php @@ -38,7 +38,7 @@ interface VcsDriverInterface * * @param string $file * @param string $identifier - * @return string + * @return string|null */ public function getFileContent($file, $identifier); @@ -46,7 +46,7 @@ interface VcsDriverInterface * Get the changedate for $identifier. * * @param string $identifier - * @return \DateTime + * @return \DateTime|null */ public function getChangeDate($identifier); @@ -73,7 +73,7 @@ interface VcsDriverInterface /** * @param string $identifier Any identifier to a specific branch/tag/commit - * @return array With type, url reference and shasum keys. + * @return array|null With type, url reference and shasum keys. */ public function getDist($identifier); diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index ccc49396a..d0aeea878 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -20,6 +20,9 @@ use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Package\Loader\InvalidPackageException; use Composer\Package\Loader\LoaderInterface; use Composer\EventDispatcher\EventDispatcher; +use Composer\Util\ProcessExecutor; +use Composer\Util\HttpDownloader; +use Composer\Util\Url; use Composer\Semver\Constraint\Constraint; use Composer\IO\IOInterface; use Composer\Config; @@ -39,6 +42,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt protected $type; protected $loader; protected $repoConfig; + protected $httpDownloader; + protected $processExecutor; protected $branchErrorOccurred = false; private $drivers; /** @var VcsDriverInterface */ @@ -48,7 +53,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt private $emptyReferences = array(); private $versionTransportExceptions = array(); - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) { parent::__construct(); $this->drivers = $drivers ?: array( @@ -72,6 +77,19 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->config = $config; $this->repoConfig = $repoConfig; $this->versionCache = $versionCache; + $this->httpDownloader = $httpDownloader; + $this->processExecutor = new ProcessExecutor($io); + } + + public function getRepoName() + { + $driverClass = get_class($this->getDriver()); + $driverType = array_search($driverClass, $this->drivers); + if (!$driverType) { + $driverType = $driverClass; + } + + return 'vcs repo ('.$driverType.' '.Url::sanitize($this->url).')'; } public function getRepoConfig() @@ -92,7 +110,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt if (isset($this->drivers[$this->type])) { $class = $this->drivers[$this->type]; - $this->driver = new $class($this->repoConfig, $this->io, $this->config); + $this->driver = new $class($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; @@ -100,7 +118,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt foreach ($this->drivers as $driver) { if ($driver::supports($this->io, $this->config, $this->url)) { - $this->driver = new $driver($this->repoConfig, $this->io, $this->config); + $this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; @@ -109,7 +127,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt foreach ($this->drivers as $driver) { if ($driver::supports($this->io, $this->config, $this->url, true)) { - $this->driver = new $driver($this->repoConfig, $this->io, $this->config); + $this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; diff --git a/src/Composer/Repository/WritableArrayRepository.php b/src/Composer/Repository/WritableArrayRepository.php index 041e40562..3580593bb 100644 --- a/src/Composer/Repository/WritableArrayRepository.php +++ b/src/Composer/Repository/WritableArrayRepository.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\AliasPackage; +use Composer\Installer\InstallationManager; /** * Writable array repository. @@ -24,7 +25,7 @@ class WritableArrayRepository extends ArrayRepository implements WritableReposit /** * {@inheritDoc} */ - public function write() + public function write($devMode, InstallationManager $installationManager) { } diff --git a/src/Composer/Repository/WritableRepositoryInterface.php b/src/Composer/Repository/WritableRepositoryInterface.php index 4500005d9..c35fdb257 100644 --- a/src/Composer/Repository/WritableRepositoryInterface.php +++ b/src/Composer/Repository/WritableRepositoryInterface.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\PackageInterface; +use Composer\Installer\InstallationManager; /** * Writable repository interface. @@ -23,8 +24,10 @@ interface WritableRepositoryInterface extends RepositoryInterface { /** * Writes repository (f.e. to the disc). + * + * @param bool $devMode Whether dev requirements were included or not in this installation */ - public function write(); + public function write($devMode, InstallationManager $installationManager); /** * Adds package to the repository. diff --git a/src/Composer/Script/CommandEvent.php b/src/Composer/Script/CommandEvent.php deleted file mode 100644 index 84c52008c..000000000 --- a/src/Composer/Script/CommandEvent.php +++ /dev/null @@ -1,22 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Script; - -/** - * The Command Event. - * - * @deprecated use Composer\Script\Event instead - */ -class CommandEvent extends Event -{ -} diff --git a/src/Composer/Script/PackageEvent.php b/src/Composer/Script/PackageEvent.php deleted file mode 100644 index 531b86a40..000000000 --- a/src/Composer/Script/PackageEvent.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Script; - -use Composer\Installer\PackageEvent as BasePackageEvent; - -/** - * The Package Event. - * - * @deprecated Use Composer\Installer\PackageEvent instead - */ -class PackageEvent extends BasePackageEvent -{ -} diff --git a/src/Composer/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index b619bda16..431abecb5 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -12,7 +12,7 @@ namespace Composer\SelfUpdate; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Config; use Composer\Json\JsonFile; @@ -21,13 +21,13 @@ use Composer\Json\JsonFile; */ class Versions { - private $rfs; + private $httpDownloader; private $config; private $channel; - public function __construct(Config $config, RemoteFilesystem $rfs) + public function __construct(Config $config, HttpDownloader $httpDownloader) { - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; $this->config = $config; } @@ -62,7 +62,7 @@ class Versions public function getLatest() { $protocol = extension_loaded('openssl') ? 'https' : 'http'; - $versions = JsonFile::parseJson($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/versions', false)); + $versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); foreach ($versions[$this->getChannel()] as $version) { if ($version['min-php'] <= PHP_VERSION_ID) { diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 72b23ba22..81eb49020 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -14,6 +14,7 @@ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; +use Composer\Downloader\TransportException; /** * @author Jordi Boggiano @@ -22,6 +23,7 @@ class AuthHelper { protected $io; protected $config; + private $displayedOriginAuthentications = array(); public function __construct(IOInterface $io, Config $config) { @@ -29,7 +31,11 @@ class AuthHelper $this->config = $config; } - public function storeAuth($originUrl, $storeAuth) + /** + * @param string $origin + * @param string|bool $storeAuth + */ + public function storeAuth($origin, $storeAuth) { $store = false; $configSource = $this->config->getAuthConfigSource(); @@ -37,7 +43,7 @@ class AuthHelper $store = $configSource; } elseif ($storeAuth === 'prompt') { $answer = $this->io->askAndValidate( - 'Do you want to store credentials for '.$originUrl.' in '.$configSource->getName().' ? [Yn] ', + 'Do you want to store credentials for '.$origin.' in '.$configSource->getName().' ? [Yn] ', function ($value) { $input = strtolower(substr(trim($value), 0, 1)); if (in_array($input, array('y','n'))) { @@ -55,9 +61,200 @@ class AuthHelper } if ($store) { $store->addConfigSetting( - 'http-basic.'.$originUrl, - $this->io->getAuthentication($originUrl) + 'http-basic.'.$origin, + $this->io->getAuthentication($origin) ); } } + + /** + * @param string $url + * @param string $origin + * @param int $statusCode HTTP status code that triggered this call + * @param string|null $reason a message/description explaining why this was called + * @param string[] $headers + * @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be + * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json + */ + public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $headers = array()) + { + $storeAuth = false; + $retry = false; + + if (in_array($origin, $this->config->get('github-domains'), true)) { + $gitHubUtil = new GitHub($this->io, $this->config, null); + $message = "\n"; + + $rateLimited = $gitHubUtil->isRateLimited($headers); + if ($rateLimited) { + $rateLimit = $gitHubUtil->getRateLimit($headers); + if ($this->io->hasAuthentication($origin)) { + $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.'; + } else { + $message = 'Create a GitHub OAuth token to go over the API rate limit.'; + } + + $message = sprintf( + 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$url.'. '.$message.' You can also wait until %s for the rate limit to reset.', + $rateLimit['limit'], + $rateLimit['reset'] + )."\n"; + } else { + $message .= 'Could not fetch '.$url.', please '; + if ($this->io->hasAuthentication($origin)) { + $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos'; + } else { + $message .= 'create a GitHub OAuth token to access private repos'; + } + } + + if (!$gitHubUtil->authorizeOAuth($origin) + && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($origin, $message)) + ) { + throw new TransportException('Could not authenticate against '.$origin, 401); + } + } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { + $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit'); + $gitLabUtil = new GitLab($this->io, $this->config, null); + + if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && in_array($auth['password'], array('gitlab-ci-token', 'private-token', 'oauth2'), true)) { + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); + } + + if (!$gitLabUtil->authorizeOAuth($origin) + && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively(parse_url($url, PHP_URL_SCHEME), $origin, $message)) + ) { + throw new TransportException('Could not authenticate against '.$origin, 401); + } + } elseif ($origin === 'bitbucket.org') { + $askForOAuthToken = true; + if ($this->io->hasAuthentication($origin)) { + $auth = $this->io->getAuthentication($origin); + if ($auth['username'] !== 'x-token-auth') { + $bitbucketUtil = new Bitbucket($this->io, $this->config); + $accessToken = $bitbucketUtil->requestToken($origin, $auth['username'], $auth['password']); + if (!empty($accessToken)) { + $this->io->setAuthentication($origin, 'x-token-auth', $accessToken); + $askForOAuthToken = false; + } + } else { + throw new TransportException('Could not authenticate against ' . $origin, 401); + } + } + + if ($askForOAuthToken) { + $message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($statusCode === 401 || $statusCode === 403) ? 'access private repos' : 'go over the API rate limit'); + $bitBucketUtil = new Bitbucket($this->io, $this->config); + if (! $bitBucketUtil->authorizeOAuth($origin) + && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($origin, $message)) + ) { + throw new TransportException('Could not authenticate against ' . $origin, 401); + } + } + } else { + // 404s are only handled for github + if ($statusCode === 404) { + return; + } + + // fail if the console is not interactive + if (!$this->io->isInteractive()) { + if ($statusCode === 401) { + $message = "The '" . $url . "' URL required authentication.\nYou must be using the interactive console to authenticate"; + } + if ($statusCode === 403) { + $message = "The '" . $url . "' URL could not be accessed: " . $reason; + } + + throw new TransportException($message, $statusCode); + } + // fail if we already have auth + if ($this->io->hasAuthentication($origin)) { + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); + } + + $this->io->writeError(' Authentication required ('.$origin.'):'); + $username = $this->io->ask(' Username: '); + $password = $this->io->askAndHideAnswer(' Password: '); + $this->io->setAuthentication($origin, $username, $password); + $storeAuth = $this->config->get('store-auths'); + } + + $retry = true; + + return array('retry' => $retry, 'storeAuth' => $storeAuth); + } + + /** + * @param array $headers + * @param string $origin + * @param string $url + * @return array updated headers array + */ + public function addAuthenticationHeader(array $headers, $origin, $url) + { + if ($this->io->hasAuthentication($origin)) { + $authenticationDisplayMessage = null; + $auth = $this->io->getAuthentication($origin); + if ($auth['password'] === 'bearer') { + $headers[] = 'Authorization: Bearer '.$auth['username']; + } elseif ('github.com' === $origin && 'x-oauth-basic' === $auth['password']) { + $headers[] = 'Authorization: token '.$auth['username']; + $authenticationDisplayMessage = 'Using GitHub token authentication'; + } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { + if ($auth['password'] === 'oauth2') { + $headers[] = 'Authorization: Bearer '.$auth['username']; + $authenticationDisplayMessage = 'Using GitLab OAuth token authentication'; + } elseif ($auth['password'] === 'private-token' || $auth['password'] === 'gitlab-ci-token') { + $headers[] = 'PRIVATE-TOKEN: '.$auth['username']; + $authenticationDisplayMessage = 'Using GitLab private token authentication'; + } + } elseif ( + 'bitbucket.org' === $origin + && $url !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL + && 'x-token-auth' === $auth['username'] + ) { + if (!$this->isPublicBitBucketDownload($url)) { + $headers[] = 'Authorization: Bearer ' . $auth['password']; + $authenticationDisplayMessage = 'Using Bitbucket OAuth token authentication'; + } + } else { + $authStr = base64_encode($auth['username'] . ':' . $auth['password']); + $headers[] = 'Authorization: Basic '.$authStr; + $authenticationDisplayMessage = 'Using HTTP basic authentication with username "' . $auth['username'] . '"'; + } + + if ($authenticationDisplayMessage && !in_array($origin, $this->displayedOriginAuthentications, true)) { + $this->io->writeError($authenticationDisplayMessage, true, IOInterface::DEBUG); + $this->displayedOriginAuthentications[] = $origin; + } + } + + return $headers; + } + + /** + * @link https://github.com/composer/composer/issues/5584 + * + * @param string $urlToBitBucketFile URL to a file at bitbucket.org. + * + * @return bool Whether the given URL is a public BitBucket download which requires no authentication. + */ + public function isPublicBitBucketDownload($urlToBitBucketFile) + { + $domain = parse_url($urlToBitBucketFile, PHP_URL_HOST); + if (strpos($domain, 'bitbucket.org') === false) { + // Bitbucket downloads are hosted on amazonaws. + // We do not need to authenticate there at all + return true; + } + + $path = parse_url($urlToBitBucketFile, PHP_URL_PATH); + + // Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever} + // {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/} + $pathParts = explode('/', $path); + + return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; + } } diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 1fc286ac4..ea02dfa0b 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -22,11 +22,17 @@ use Composer\Downloader\TransportException; */ class Bitbucket { + /** @var IOInterface */ private $io; + /** @var Config */ private $config; + /** @var ProcessExecutor */ private $process; - private $remoteFilesystem; + /** @var HttpDownloader */ + private $httpDownloader; + /** @var array */ private $token = array(); + /** @var int|null */ private $time; const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; @@ -37,15 +43,15 @@ class Bitbucket * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking * @param int $time Timestamp, injectable for mocking */ - public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null, $time = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null, $time = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); $this->time = $time; } @@ -90,7 +96,7 @@ class Bitbucket private function requestAccessToken($originUrl) { try { - $json = $this->remoteFilesystem->getContents($originUrl, self::OAUTH2_ACCESS_TOKEN_URL, false, array( + $response = $this->httpDownloader->get(self::OAUTH2_ACCESS_TOKEN_URL, array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', @@ -98,7 +104,7 @@ class Bitbucket ), )); - $this->token = json_decode($json, true); + $this->token = $response->decodeJson(); } catch (TransportException $e) { if ($e->getCode() === 400) { $this->io->writeError('Invalid OAuth consumer provided.'); diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 008ed584a..8df7de8b5 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -23,6 +23,7 @@ use Symfony\Component\Finder\Finder; */ class Filesystem { + /** @var ProcessExecutor */ private $processExecutor; public function __construct(ProcessExecutor $executor = null) @@ -291,6 +292,7 @@ class Filesystem $this->ensureDirectoryExists($target); $result = true; + /** @var RecursiveDirectoryIterator $ri */ foreach ($ri as $file) { $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathName(); if ($file->isDir()) { @@ -539,6 +541,9 @@ class Filesystem return $size; } + /** + * @return ProcessExecutor + */ protected function getProcess() { return $this->processExecutor; diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 27bfe766f..91da871da 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -248,6 +248,10 @@ class Git public function syncMirror($url, $dir) { + if (getenv('COMPOSER_DISABLE_NETWORK')) { + return false; + } + // update the repo if it is a valid git repository if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { try { @@ -370,27 +374,16 @@ class Git return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')'; } - public static function sanitizeUrl($message) - { - return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { - return '://***:***@'; - } - - return '://' . $m[1] . ':***@'; - }, $message); - } - private function throwException($message, $url) { // git might delete a directory when it fails and php will not know clearstatcache(); if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } - throw new \RuntimeException(self::sanitizeUrl($message)); + throw new \RuntimeException(Url::sanitize($message)); } /** diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 1eca1a9bb..d6bfb62ca 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -22,10 +22,14 @@ use Composer\Downloader\TransportException; */ class GitHub { + /** @var IOInterface */ protected $io; + /** @var Config */ protected $config; + /** @var ProcessExecutor */ protected $process; - protected $remoteFilesystem; + /** @var HttpDownloader */ + protected $httpDownloader; /** * Constructor. @@ -33,14 +37,14 @@ class GitHub * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ - public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** @@ -104,7 +108,7 @@ class GitHub try { $apiUrl = ('github.com' === $originUrl) ? 'api.github.com/' : $originUrl . '/api/v3/'; - $this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl, false, array( + $this->httpDownloader->get('https://'. $apiUrl, array( 'retry-auth-failure' => false, )); } catch (TransportException $e) { diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index 7a69ad251..fb2489b01 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -23,10 +23,14 @@ use Composer\Json\JsonFile; */ class GitLab { + /** @var IOInterface */ protected $io; + /** @var Config */ protected $config; + /** @var ProcessExecutor */ protected $process; - protected $remoteFilesystem; + /** @var HttpDownloader */ + protected $httpDownloader; /** * Constructor. @@ -34,14 +38,14 @@ class GitLab * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ - public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** @@ -163,10 +167,10 @@ class GitLab ), ); - $json = $this->remoteFilesystem->getContents($originUrl, $scheme.'://'.$apiUrl.'/oauth/token', false, $options); + $token = $this->httpDownloader->get($scheme.'://'.$apiUrl.'/oauth/token', $options)->decodeJson(); $this->io->writeError('Token successfully created'); - return JsonFile::parseJson($json); + return $token; } } diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 3681ad5c7..d0b7fe79f 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -72,23 +72,12 @@ class Hg $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); } - public static function sanitizeUrl($message) - { - return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { - return '://***:***@'; - } - - return '://' . $m[1] . ':***@'; - }, $message); - } - private function throwException($message, $url) { if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { - throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } - throw new \RuntimeException(self::sanitizeUrl($message)); + throw new \RuntimeException(Url::sanitize($message)); } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php new file mode 100644 index 000000000..017b2d1a2 --- /dev/null +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -0,0 +1,464 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +use Composer\Config; +use Composer\IO\IOInterface; +use Composer\Downloader\TransportException; +use Composer\CaBundle\CaBundle; +use Composer\Util\StreamContextFactory; +use Composer\Util\AuthHelper; +use Composer\Util\Url; +use Composer\Util\HttpDownloader; +use React\Promise\Promise; + +/** + * @author Jordi Boggiano + * @author Nicolas Grekas + */ +class CurlDownloader +{ + private $multiHandle; + private $shareHandle; + private $jobs = array(); + /** @var IOInterface */ + private $io; + /** @var Config */ + private $config; + /** @var AuthHelper */ + private $authHelper; + private $selectTimeout = 5.0; + private $maxRedirects = 20; + protected $multiErrors = array( + CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), + CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."), + CURLM_OUT_OF_MEMORY => array('CURLM_OUT_OF_MEMORY', 'You are doomed.'), + CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!') + ); + + private static $options = array( + 'http' => array( + 'method' => CURLOPT_CUSTOMREQUEST, + 'content' => CURLOPT_POSTFIELDS, + 'header' => CURLOPT_HTTPHEADER, + ), + 'ssl' => array( + 'cafile' => CURLOPT_CAINFO, + 'capath' => CURLOPT_CAPATH, + ), + ); + + private static $timeInfo = array( + 'total_time' => true, + 'namelookup_time' => true, + 'connect_time' => true, + 'pretransfer_time' => true, + 'starttransfer_time' => true, + 'redirect_time' => true, + ); + + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) + { + $this->io = $io; + $this->config = $config; + + $this->multiHandle = $mh = curl_multi_init(); + if (function_exists('curl_multi_setopt')) { + curl_multi_setopt($mh, CURLMOPT_PIPELINING, PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); + if (defined('CURLMOPT_MAX_HOST_CONNECTIONS')) { + curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8); + } + } + + if (function_exists('curl_share_init')) { + $this->shareHandle = $sh = curl_share_init(); + curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); + curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + } + + $this->authHelper = new AuthHelper($io, $config); + } + + public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) + { + $attributes = array(); + if (isset($options['retry-auth-failure'])) { + $attributes['retryAuthFailure'] = $options['retry-auth-failure']; + unset($options['retry-auth-failure']); + } + + return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes); + } + + private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) + { + $attributes = array_merge(array( + 'retryAuthFailure' => true, + 'redirects' => 0, + 'storeAuth' => false, + ), $attributes); + + $originalOptions = $options; + + // check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 + if (!preg_match('{^http://(repo\.)?packagist\.org/p/}', $url) || (false === strpos($url, '$') && false === strpos($url, '%24'))) { + $this->config->prohibitUrlByConfig($url, $this->io); + } + + $curlHandle = curl_init(); + $headerHandle = fopen('php://temp/maxmemory:32768', 'w+b'); + + if ($copyTo) { + $errorMessage = ''; + set_error_handler(function ($code, $msg) use (&$errorMessage) { + if ($errorMessage) { + $errorMessage .= "\n"; + } + $errorMessage .= preg_replace('{^fopen\(.*?\): }', '', $msg); + }); + $bodyHandle = fopen($copyTo.'~', 'w+b'); + restore_error_handler(); + if (!$bodyHandle) { + throw new TransportException('The "'.$url.'" file could not be written to '.$copyTo.': '.$errorMessage); + } + } else { + $bodyHandle = @fopen('php://temp/maxmemory:524288', 'w+b'); + } + + curl_setopt($curlHandle, CURLOPT_URL, $url); + curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false); + //curl_setopt($curlHandle, CURLOPT_DNS_USE_GLOBAL_CACHE, false); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curlHandle, CURLOPT_TIMEOUT, 60); + curl_setopt($curlHandle, CURLOPT_WRITEHEADER, $headerHandle); + curl_setopt($curlHandle, CURLOPT_FILE, $bodyHandle); + curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip"); + curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS); + if (defined('CURLOPT_SSL_FALSESTART')) { + curl_setopt($curlHandle, CURLOPT_SSL_FALSESTART, true); + } + if (function_exists('curl_share_init')) { + curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle); + } + + if (!isset($options['http']['header'])) { + $options['http']['header'] = array(); + } + + $options['http']['header'] = array_diff($options['http']['header'], array('Connection: close')); + $options['http']['header'][] = 'Connection: keep-alive'; + + $version = curl_version(); + $features = $version['features']; + if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { + curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); + } + + $options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url); + $options = StreamContextFactory::initOptions($url, $options); + + foreach (self::$options as $type => $curlOptions) { + foreach ($curlOptions as $name => $curlOption) { + if (isset($options[$type][$name])) { + curl_setopt($curlHandle, $curlOption, $options[$type][$name]); + } + } + } + + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); + + $this->jobs[(int) $curlHandle] = array( + 'url' => $url, + 'origin' => $origin, + 'attributes' => $attributes, + 'options' => $originalOptions, + 'progress' => $progress, + 'curlHandle' => $curlHandle, + 'filename' => $copyTo, + 'headerHandle' => $headerHandle, + 'bodyHandle' => $bodyHandle, + 'resolve' => $resolve, + 'reject' => $reject, + ); + + $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; + $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : ''; + if ($attributes['redirects'] === 0) { + $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); + } + + $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); +// TODO progress + //$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); + } + + public function tick() + { + if (!$this->jobs) { + return; + } + + $active = true; + $this->checkCurlResult(curl_multi_exec($this->multiHandle, $active)); + if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) { + // sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select + usleep(150); + } + + while ($progress = curl_multi_info_read($this->multiHandle)) { + $curlHandle = $progress['handle']; + $i = (int) $curlHandle; + if (!isset($this->jobs[$i])) { + continue; + } + + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); + $job = $this->jobs[$i]; + unset($this->jobs[$i]); + curl_multi_remove_handle($this->multiHandle, $curlHandle); + $error = curl_error($curlHandle); + $errno = curl_errno($curlHandle); + curl_close($curlHandle); + + $headers = null; + $statusCode = null; + $response = null; + try { +// TODO progress + //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); + if (CURLE_OK !== $errno || $error) { + throw new TransportException($error); + } + + $statusCode = $progress['http_code']; + rewind($job['headerHandle']); + $headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle']))); + fclose($job['headerHandle']); + + // prepare response object + if ($job['filename']) { + $contents = $job['filename'].'~'; + if ($statusCode >= 300) { + rewind($job['bodyHandle']); + $contents = stream_get_contents($job['bodyHandle']); + } + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); + $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); + } else { + rewind($job['bodyHandle']); + $contents = stream_get_contents($job['bodyHandle']); + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); + $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); + } + fclose($job['bodyHandle']); + + if ($response->getStatusCode() >= 400 && $response->getHeader('content-type') === 'application/json') { + HttpDownloader::outputWarnings($this->io, $job['origin'], json_decode($response->getBody(), true)); + } + + $result = $this->isAuthenticatedRetryNeeded($job, $response); + if ($result['retry']) { + $this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth'])); + continue; + } + + // handle 3xx redirects, 304 Not Modified is excluded + if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) { + $location = $this->handleRedirect($job, $response); + if ($location) { + $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); + continue; + } + } + + // 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']) { + $this->authHelper->storeAuth($job['origin'], $job['attributes']['storeAuth']); + } + + // resolve promise + if ($job['filename']) { + rename($job['filename'].'~', $job['filename']); + call_user_func($job['resolve'], $response); + } else { + call_user_func($job['resolve'], $response); + } + } catch (\Exception $e) { + if ($e instanceof TransportException && $headers) { + $e->setHeaders($headers); + $e->setStatusCode($statusCode); + } + if ($e instanceof TransportException && $response) { + $e->setResponse($response->getBody()); + } + + if (is_resource($job['headerHandle'])) { + fclose($job['headerHandle']); + } + if (is_resource($job['bodyHandle'])) { + fclose($job['bodyHandle']); + } + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + call_user_func($job['reject'], $e); + } + } + + foreach ($this->jobs as $i => $curlHandle) { + if (!isset($this->jobs[$i])) { + continue; + } + $curlHandle = $this->jobs[$i]['curlHandle']; + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); + + if ($this->jobs[$i]['progress'] !== $progress) { + $previousProgress = $this->jobs[$i]['progress']; + $this->jobs[$i]['progress'] = $progress; + + // TODO + //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); + } + } + } + + private function handleRedirect(array $job, Response $response) + { + if ($locationHeader = $response->getHeader('location')) { + if (parse_url($locationHeader, PHP_URL_SCHEME)) { + // Absolute URL; e.g. https://example.com/composer + $targetUrl = $locationHeader; + } elseif (parse_url($locationHeader, PHP_URL_HOST)) { + // Scheme relative; e.g. //example.com/foo + $targetUrl = parse_url($job['url'], PHP_URL_SCHEME).':'.$locationHeader; + } elseif ('/' === $locationHeader[0]) { + // Absolute path; e.g. /foo + $urlHost = parse_url($job['url'], PHP_URL_HOST); + + // Replace path using hostname as an anchor. + $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $job['url']); + } else { + // Relative path; e.g. foo + // This actually differs from PHP which seems to add duplicate slashes. + $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $job['url']); + } + } + + if (!empty($targetUrl)) { + $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); + + return $targetUrl; + } + + throw new TransportException('The "'.$job['url'].'" file could not be downloaded, got redirect without Location ('.$response->getStatusMessage().')'); + } + + private function isAuthenticatedRetryNeeded(array $job, Response $response) + { + if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders()); + + if ($result['retry']) { + return $result; + } + } + + $locationHeader = $response->getHeader('location'); + $needsAuthRetry = false; + + // check for bitbucket login page asking to authenticate + if ( + $job['origin'] === 'bitbucket.org' + && !$this->authHelper->isPublicBitBucketDownload($job['url']) + && substr($job['url'], -4) === '.zip' + && (!$locationHeader || substr($locationHeader, -4) !== '.zip') + && preg_match('{^text/html\b}i', $response->getHeader('content-type')) + ) { + $needsAuthRetry = 'Bitbucket requires authentication and it was not provided'; + } + + // check for gitlab 404 when downloading archives + if ( + $response->getStatusCode() === 404 + && $this->config && in_array($job['origin'], $this->config->get('gitlab-domains'), true) + && false !== strpos($job['url'], 'archive.zip') + ) { + $needsAuthRetry = 'GitLab requires authentication and it was not provided'; + } + + if ($needsAuthRetry) { + if ($job['attributes']['retryAuthFailure']) { + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401); + if ($result['retry']) { + return $result; + } + } + + throw $this->failResponse($job, $response, $needsAuthRetry); + } + + return array('retry' => false, 'storeAuth' => false); + } + + private function restartJob(array $job, $url, array $attributes = array()) + { + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + + $attributes = array_merge($job['attributes'], $attributes); + $origin = Url::getOrigin($this->config, $url); + + $this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['options'], $job['filename'], $attributes); + } + + private function failResponse(array $job, Response $response, $errorMessage) + { + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + + return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')', $response->getStatusCode()); + } + + private function onProgress($curlHandle, callable $notify, array $progress, array $previousProgress) + { + // TODO add support for progress + if (300 <= $progress['http_code'] && $progress['http_code'] < 400) { + return; + } + if ($previousProgress['download_content_length'] < $progress['download_content_length']) { + $notify(STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false); + } + if ($previousProgress['size_download'] < $progress['size_download']) { + $notify(STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['download_content_length'], false); + } + } + + private function checkCurlResult($code) + { + if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) { + throw new \RuntimeException(isset($this->multiErrors[$code]) + ? "cURL error: {$code} ({$this->multiErrors[$code][0]}): cURL message: {$this->multiErrors[$code][1]}" + : 'Unexpected cURL error: ' . $code + ); + } + } +} diff --git a/src/Composer/Util/Http/Response.php b/src/Composer/Util/Http/Response.php new file mode 100644 index 000000000..1b4581331 --- /dev/null +++ b/src/Composer/Util/Http/Response.php @@ -0,0 +1,104 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +use Composer\Json\JsonFile; + +class Response +{ + private $request; + private $code; + private $headers; + private $body; + + public function __construct(array $request, $code, array $headers, $body) + { + if (!isset($request['url'])) { + throw new \LogicException('url key missing from request array'); + } + $this->request = $request; + $this->code = (int) $code; + $this->headers = $headers; + $this->body = $body; + } + + public function getStatusCode() + { + return $this->code; + } + + /** + * @return string|null + */ + public function getStatusMessage() + { + $value = null; + foreach ($this->headers as $header) { + if (preg_match('{^HTTP/\S+ \d+}i', $header)) { + // In case of redirects, headers contain the headers of all responses + // so we can not return directly and need to keep iterating + $value = $header; + } + } + + return $value; + } + + public function getHeaders() + { + return $this->headers; + } + + public function getHeader($name) + { + return self::findHeaderValue($this->headers, $name); + } + + public function getBody() + { + return $this->body; + } + + public function decodeJson() + { + return JsonFile::parseJson($this->body, $this->request['url']); + } + + public function collect() + { + $this->request = $this->code = $this->headers = $this->body = null; + } + + /** + * @param array $headers array of returned headers like from getLastHeaders() + * @param string $name header name (case insensitive) + * @return string|null + */ + public static function findHeaderValue(array $headers, $name) + { + $value = null; + foreach ($headers as $header) { + if (preg_match('{^'.preg_quote($name).':\s*(.+?)\s*$}i', $header, $match)) { + $value = $match[1]; + } elseif (preg_match('{^HTTP/}i', $header)) { + // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary + // + // In case of redirects, http_response_headers contains the headers of all responses + // so we reset the flag when a new response is being parsed as we are only interested in the last response + $value = null; + } + } + + return $value; + } +} diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php new file mode 100644 index 000000000..8a117a232 --- /dev/null +++ b/src/Composer/Util/HttpDownloader.php @@ -0,0 +1,339 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Config; +use Composer\IO\IOInterface; +use Composer\Downloader\TransportException; +use Composer\CaBundle\CaBundle; +use Composer\Util\Http\Response; +use Composer\Composer; +use Composer\Package\Version\VersionParser; +use Composer\Semver\Constraint\Constraint; +use React\Promise\Promise; + +/** + * @author Jordi Boggiano + */ +class HttpDownloader +{ + const STATUS_QUEUED = 1; + const STATUS_STARTED = 2; + const STATUS_COMPLETED = 3; + const STATUS_FAILED = 4; + + private $io; + private $config; + private $jobs = array(); + private $options = array(); + private $runningJobs = 0; + private $maxJobs = 10; + private $lastProgress; + private $disableTls = false; + private $curl; + private $rfs; + private $idGen = 0; + private $disabled; + + /** + * @param IOInterface $io The IO instance + * @param Config $config The config + * @param array $options The options + * @param bool $disableTls + */ + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) + { + $this->io = $io; + + $this->disabled = (bool) getenv('COMPOSER_DISABLE_NETWORK'); + + // Setup TLS options + // The cafile option can be set via config.json + if ($disableTls === false) { + $this->options = StreamContextFactory::getTlsDefaults($options, $io); + } else { + $this->disableTls = true; + } + + // handle the other externally set options normally. + $this->options = array_replace_recursive($this->options, $options); + $this->config = $config; + + // TODO enable curl only on 5.6+ if older versions cause any problem + if (extension_loaded('curl')) { + $this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls); + } + + $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls); + } + + public function get($url, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true); + $this->wait($job['id']); + + return $this->getResponse($job['id']); + } + + public function add($url, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false)); + + return $promise; + } + + public function copy($url, $to, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true); + $this->wait($job['id']); + + return $this->getResponse($job['id']); + } + + public function addCopy($url, $to, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to)); + + return $promise; + } + + /** + * Retrieve the options set in the constructor + * + * @return array Options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Merges new options + * + * @return void + */ + public function setOptions(array $options) + { + $this->options = array_replace_recursive($this->options, $options); + } + + private function addJob($request, $sync = false) + { + $job = array( + 'id' => $this->idGen++, + 'status' => self::STATUS_QUEUED, + 'request' => $request, + 'sync' => $sync, + 'origin' => Url::getOrigin($this->config, $request['url']), + ); + + // 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])); + } + + $rfs = $this->rfs; + + if ($this->curl && preg_match('{^https?://}i', $job['request']['url'])) { + $resolver = function ($resolve, $reject) use (&$job) { + $job['status'] = HttpDownloader::STATUS_QUEUED; + $job['resolve'] = $resolve; + $job['reject'] = $reject; + }; + } else { + $resolver = function ($resolve, $reject) use (&$job, $rfs) { + // start job + $url = $job['request']['url']; + $options = $job['request']['options']; + + $job['status'] = HttpDownloader::STATUS_STARTED; + + if ($job['request']['copyTo']) { + $result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options); + + $headers = $rfs->getLastHeaders(); + $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $job['request']['copyTo'].'~'); + + $resolve($response); + } else { + $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options); + $headers = $rfs->getLastHeaders(); + $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body); + + $resolve($response); + } + }; + } + + $downloader = $this; + $io = $this->io; + + $canceler = function () {}; + + $promise = new Promise($resolver, $canceler); + $promise->then(function ($response) use (&$job, $downloader) { + $job['status'] = HttpDownloader::STATUS_COMPLETED; + $job['response'] = $response; + + // 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) { + $job['status'] = HttpDownloader::STATUS_FAILED; + $job['exception'] = $e; + + $downloader->markJobDone(); + $downloader->scheduleNextJob(); + + throw $e; + }); + $this->jobs[$job['id']] =& $job; + + if ($this->runningJobs < $this->maxJobs) { + $this->startJob($job['id']); + } + + return array($job, $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++; + + $resolve = $job['resolve']; + $reject = $job['reject']; + $url = $job['request']['url']; + $options = $job['request']['options']; + $origin = $job['origin']; + + if ($this->disabled) { + if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) { + $resolve(new Response(array('url' => $url), 304, array(), '')); + } else { + $e = new TransportException('Network disabled, request canceled: '.$url, 499); + $e->setStatusCode(499); + $reject($e); + } + return; + } + + if ($job['request']['copyTo']) { + $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); + } else { + $this->curl->download($resolve, $reject, $origin, $url, $options); + } + } + + /** + * @private + */ + public function markJobDone() + { + $this->runningJobs--; + } + + /** + * @private + */ + public function scheduleNextJob() + { + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_QUEUED) { + $this->startJob($job['id']); + if ($this->runningJobs >= $this->maxJobs) { + return; + } + } + } + } + + public function wait($index = null, $progress = false) + { + while (true) { + 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; + } + } 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); + } + } + + private function getResponse($index) + { + if (!isset($this->jobs[$index])) { + throw new \LogicException('Invalid request id'); + } + + if ($this->jobs[$index]['status'] === self::STATUS_FAILED) { + throw $this->jobs[$index]['exception']; + } + + if (!isset($this->jobs[$index]['response'])) { + throw new \LogicException('Response not available yet, call wait() first'); + } + + $resp = $this->jobs[$index]['response']; + + unset($this->jobs[$index]); + + return $resp; + } + + public static function outputWarnings(IOInterface $io, $url, $data) + { + foreach (array('warning', 'info') as $type) { + if (empty($data[$type])) { + continue; + } + + if (!empty($data[$type . '-versions'])) { + $versionParser = new VersionParser(); + $constraint = $versionParser->parseConstraints($data[$type . '-versions']); + $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion())); + if (!$constraint->matches($composer)) { + continue; + } + } + + $io->writeError('<'.$type.'>'.ucfirst($type).' from '.$url.': '.$data[$type].''); + } + } +} diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php new file mode 100644 index 000000000..dfaa2ac53 --- /dev/null +++ b/src/Composer/Util/Loop.php @@ -0,0 +1,48 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Util\HttpDownloader; +use React\Promise\Promise; + +/** + * @author Jordi Boggiano + */ +class Loop +{ + private $httpDownloader; + + public function __construct(HttpDownloader $httpDownloader) + { + $this->httpDownloader = $httpDownloader; + } + + public function wait(array $promises) + { + /** @var \Exception|null */ + $uncaught = null; + + \React\Promise\all($promises)->then( + function () { }, + function ($e) use (&$uncaught) { + $uncaught = $e; + } + ); + + $this->httpDownloader->wait(); + + if ($uncaught) { + throw $uncaught; + } + } +} diff --git a/src/Composer/Util/MetadataMinifier.php b/src/Composer/Util/MetadataMinifier.php new file mode 100644 index 000000000..ba4cc0a93 --- /dev/null +++ b/src/Composer/Util/MetadataMinifier.php @@ -0,0 +1,78 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +class MetadataMinifier +{ + public static function expand(array $versions) + { + $expanded = array(); + $expandedVersion = null; + foreach ($versions as $versionData) { + if (!$expandedVersion) { + $expandedVersion = $versionData; + $expanded[] = $expandedVersion; + continue; + } + + // add any changes from the previous version to the expanded one + foreach ($versionData as $key => $val) { + if ($val === '__unset') { + unset($expandedVersion[$key]); + } else { + $expandedVersion[$key] = $val; + } + } + + $expanded[] = $expandedVersion; + } + + return $expanded; + } + + public static function minify(array $versions) + { + $minifiedVersions = array(); + + $lastKnownVersionData = null; + foreach ($versions as $version) { + if (!$lastKnownVersionData) { + $lastKnownVersionData = $version; + $minifiedVersions[] = $version; + continue; + } + + $minifiedVersion = array(); + + // add any changes from the previous version + foreach ($version as $key => $val) { + if (!isset($lastKnownVersionData[$key]) || $lastKnownVersionData[$key] !== $val) { + $minifiedVersion[$key] = $val; + $lastKnownVersionData[$key] = $val; + } + } + + // store any deletions from the previous version for keys missing in current one + foreach ($lastKnownVersionData as $key => $val) { + if (!isset($version[$key])) { + $minifiedVersion[$key] = "__unset"; + unset($lastKnownVersionData[$key]); + } + } + + $minifiedVersions[] = $minifiedVersion; + } + + return $minifiedVersions; + } +} diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 83f19cf2d..a30a04d15 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -15,6 +15,7 @@ namespace Composer\Util; use Composer\IO\IOInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessUtils; +use Symfony\Component\Process\Exception\RuntimeException; /** * @author Robert Schönthal @@ -42,6 +43,27 @@ class ProcessExecutor * @return int statuscode */ public function execute($command, &$output = null, $cwd = null) + { + if (func_num_args() > 1) { + return $this->doExecute($command, $cwd, false, $output); + } + + return $this->doExecute($command, $cwd, false); + } + + /** + * runs a process on the commandline in TTY mode + * + * @param string $command the command to execute + * @param string $cwd the working directory + * @return int statuscode + */ + public function executeTty($command, $cwd = null) + { + return $this->doExecute($command, $cwd, true); + } + + private function doExecute($command, $cwd, $tty, &$output = null) { if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) { @@ -61,7 +83,7 @@ class ProcessExecutor $cwd = realpath(getcwd()); } - $this->captureOutput = func_num_args() > 1; + $this->captureOutput = func_num_args() > 3; $this->errorOutput = null; // TODO in v3, commands should be passed in as arrays of cmd + args @@ -70,6 +92,13 @@ class ProcessExecutor } else { $process = new Process($command, $cwd, null, null, static::getTimeout()); } + if (!Platform::isWindows() && $tty) { + try { + $process->setTty(true); + } catch (RuntimeException $e) { + // ignore TTY enabling errors + } + } $callback = is_callable($output) ? $output : array($this, 'outputHandler'); $process->run($callback); @@ -112,18 +141,10 @@ class ProcessExecutor return; } - if (method_exists($this->io, 'writeRaw')) { - if (Process::ERR === $type) { - $this->io->writeErrorRaw($buffer, false); - } else { - $this->io->writeRaw($buffer, false); - } + if (Process::ERR === $type) { + $this->io->writeErrorRaw($buffer, false); } else { - if (Process::ERR === $type) { - $this->io->writeError($buffer, false); - } else { - $this->io->write($buffer, false); - } + $this->io->writeRaw($buffer, false); } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 9328d5bdd..6312deabc 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -13,13 +13,11 @@ namespace Composer\Util; use Composer\Config; -use Composer\Composer; -use Composer\Semver\Constraint\Constraint; -use Composer\Package\Version\VersionParser; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; -use Psr\Log\LoggerInterface; +use Composer\Util\HttpDownloader; +use Composer\Util\Http\Response; /** * @author François Pluchino @@ -44,10 +42,10 @@ class RemoteFilesystem private $retryAuthFailure; private $lastHeaders; private $storeAuth; + private $authHelper; private $degradedMode = false; private $redirects; private $maxRedirects = 20; - private $displayedOriginAuthentications = array(); /** * Constructor. @@ -57,14 +55,14 @@ class RemoteFilesystem * @param array $options The options * @param bool $disableTls */ - public function __construct(IOInterface $io, Config $config = null, array $options = array(), $disableTls = false) + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) { $this->io = $io; // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { - $this->options = $this->getTlsDefaults($options); + $this->options = StreamContextFactory::getTlsDefaults($options, $io); } else { $this->disableTls = true; } @@ -72,6 +70,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); } /** @@ -145,32 +144,11 @@ class RemoteFilesystem return $this->lastHeaders; } - /** - * @param array $headers array of returned headers like from getLastHeaders() - * @param string $name header name (case insensitive) - * @return string|null - */ - public function findHeaderValue(array $headers, $name) - { - $value = null; - foreach ($headers as $header) { - if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) { - $value = $match[1]; - } elseif (preg_match('{^HTTP/}i', $header)) { - // In case of redirects, http_response_headers contains the headers of all responses - // so we reset the flag when a new response is being parsed as we are only interested in the last response - $value = null; - } - } - - return $value; - } - /** * @param array $headers array of returned headers like from getLastHeaders() * @return int|null */ - public function findStatusCode(array $headers) + public static function findStatusCode(array $headers) { $value = null; foreach ($headers as $header) { @@ -218,27 +196,6 @@ class RemoteFilesystem */ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) { - if (strpos($originUrl, '.github.com') === (strlen($originUrl) - 11)) { - $originUrl = 'github.com'; - } - - // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl - // is the host without the path, so we look for the registered gitlab-domains matching the host here - if ( - $this->config - && is_array($this->config->get('gitlab-domains')) - && false === strpos($originUrl, '/') - && !in_array($originUrl, $this->config->get('gitlab-domains')) - ) { - foreach ($this->config->get('gitlab-domains') as $gitlabDomain) { - if (0 === strpos($gitlabDomain, $originUrl)) { - $originUrl = $gitlabDomain; - break; - } - } - unset($gitlabDomain); - } - $this->scheme = parse_url($fileUrl, PHP_URL_SCHEME); $this->bytesMax = 0; $this->originUrl = $originUrl; @@ -250,11 +207,6 @@ class RemoteFilesystem $this->lastHeaders = array(); $this->redirects = 1; // The first request counts. - // capture username/password from URL if there is one - if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $fileUrl, $match)) { - $this->io->setAuthentication($originUrl, rawurldecode($match[1]), rawurldecode($match[2])); - } - $tempAdditionalOptions = $additionalOptions; if (isset($tempAdditionalOptions['retry-auth-failure'])) { $this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure']; @@ -275,14 +227,6 @@ class RemoteFilesystem $origFileUrl = $fileUrl; - if (isset($options['github-token'])) { - // only add the access_token if it is actually a github URL (in case we were redirected to S3) - if (preg_match('{^https?://([a-z0-9-]+\.)*github\.com/}', $fileUrl)) { - $options['http']['header'][] = 'Authorization: token '.$options['github-token']; - } - unset($options['github-token']); - } - if (isset($options['gitlab-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token']; unset($options['gitlab-token']); @@ -302,7 +246,7 @@ class RemoteFilesystem $actualContextOptions = stream_context_get_options($ctx); $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : ''; - $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $this->stripCredentialsFromUrl($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); + $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $actualContextOptions); // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 @@ -330,16 +274,16 @@ class RemoteFilesystem if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); - if ($statusCode >= 400 && $this->findHeaderValue($http_response_header, 'content-type') === 'application/json') { - self::outputWarnings($this->io, $originUrl, json_decode($result, true)); + if ($statusCode >= 400 && Response::findHeaderValue($http_response_header, 'content-type') === 'application/json') { + HttpDownloader::outputWarnings($this->io, $originUrl, json_decode($result, true)); } if (in_array($statusCode, array(401, 403)) && $this->retryAuthFailure) { - $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), null, $http_response_header); + $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), $http_response_header); } } - $contentLength = !empty($http_response_header[0]) ? $this->findHeaderValue($http_response_header, 'content-length') : null; + $contentLength = !empty($http_response_header[0]) ? Response::findHeaderValue($http_response_header, 'content-length') : null; if ($contentLength && Platform::strlen($result) < $contentLength) { // alas, this is not possible via the stream callback because STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP $e = new TransportException('Content-Length mismatch, received '.Platform::strlen($result).' bytes out of the expected '.$contentLength); @@ -396,13 +340,13 @@ class RemoteFilesystem $locationHeader = null; if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); - $contentType = $this->findHeaderValue($http_response_header, 'content-type'); - $locationHeader = $this->findHeaderValue($http_response_header, 'location'); + $contentType = Response::findHeaderValue($http_response_header, 'content-type'); + $locationHeader = Response::findHeaderValue($http_response_header, 'location'); } // check for bitbucket login page asking to authenticate if ($originUrl === 'bitbucket.org' - && !$this->isPublicBitBucketDownload($fileUrl) + && !$this->authHelper->isPublicBitBucketDownload($fileUrl) && substr($fileUrl, -4) === '.zip' && (!$locationHeader || substr($locationHeader, -4) !== '.zip') && $contentType && preg_match('{^text/html\b}i', $contentType) @@ -434,7 +378,7 @@ class RemoteFilesystem // fail 4xx and 5xx responses and capture the response if ($statusCode && $statusCode >= 400 && $statusCode <= 599) { if (!$this->retry) { - if ($this->progress && !$this->retry && !$isRedirect) { + if ($this->progress && !$isRedirect) { $this->io->overwriteError("Downloading (failed)", false); } @@ -453,7 +397,7 @@ class RemoteFilesystem // decode gzip if ($result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http' && !$hasFollowedRedirect) { - $contentEncoding = $this->findHeaderValue($http_response_header, 'content-encoding'); + $contentEncoding = Response::findHeaderValue($http_response_header, 'content-encoding'); $decode = $contentEncoding && 'gzip' === strtolower($contentEncoding); if ($decode) { @@ -548,8 +492,7 @@ class RemoteFilesystem $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); if ($this->storeAuth && $this->config) { - $authHelper = new AuthHelper($this->io, $this->config); - $authHelper->storeAuth($this->originUrl, $this->storeAuth); + $this->authHelper->storeAuth($this->originUrl, $this->storeAuth); $this->storeAuth = false; } @@ -652,109 +595,16 @@ class RemoteFilesystem } } - protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array()) + protected function promptAuthAndRetry($httpStatus, $reason = null, $headers = array()) { - if ($this->config && in_array($this->originUrl, $this->config->get('github-domains'), true)) { - $gitHubUtil = new GitHub($this->io, $this->config, null); - $message = "\n"; + $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $headers); - $rateLimited = $gitHubUtil->isRateLimited($headers); - if ($rateLimited) { - $rateLimit = $gitHubUtil->getRateLimit($headers); - if ($this->io->hasAuthentication($this->originUrl)) { - $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.'; - } else { - $message = 'Create a GitHub OAuth token to go over the API rate limit.'; - } + $this->storeAuth = $result['storeAuth']; + $this->retry = $result['retry']; - $message = sprintf( - 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$this->fileUrl.'. '.$message.' You can also wait until %s for the rate limit to reset.', - $rateLimit['limit'], - $rateLimit['reset'] - )."\n"; - } else { - $message .= 'Could not fetch '.$this->fileUrl.', please '; - if ($this->io->hasAuthentication($this->originUrl)) { - $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos'; - } else { - $message .= 'create a GitHub OAuth token to access private repos'; - } - } - - if (!$gitHubUtil->authorizeOAuth($this->originUrl) - && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against '.$this->originUrl, 401); - } - } elseif ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) { - $message = "\n".'Could not fetch '.$this->fileUrl.', enter your ' . $this->originUrl . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit'); - $gitLabUtil = new GitLab($this->io, $this->config, null); - - if ($this->io->hasAuthentication($this->originUrl) && ($auth = $this->io->getAuthentication($this->originUrl)) && in_array($auth['password'], array('gitlab-ci-token', 'private-token', 'oauth2'), true)) { - throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus); - } - - if (!$gitLabUtil->authorizeOAuth($this->originUrl) - && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against '.$this->originUrl, 401); - } - } elseif ($this->config && $this->originUrl === 'bitbucket.org') { - $askForOAuthToken = true; - if ($this->io->hasAuthentication($this->originUrl)) { - $auth = $this->io->getAuthentication($this->originUrl); - if ($auth['username'] !== 'x-token-auth') { - $bitbucketUtil = new Bitbucket($this->io, $this->config); - $accessToken = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']); - if (!empty($accessToken)) { - $this->io->setAuthentication($this->originUrl, 'x-token-auth', $accessToken); - $askForOAuthToken = false; - } - } else { - throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); - } - } - - if ($askForOAuthToken) { - $message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to ' . (($httpStatus === 401 || $httpStatus === 403) ? 'access private repos' : 'go over the API rate limit'); - $bitBucketUtil = new Bitbucket($this->io, $this->config); - if (! $bitBucketUtil->authorizeOAuth($this->originUrl) - && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); - } - } - } else { - // 404s are only handled for github - if ($httpStatus === 404) { - return; - } - - // fail if the console is not interactive - if (!$this->io->isInteractive()) { - if ($httpStatus === 401) { - $message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console to authenticate"; - } - if ($httpStatus === 403) { - $message = "The '" . $this->fileUrl . "' URL could not be accessed: " . $reason; - } - - throw new TransportException($message, $httpStatus); - } - // fail if we already have auth - if ($this->io->hasAuthentication($this->originUrl)) { - throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus); - } - - $this->io->writeError(' Authentication required ('.$this->originUrl.'):'); - $username = $this->io->ask(' Username: '); - $password = $this->io->askAndHideAnswer(' Password: '); - $this->io->setAuthentication($this->originUrl, $username, $password); - $this->storeAuth = $this->config->get('store-auths'); + if ($this->retry) { + throw new TransportException('RETRY'); } - - $this->retry = true; - throw new TransportException('RETRY'); } protected function getOptionsForUrl($originUrl, $additionalOptions) @@ -814,40 +664,7 @@ class RemoteFilesystem $headers[] = 'Connection: close'; } - if ($this->io->hasAuthentication($originUrl)) { - $authenticationDisplayMessage = null; - $auth = $this->io->getAuthentication($originUrl); - if ($auth['password'] === 'bearer') { - $headers[] = 'Authorization: Bearer '.$auth['username']; - } elseif ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) { - $options['github-token'] = $auth['username']; - $authenticationDisplayMessage = 'Using GitHub token authentication'; - } elseif ($this->config && in_array($originUrl, $this->config->get('gitlab-domains'), true)) { - if ($auth['password'] === 'oauth2') { - $headers[] = 'Authorization: Bearer '.$auth['username']; - $authenticationDisplayMessage = 'Using GitLab OAuth token authentication'; - } elseif ($auth['password'] === 'private-token' || $auth['password'] === 'gitlab-ci-token') { - $headers[] = 'PRIVATE-TOKEN: '.$auth['username']; - $authenticationDisplayMessage = 'Using GitLab private token authentication'; - } - } elseif ('bitbucket.org' === $originUrl - && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL && 'x-token-auth' === $auth['username'] - ) { - if (!$this->isPublicBitBucketDownload($this->fileUrl)) { - $headers[] = 'Authorization: Bearer ' . $auth['password']; - $authenticationDisplayMessage = 'Using Bitbucket OAuth token authentication'; - } - } else { - $authStr = base64_encode($auth['username'] . ':' . $auth['password']); - $headers[] = 'Authorization: Basic '.$authStr; - $authenticationDisplayMessage = 'Using HTTP basic authentication with username "' . $auth['username'] . '"'; - } - - if ($authenticationDisplayMessage && !in_array($originUrl, $this->displayedOriginAuthentications, true)) { - $this->io->writeError($authenticationDisplayMessage, true, IOInterface::DEBUG); - $this->displayedOriginAuthentications[] = $originUrl; - } - } + $headers = $this->authHelper->addAuthenticationHeader($headers, $originUrl, $this->fileUrl); $options['http']['follow_location'] = 0; @@ -863,7 +680,7 @@ class RemoteFilesystem private function handleRedirect(array $http_response_header, array $additionalOptions, $result) { - if ($locationHeader = $this->findHeaderValue($http_response_header, 'location')) { + if ($locationHeader = Response::findHeaderValue($http_response_header, 'location')) { if (parse_url($locationHeader, PHP_URL_SCHEME)) { // Absolute URL; e.g. https://example.com/composer $targetUrl = $locationHeader; @@ -887,7 +704,7 @@ class RemoteFilesystem $this->redirects++; $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $this->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); $additionalOptions['redirects'] = $this->redirects; @@ -905,111 +722,6 @@ class RemoteFilesystem return false; } - /** - * @param array $options - * - * @return array - */ - private function getTlsDefaults(array $options) - { - $ciphers = implode(':', array( - 'ECDHE-RSA-AES128-GCM-SHA256', - 'ECDHE-ECDSA-AES128-GCM-SHA256', - 'ECDHE-RSA-AES256-GCM-SHA384', - 'ECDHE-ECDSA-AES256-GCM-SHA384', - 'DHE-RSA-AES128-GCM-SHA256', - 'DHE-DSS-AES128-GCM-SHA256', - 'kEDH+AESGCM', - 'ECDHE-RSA-AES128-SHA256', - 'ECDHE-ECDSA-AES128-SHA256', - 'ECDHE-RSA-AES128-SHA', - 'ECDHE-ECDSA-AES128-SHA', - 'ECDHE-RSA-AES256-SHA384', - 'ECDHE-ECDSA-AES256-SHA384', - 'ECDHE-RSA-AES256-SHA', - 'ECDHE-ECDSA-AES256-SHA', - 'DHE-RSA-AES128-SHA256', - 'DHE-RSA-AES128-SHA', - 'DHE-DSS-AES128-SHA256', - 'DHE-RSA-AES256-SHA256', - 'DHE-DSS-AES256-SHA', - 'DHE-RSA-AES256-SHA', - 'AES128-GCM-SHA256', - 'AES256-GCM-SHA384', - 'AES128-SHA256', - 'AES256-SHA256', - 'AES128-SHA', - 'AES256-SHA', - 'AES', - 'CAMELLIA', - 'DES-CBC3-SHA', - '!aNULL', - '!eNULL', - '!EXPORT', - '!DES', - '!RC4', - '!MD5', - '!PSK', - '!aECDH', - '!EDH-DSS-DES-CBC3-SHA', - '!EDH-RSA-DES-CBC3-SHA', - '!KRB5-DES-CBC3-SHA', - )); - - /** - * CN_match and SNI_server_name are only known once a URL is passed. - * They will be set in the getOptionsForUrl() method which receives a URL. - * - * cafile or capath can be overridden by passing in those options to constructor. - */ - $defaults = array( - 'ssl' => array( - 'ciphers' => $ciphers, - 'verify_peer' => true, - 'verify_depth' => 7, - 'SNI_enabled' => true, - 'capture_peer_cert' => true, - ), - ); - - if (isset($options['ssl'])) { - $defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']); - } - - $caBundleLogger = $this->io instanceof LoggerInterface ? $this->io : null; - - /** - * Attempt to find a local cafile or throw an exception if none pre-set - * The user may go download one if this occurs. - */ - if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) { - $result = CaBundle::getSystemCaRootBundlePath($caBundleLogger); - - if (is_dir($result)) { - $defaults['ssl']['capath'] = $result; - } else { - $defaults['ssl']['cafile'] = $result; - } - } - - if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $caBundleLogger))) { - throw new TransportException('The configured cafile was not valid or could not be read.'); - } - - if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { - throw new TransportException('The configured capath was not valid or could not be read.'); - } - - /** - * Disable TLS compression to prevent CRIME attacks where supported. - */ - if (PHP_VERSION_ID >= 50413) { - $defaults['ssl']['disable_compression'] = true; - } - - return $defaults; - } - /** * Fetch certificate common name and fingerprint for validation of SAN. * @@ -1079,69 +791,4 @@ class RemoteFilesystem return parse_url($url, PHP_URL_HOST).':'.$port; } - - /** - * @link https://github.com/composer/composer/issues/5584 - * - * @param string $urlToBitBucketFile URL to a file at bitbucket.org. - * - * @return bool Whether the given URL is a public BitBucket download which requires no authentication. - */ - private function isPublicBitBucketDownload($urlToBitBucketFile) - { - $domain = parse_url($urlToBitBucketFile, PHP_URL_HOST); - if (strpos($domain, 'bitbucket.org') === false) { - // Bitbucket downloads are hosted on amazonaws. - // We do not need to authenticate there at all - return true; - } - - $path = parse_url($urlToBitBucketFile, PHP_URL_PATH); - - // Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever} - // {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/} - $pathParts = explode('/', $path); - - return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; - } - - public static function outputWarnings(IOInterface $io, $url, $data) - { - foreach (array('warning', 'info') as $type) { - if (empty($data[$type])) { - continue; - } - - if (!empty($data[$type . '-versions'])) { - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($data[$type . '-versions']); - $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion())); - if (!$constraint->matches($composer)) { - continue; - } - } - - $io->writeError('<'.$type.'>'.ucfirst($type).' from '.$url.': '.$data[$type].''); - } - } - - public static function getOrigin($urlOrPath) - { - $hostPort = parse_url($urlOrPath, PHP_URL_HOST); - if (!$hostPort) { - return $urlOrPath; - } - if (parse_url($urlOrPath, PHP_URL_PORT)) { - $hostPort .= ':'.parse_url($urlOrPath, PHP_URL_PORT); - } - - return $hostPort; - } - - private function stripCredentialsFromUrl($url) - { - // GitHub repository rename result in redirect locations containing the access_token as GET parameter - // e.g. https://api.github.com/repositories/9999999999?access_token=github_token - return preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); - } } diff --git a/src/Composer/Util/SpdxLicense.php b/src/Composer/Util/SpdxLicense.php deleted file mode 100644 index be4efdc54..000000000 --- a/src/Composer/Util/SpdxLicense.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Util; - -use Composer\Spdx\SpdxLicenses; - -trigger_error('The ' . __NAMESPACE__ . '\SpdxLicense class is deprecated, use Composer\Spdx\SpdxLicenses instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Spdx\SpdxLicenses instead - */ -class SpdxLicense extends SpdxLicenses -{ -} diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index da3e578bd..09291a908 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -13,6 +13,9 @@ namespace Composer\Util; use Composer\Composer; +use Composer\CaBundle\CaBundle; +use Composer\Downloader\TransportException; +use Psr\Log\LoggerInterface; /** * Allows the creation of a basic context supporting http proxy @@ -39,6 +42,32 @@ final class StreamContextFactory 'max_redirects' => 20, )); + $options = array_replace_recursive($options, self::initOptions($url, $defaultOptions)); + unset($defaultOptions['http']['header']); + $options = array_replace_recursive($options, $defaultOptions); + + if (isset($options['http']['header'])) { + $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); + } + + return stream_context_create($options, $defaultParams); + } + + /** + * @param string $url + * @param array $options + * @return array ['http' => ['header' => [...], 'proxy' => '..', 'request_fulluri' => bool]] formatted as a stream context array + */ + public static function initOptions($url, array $options) + { + // Make sure the headers are in an array form + if (!isset($options['http']['header'])) { + $options['http']['header'] = array(); + } + if (is_string($options['http']['header'])) { + $options['http']['header'] = explode("\r\n", $options['http']['header']); + } + // Handle HTTP_PROXY/http_proxy on CLI only for security reasons if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); @@ -85,15 +114,15 @@ final class StreamContextFactory // enabled request_fulluri unless it is explicitly disabled switch (parse_url($url, PHP_URL_SCHEME)) { - case 'http': // default request_fulluri to true + case 'http': // default request_fulluri to true for HTTP $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI'); if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { $options['http']['request_fulluri'] = true; } break; - case 'https': // default request_fulluri to true + case 'https': // default request_fulluri to false for HTTPS $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); - if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { + if (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv) { $options['http']['request_fulluri'] = true; } break; @@ -115,42 +144,139 @@ final class StreamContextFactory } $auth = base64_encode($auth); - // Preserve headers if already set in default options - if (isset($defaultOptions['http']['header'])) { - if (is_string($defaultOptions['http']['header'])) { - $defaultOptions['http']['header'] = array($defaultOptions['http']['header']); - } - $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; - } else { - $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}"); - } + $options['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; } } - $options = array_replace_recursive($options, $defaultOptions); - - if (isset($options['http']['header'])) { - $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); - } - if (defined('HHVM_VERSION')) { $phpVersion = 'HHVM ' . HHVM_VERSION; } else { $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; } + if (extension_loaded('curl')) { + $curl = curl_version(); + $httpVersion = 'curl '.$curl['version']; + } else { + $httpVersion = 'streams'; + } + if (!isset($options['http']['header']) || false === stripos(implode('', $options['http']['header']), 'user-agent')) { $options['http']['header'][] = sprintf( - 'User-Agent: Composer/%s (%s; %s; %s%s)', + 'User-Agent: Composer/%s (%s; %s; %s; %s%s)', Composer::getVersion(), function_exists('php_uname') ? php_uname('s') : 'Unknown', function_exists('php_uname') ? php_uname('r') : 'Unknown', $phpVersion, + $httpVersion, getenv('CI') ? '; CI' : '' ); } - return stream_context_create($options, $defaultParams); + return $options; + } + + /** + * @param array $options + * + * @return array + */ + public static function getTlsDefaults(array $options, LoggerInterface $logger = null) + { + $ciphers = implode(':', array( + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'DHE-DSS-AES128-GCM-SHA256', + 'kEDH+AESGCM', + 'ECDHE-RSA-AES128-SHA256', + 'ECDHE-ECDSA-AES128-SHA256', + 'ECDHE-RSA-AES128-SHA', + 'ECDHE-ECDSA-AES128-SHA', + 'ECDHE-RSA-AES256-SHA384', + 'ECDHE-ECDSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA', + 'ECDHE-ECDSA-AES256-SHA', + 'DHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA', + 'DHE-DSS-AES128-SHA256', + 'DHE-RSA-AES256-SHA256', + 'DHE-DSS-AES256-SHA', + 'DHE-RSA-AES256-SHA', + 'AES128-GCM-SHA256', + 'AES256-GCM-SHA384', + 'AES128-SHA256', + 'AES256-SHA256', + 'AES128-SHA', + 'AES256-SHA', + 'AES', + 'CAMELLIA', + 'DES-CBC3-SHA', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!RC4', + '!MD5', + '!PSK', + '!aECDH', + '!EDH-DSS-DES-CBC3-SHA', + '!EDH-RSA-DES-CBC3-SHA', + '!KRB5-DES-CBC3-SHA', + )); + + /** + * CN_match and SNI_server_name are only known once a URL is passed. + * They will be set in the getOptionsForUrl() method which receives a URL. + * + * cafile or capath can be overridden by passing in those options to constructor. + */ + $defaults = array( + 'ssl' => array( + 'ciphers' => $ciphers, + 'verify_peer' => true, + 'verify_depth' => 7, + 'SNI_enabled' => true, + 'capture_peer_cert' => true, + ), + ); + + if (isset($options['ssl'])) { + $defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']); + } + + /** + * Attempt to find a local cafile or throw an exception if none pre-set + * The user may go download one if this occurs. + */ + if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) { + $result = CaBundle::getSystemCaRootBundlePath($logger); + + if (is_dir($result)) { + $defaults['ssl']['capath'] = $result; + } else { + $defaults['ssl']['cafile'] = $result; + } + } + + if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $logger))) { + throw new TransportException('The configured cafile was not valid or could not be read.'); + } + + if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { + throw new TransportException('The configured capath was not valid or could not be read.'); + } + + /** + * Disable TLS compression to prevent CRIME attacks where supported. + */ + if (PHP_VERSION_ID >= 50413) { + $defaults['ssl']['disable_compression'] = true; + } + + return $defaults; } /** diff --git a/src/Composer/Util/Svn.php b/src/Composer/Util/Svn.php index 58114ac93..be1a81c91 100644 --- a/src/Composer/Util/Svn.php +++ b/src/Composer/Util/Svn.php @@ -304,7 +304,7 @@ class Svn $this->createAuthFromUrl(); } - return $this->hasAuth; + return (bool) $this->hasAuth; } /** diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index 4a5d5f90c..2da171556 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -19,6 +19,12 @@ use Composer\Config; */ class Url { + /** + * @param Config $config + * @param string $url + * @param string $ref + * @return string the updated URL + */ public static function updateDistReference(Config $config, $url, $ref) { $host = parse_url($url, PHP_URL_HOST); @@ -52,4 +58,65 @@ class Url return $url; } + + /** + * @param string $url + * @return string + */ + public static function getOrigin(Config $config, $url) + { + if (0 === strpos($url, 'file://')) { + return $url; + } + + $origin = (string) parse_url($url, PHP_URL_HOST); + if ($port = parse_url($url, PHP_URL_PORT)) { + $origin .= ':'.$port; + } + + if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { + return 'github.com'; + } + + if ($origin === 'repo.packagist.org') { + return 'packagist.org'; + } + + if ($origin === '') { + $origin = $url; + } + + // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl + // is the host without the path, so we look for the registered gitlab-domains matching the host here + if ( + is_array($config->get('gitlab-domains')) + && false === strpos($origin, '/') + && !in_array($origin, $config->get('gitlab-domains')) + ) { + foreach ($config->get('gitlab-domains') as $gitlabDomain) { + if (0 === strpos($gitlabDomain, $origin)) { + return $gitlabDomain; + } + } + } + + return $origin; + } + + public static function sanitize($url) + { + // GitHub repository rename result in redirect locations containing the access_token as GET parameter + // e.g. https://api.github.com/repositories/9999999999?access_token=github_token + $url = preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); + + $url = preg_replace_callback('{://(?P[^:/\s@]+):(?P[^@\s/]+)@}i', function ($m) { + if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) { + return '://***:***@'; + } + + return '://'.$m['user'].':***@'; + }, $url); + + return $url; + } } diff --git a/src/Composer/XdebugHandler.php b/src/Composer/XdebugHandler.php deleted file mode 100644 index eb94e93f4..000000000 --- a/src/Composer/XdebugHandler.php +++ /dev/null @@ -1,31 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer; - -use Symfony\Component\Console\Output\OutputInterface; - -trigger_error('The ' . __NAMESPACE__ . '\XdebugHandler class is deprecated, use Composer\XdebugHandler\XdebugHandler instead,', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\XdebugHandler\XdebugHandler instead - */ -class XdebugHandler extends XdebugHandler\XdebugHandler -{ - const ENV_ALLOW = 'COMPOSER_ALLOW_XDEBUG'; - const ENV_VERSION = 'COMPOSER_XDEBUG_VERSION'; - - public function __construct(OutputInterface $output) - { - parent::__construct('composer', '--ansi'); - } -} diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 3cd8f7ff2..6cb9b76dc 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -604,8 +604,6 @@ class AutoloadGeneratorTest extends TestCase { $package = new Package('a', '1.0', '1.0'); - $this->markTestSkipped('Skipped until ClassMapGenerator ignoring of invalid PSR-x classes is enabled'); - $package->setAutoload(array( 'psr-0' => array('psr0_' => 'psr0/'), 'psr-4' => array('psr4\\' => 'psr4/'), diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php index 693753a3e..dffa8390a 100644 --- a/tests/Composer/Test/CacheTest.php +++ b/tests/Composer/Test/CacheTest.php @@ -13,6 +13,7 @@ namespace Composer\Test; use Composer\Test\TestCase; +use Composer\Cache; use Composer\Util\Filesystem; class CacheTest extends TestCase @@ -20,6 +21,7 @@ class CacheTest extends TestCase private $files; private $root; private $finder; + private $filesystem; private $cache; public function setUp() @@ -34,6 +36,7 @@ class CacheTest extends TestCase } $this->finder = $this->getMockBuilder('Symfony\Component\Finder\Finder')->disableOriginalConstructor()->getMock(); + $this->filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $this->cache = $this->getMockBuilder('Composer\Cache') @@ -105,10 +108,8 @@ class CacheTest extends TestCase public function testClearCache() { + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->cache = new Cache($io, $this->root, 'a-z0-9.', $this->filesystem); $this->assertTrue($this->cache->clear()); - - for ($i = 0; $i < 3; $i++) { - $this->assertFileNotExists("{$this->root}/cached.file{$i}.zip"); - } } } diff --git a/tests/Composer/Test/Command/ArchiveCommandTest.php b/tests/Composer/Test/Command/ArchiveCommandTest.php index 4a777988d..41a5c0993 100644 --- a/tests/Composer/Test/Command/ArchiveCommandTest.php +++ b/tests/Composer/Test/Command/ArchiveCommandTest.php @@ -95,9 +95,9 @@ class ArchiveCommandTest extends TestCase null, false, null - ); + )->willReturn(0); $command->method('isInteractive')->willReturn(false); - $command->run($input, $output); + $this->assertEquals(0, $command->run($input, $output)); } } diff --git a/tests/Composer/Test/Command/RunScriptCommandTest.php b/tests/Composer/Test/Command/RunScriptCommandTest.php index 97acc6d26..6a2adbd2d 100644 --- a/tests/Composer/Test/Command/RunScriptCommandTest.php +++ b/tests/Composer/Test/Command/RunScriptCommandTest.php @@ -66,7 +66,8 @@ class RunScriptCommandTest extends TestCase $ed->expects($this->once()) ->method('dispatchScript') - ->with($scriptName, $expectedDevMode, array()); + ->with($scriptName, $expectedDevMode, array()) + ->willReturn(0); $composer = $this->createComposerInstance(); $composer->setEventDispatcher($ed); diff --git a/tests/Composer/Test/ComposerTest.php b/tests/Composer/Test/ComposerTest.php index c2c425e76..87270baae 100644 --- a/tests/Composer/Test/ComposerTest.php +++ b/tests/Composer/Test/ComposerTest.php @@ -57,7 +57,7 @@ class ComposerTest extends TestCase public function testSetGetInstallationManager() { $composer = new Composer(); - $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock(); + $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); $composer->setInstallationManager($manager); $this->assertSame($manager, $composer->getInstallationManager()); diff --git a/tests/Composer/Test/Console/HtmlOutputFormatterTest.php b/tests/Composer/Test/Console/HtmlOutputFormatterTest.php index d23bcddb2..a105eec0d 100644 --- a/tests/Composer/Test/Console/HtmlOutputFormatterTest.php +++ b/tests/Composer/Test/Console/HtmlOutputFormatterTest.php @@ -24,7 +24,7 @@ class HtmlOutputFormatterTest extends TestCase 'warning' => new OutputFormatterStyle('black', 'yellow'), )); - return $this->assertEquals( + $this->assertEquals( 'text green yellow black w/ yello bg', $formatter->format('text green yellow black w/ yello bg') ); diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 2611d772f..8a0e5a15a 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -13,30 +13,32 @@ namespace Composer\Test\DependencyResolver; use Composer\Repository\ArrayRepository; +use Composer\Repository\LockArrayRepository; use Composer\Repository\RepositoryInterface; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; use Composer\Package\Link; use Composer\Package\AliasPackage; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Test\TestCase; class DefaultPolicyTest extends TestCase { - /** @var Pool */ - protected $pool; + /** @var RepositorySet */ + protected $repositorySet; /** @var ArrayRepository */ protected $repo; - /** @var ArrayRepository */ - protected $repoInstalled; + /** @var LockArrayRepository */ + protected $repoLocked; /** @var DefaultPolicy */ protected $policy; public function setUp() { - $this->pool = new Pool('dev'); + $this->repositorySet = new RepositorySet('dev'); $this->repo = new ArrayRepository; - $this->repoInstalled = new ArrayRepository; + $this->repoLocked = new LockArrayRepository; $this->policy = new DefaultPolicy; } @@ -44,12 +46,14 @@ class DefaultPolicyTest extends TestCase public function testSelectSingle() { $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -58,12 +62,14 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -72,12 +78,14 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -86,13 +94,15 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); $policy = new DefaultPolicy(true); - $selected = $policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -101,46 +111,14 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', 'dev-foo')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); - - $this->assertSame($expected, $selected); - } - - public function testSelectNewestOverInstalled() - { - $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); - $this->repoInstalled->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($this->repo); - - $literals = array($packageA->getId(), $packageAInstalled->getId()); - $expected = array($packageA->getId()); - - $selected = $this->policy->selectPreferredPackages($this->pool, $this->mapFromRepo($this->repoInstalled), $literals); - - $this->assertSame($expected, $selected); - } - - public function testSelectFirstRepo() - { - $otherRepository = new ArrayRepository; - - $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); - $otherRepository->addPackage($packageAImportant = $this->getPackage('A', '1.0')); - - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($otherRepository); - $this->pool->addRepository($this->repo); - - $literals = array($packageA->getId(), $packageAImportant->getId()); - $expected = array($packageAImportant->getId()); - - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -155,21 +133,25 @@ class DefaultPolicyTest extends TestCase $repo2->addPackage($package3 = $this->getPackage('A', '1.1')); $repo2->addPackage($package4 = $this->getPackage('A', '1.2')); - $this->pool->addRepository($repo1); - $this->pool->addRepository($repo2); + $this->repositorySet->addRepository($repo1); + $this->repositorySet->addRepository($repo2); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); - $this->pool = new Pool('dev'); - $this->pool->addRepository($repo2); - $this->pool->addRepository($repo1); + $this->repositorySet = new RepositorySet('dev'); + $this->repositorySet->addRepository($repo2); + $this->repositorySet->addRepository($repo1); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $expected = array($package4->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -186,11 +168,13 @@ class DefaultPolicyTest extends TestCase $repoImportant->addPackage($packageA2AliasImportant = new AliasPackage($packageA2Important, '2.1.9999999.9999999-dev', '2.1.x-dev')); $packageAAliasImportant->setRootPackageAlias(true); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($repoImportant); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($repoImportant); + $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); - $packages = $this->pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); + + $packages = $pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); $literals = array(); foreach ($packages as $package) { $literals[] = $package->getId(); @@ -198,7 +182,7 @@ class DefaultPolicyTest extends TestCase $expected = array($packageAAliasImportant->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -211,12 +195,14 @@ class DefaultPolicyTest extends TestCase $packageA->setProvides(array(new Link('A', 'X', new Constraint('==', '1.0'), 'provides'))); $packageB->setProvides(array(new Link('B', 'X', new Constraint('==', '1.0'), 'provides'))); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackages(array('A', 'B'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -228,12 +214,14 @@ class DefaultPolicyTest extends TestCase $packageB->setReplaces(array(new Link('B', 'A', new Constraint('==', '1.0'), 'replaces'))); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackages(array('A', 'B'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -247,12 +235,14 @@ class DefaultPolicyTest extends TestCase $packageA->setReplaces(array(new Link('vendor-a/replacer', 'vendor-a/package', new Constraint('==', '1.0'), 'replaces'))); $packageB->setReplaces(array(new Link('vendor-b/replacer', 'vendor-a/package', new Constraint('==', '1.0'), 'replaces'))); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); $this->assertEquals($expected, $selected); // test with reversed order in repo @@ -260,38 +250,32 @@ class DefaultPolicyTest extends TestCase $repo->addPackage($packageA = clone $packageA); $repo->addPackage($packageB = clone $packageB); - $pool = new Pool('dev'); - $pool->addRepository($this->repo); + $repositorySet = new RepositorySet('dev'); + $repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); $this->assertSame($expected, $selected); } - protected function mapFromRepo(RepositoryInterface $repo) - { - $map = array(); - foreach ($repo->getPackages() as $package) { - $map[$package->getId()] = true; - } - - return $map; - } - public function testSelectLowest() { $policy = new DefaultPolicy(false, true); $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); - $selected = $policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } diff --git a/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test new file mode 100644 index 000000000..f1dbd69fe --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test @@ -0,0 +1,23 @@ +--TEST-- +Fixed packages do not get loaded from the repos + +--REQUEST-- +{ + "some/pkg": "*" +} + +--FIXED-- +[ + {"name": "some/pkg", "version": "1.0.3", "id": 1} +] + +--PACKAGES-- +[ + {"name": "some/pkg", "version": "1.0.0"}, + {"name": "some/pkg", "version": "1.1.0"} +] + +--EXPECT-- +[ + 1 +] diff --git a/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test new file mode 100644 index 000000000..5a6ca8f2f --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test @@ -0,0 +1,44 @@ +--TEST-- +Stability flags apply + +--ROOT-- +{ + "stability-flags": { + "flagged/pkg": "alpha" + }, + "minimum-stability": "RC", + "aliases": [ + { + "package": "default/pkg", + "version": "1.0.0-RC", + "alias": "1.2.0" + } + ] +} + +--REQUEST-- +{ + "flagged/pkg": "*", + "default/pkg": "*" +} + +--PACKAGES-- +[ + {"name": "flagged/pkg", "version": "1.0.0", "id": 1}, + {"name": "flagged/pkg", "version": "1.0.0-beta", "id": 2}, + {"name": "flagged/pkg", "version": "1.0.0-dev", "id": 3}, + {"name": "flagged/pkg", "version": "1.0.0-RC", "id": 4}, + {"name": "default/pkg", "version": "1.0.0", "id": 5}, + {"name": "default/pkg", "version": "1.0.0-RC", "id": 6}, + {"name": "default/pkg", "version": "1.0.0-alpha", "id": 7} +] + +--EXPECT-- +[ + 1, + 2, + 4, + 5, + 6, + "default/pkg-1.2.0.0 alias of 6" +] diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php new file mode 100644 index 000000000..f51661db6 --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -0,0 +1,194 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\DependencyResolver; + +use Composer\IO\NullIO; +use Composer\Repository\ArrayRepository; +use Composer\Repository\LockArrayRepository; +use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\PoolBuilder; +use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Solver; +use Composer\DependencyResolver\SolverProblemsException; +use Composer\Package\BasePackage; +use Composer\Package\AliasPackage; +use Composer\Json\JsonFile; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\Version\VersionParser; +use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\RepositorySet; +use Composer\Test\TestCase; +use Composer\Semver\Constraint\MultiConstraint; + +class PoolBuilderTest extends TestCase +{ + /** + * @dataProvider getIntegrationTests + */ + public function testPoolBuilder($file, $message, $expect, $root, $requestData, $packages, $fixed) + { + $rootAliases = !empty($root['aliases']) ? $root['aliases'] : array(); + $minimumStability = !empty($root['minimum-stability']) ? $root['minimum-stability'] : 'stable'; + $stabilityFlags = !empty($root['stability-flags']) ? $root['stability-flags'] : array(); + $stabilityFlags = array_map(function ($stability) { + return BasePackage::$stabilities[$stability]; + }, $stabilityFlags); + + $parser = new VersionParser(); + $normalizedAliases = array(); + foreach ($rootAliases as $alias) { + $normalizedAliases[$alias['package']][$parser->normalize($alias['version'])] = array( + 'alias' => $alias['alias'], + 'alias_normalized' => $parser->normalize($alias['alias']), + ); + } + + $loader = new ArrayLoader(); + $packageIds = array(); + $loadPackage = function ($data) use ($loader, &$packageIds) { + if (!empty($data['id'])) { + $id = $data['id']; + unset($data['id']); + } + + $pkg = $loader->load($data); + + if (!empty($id)) { + if (!empty($packageIds[$id])) { + throw new \LogicException('Duplicate package id '.$id.' defined'); + } + $packageIds[$id] = $pkg; + } + + return $pkg; + }; + + $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $normalizedAliases); + $repositorySet->addRepository($repo = new ArrayRepository()); + foreach ($packages as $package) { + $repo->addPackage($loadPackage($package)); + } + + $request = new Request(); + foreach ($requestData as $package => $constraint) { + $request->requireName($package, $parser->parseConstraints($constraint)); + } + + foreach ($fixed as $fixedPackage) { + $request->fixPackage($loadPackage($fixedPackage)); + } + + $pool = $repositorySet->createPool($request, new NullIO()); + for ($i = 1, $count = count($pool); $i <= $count; $i++) { + $result[] = $pool->packageById($i); + } + + $result = array_map(function ($package) use ($packageIds) { + if ($id = array_search($package, $packageIds, true)) { + return $id; + } + + if ($package instanceof AliasPackage && $id = array_search($package->getAliasOf(), $packageIds, true)) { + return (string) $package->getName().'-'.$package->getVersion() .' alias of '.$id; + } + + return (string) $package; + }, $result); + + $this->assertSame($expect, $result); + } + + public function getIntegrationTests() + { + $fixturesDir = realpath(__DIR__.'/Fixtures/poolbuilder/'); + $tests = array(); + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!preg_match('/\.test$/', $file)) { + continue; + } + + try { + $testData = $this->readTestFile($file, $fixturesDir); + + $message = $testData['TEST']; + + $request = JsonFile::parseJson($testData['REQUEST']); + $root = !empty($testData['ROOT']) ? JsonFile::parseJson($testData['ROOT']) : array(); + + $packages = JsonFile::parseJson($testData['PACKAGES']); + $fixed = array(); + if (!empty($testData['FIXED'])) { + $fixed = JsonFile::parseJson($testData['FIXED']); + } + $expect = JsonFile::parseJson($testData['EXPECT']); + } catch (\Exception $e) { + die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); + } + + $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $expect, $root, $request, $packages, $fixed); + } + + return $tests; + } + + protected function readTestFile(\SplFileInfo $file, $fixturesDir) + { + $tokens = preg_split('#(?:^|\n*)--([A-Z-]+)--\n#', file_get_contents($file->getRealPath()), null, PREG_SPLIT_DELIM_CAPTURE); + + $sectionInfo = array( + 'TEST' => true, + 'ROOT' => false, + 'REQUEST' => true, + 'FIXED' => false, + 'PACKAGES' => true, + 'EXPECT' => true, + ); + + $section = null; + foreach ($tokens as $i => $token) { + if (null === $section && empty($token)) { + continue; // skip leading blank + } + + if (null === $section) { + if (!isset($sectionInfo[$token])) { + throw new \RuntimeException(sprintf( + 'The test file "%s" must not contain a section named "%s".', + str_replace($fixturesDir.'/', '', $file), + $token + )); + } + $section = $token; + continue; + } + + $sectionData = $token; + + $data[$section] = $sectionData; + $section = $sectionData = null; + } + + foreach ($sectionInfo as $section => $required) { + if ($required && !isset($data[$section])) { + throw new \RuntimeException(sprintf( + 'The test file "%s" must have a section named "%s".', + str_replace($fixturesDir.'/', '', $file), + $section + )); + } + } + + return $data; + } +} diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index aa38fa31d..ceeb928ba 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -21,91 +21,24 @@ class PoolTest extends TestCase { public function testPool() { - $pool = new Pool; - $repo = new ArrayRepository; $package = $this->getPackage('foo', '1'); - $repo->addPackage($package); - $pool->addRepository($repo); + $pool = $this->createPool(array($package)); $this->assertEquals(array($package), $pool->whatProvides('foo')); $this->assertEquals(array($package), $pool->whatProvides('foo')); } - public function testPoolIgnoresIrrelevantPackages() - { - $pool = new Pool('stable', array('bar' => BasePackage::STABILITY_BETA)); - $repo = new ArrayRepository; - $repo->addPackage($package = $this->getPackage('bar', '1')); - $repo->addPackage($betaPackage = $this->getPackage('bar', '1-beta')); - $repo->addPackage($alphaPackage = $this->getPackage('bar', '1-alpha')); - $repo->addPackage($package2 = $this->getPackage('foo', '1')); - $repo->addPackage($rcPackage2 = $this->getPackage('foo', '1rc')); - - $pool->addRepository($repo); - - $this->assertEquals(array($package, $betaPackage), $pool->whatProvides('bar')); - $this->assertEquals(array($package2), $pool->whatProvides('foo')); - } - - /** - * @expectedException \RuntimeException - */ - public function testGetPriorityForNotRegisteredRepository() - { - $pool = new Pool; - $repository = new ArrayRepository; - - $pool->getPriority($repository); - } - - public function testGetPriorityWhenRepositoryIsRegistered() - { - $pool = new Pool; - $firstRepository = new ArrayRepository; - $pool->addRepository($firstRepository); - $secondRepository = new ArrayRepository; - $pool->addRepository($secondRepository); - - $firstPriority = $pool->getPriority($firstRepository); - $secondPriority = $pool->getPriority($secondRepository); - - $this->assertEquals(0, $firstPriority); - $this->assertEquals(-1, $secondPriority); - } - - public function testWhatProvidesSamePackageForDifferentRepositories() - { - $pool = new Pool; - $firstRepository = new ArrayRepository; - $secondRepository = new ArrayRepository; - - $firstPackage = $this->getPackage('foo', '1'); - $secondPackage = $this->getPackage('foo', '1'); - $thirdPackage = $this->getPackage('foo', '2'); - - $firstRepository->addPackage($firstPackage); - $secondRepository->addPackage($secondPackage); - $secondRepository->addPackage($thirdPackage); - - $pool->addRepository($firstRepository); - $pool->addRepository($secondRepository); - - $this->assertEquals(array($firstPackage, $secondPackage, $thirdPackage), $pool->whatProvides('foo')); - } - public function testWhatProvidesPackageWithConstraint() { - $pool = new Pool; - $repository = new ArrayRepository; $firstPackage = $this->getPackage('foo', '1'); $secondPackage = $this->getPackage('foo', '2'); - $repository->addPackage($firstPackage); - $repository->addPackage($secondPackage); - - $pool->addRepository($repository); + $pool = $this->createPool(array( + $firstPackage, + $secondPackage, + )); $this->assertEquals(array($firstPackage, $secondPackage), $pool->whatProvides('foo')); $this->assertEquals(array($secondPackage), $pool->whatProvides('foo', $this->getVersionConstraint('==', '2'))); @@ -113,20 +46,22 @@ class PoolTest extends TestCase public function testPackageById() { - $pool = new Pool; - $repository = new ArrayRepository; $package = $this->getPackage('foo', '1'); - $repository->addPackage($package); - $pool->addRepository($repository); + $pool = $this->createPool(array($package)); $this->assertSame($package, $pool->packageById(1)); } public function testWhatProvidesWhenPackageCannotBeFound() { - $pool = new Pool; + $pool = $this->createPool(); $this->assertEquals(array(), $pool->whatProvides('foo')); } + + protected function createPool(array $packages = array()) + { + return new Pool($packages); + } } diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index dfa411ed9..e2a3ce9e6 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -18,7 +18,7 @@ use Composer\Test\TestCase; class RequestTest extends TestCase { - public function testRequestInstallAndRemove() + public function testRequestInstall() { $repo = new ArrayRepository; $foo = $this->getPackage('foo', '1'); @@ -30,17 +30,13 @@ class RequestTest extends TestCase $repo->addPackage($foobar); $request = new Request(); - $request->install('foo'); - $request->fix('bar'); - $request->remove('foobar'); + $request->requireName('foo'); $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null, 'fixed' => false), - array('cmd' => 'install', 'packageName' => 'bar', 'constraint' => null, 'fixed' => true), - array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null, 'fixed' => false), + 'foo' => null, ), - $request->getJobs() + $request->getRequires() ); } @@ -56,25 +52,13 @@ class RequestTest extends TestCase $repo2->addPackage($foo2); $request = new Request(); - $request->install('foo', $constraint = $this->getVersionConstraint('=', '1')); + $request->requireName('foo', $constraint = $this->getVersionConstraint('=', '1')); $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint, 'fixed' => false), + 'foo' => $constraint, ), - $request->getJobs() - ); - } - - public function testUpdateAll() - { - $request = new Request(); - - $request->updateAll(); - - $this->assertEquals( - array(array('cmd' => 'update-all')), - $request->getJobs() + $request->getRequires() ); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index f38c48e10..4ebf37bf3 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -17,6 +17,7 @@ use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\RuleSetIterator; use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; use Composer\Test\TestCase; class RuleSetIteratorTest extends TestCase @@ -26,12 +27,12 @@ class RuleSetIteratorTest extends TestCase protected function setUp() { - $this->pool = new Pool; + $this->pool = new Pool(); $this->rules = array( - RuleSet::TYPE_JOB => array( - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), + RuleSet::TYPE_REQUEST => array( + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), ), RuleSet::TYPE_LEARNED => array( new GenericRule(array(), Rule::RULE_INTERNAL_ALLOW_UPDATE, null), @@ -50,8 +51,8 @@ class RuleSetIteratorTest extends TestCase } $expected = array( - $this->rules[RuleSet::TYPE_JOB][0], - $this->rules[RuleSet::TYPE_JOB][1], + $this->rules[RuleSet::TYPE_REQUEST][0], + $this->rules[RuleSet::TYPE_REQUEST][1], $this->rules[RuleSet::TYPE_LEARNED][0], ); @@ -68,8 +69,8 @@ class RuleSetIteratorTest extends TestCase } $expected = array( - RuleSet::TYPE_JOB, - RuleSet::TYPE_JOB, + RuleSet::TYPE_REQUEST, + RuleSet::TYPE_REQUEST, RuleSet::TYPE_LEARNED, ); diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 22c1c15d0..5a89ddf79 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -16,25 +16,19 @@ use Composer\DependencyResolver\GenericRule; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; use Composer\Repository\ArrayRepository; use Composer\Test\TestCase; class RuleSetTest extends TestCase { - protected $pool; - - public function setUp() - { - $this->pool = new Pool; - } - public function testAdd() { $rules = array( RuleSet::TYPE_PACKAGE => array(), - RuleSet::TYPE_JOB => array( - new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null), + RuleSet::TYPE_REQUEST => array( + new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null), ), RuleSet::TYPE_LEARNED => array( new GenericRule(array(), Rule::RULE_INTERNAL_ALLOW_UPDATE, null), @@ -43,9 +37,9 @@ class RuleSetTest extends TestCase $ruleSet = new RuleSet; - $ruleSet->add($rules[RuleSet::TYPE_JOB][0], RuleSet::TYPE_JOB); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][0], RuleSet::TYPE_REQUEST); $ruleSet->add($rules[RuleSet::TYPE_LEARNED][0], RuleSet::TYPE_LEARNED); - $ruleSet->add($rules[RuleSet::TYPE_JOB][1], RuleSet::TYPE_JOB); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][1], RuleSet::TYPE_REQUEST); $this->assertEquals($rules, $ruleSet->getRules()); } @@ -53,20 +47,20 @@ class RuleSetTest extends TestCase public function testAddIgnoresDuplicates() { $rules = array( - RuleSet::TYPE_JOB => array( - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), + RuleSet::TYPE_REQUEST => array( + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), ), ); $ruleSet = new RuleSet; - $ruleSet->add($rules[RuleSet::TYPE_JOB][0], RuleSet::TYPE_JOB); - $ruleSet->add($rules[RuleSet::TYPE_JOB][1], RuleSet::TYPE_JOB); - $ruleSet->add($rules[RuleSet::TYPE_JOB][2], RuleSet::TYPE_JOB); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][0], RuleSet::TYPE_REQUEST); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][1], RuleSet::TYPE_REQUEST); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][2], RuleSet::TYPE_REQUEST); - $this->assertCount(1, $ruleSet->getIteratorFor(array(RuleSet::TYPE_JOB))); + $this->assertCount(1, $ruleSet->getIteratorFor(array(RuleSet::TYPE_REQUEST))); } /** @@ -76,15 +70,15 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $ruleSet->add(new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), 7); + $ruleSet->add(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), 7); } public function testCount() { $ruleSet = new RuleSet; - $ruleSet->add(new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null), RuleSet::TYPE_JOB); - $ruleSet->add(new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null), RuleSet::TYPE_JOB); + $ruleSet->add(new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null), RuleSet::TYPE_REQUEST); + $ruleSet->add(new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null), RuleSet::TYPE_REQUEST); $this->assertEquals(2, $ruleSet->count()); } @@ -93,8 +87,8 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); - $ruleSet->add($rule, RuleSet::TYPE_JOB); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); + $ruleSet->add($rule, RuleSet::TYPE_REQUEST); $this->assertSame($rule, $ruleSet->ruleById[0]); } @@ -103,9 +97,9 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule1 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null); - $ruleSet->add($rule1, RuleSet::TYPE_JOB); + $rule1 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null); + $ruleSet->add($rule1, RuleSet::TYPE_REQUEST); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); $iterator = $ruleSet->getIterator(); @@ -118,10 +112,10 @@ class RuleSetTest extends TestCase public function testGetIteratorFor() { $ruleSet = new RuleSet; - $rule1 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null); + $rule1 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null); - $ruleSet->add($rule1, RuleSet::TYPE_JOB); + $ruleSet->add($rule1, RuleSet::TYPE_REQUEST); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); $iterator = $ruleSet->getIteratorFor(RuleSet::TYPE_LEARNED); @@ -132,29 +126,32 @@ class RuleSetTest extends TestCase public function testGetIteratorWithout() { $ruleSet = new RuleSet; - $rule1 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null); + $rule1 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null); - $ruleSet->add($rule1, RuleSet::TYPE_JOB); + $ruleSet->add($rule1, RuleSet::TYPE_REQUEST); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); - $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_JOB); + $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_REQUEST); $this->assertSame($rule2, $iterator->current()); } public function testPrettyString() { - $repo = new ArrayRepository; - $repo->addPackage($p = $this->getPackage('foo', '2.1')); - $this->pool->addRepository($repo); + $pool = new Pool(array( + $p = $this->getPackage('foo', '2.1'), + )); + + $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); + $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); $ruleSet = new RuleSet; $literal = $p->getId(); - $rule = new GenericRule(array($literal), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array($literal), Rule::RULE_ROOT_REQUIRE, array('packageName' => 'foo/bar', 'constraint' => null)); - $ruleSet->add($rule, RuleSet::TYPE_JOB); + $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertContains('JOB : Install command rule (install foo 2.1)', $ruleSet->getPrettyString($this->pool)); + $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 19f5fddea..f819397fb 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -16,21 +16,16 @@ use Composer\DependencyResolver\GenericRule; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; +use Composer\Package\Link; use Composer\Repository\ArrayRepository; use Composer\Test\TestCase; class RuleTest extends TestCase { - protected $pool; - - public function setUp() - { - $this->pool = new Pool; - } - public function testGetHash() { - $rule = new GenericRule(array(123), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(123), Rule::RULE_ROOT_REQUIRE, null); $hash = unpack('ihash', md5('123', true)); $this->assertEquals($hash['hash'], $rule->getHash()); @@ -38,39 +33,39 @@ class RuleTest extends TestCase public function testEqualsForRulesWithDifferentHashes() { - $rule = new GenericRule(array(1, 2), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1, 3), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 2), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1, 3), Rule::RULE_ROOT_REQUIRE, null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithDifferLiteralsQuantity() { - $rule = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithSameLiterals() { - $rule = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); $this->assertTrue($rule->equals($rule2)); } public function testSetAndGetType() { - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); - $rule->setType(RuleSet::TYPE_JOB); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); + $rule->setType(RuleSet::TYPE_REQUEST); - $this->assertEquals(RuleSet::TYPE_JOB, $rule->getType()); + $this->assertEquals(RuleSet::TYPE_REQUEST, $rule->getType()); } public function testEnable() { - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); $rule->disable(); $rule->enable(); @@ -80,7 +75,7 @@ class RuleTest extends TestCase public function testDisable() { - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); $rule->enable(); $rule->disable(); @@ -90,8 +85,8 @@ class RuleTest extends TestCase public function testIsAssertions() { - $rule = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); $this->assertFalse($rule->isAssertion()); $this->assertTrue($rule2->isAssertion()); @@ -99,13 +94,16 @@ class RuleTest extends TestCase public function testPrettyString() { - $repo = new ArrayRepository; - $repo->addPackage($p1 = $this->getPackage('foo', '2.1')); - $repo->addPackage($p2 = $this->getPackage('baz', '1.1')); - $this->pool->addRepository($repo); + $pool = new Pool(array( + $p1 = $this->getPackage('foo', '2.1'), + $p2 = $this->getPackage('baz', '1.1'), + )); - $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_JOB_INSTALL, null); + $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); + $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $this->assertEquals('Install command rule (don\'t install baz 1.1|install foo 2.1)', $rule->getPrettyString($this->pool)); + $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo')); + + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 4dadee7d8..3090d9d83 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -14,33 +14,36 @@ namespace Composer\Test\DependencyResolver; use Composer\IO\NullIO; use Composer\Repository\ArrayRepository; +use Composer\Repository\LockArrayRepository; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\Package\Link; +use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\RepositorySet; use Composer\Test\TestCase; use Composer\Semver\Constraint\MultiConstraint; class SolverTest extends TestCase { - protected $pool; + protected $repoSet; protected $repo; - protected $repoInstalled; + protected $repoLocked; protected $request; protected $policy; protected $solver; + protected $pool; public function setUp() { - $this->pool = new Pool; + $this->repoSet = new RepositorySet(); $this->repo = new ArrayRepository; - $this->repoInstalled = new ArrayRepository; + $this->repoLocked = new LockArrayRepository; - $this->request = new Request(); + $this->request = new Request($this->repoLocked); $this->policy = new DefaultPolicy; - $this->solver = new Solver($this->policy, $this->pool, $this->repoInstalled, new NullIO()); } public function testSolverInstallSingle() @@ -48,16 +51,16 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), )); } - public function testSolverRemoveIfNotInstalled() + public function testSolverRemoveIfNotRequested() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); $this->checkSolverResult(array( @@ -70,8 +73,9 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('B', $this->getVersionConstraint('==', '1')); + $this->request->requireName('B', $this->getVersionConstraint('==', '1')); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -79,7 +83,7 @@ class SolverTest extends TestCase $problems = $e->getProblems(); $this->assertCount(1, $problems); $this->assertEquals(2, $e->getCode()); - $this->assertEquals("\n - The requested package b could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString()); + $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -91,11 +95,10 @@ class SolverTest extends TestCase $repo1->addPackage($foo1 = $this->getPackage('foo', '1')); $repo2->addPackage($foo2 = $this->getPackage('foo', '1')); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($repo1); - $this->pool->addRepository($repo2); + $this->repoSet->addRepository($repo1); + $this->repoSet->addRepository($repo2); - $this->request->install('foo'); + $this->request->requireName('foo'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $foo1), @@ -112,7 +115,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -138,7 +141,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $newPackageB11), @@ -162,9 +165,9 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('B'); - $this->request->install('C'); + $this->request->requireName('A'); + $this->request->requireName('B'); + $this->request->requireName('C'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -173,62 +176,38 @@ class SolverTest extends TestCase )); } - public function testSolverInstallInstalled() + public function testSolverFixLocked() { - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->fixPackage($packageA); $this->checkSolverResult(array()); } - public function testSolverInstallInstalledWithAlternative() + public function testSolverFixLockedWithAlternative() { $this->repo->addPackage($this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); - - $this->checkSolverResult(array()); - } - - public function testSolverRemoveSingle() - { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->reposComplete(); - - $this->request->remove('A'); - - $this->checkSolverResult(array( - array('job' => 'remove', 'package' => $packageA), - )); - } - - public function testSolverRemoveUninstalled() - { - $this->repo->addPackage($this->getPackage('A', '1.0')); - $this->reposComplete(); - - $this->request->remove('A'); + $this->request->fixPackage($packageA); $this->checkSolverResult(array()); } public function testSolverUpdateDoesOnlyUpdate() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); $this->reposComplete(); $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires'))); - $this->request->install('A', $this->getVersionConstraint('=', '1.0.0.0')); - $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); - $this->request->update('B', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->fixPackage($packageA); + $this->request->requireName('B', $this->getVersionConstraint('=', '1.1.0.0')); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -237,12 +216,11 @@ class SolverTest extends TestCase public function testSolverUpdateSingle() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->reposComplete(); - $this->request->install('A'); - $this->request->update('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA), @@ -251,8 +229,8 @@ class SolverTest extends TestCase public function testSolverUpdateAll() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); @@ -261,8 +239,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->updateAll(); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -272,28 +249,26 @@ class SolverTest extends TestCase public function testSolverUpdateCurrent() { - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($this->getPackage('A', '1.0')); $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); - $this->request->update('A'); + $this->request->requireName('A'); $this->checkSolverResult(array()); } public function testSolverUpdateOnlyUpdatesSelectedPackage() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($packageAnewer = $this->getPackage('A', '1.1')); $this->repo->addPackage($packageBnewer = $this->getPackage('B', '1.1')); $this->reposComplete(); - $this->request->install('A'); - $this->request->install('B'); - $this->request->update('A'); + $this->request->requireName('A'); + $this->request->fixPackage($packageB); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $packageAnewer), @@ -302,13 +277,12 @@ class SolverTest extends TestCase public function testSolverUpdateConstrained() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A'); + $this->request->requireName('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -319,13 +293,12 @@ class SolverTest extends TestCase public function testSolverUpdateFullyConstrained() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -336,32 +309,31 @@ class SolverTest extends TestCase public function testSolverUpdateFullyConstrainedPrunesInstalledPackages() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array( + array( + 'job' => 'remove', + 'package' => $packageB, + ), array( 'job' => 'update', 'from' => $packageA, 'to' => $newPackageA, ), - array( - 'job' => 'remove', - 'package' => $packageB, - ), )); } public function testSolverAllJobs() { - $this->repoInstalled->addPackage($packageD = $this->getPackage('D', '1.0')); - $this->repoInstalled->addPackage($oldPackageC = $this->getPackage('C', '1.0')); + $this->repoLocked->addPackage($packageD = $this->getPackage('D', '1.0')); + $this->repoLocked->addPackage($oldPackageC = $this->getPackage('C', '1.0')); $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); @@ -372,16 +344,14 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('C'); - $this->request->update('C'); - $this->request->remove('D'); + $this->request->requireName('A'); + $this->request->requireName('C'); $this->checkSolverResult(array( - array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), + array('job' => 'remove', 'package' => $packageD), array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA), - array('job' => 'remove', 'package' => $packageD), + array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), )); } @@ -396,7 +366,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $middlePackageB), @@ -406,16 +376,17 @@ class SolverTest extends TestCase public function testSolverObsolete() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); $packageB->setReplaces(array('a' => new Link('B', 'A', new MultiConstraint(array())))); $this->reposComplete(); - $this->request->install('B'); + $this->request->requireName('B'); $this->checkSolverResult(array( - array('job' => 'update', 'from' => $packageA, 'to' => $packageB), + array('job' => 'remove', 'package' => $packageA), + array('job' => 'install', 'package' => $packageB), )); } @@ -426,7 +397,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -442,10 +413,11 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); // must explicitly pick the provider, so error in this case $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->createSolver(); $this->solver->solve($this->request); } @@ -459,7 +431,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -476,9 +448,10 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->createSolver(); $this->solver->solve($this->request); } @@ -492,8 +465,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('Q'); + $this->request->requireName('A'); + $this->request->requireName('Q'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageQ), @@ -530,7 +503,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('X'); + $this->request->requireName('X'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $newPackageB), @@ -549,7 +522,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB2), @@ -573,13 +546,13 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('C'); + $this->request->requireName('A'); + $this->request->requireName('C'); $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), - array('job' => 'install', 'package' => $packageB), )); } @@ -611,8 +584,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('D'); + $this->request->requireName('A'); + $this->request->requireName('D'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageD2), @@ -647,10 +620,11 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); + $this->request->requireName('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->createSolver(); $this->solver->solve($this->request); } @@ -664,9 +638,10 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('B'); + $this->request->requireName('A'); + $this->request->requireName('B'); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -676,10 +651,10 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; - $msg .= " - B 1.0 conflicts with A[1.0].\n"; - $msg .= " - Installation request for b -> satisfiable by B[1.0].\n"; - $this->assertEquals($msg, $e->getMessage()); + $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; + $msg .= " - A 1.0 conflicts with B 1.0.\n"; + $msg .= " - Root composer.json requires b -> satisfiable by B[1.0].\n"; + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -694,8 +669,9 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -706,15 +682,9 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; - $msg .= " - A 1.0 requires b >= 2.0 -> no matching package found.\n\n"; - $msg .= "Potential causes:\n"; - $msg .= " - A typo in the package name\n"; - $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n"; - $msg .= " see for more details.\n"; - $msg .= " - It's a private package and you forgot to add a custom repository to find it\n\n"; - $msg .= "Read for further common problems."; - $this->assertEquals($msg, $e->getMessage()); + $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; + $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match your constraint.\n"; + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -741,8 +711,9 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -755,10 +726,10 @@ class SolverTest extends TestCase $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; - $msg .= " - Can only install one of: B[0.9, 1.0].\n"; + $msg .= " - You can only install one version of a package, so only one of these can be installed: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; - $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; - $this->assertEquals($msg, $e->getMessage()); + $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -780,8 +751,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('symfony/twig-bridge'); - $this->request->install('twig/twig'); + $this->request->requireName('symfony/twig-bridge'); + $this->request->requireName('twig/twig'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageTwig16), @@ -806,11 +777,11 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('==', '1.1.0.0')); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA2), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA2), array('job' => 'install', 'package' => $packageA2Alias), )); } @@ -828,8 +799,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('==', '2.0')); - $this->request->install('B'); + $this->request->requireName('A', $this->getVersionConstraint('==', '2.0')); + $this->request->requireName('B'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -889,7 +860,9 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->requireName('A'); + + $this->createSolver(); // check correct setup for assertion later $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); @@ -911,24 +884,32 @@ class SolverTest extends TestCase protected function reposComplete() { - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($this->repo); + $this->repoSet->addRepository($this->repo); + $this->repoSet->addRepository($this->repoLocked); + } + + protected function createSolver() + { + $io = new NullIO(); + $this->pool = $this->repoSet->createPool($this->request, $io); + $this->solver = new Solver($this->policy, $this->pool, $io); } protected function checkSolverResult(array $expected) { + $this->createSolver(); $transaction = $this->solver->solve($this->request); $result = array(); - foreach ($transaction as $operation) { - if ('update' === $operation->getJobType()) { + foreach ($transaction->getOperations() as $operation) { + if ('update' === $operation->getOperationType()) { $result[] = array( 'job' => 'update', 'from' => $operation->getInitialPackage(), 'to' => $operation->getTargetPackage(), ); } else { - $job = ('uninstall' === $operation->getJobType() ? 'remove' : 'install'); + $job = ('uninstall' === $operation->getOperationType() ? 'remove' : 'install'); $result[] = array( 'job' => $job, 'package' => $operation->getPackage(), diff --git a/tests/Composer/Test/DependencyResolver/TransactionTest.php b/tests/Composer/Test/DependencyResolver/TransactionTest.php new file mode 100644 index 000000000..8b3e66b68 --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/TransactionTest.php @@ -0,0 +1,90 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\DependencyResolver; + +use Composer\DependencyResolver\Transaction; +use Composer\Package\Link; +use Composer\Test\TestCase; + +class TransactionTest extends TestCase +{ + public function setUp() + { + } + + public function testTransactionGenerationAndSorting() + { + $presentPackages = array( + $packageA = $this->getPackage('a/a', 'dev-master'), + $packageAalias = $this->getAliasPackage($packageA, '1.0.x-dev'), + $packageB = $this->getPackage('b/b', '1.0.0'), + $packageE = $this->getPackage('e/e', 'dev-foo'), + $packageEalias = $this->getAliasPackage($packageE, '1.0.x-dev'), + $packageC = $this->getPackage('c/c', '1.0.0'), + ); + $resultPackages = array( + $packageA, + $packageAalias, + $packageBnew = $this->getPackage('b/b', '2.1.3'), + $packageD = $this->getPackage('d/d', '1.2.3'), + $packageF = $this->getPackage('f/f', '1.0.0'), + $packageFalias1 = $this->getAliasPackage($packageF, 'dev-foo'), + $packageG = $this->getPackage('g/g', '1.0.0'), + $packageA0first = $this->getPackage('a0/first', '1.2.3'), + $packageFalias2 = $this->getAliasPackage($packageF, 'dev-bar'), + ); + + $packageD->setRequires(array( + 'f/f' => new Link('d/d', 'f/f', $this->getVersionConstraint('>', '0.2'), 'requires'), + 'g/provider' => new Link('d/d', 'g/provider', $this->getVersionConstraint('>', '0.2'), 'requires'), + )); + $packageG->setProvides(array('g/provider' => new Link('g/g', 'g/provider', $this->getVersionConstraint('==', '1.0.0'), 'provides'))); + + $expectedOperations = array( + array('job' => 'uninstall', 'package' => $packageC), + array('job' => 'uninstall', 'package' => $packageE), + array('job' => 'install', 'package' => $packageA0first), + array('job' => 'update', 'from' => $packageB, 'to' => $packageBnew), + array('job' => 'install', 'package' => $packageG), + array('job' => 'install', 'package' => $packageF), + array('job' => 'markAliasInstalled', 'package' => $packageFalias1), + array('job' => 'markAliasInstalled', 'package' => $packageFalias2), + array('job' => 'install', 'package' => $packageD), + array('job' => 'markAliasUninstalled', 'package' => $packageEalias), + ); + + $transaction = new Transaction($presentPackages, $resultPackages); + $this->checkTransactionOperations($transaction, $expectedOperations); + } + + protected function checkTransactionOperations(Transaction $transaction, array $expected) + { + $result = array(); + foreach ($transaction->getOperations() as $operation) { + if ('update' === $operation->getOperationType()) { + $result[] = array( + 'job' => 'update', + 'from' => $operation->getInitialPackage(), + 'to' => $operation->getTargetPackage(), + ); + } else { + $result[] = array( + 'job' => $operation->getOperationType(), + 'package' => $operation->getPackage(), + ); + } + } + + $this->assertEquals($expected, $result); + } +} diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index a6a9b6b61..793c5eab5 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -16,6 +16,8 @@ use Composer\Test\TestCase; class ArchiveDownloaderTest extends TestCase { + protected $config; + public function testGetFileName() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); @@ -28,8 +30,13 @@ class ArchiveDownloaderTest extends TestCase $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); + $this->config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue('/vendor')); + $first = $method->invoke($downloader, $packageMock, '/path'); - $this->assertRegExp('#/path/[a-z0-9]+\.js#', $first); + $this->assertRegExp('#/vendor/composer/[a-z0-9]+\.js#', $first); $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path')); } @@ -156,7 +163,11 @@ class ArchiveDownloaderTest extends TestCase { return $this->getMockForAbstractClass( 'Composer\Downloader\ArchiveDownloader', - array($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getMockBuilder('Composer\Config')->getMock()) + array( + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), + $this->config = $this->getMockBuilder('Composer\Config')->getMock(), + new \Composer\Util\HttpDownloader($io, $this->config), + ) ); } } diff --git a/tests/Composer/Test/Downloader/DownloadManagerTest.php b/tests/Composer/Test/Downloader/DownloadManagerTest.php index b5c63a8d9..4253882b6 100644 --- a/tests/Composer/Test/Downloader/DownloadManagerTest.php +++ b/tests/Composer/Test/Downloader/DownloadManagerTest.php @@ -50,7 +50,7 @@ class DownloadManagerTest extends TestCase $this->setExpectedException('InvalidArgumentException'); - $manager->getDownloaderForInstalledPackage($package); + $manager->getDownloaderForPackage($package); } public function testGetDownloaderForCorrectlyInstalledDistPackage() @@ -82,7 +82,7 @@ class DownloadManagerTest extends TestCase ->with('pear') ->will($this->returnValue($downloader)); - $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package)); + $this->assertSame($downloader, $manager->getDownloaderForPackage($package)); } public function testGetDownloaderForIncorrectlyInstalledDistPackage() @@ -116,7 +116,7 @@ class DownloadManagerTest extends TestCase $this->setExpectedException('LogicException'); - $manager->getDownloaderForInstalledPackage($package); + $manager->getDownloaderForPackage($package); } public function testGetDownloaderForCorrectlyInstalledSourcePackage() @@ -148,7 +148,7 @@ class DownloadManagerTest extends TestCase ->with('git') ->will($this->returnValue($downloader)); - $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package)); + $this->assertSame($downloader, $manager->getDownloaderForPackage($package)); } public function testGetDownloaderForIncorrectlyInstalledSourcePackage() @@ -182,7 +182,7 @@ class DownloadManagerTest extends TestCase $this->setExpectedException('LogicException'); - $manager->getDownloaderForInstalledPackage($package); + $manager->getDownloaderForPackage($package); } public function testGetDownloaderForMetapackage() @@ -195,7 +195,7 @@ class DownloadManagerTest extends TestCase $manager = new DownloadManager($this->io, false, $this->filesystem); - $this->assertNull($manager->getDownloaderForInstalledPackage($package)); + $this->assertNull($manager->getDownloaderForPackage($package)); } public function testFullPackageDownload() @@ -223,11 +223,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -274,16 +274,16 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->at(0)) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloaderFail)); $manager ->expects($this->at(1)) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloaderSuccess)); @@ -333,11 +333,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -369,11 +369,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -399,11 +399,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue(null)); // There is no downloader for Metapackages. @@ -435,11 +435,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -472,11 +472,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -509,11 +509,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -550,33 +550,30 @@ class DownloadManagerTest extends TestCase $initial ->expects($this->once()) ->method('getDistType') - ->will($this->returnValue('pear')); + ->will($this->returnValue('zip')); $target = $this->createPackageMock(); $target ->expects($this->once()) - ->method('getDistType') - ->will($this->returnValue('pear')); + ->method('getInstallationSource') + ->will($this->returnValue('dist')); $target ->expects($this->once()) - ->method('setInstallationSource') - ->with('dist'); + ->method('getDistType') + ->will($this->returnValue('zip')); - $pearDownloader = $this->createDownloaderMock(); - $pearDownloader + $zipDownloader = $this->createDownloaderMock(); + $zipDownloader ->expects($this->once()) ->method('update') ->with($initial, $target, 'vendor/bundles/FOS/UserBundle'); + $zipDownloader + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) - ->getMock(); - $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($pearDownloader)); + $manager = new DownloadManager($this->io, false, $this->filesystem); + $manager->setDownloader('zip', $zipDownloader); $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle'); } @@ -591,113 +588,89 @@ class DownloadManagerTest extends TestCase $initial ->expects($this->once()) ->method('getDistType') - ->will($this->returnValue('pear')); + ->will($this->returnValue('xz')); $target = $this->createPackageMock(); $target - ->expects($this->once()) + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + $target + ->expects($this->any()) ->method('getDistType') - ->will($this->returnValue('composer')); + ->will($this->returnValue('zip')); - $pearDownloader = $this->createDownloaderMock(); - $pearDownloader + $xzDownloader = $this->createDownloaderMock(); + $xzDownloader ->expects($this->once()) ->method('remove') ->with($initial, 'vendor/bundles/FOS/UserBundle'); + $xzDownloader + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage', 'download')) - ->getMock(); - $manager + $zipDownloader = $this->createDownloaderMock(); + $zipDownloader ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($pearDownloader)); - $manager - ->expects($this->once()) - ->method('download') - ->with($target, 'vendor/bundles/FOS/UserBundle', false); + ->method('install') + ->with($target, 'vendor/bundles/FOS/UserBundle'); + $zipDownloader + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + + $manager = new DownloadManager($this->io, false, $this->filesystem); + $manager->setDownloader('xz', $xzDownloader); + $manager->setDownloader('zip', $zipDownloader); $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle'); } - public function testUpdateSourceWithEqualTypes() + /** + * @dataProvider updatesProvider + */ + public function testGetAvailableSourcesUpdateSticksToSameSource($prevPkgSource, $prevPkgIsDev, $targetAvailable, $targetIsDev, $expected) { - $initial = $this->createPackageMock(); - $initial - ->expects($this->once()) - ->method('getInstallationSource') - ->will($this->returnValue('source')); - $initial - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('svn')); + $initial = null; + if ($prevPkgSource) { + $initial = $this->prophesize('Composer\Package\PackageInterface'); + $initial->getInstallationSource()->willReturn($prevPkgSource); + $initial->isDev()->willReturn($prevPkgIsDev); + } - $target = $this->createPackageMock(); - $target - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('svn')); + $target = $this->prophesize('Composer\Package\PackageInterface'); + $target->getSourceType()->willReturn(in_array('source', $targetAvailable, true) ? 'git' : null); + $target->getDistType()->willReturn(in_array('dist', $targetAvailable, true) ? 'zip' : null); + $target->isDev()->willReturn($targetIsDev); - $svnDownloader = $this->createDownloaderMock(); - $svnDownloader - ->expects($this->once()) - ->method('update') - ->with($initial, $target, 'vendor/pkg'); - - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage', 'download')) - ->getMock(); - $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($svnDownloader)); - - $manager->update($initial, $target, 'vendor/pkg'); + $manager = new DownloadManager($this->io, false, $this->filesystem); + $method = new \ReflectionMethod($manager, 'getAvailableSources'); + $method->setAccessible(true); + $this->assertEquals($expected, $method->invoke($manager, $target->reveal(), $initial ? $initial->reveal() : null)); } - public function testUpdateSourceWithNotEqualTypes() + public static function updatesProvider() { - $initial = $this->createPackageMock(); - $initial - ->expects($this->once()) - ->method('getInstallationSource') - ->will($this->returnValue('source')); - $initial - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('svn')); - - $target = $this->createPackageMock(); - $target - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('git')); - - $svnDownloader = $this->createDownloaderMock(); - $svnDownloader - ->expects($this->once()) - ->method('remove') - ->with($initial, 'vendor/pkg'); - - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage', 'download')) - ->getMock(); - $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($svnDownloader)); - $manager - ->expects($this->once()) - ->method('download') - ->with($target, 'vendor/pkg', true); - - $manager->update($initial, $target, 'vendor/pkg'); + return array( + // prevPkg source, prevPkg isDev, pkg available, pkg isDev, expected + // updates keep previous source as preference + array('source', false, array('source', 'dist'), false, array('source', 'dist')), + array('dist', false, array('source', 'dist'), false, array('dist', 'source')), + // updates do not keep previous source if target package does not have it + array('source', false, array('dist'), false, array('dist')), + array('dist', false, array('source'), false, array('source')), + // updates do not keep previous source if target is dev and prev wasn't dev and installed from dist + array('source', false, array('source', 'dist'), true, array('source', 'dist')), + array('dist', false, array('source', 'dist'), true, array('source', 'dist')), + // install picks the right default + array(null, null, array('source', 'dist'), true, array('source', 'dist')), + array(null, null, array('dist'), true, array('dist')), + array(null, null, array('source'), true, array('source')), + array(null, null, array('source', 'dist'), false, array('dist', 'source')), + array(null, null, array('dist'), false, array('dist')), + array(null, null, array('source'), false, array('source')), + ); } public function testUpdateMetapackage() @@ -707,11 +680,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->expects($this->exactly(2)) + ->method('getDownloaderForPackage') ->with($initial) ->will($this->returnValue(null)); // There is no downloader for metapackages. @@ -730,11 +703,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($pearDownloader)); @@ -747,11 +720,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue(null)); // There is no downloader for metapackages. @@ -790,11 +763,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -833,11 +806,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -879,11 +852,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'source')); @@ -926,11 +899,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'source')); @@ -973,11 +946,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'auto')); @@ -1020,11 +993,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'auto')); @@ -1063,11 +1036,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'source')); @@ -1106,11 +1079,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'dist')); diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 8a78c241b..af5a9da11 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -15,16 +15,26 @@ namespace Composer\Test\Downloader; use Composer\Downloader\FileDownloader; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; +use Composer\Util\Loop; class FileDownloaderTest extends TestCase { - protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null) + private $httpDownloader; + private $config; + + protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $httpDownloader = null, $filesystem = null) { $io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $config = $config ?: $this->getMockBuilder('Composer\Config')->getMock(); - $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); + $this->config = $config ?: $this->getMockBuilder('Composer\Config')->getMock(); + $httpDownloader = $httpDownloader ?: $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); + $httpDownloader + ->expects($this->any()) + ->method('addCopy') + ->will($this->returnValue(\React\Promise\resolve(new Response(array('url' => 'http://example.org/'), 200, array(), 'file~')))); + $this->httpDownloader = $httpDownloader; - return new FileDownloader($io, $config, $eventDispatcher, $cache, $rfs, $filesystem); + return new FileDownloader($io, $this->config, $httpDownloader, $eventDispatcher, $cache, $filesystem); } /** @@ -45,11 +55,11 @@ class FileDownloaderTest extends TestCase public function testDownloadToExistingFile() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); - $packageMock->expects($this->once()) + $packageMock->expects($this->any()) ->method('getDistUrl') ->will($this->returnValue('url')) ; - $packageMock->expects($this->once()) + $packageMock->expects($this->any()) ->method('getDistUrls') ->will($this->returnValue(array('url'))) ; @@ -84,7 +94,12 @@ class FileDownloaderTest extends TestCase $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); - $this->assertEquals('/path/script.js', $method->invoke($downloader, $packageMock, '/path')); + $this->config->expects($this->once()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue('/vendor')); + + $this->assertRegExp('#/vendor/composer/[a-z0-9]+\.js#', $method->invoke($downloader, $packageMock, '/path')); } public function testDownloadButFileIsUnsaved() @@ -117,9 +132,18 @@ class FileDownloaderTest extends TestCase ; $downloader = $this->getDownloader($ioMock); + + $this->config->expects($this->once()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue($path.'/vendor')); + try { - $downloader->download($packageMock, $path); - $this->fail(); + $promise = $downloader->download($packageMock, $path); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + + $this->fail('Download was expected to throw'); } catch (\Exception $e) { if (is_dir($path)) { $fs = new Filesystem(); @@ -128,7 +152,7 @@ class FileDownloaderTest extends TestCase unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e); + $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); $this->assertContains('could not be saved to', $e->getMessage()); } } @@ -187,12 +211,25 @@ class FileDownloaderTest extends TestCase $path = $this->getUniqueTmpDirectory(); $downloader = $this->getDownloader(null, null, null, null, null, $filesystem); + // make sure the file expected to be downloaded is on disk already - touch($path.'/script.js'); + $this->config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue($path.'/vendor')); + + $method = new \ReflectionMethod($downloader, 'getFileName'); + $method->setAccessible(true); + $dlFile = $method->invoke($downloader, $packageMock, $path); + mkdir(dirname($dlFile), 0777, true); + touch($dlFile); try { - $downloader->download($packageMock, $path); - $this->fail(); + $promise = $downloader->download($packageMock, $path); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + + $this->fail('Download was expected to throw'); } catch (\Exception $e) { if (is_dir($path)) { $fs = new Filesystem(); @@ -201,7 +238,7 @@ class FileDownloaderTest extends TestCase unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e); + $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); $this->assertContains('checksum verification', $e->getMessage()); } } @@ -217,7 +254,7 @@ class FileDownloaderTest extends TestCase ->will($this->returnValue('1.2.0.0')); $newPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); - $newPackage->expects($this->once()) + $newPackage->expects($this->any()) ->method('getFullPrettyVersion') ->will($this->returnValue('1.0.0')); $newPackage->expects($this->once()) @@ -232,17 +269,37 @@ class FileDownloaderTest extends TestCase $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) + ->method('writeError') + ->with($this->stringContains('Downloading')); + + $ioMock->expects($this->at(1)) ->method('writeError') ->with($this->stringContains('Downgrading')); $path = $this->getUniqueTmpDirectory(); - touch($path.'/script.js'); $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $filesystem->expects($this->once()) ->method('removeDirectory') ->will($this->returnValue(true)); $downloader = $this->getDownloader($ioMock, null, null, null, null, $filesystem); + + // make sure the file expected to be downloaded is on disk already + $this->config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue($path.'/vendor')); + + $method = new \ReflectionMethod($downloader, 'getFileName'); + $method->setAccessible(true); + $dlFile = $method->invoke($downloader, $newPackage, $path); + mkdir(dirname($dlFile), 0777, true); + touch($dlFile); + + $promise = $downloader->download($newPackage, $path, $oldPackage); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + $downloader->update($oldPackage, $newPackage, $path); } } diff --git a/tests/Composer/Test/Downloader/FossilDownloaderTest.php b/tests/Composer/Test/Downloader/FossilDownloaderTest.php index 97706435d..40ab561db 100644 --- a/tests/Composer/Test/Downloader/FossilDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FossilDownloaderTest.php @@ -48,7 +48,7 @@ class FossilDownloaderTest extends TestCase /** * @expectedException \InvalidArgumentException */ - public function testDownloadForPackageWithoutSourceReference() + public function testInstallForPackageWithoutSourceReference() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->once()) @@ -56,10 +56,10 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); - $downloader->download($packageMock, '/path'); + $downloader->install($packageMock, '/path'); } - public function testDownload() + public function testInstall() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -89,7 +89,7 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'repo'); + $downloader->install($packageMock, 'repo'); } /** @@ -104,7 +104,9 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock); $downloader->update($initialPackageMock, $sourcePackageMock, '/path'); + $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock); } public function testUpdate() @@ -140,7 +142,9 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testRemove() diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 393ecfc5f..4c1e37806 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\Platform; +use Prophecy\Argument; class GitDownloaderTest extends TestCase { @@ -80,6 +81,9 @@ class GitDownloaderTest extends TestCase $downloader = $this->getDownloaderMock(); $downloader->download($packageMock, '/path'); + $downloader->prepare('install', $packageMock, '/path'); + $downloader->install($packageMock, '/path'); + $downloader->cleanup('install', $packageMock, '/path'); } public function testDownload() @@ -131,6 +135,9 @@ class GitDownloaderTest extends TestCase $downloader = $this->getDownloaderMock(null, null, $processExecutor); $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } public function testDownloadWithCache() @@ -211,6 +218,9 @@ class GitDownloaderTest extends TestCase $downloader = $this->getDownloaderMock(null, $config, $processExecutor); $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); @rmdir($cachePath); } @@ -281,6 +291,9 @@ class GitDownloaderTest extends TestCase $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } public function pushUrlProvider() @@ -345,11 +358,11 @@ class GitDownloaderTest extends TestCase $downloader = $this->getDownloaderMock(null, $config, $processExecutor); $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } - /** - * @expectedException \RuntimeException - */ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { $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'"); @@ -374,8 +387,20 @@ class GitDownloaderTest extends TestCase ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(1)); - $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe + try { + $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); + $this->fail('This test should throw'); + } catch (\RuntimeException $e) { + if ('RuntimeException' !== get_class($e)) { + throw $e; + } + $this->assertEquals('RuntimeException', get_class($e)); + } } /** @@ -390,12 +415,15 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->download($sourcePackageMock, '/path', $initialPackageMock); + $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock); $downloader->update($initialPackageMock, $sourcePackageMock, '/path'); + $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock); } public function testUpdate() { - $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -407,44 +435,27 @@ class GitDownloaderTest extends TestCase $packageMock->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.0.0.0')); - $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) - ->will($this->returnValue(0)); - $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(2)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) - ->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)) - ->method('execute') - ->with($this->equalTo('git branch -r')) - ->will($this->returnValue(0)); - $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($this->workingDir))) - ->will($this->returnValue(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); + $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0); + $process->execute($expectedGitUpdateCommand, null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); + $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testUpdateWithNewRepoUrl() { - $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -459,27 +470,20 @@ class GitDownloaderTest extends TestCase $packageMock->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.0.0.0')); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) + ->with($this->equalTo($this->winCompat("git --version"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnCallback(function ($cmd, &$output, $cwd) { - $output = 'origin https://github.com/old/url (fetch) -origin https://github.com/old/url (push) -composer https://github.com/old/url (fetch) -composer https://github.com/old/url (push) -'; - - return 0; - })); + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') ->with($this->equalTo($this->winCompat("git remote -v"))) @@ -497,26 +501,41 @@ composer https://github.com/old/url (push) ->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)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git remote -v"))) + ->will($this->returnCallback(function ($cmd, &$output, $cwd) { + $output = 'origin https://github.com/old/url (fetch) +origin https://github.com/old/url (push) +composer https://github.com/old/url (fetch) +composer https://github.com/old/url (push) +'; + + return 0; + })); + $processExecutor->expects($this->at(8)) ->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(8)) + $processExecutor->expects($this->at(9)) ->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)); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } /** * @group failing - * @expectedException \RuntimeException */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand2 = $this->winCompat("git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'git@github.com:composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -528,37 +547,39 @@ composer https://github.com/old/url (push) $packageMock->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.0.0.0')); - $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) - ->will($this->returnValue(0)); - $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(2)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) - ->method('execute') - ->with($this->equalTo($expectedGitUpdateCommand)) - ->will($this->returnValue(1)); + + $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); + $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0); + $process->execute($expectedGitUpdateCommand, null, $this->winCompat($this->workingDir))->willReturn(1)->shouldBeCalled(); + $process->execute($expectedGitUpdateCommand2, null, $this->winCompat($this->workingDir))->willReturn(1)->shouldBeCalled(); + $process->getErrorOutput()->willReturn(''); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); - $downloader->update($packageMock, $packageMock, $this->workingDir); + + // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe + try { + $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); + $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); + $this->fail('This test should throw'); + } catch (\RuntimeException $e) { + if ('RuntimeException' !== get_class($e)) { + throw $e; + } + $this->assertEquals('RuntimeException', get_class($e)); + } } public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover() { - $expectedFirstGitUpdateCommand = $this->winCompat("(git remote set-url composer '' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer ''"); - $expectedSecondGitUpdateCommand = $this->winCompat("(git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer 'https://github.com/composer/composer'"); + $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."'"); + $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -569,52 +590,25 @@ composer https://github.com/old/url (push) ->will($this->returnValue('1.0.0.0')); $packageMock->expects($this->any()) ->method('getSourceUrls') - ->will($this->returnValue(array('/foo/bar', 'https://github.com/composer/composer'))); - $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) - ->will($this->returnValue(0)); - $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(2)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) - ->method('execute') - ->with($this->equalTo($expectedFirstGitUpdateCommand)) - ->will($this->returnValue(1)); - $processExecutor->expects($this->at(6)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git --version"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(7)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(8)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(9)) - ->method('execute') - ->with($this->equalTo($expectedSecondGitUpdateCommand)) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(11)) - ->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)); + ->will($this->returnValue(array(Platform::isWindows() ? 'C:\\' : '/', 'https://github.com/composer/composer'))); + + $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); + $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0); + $process->execute($expectedFirstGitUpdateCommand, Argument::cetera())->willReturn(1)->shouldBeCalled(); + $process->execute($expectedSecondGitUpdateCommand, Argument::cetera())->willReturn(0)->shouldBeCalled(); + $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); + $process->getErrorOutput()->willReturn(''); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testDowngradeShowsAppropriateMessage() @@ -659,7 +653,10 @@ composer https://github.com/old/url (push) $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor); + $downloader->download($newPackage, $this->workingDir, $oldPackage); + $downloader->prepare('update', $newPackage, $this->workingDir, $oldPackage); $downloader->update($oldPackage, $newPackage, $this->workingDir); + $downloader->cleanup('update', $newPackage, $this->workingDir, $oldPackage); } public function testNotUsingDowngradingWithReferences() @@ -694,11 +691,14 @@ composer https://github.com/old/url (push) $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) ->method('writeError') - ->with($this->stringContains('updating')); + ->with($this->stringContains('Upgrading')); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor); + $downloader->download($newPackage, $this->workingDir, $oldPackage); + $downloader->prepare('update', $newPackage, $this->workingDir, $oldPackage); $downloader->update($oldPackage, $newPackage, $this->workingDir); + $downloader->cleanup('update', $newPackage, $this->workingDir, $oldPackage); } public function testRemove() @@ -718,7 +718,9 @@ composer https://github.com/old/url (push) ->will($this->returnValue(true)); $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem); + $downloader->prepare('uninstall', $packageMock, 'composerPath'); $downloader->remove($packageMock, 'composerPath'); + $downloader->cleanup('uninstall', $packageMock, 'composerPath'); } public function testGetInstallationSource() diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index c074e18dd..0b00bd7d2 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -56,7 +56,7 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); - $downloader->download($packageMock, '/path'); + $downloader->install($packageMock, '/path'); } public function testDownload() @@ -83,7 +83,7 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); } /** @@ -98,7 +98,9 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock); $downloader->update($initialPackageMock, $sourcePackageMock, '/path'); + $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock); } public function testUpdate() @@ -129,7 +131,9 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testRemove() @@ -148,7 +152,9 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(true)); $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem); + $downloader->prepare('uninstall', $packageMock, 'composerPath'); $downloader->remove($packageMock, 'composerPath'); + $downloader->cleanup('uninstall', $packageMock, 'composerPath'); } public function testGetInstallationSource() diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index 3b5f4165c..ca9386332 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Repository\VcsRepository; use Composer\IO\IOInterface; use Composer\Test\TestCase; +use Composer\Factory; use Composer\Util\Filesystem; /** @@ -96,7 +97,7 @@ class PerforceDownloaderTest extends TestCase { $repository = $this->getMockBuilder('Composer\Repository\VcsRepository') ->setMethods(array('getRepoConfig')) - ->setConstructorArgs(array($repoConfig, $io, $config)) + ->setConstructorArgs(array($repoConfig, $io, $config, Factory::createHttpDownloader($io, $config))) ->getMock(); $repository->expects($this->any())->method('getRepoConfig')->will($this->returnValue($repoConfig)); @@ -123,7 +124,7 @@ class PerforceDownloaderTest extends TestCase * @depends testInitPerforceInstantiatesANewPerforceObject * @depends testInitPerforceDoesNothingIfPerforceAlreadySet */ - public function testDoDownloadWithTag() + public function testDoInstallWithTag() { //I really don't like this test but the logic of each Perforce method is tested in the Perforce class. Really I am just enforcing workflow. $ref = 'SOURCE_REF@123'; @@ -131,7 +132,7 @@ class PerforceDownloaderTest extends TestCase $this->package->expects($this->once())->method('getSourceReference')->will($this->returnValue($ref)); $this->io->expects($this->once())->method('writeError')->with($this->stringContains('Cloning '.$ref)); $perforceMethods = array('setStream', 'p4Login', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase', 'cleanupClientSpec'); - $perforce = $this->getMockBuilder('Composer\Util\Perforce', $perforceMethods)->disableOriginalConstructor()->getMock(); + $perforce = $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); $perforce->expects($this->at(0))->method('initializePath')->with($this->equalTo($this->testPath)); $perforce->expects($this->at(1))->method('setStream')->with($this->equalTo($ref)); $perforce->expects($this->at(2))->method('p4Login'); @@ -140,21 +141,21 @@ class PerforceDownloaderTest extends TestCase $perforce->expects($this->at(5))->method('syncCodeBase')->with($label); $perforce->expects($this->at(6))->method('cleanupClientSpec'); $this->downloader->setPerforce($perforce); - $this->downloader->doDownload($this->package, $this->testPath, 'url'); + $this->downloader->doInstall($this->package, $this->testPath, 'url'); } /** * @depends testInitPerforceInstantiatesANewPerforceObject * @depends testInitPerforceDoesNothingIfPerforceAlreadySet */ - public function testDoDownloadWithNoTag() + public function testDoInstallWithNoTag() { $ref = 'SOURCE_REF'; $label = null; $this->package->expects($this->once())->method('getSourceReference')->will($this->returnValue($ref)); $this->io->expects($this->once())->method('writeError')->with($this->stringContains('Cloning '.$ref)); $perforceMethods = array('setStream', 'p4Login', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase', 'cleanupClientSpec'); - $perforce = $this->getMockBuilder('Composer\Util\Perforce', $perforceMethods)->disableOriginalConstructor()->getMock(); + $perforce = $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); $perforce->expects($this->at(0))->method('initializePath')->with($this->equalTo($this->testPath)); $perforce->expects($this->at(1))->method('setStream')->with($this->equalTo($ref)); $perforce->expects($this->at(2))->method('p4Login'); @@ -163,6 +164,6 @@ class PerforceDownloaderTest extends TestCase $perforce->expects($this->at(5))->method('syncCodeBase')->with($label); $perforce->expects($this->at(6))->method('cleanupClientSpec'); $this->downloader->setPerforce($perforce); - $this->downloader->doDownload($this->package, $this->testPath, 'url'); + $this->downloader->doInstall($this->package, $this->testPath, 'url'); } } diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index 6df782ddb..f770b0d35 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -16,7 +16,8 @@ use Composer\Downloader\XzDownloader; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\Platform; -use Composer\Util\RemoteFilesystem; +use Composer\Util\Loop; +use Composer\Util\HttpDownloader; class XzDownloaderTest extends TestCase { @@ -66,10 +67,14 @@ class XzDownloaderTest extends TestCase ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); - $downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io)); + $downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); try { - $downloader->download($packageMock, $this->getUniqueTmpDirectory()); + $promise = $downloader->download($packageMock, $this->testDir.'/install-path'); + $loop = new Loop($httpDownloader); + $loop->wait(array($promise)); + $downloader->install($packageMock, $this->testDir.'/install-path'); + $this->fail('Download of invalid tarball should throw an exception'); } catch (\RuntimeException $e) { $this->assertRegexp('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage()); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 4ff3f055c..e3bbe45a8 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -16,6 +16,8 @@ use Composer\Downloader\ZipDownloader; use Composer\Package\PackageInterface; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\HttpDownloader; +use Composer\Util\Loop; class ZipDownloaderTest extends TestCase { @@ -23,14 +25,19 @@ class ZipDownloaderTest extends TestCase * @var string */ private $testDir; + private $httpDownloader; private $io; private $config; + private $package; public function setUp() { $this->testDir = $this->getUniqueTmpDirectory(); $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $this->config = $this->getMockBuilder('Composer\Config')->getMock(); + $dlConfig = $this->getMockBuilder('Composer\Config')->getMock(); + $this->httpDownloader = new HttpDownloader($this->io, $dlConfig); + $this->package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); } public function tearDown() @@ -62,43 +69,34 @@ class ZipDownloaderTest extends TestCase $this->markTestSkipped('zip extension missing'); } - $this->config->expects($this->at(0)) - ->method('get') - ->with('disable-tls') - ->will($this->returnValue(false)); - $this->config->expects($this->at(1)) - ->method('get') - ->with('cafile') - ->will($this->returnValue(null)); - $this->config->expects($this->at(2)) - ->method('get') - ->with('capath') - ->will($this->returnValue(null)); - $this->config->expects($this->at(3)) + $this->config->expects($this->any()) ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); - $packageMock->expects($this->any()) + $this->package->expects($this->any()) ->method('getDistUrl') ->will($this->returnValue($distUrl = 'file://'.__FILE__)) ; - $packageMock->expects($this->any()) + $this->package->expects($this->any()) ->method('getDistUrls') ->will($this->returnValue(array($distUrl))) ; - $packageMock->expects($this->atLeastOnce()) + $this->package->expects($this->atLeastOnce()) ->method('getTransportOptions') ->will($this->returnValue(array())) ; - $downloader = new ZipDownloader($this->io, $this->config); + $downloader = new ZipDownloader($this->io, $this->config, $this->httpDownloader); $this->setPrivateProperty('hasSystemUnzip', false); try { - $downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test'); + $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test'); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + $downloader->install($this->package, $path); + $this->fail('Download of invalid zip files should throw an exception'); } catch (\Exception $e) { $this->assertContains('is not a zip archive', $e->getMessage()); @@ -117,8 +115,7 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasZipArchive', true); - $downloader = new MockedZipDownloader($this->io, $this->config); - + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) ->method('open') @@ -128,7 +125,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(false)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -143,8 +140,7 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasZipArchive', true); - $downloader = new MockedZipDownloader($this->io, $this->config); - + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) ->method('open') @@ -154,7 +150,7 @@ class ZipDownloaderTest extends TestCase ->will($this->throwException(new \ErrorException('Not a directory'))); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -168,8 +164,7 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasZipArchive', true); - $downloader = new MockedZipDownloader($this->io, $this->config); - + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) ->method('open') @@ -179,7 +174,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(true)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -199,8 +194,8 @@ class ZipDownloaderTest extends TestCase ->method('execute') ->will($this->returnValue(1)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } public function testSystemUnzipOnlyGood() @@ -216,8 +211,8 @@ class ZipDownloaderTest extends TestCase ->method('execute') ->will($this->returnValue(0)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } public function testNonWindowsFallbackGood() @@ -243,9 +238,9 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(true)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -275,9 +270,9 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } public function testWindowsFallbackGood() @@ -303,9 +298,9 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -335,21 +330,26 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } } class MockedZipDownloader extends ZipDownloader { - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { return; } - public function extract($file, $path) + public function install(PackageInterface $package, $path, $output = true) { - parent::extract($file, $path); + return; + } + + public function extract(PackageInterface $package, $file, $path) + { + parent::extract($package, $file, $path); } } diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index f1a7bb1db..c038681da 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -21,7 +21,7 @@ use Composer\Composer; use Composer\Test\TestCase; use Composer\IO\BufferIO; use Composer\Script\ScriptEvents; -use Composer\Script\CommandEvent; +use Composer\Script\Event as ScriptEvent; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Output\OutputInterface; @@ -52,30 +52,6 @@ class EventDispatcherTest extends TestCase $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); } - /** - * @group legacy - */ - public function testDispatcherCanConvertScriptEventToCommandEventForListener() - { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $dispatcher = $this->getDispatcherStubForListenersTest(array( - 'Composer\Test\EventDispatcher\EventDispatcherTest::expectsCommandEvent', - ), $io); - - $this->setExpectedException('PHPUnit\Framework\Error\Deprecated'); - $this->assertEquals(1, $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false)); - } - - public function testDispatcherDoesNotAttemptConversionForListenerWithoutTypehint() - { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $dispatcher = $this->getDispatcherStubForListenersTest(array( - 'Composer\Test\EventDispatcher\EventDispatcherTest::expectsVariableEvent', - ), $io); - - $this->assertEquals(1, $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false)); - } - /** * @dataProvider getValidCommands * @param string $command @@ -125,7 +101,7 @@ class EventDispatcherTest extends TestCase $composer->setPackage($package); $composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest()); - $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->getMock()); + $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock()); $dispatcher = new EventDispatcher( $composer, @@ -195,6 +171,49 @@ class EventDispatcherTest extends TestCase return $rm; } + public function testDispatcherRemoveListener() + { + $composer = $this->createComposerInstance(); + + $composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest()); + $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock()); + + $dispatcher = new EventDispatcher( + $composer, + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), + $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock() + ); + + $listener = array($this, 'someMethod'); + $listener2 = array($this, 'someMethod2'); + $listener3 = 'Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod'; + + $dispatcher->addListener('ev1', $listener, 0); + $dispatcher->addListener('ev1', $listener, 1); + $dispatcher->addListener('ev1', $listener2, 1); + $dispatcher->addListener('ev1', $listener3); + $dispatcher->addListener('ev2', $listener3); + $dispatcher->addListener('ev2', $listener); + $dispatcher->dispatch('ev1'); + $dispatcher->dispatch('ev2'); + + $expected = '> ev1: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL + .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod2'.PHP_EOL + .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL + .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL + .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL + .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); + + $dispatcher->removeListener($this); + $dispatcher->dispatch('ev1'); + $dispatcher->dispatch('ev2'); + + $expected .= '> ev1: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL + .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); + } + public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack() { $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); @@ -358,7 +377,7 @@ class EventDispatcherTest extends TestCase return array(); })); - $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io)); + $dispatcher->dispatch('root', new ScriptEvent('root', $composer, $io)); $expected = '> root: @group'.PHP_EOL. '> group: echo -n foo'.PHP_EOL. '> group: @subgroup'.PHP_EOL. @@ -399,7 +418,7 @@ class EventDispatcherTest extends TestCase return array(); })); - $dispatcher->dispatch('helloWorld', new CommandEvent('helloWorld', $composer, $io)); + $dispatcher->dispatch('helloWorld', new ScriptEvent('helloWorld', $composer, $io)); $expected = "> helloWorld: @hello World".PHP_EOL. "> hello: echo Hello " .escapeshellarg('World').PHP_EOL; @@ -437,7 +456,7 @@ class EventDispatcherTest extends TestCase return array(); })); - $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io)); + $dispatcher->dispatch('root', new ScriptEvent('root', $composer, $io)); } private function getDispatcherStubForListenersTest($listeners, $io) @@ -487,7 +506,7 @@ class EventDispatcherTest extends TestCase ->with($this->equalTo('> echo foo')); $io->expects($this->once()) - ->method('write') + ->method('writeRaw') ->with($this->equalTo('foo'.PHP_EOL), false); $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); @@ -519,6 +538,10 @@ class EventDispatcherTest extends TestCase ->willReturn('> exit 1'); $io->expects($this->at(2)) + ->method('isInteractive') + ->willReturn(1); + + $io->expects($this->at(3)) ->method('writeError') ->with($this->equalTo('Script '.$code.' handling the post-install-cmd event returned with error code 1')); @@ -542,13 +565,9 @@ class EventDispatcherTest extends TestCase ->method('getListeners') ->will($this->returnValue(array())); - $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); - $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); - $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); - $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, true, $policy, $pool, $installedRepo, $request); - $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, true, $policy, $pool, $installedRepo, $request, array()); + $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_OPERATIONS_EXEC, true, true, $transaction); } public static function call() @@ -556,21 +575,16 @@ class EventDispatcherTest extends TestCase throw new \RuntimeException(); } - public static function expectsCommandEvent(CommandEvent $event) - { - return false; - } - - public static function expectsVariableEvent($event) - { - return false; - } - public static function someMethod() { return true; } + public static function someMethod2() + { + return true; + } + private function createComposerInstance() { $composer = new Composer; diff --git a/tests/Composer/Test/FactoryTest.php b/tests/Composer/Test/FactoryTest.php index 6704e5b15..96b0e95d5 100644 --- a/tests/Composer/Test/FactoryTest.php +++ b/tests/Composer/Test/FactoryTest.php @@ -35,6 +35,6 @@ class FactoryTest extends TestCase ->with($this->equalTo('disable-tls')) ->will($this->returnValue(true)); - Factory::createRemoteFilesystem($ioMock, $config); + Factory::createHttpDownloader($ioMock, $config); } } diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index 7eba0a6f0..3ad5b0428 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -22,14 +22,18 @@ Abandoned packages are flagged } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) + - Locking c/c (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals Package a/a is abandoned, you should avoid using it. No replacement was suggested. Package c/c is abandoned, you should avoid using it. Use b/b instead. -Writing lock file Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test index d1609ed9a..df25f7478 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test +++ b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test @@ -28,4 +28,5 @@ install --EXPECT-- Installing a/aliased (dev-master abcd) Marking a/aliased (1.0.0) as installed, alias of a/aliased (dev-master abcd) +Marking a/aliased (9999999-dev abcd) as installed, alias of a/aliased (dev-master abcd) Installing b/requirer (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 cda5d31d3..c13d5fc0e 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -14,12 +14,12 @@ 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": "" } - } - ] - }, - { - "type": "package", - "package": [ + }, + { + "name": "a/req", "version": "dev-master", + "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, + "source": { "reference": "master", "type": "git", "url": "" } + }, { "name": "a/a", "version": "dev-master", "require": { "a/req": "dev-master" } @@ -27,11 +27,6 @@ Aliases take precedence over default package even if default is selected { "name": "a/b", "version": "dev-master", "require": { "a/req": "dev-master" } - }, - { - "name": "a/req", "version": "dev-master", - "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, - "source": { "reference": "master", "type": "git", "url": "" } } ] } @@ -43,10 +38,51 @@ Aliases take precedence over default package even if default is selected }, "minimum-stability": "dev" } +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "require": { "a/req": "dev-master" }, + "type": "library" + }, + { + "name": "a/b", "version": "dev-master", + "require": { "a/req": "dev-master" }, + "type": "library" + }, + { + "name": "a/req", "version": "dev-feature-foo", + "source": { "reference": "feat.f", "type": "git", "url": "" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [ + { + "alias": "dev-master", + "alias_normalized": "dev-master", + "version": "dev-feature-foo", + "package": "a/req" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "a/a": 20, + "a/b": 20, + "a/req": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- install --EXPECT-- Installing a/req (dev-feature-foo feat.f) Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f) Installing a/a (dev-master) +Marking a/a (9999999-dev) as installed, alias of a/a (dev-master) Installing a/b (dev-master) +Marking a/b (9999999-dev) as installed, alias of a/b (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority.test b/tests/Composer/Test/Fixtures/installer/aliased-priority.test index 97ffe5521..8dd0e8470 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority.test @@ -51,6 +51,6 @@ install Installing a/c (dev-feature-foo feat.f) Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f) Installing a/b (dev-master forked) +Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) Installing a/a (dev-master master) Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master) -Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index db4ef23c0..c9a9dba6e 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -19,16 +19,16 @@ Broken dependencies should not lead to a replacer being installed which is not m } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - c/c 1.0.0 requires x/x 1.0 -> no matching package found. + - c/c 1.0.0 requires x/x 1.0 -> could not be found in any version, there may be a typo in the package name. - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. - - Installation request for b/b 1.* -> satisfiable by b/b[1.0.0]. + - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0]. Potential causes: - A typo in the package name diff --git a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test index c89beef6b..2a2cc4b0d 100644 --- a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test +++ b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test @@ -2,7 +2,7 @@ Circular dependencies are possible between packages --COMPOSER-- { - "name": "root", + "name": "root/pkg", "version": "dev-master", "require": { "require/itself": "1.0.0", @@ -32,5 +32,5 @@ Circular dependencies are possible between packages --RUN-- update -v --EXPECT-- -Installing require/itself (1.0.0) Installing regular/pkg (1.0.0) +Installing require/itself (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test new file mode 100644 index 000000000..a59b2ef98 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test @@ -0,0 +1,38 @@ +--TEST-- +Test the error output of solver problems for conflicts between two dependents +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": "1.0.0"} }, + { "name": "victim/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "conflicter/pkg": "1.0.0", + "victim/pkg": "1.0.0" + } +} + + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. + - conflicter/pkg 1.0.0 conflicts with victim/pkg 1.0.0. + - Root composer.json requires victim/pkg 1.0.0 -> satisfiable by victim/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test b/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test new file mode 100644 index 000000000..8df58ef44 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test @@ -0,0 +1,40 @@ +--TEST-- +Test conflicts between a dependency's requirements and the root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "requirer/pkg", "version": "1.0.0", "require": { + "dependency/pkg": "1.0.0", + "dependency/unstable-pkg": "1.0.0-dev" + } }, + { "name": "dependency/pkg", "version": "2.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "requirer/pkg": "1.*", + "dependency/pkg": "2.*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test b/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test index b274c5de2..0e130e747 100644 --- a/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test +++ b/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test @@ -6,19 +6,19 @@ Disjunctive multi constraints work { "type": "package", "package": [ - { "name": "foo", "version": "1.1.0" }, - { "name": "foo", "version": "1.0.0" }, - { "name": "bar", "version": "1.1.0", "require": { "foo": "1.0.*" } } + { "name": "foo/pkg", "version": "1.1.0" }, + { "name": "foo/pkg", "version": "1.0.0" }, + { "name": "bar/pkg", "version": "1.1.0", "require": { "foo/pkg": "1.0.*" } } ] } ], "require": { - "bar": "1.*", - "foo": "1.0.*|1.1.*" + "bar/pkg": "1.*", + "foo/pkg": "1.0.*|1.1.*" } } --RUN-- install --EXPECT-- -Installing foo (1.0.0) -Installing bar (1.1.0) +Installing foo/pkg (1.0.0) +Installing bar/pkg (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index ee221dab0..dcc94dff2 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -28,16 +28,16 @@ Present a clear error message when config.platform.php version results in a conf } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Installation request for a/a ~1.0 -> satisfiable by a/a[1.0.0]. - - a/a 1.0.0 requires php 5.5 -> your PHP version (%s) overridden by "config.platform.php" version (5.3) does not satisfy that requirement. + - Root composer.json requires a/a ~1.0 -> satisfiable by a/a[1.0.0]. + - a/a 1.0.0 requires php 5.5 -> your php version (5.3; overridden via config.platform, actual: %s) does not satisfy that requirement. --EXPECT-- 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..b8968c35b 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -29,17 +29,39 @@ that are also a root package, when that root package is also explicitly whitelis { "name": "a/a", "version": "1.0.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] - +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "1.0.0" + }, + { + "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update a/a b/b --with-dependencies --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 0 installs, 2 updates, 0 removals +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Upgrading a/a (1.0.0 => 1.1.0) + - Upgrading b/b (1.0.0 => 1.1.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- -Updating a/a (1.0.0) to a/a (1.1.0) -Updating b/b (1.0.0) to b/b (1.1.0) +Upgrading a/a (1.0.0 => 1.1.0) +Upgrading b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 1f4b1af27..dc722c379 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -14,7 +14,7 @@ dependency of one the requirements that is whitelisted for update. { "name": "a/a", "version": "1.0.0" }, { "name": "a/a", "version": "1.1.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }, - { "name": "b/b", "version": "1.1.0", "require": { "a/b": "~1.1" } } + { "name": "b/b", "version": "1.1.0", "require": { "a/a": "~1.1" } } ] } ], @@ -30,15 +30,32 @@ dependency of one the requirements that is whitelisted for update. { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/b --with-dependencies --EXPECT-OUTPUT-- -Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information -Updating dependencies (including require-dev) -Nothing to install or update +Updating dependencies +Dependency "a/a" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies. +Nothing to modify in lock file Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index f535caa7e..6fd1f7b53 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -32,5 +32,6 @@ install --EXPECT-- Installing b/b (dev-foo) Marking b/b (dev-master) as installed, alias of b/b (dev-foo) -Installing a/a (dev-master) Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) +Installing a/a (dev-master) +Marking a/a (9999999-dev) as installed, alias of a/a (dev-master) 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 5846d13c0..400e932ed 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -53,3 +53,4 @@ install --prefer-dist } --EXPECT-- Installing a/a (dev-master) +Marking a/a (9999999-dev) as installed, alias of a/a (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test index 7bb69f131..0bba90cff 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -6,22 +6,22 @@ Requirements from the composer file are not installed if the lock file is presen { "type": "package", "package": [ - { "name": "required", "version": "1.0.0" }, - { "name": "newly-required", "version": "1.0.0" } + { "name": "required/pkg", "version": "1.0.0" }, + { "name": "newly-required/pkg", "version": "1.0.0" } ] } ], "require": { - "required": "1.0.0", - "newly-required": "1.0.0" + "required/pkg": "1.0.0", + "newly-required/pkg": "1.0.0" } } --LOCK-- { "packages": [ - { "name": "required", "version": "1.0.0" } + { "name": "required/pkg", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -31,4 +31,4 @@ Requirements from the composer file are not installed if the lock file is presen --RUN-- install --EXPECT-- -Installing required (1.0.0) \ No newline at end of file +Installing required/pkg (1.0.0) 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..b996ff65b 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,26 +6,26 @@ 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": "fixed-dependency", "version": "1.1.0" }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.1.0" }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ] } ], "require": { - "whitelisted": "1.*", - "fixed-dependency": "1.*" + "whitelisted/pkg": "1.*", + "fixed/dependency": "1.*" } } --LOCK-- { "packages": [ - { "name": "whitelisted", "version": "1.1.0" }, - { "name": "fixed-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "fixed/dependency", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], @@ -33,12 +33,12 @@ 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": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ] --RUN-- install --EXPECT-- -Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Removing old/dependency (1.0.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-funding-notice.test b/tests/Composer/Test/Fixtures/installer/install-funding-notice.test index a73badc77..cbdc206d8 100644 --- a/tests/Composer/Test/Fixtures/installer/install-funding-notice.test +++ b/tests/Composer/Test/Fixtures/installer/install-funding-notice.test @@ -41,10 +41,16 @@ Installs a simple package with exact match requirement --RUN-- install --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 3 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 3 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) + - Locking b/b (1.0.0) + - Locking d/d (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 3 installs, 0 updates, 0 removals Generating autoload files 2 packages you are using are looking for funding. Use the `composer fund` command to find out more! diff --git a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test index 5acb7a069..5f6459799 100644 --- a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test @@ -29,7 +29,7 @@ Installing an old alias that doesn't exist anymore from a lock is possible "type": "library" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/install-reference.test b/tests/Composer/Test/Fixtures/installer/install-reference.test index f8e696f99..74bf6e40a 100644 --- a/tests/Composer/Test/Fixtures/installer/install-reference.test +++ b/tests/Composer/Test/Fixtures/installer/install-reference.test @@ -21,3 +21,4 @@ Installs a dev package forcing it's reference install --EXPECT-- Installing a/a (dev-master def000) +Marking a/a (9999999-dev def000) as installed, alias of a/a (dev-master def000) 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-allow-listed-unstable.test similarity index 62% rename from tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test rename to tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test index 3a428c97c..25bd4a9c6 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-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 if an unstable package is not allowed anymore by latest composer.json it should fail --COMPOSER-- { "repositories": [ @@ -48,24 +48,15 @@ Partial update from lock file should apply lock file and downgrade unstable pack ] --RUN-- update c/uptodate ---EXPECT-LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0", "type": "library" }, - { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "2.0.0", "type": "library" }, - { "name": "d/removed", "version": "1.0.0", "type": "library" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} --EXPECT-- -Downgrading b/unstable (1.1.0-alpha) to b/unstable (1.0.0) -Updating a/old (0.9.0) to a/old (1.0.0) -Installing d/removed (1.0.0) + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command. 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..f45f6d528 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 @@ -93,5 +93,5 @@ update b/b "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) +Upgrading a/a (dev-master oldmaster-a => dev-master newmaster-a) +Upgrading b/b (dev-master oldmaster-b => dev-master newmaster-b2) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index a54d66ee9..bce29a21f 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -74,8 +74,8 @@ update b/unstable "platform-dev": [] } --EXPECT-- -Downgrading b/unstable (1.1.0-alpha) to b/unstable (1.0.0) -Updating a/old (0.9.0) to a/old (1.0.0) -Downgrading c/uptodate (2.0.0) to c/uptodate (1.0.0) +Upgrading a/old (0.9.0 => 1.0.0) +Downgrading b/unstable (1.1.0-alpha => 1.0.0) +Downgrading c/uptodate (2.0.0 => 1.0.0) Installing d/removed (1.0.0) Installing e/newreq (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index 9a6f0ab9e..7530ca862 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -97,9 +97,9 @@ update b/b "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) +Upgrading a/a (dev-master oldmaster-a => dev-master newmaster-a) Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a) +Upgrading b/b (dev-master oldmaster-b => dev-master newmaster-b2) Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2) -Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) +Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) 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..74007af7b 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 error --COMPOSER-- { "repositories": [ @@ -30,22 +30,8 @@ Partial update without lock file should update everything whitelisted, remove ov ] --RUN-- update b/unstable ---EXPECT-LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0", "type": "library" }, - { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, - { "name": "d/removed", "version": "1.0.0", "type": "library" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} +--EXPECT-OUTPUT-- +Cannot update only a partial set of packages without a lock file present. +--EXPECT-EXIT-CODE-- +1 --EXPECT-- -Downgrading b/unstable (1.1.0-alpha) to b/unstable (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test index 009eb576d..6a8b2030c 100644 --- a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test +++ b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test @@ -6,26 +6,26 @@ Composer installers and their requirements are installed first { "type": "package", "package": [ - { "name": "pkg", "version": "1.0.0" }, - { "name": "pkg2", "version": "1.0.0" }, - { "name": "inst", "version": "1.0.0", "type": "composer-plugin" }, - { "name": "inst-with-req", "version": "1.0.0", "type": "composer-plugin", "require": { "php": ">=5", "ext-json": "*", "composer-plugin-api": "*" } }, - { "name": "inst-with-req2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } } + { "name": "pkg/1", "version": "1.0.0" }, + { "name": "pkg/2", "version": "1.0.0" }, + { "name": "inst/pkg", "version": "1.0.0", "type": "composer-plugin" }, + { "name": "inst/with-req", "version": "1.0.0", "type": "composer-plugin", "require": { "php": ">=5", "ext-json": "*", "composer-plugin-api": "*" } }, + { "name": "inst/with-req2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg/2": "*" } } ] } ], "require": { - "pkg": "1.0.0", - "inst": "1.0.0", - "inst-with-req2": "1.0.0", - "inst-with-req": "1.0.0" + "pkg/1": "1.0.0", + "inst/pkg": "1.0.0", + "inst/with-req2": "1.0.0", + "inst/with-req": "1.0.0" } } --RUN-- install --EXPECT-- -Installing inst (1.0.0) -Installing inst-with-req (1.0.0) -Installing pkg2 (1.0.0) -Installing inst-with-req2 (1.0.0) -Installing pkg (1.0.0) +Installing inst/pkg (1.0.0) +Installing inst/with-req (1.0.0) +Installing pkg/2 (1.0.0) +Installing inst/with-req2 (1.0.0) +Installing pkg/1 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test new file mode 100644 index 000000000..3a9700103 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test @@ -0,0 +1,49 @@ +--TEST-- +Test that names provided by a dependent and root package cause a conflict only for replace +--COMPOSER-- +{ + "version": "1.2.3", + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "provider/pkg", + "version": "1.0.0", + "provide": { "root-provided/transitive-provided": "2.*", "root-replaced/transitive-provided": "2.*" }, + "replace": { "root-provided/transitive-replaced": "2.*", "root-replaced/transitive-replaced": "2.*" } + } + ] + } + ], + "require": { + "provider/pkg": "*" + }, + "provide": { + "root-provided/transitive-replaced": "2.*", + "root-provided/transitive-provided": "2.*" + }, + "replace": { + "root-replaced/transitive-replaced": "2.*", + "root-replaced/transitive-provided": "2.*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - __root__ is present at version 1.2.3 and cannot be modified by Composer + - provider/pkg 1.0.0 cannot be installed as that would require removing __root__ 1.2.3. They both replace root-replaced/transitive-replaced and thus cannot coexist. + - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test new file mode 100644 index 000000000..343dab537 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test @@ -0,0 +1,45 @@ +--TEST-- +Test that names provided by two dependents cause a conflict +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "provider/pkg", + "version": "1.0.0", + "provide": { "third/pkg": "2.*" } + }, + { + "name": "replacer/pkg", + "version": "1.0.0", + "replace": { "third/pkg": "2.*" } + } + ] + } + ], + "require": { + "provider/pkg": "*", + "replacer/pkg": "*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + - Only one of these can be installed: replacer/pkg 1.0.0, provider/pkg 1.0.0. + - Root composer.json requires replacer/pkg * -> satisfiable by replacer/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test new file mode 100644 index 000000000..1c2ea0ceb --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -0,0 +1,54 @@ +--TEST-- +Test that a replacer can not be installed together with another version of the package it replaces +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "replacer/pkg", "version": "2.0.0", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.1", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.2", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.3", "replace": { "regular/pkg": "self.version" }}, + {"name": "regular/pkg", "version": "1.0.0"}, + {"name": "regular/pkg", "version": "1.0.1"}, + {"name": "regular/pkg", "version": "1.0.2"}, + {"name": "regular/pkg", "version": "1.0.3"}, + {"name": "regular/pkg", "version": "2.0.0"}, + {"name": "regular/pkg", "version": "2.0.1"} + ] + } + ], + "require": { + "regular/pkg": "1.*", + "replacer/pkg": "2.*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Conclusion: don't install regular/pkg 1.0.3, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Conclusion: don't install regular/pkg 1.0.2, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Conclusion: don't install regular/pkg 1.0.1, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test new file mode 100644 index 000000000..b5d3fefe7 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test @@ -0,0 +1,52 @@ +--TEST-- +Test that a requirement can be satisfied by a providing package required in require-dev. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "provider/requirer", "version": "1.0.0", "type": "metapackage", "require": {"b/b": "1.0.0"}}, + {"name": "b/b", "version": "1.0.0", "type": "metapackage", "provide": {"provided/pkg": "1.0.0"}} + ] + } + ], + "require": { + "provided/pkg": "1.0.0" + }, + "require-dev": { + "provider/requirer": "1.0.0" + } +} + +--RUN-- +update --no-dev + +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "b/b", + "version": "1.0.0", + "type": "metapackage", + "provide": {"provided/pkg": "1.0.0"} + } + ], + "packages-dev": [ + { + "name": "provider/requirer", + "version": "1.0.0", + "type": "metapackage", + "require": {"b/b": "1.0.0"} + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Installing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test new file mode 100644 index 000000000..be425010c --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test @@ -0,0 +1,36 @@ +--TEST-- +Test that providers can be installed if they are selected and the package they provide is not installable +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "foo/polyfill", + "provide": { + "foo/standard": "1.0.0" + }, + "version": "1.0.0" + }, + { + "name": "foo/standard", + "require": { + "foo/does-not-exist": "1.0.0" + }, + "version": "1.0.0" + } + ] + } + ], + "require": { + "foo/standard": "1.0.0", + "foo/polyfill": "1.0.0" + } +} + +--RUN-- +update + +--EXPECT-- +Installing foo/polyfill (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json new file mode 100644 index 000000000..9abe90dd8 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json @@ -0,0 +1,34 @@ +--TEST-- +Test that providers can be installed in conjunction with the package they provide if they are selected and the package they provide is also installable +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "foo/polyfill", + "provide": { + "foo/standard": "1.0.0" + }, + "version": "1.0.0" + }, + { + "name": "foo/standard", + "version": "1.0.0" + } + ] + } + ], + "require": { + "foo/standard": "1.0.0", + "foo/polyfill": "1.0.0" + } +} + +--RUN-- +update + +--EXPECT-- +Installing foo/standard (1.0.0) +Installing foo/polyfill (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test new file mode 100644 index 000000000..816b8efe9 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test @@ -0,0 +1,55 @@ +--TEST-- +Test that providers can not be installed if they are not selected +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "foo/polyfill", + "provide": { + "foo/standard": "1.0.0" + }, + "version": "1.0.0" + }, + { + "name": "foo/standard", + "require": { + "foo/does-not-exist": "1.0.0" + }, + "version": "1.0.0" + } + ] + } + ], + "require": { + "foo/standard": "1.0.0" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires foo/standard 1.0.0 -> satisfiable by foo/standard[1.0.0]. + - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> could not be found in any version, there may be a typo in the package name. + +Potential causes: + - A typo in the package name + - The package is not available in a stable-enough version according to your minimum-stability setting + see for more details. + - It's a private package and you forgot to add a custom repository to find it + +Read for further common problems. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/replace-priorities.test b/tests/Composer/Test/Fixtures/installer/replace-priorities.test index d69dd9a22..e561b548b 100644 --- a/tests/Composer/Test/Fixtures/installer/replace-priorities.test +++ b/tests/Composer/Test/Fixtures/installer/replace-priorities.test @@ -6,28 +6,28 @@ Replace takes precedence only in higher priority repositories and if explicitly { "type": "package", "package": [ - { "name": "forked", "version": "1.1.0", "replace": { "package2": "1.1.0" } } + { "name": "forked/pkg", "version": "1.1.0", "replace": { "package/2": "1.1.0" } } ] }, { "type": "package", "package": [ - { "name": "package", "version": "1.0.0" }, - { "name": "package2", "version": "1.0.0" }, - { "name": "package3", "version": "1.0.0", "require": { "forked": "*" } }, - { "name": "hijacker", "version": "1.1.0", "replace": { "package": "1.1.0" } } + { "name": "package/1", "version": "1.0.0" }, + { "name": "package/2", "version": "1.0.0" }, + { "name": "package/3", "version": "1.0.0", "require": { "forked/pkg": "*" } }, + { "name": "hijacker/pkg", "version": "1.1.0", "replace": { "package/1": "1.1.0" } } ] } ], "require": { - "package": "1.*", - "package2": "1.*", - "package3": "1.*" + "package/1": "1.*", + "package/2": "1.*", + "package/3": "1.*" } } --RUN-- install --EXPECT-- -Installing package (1.0.0) -Installing forked (1.1.0) -Installing package3 (1.0.0) +Installing package/1 (1.0.0) +Installing forked/pkg (1.1.0) +Installing package/3 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test new file mode 100644 index 000000000..bc06179e0 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test @@ -0,0 +1,35 @@ +--TEST-- +Packages found in a higher priority repository take precedence even if they are not found in the requested version +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "1.0.0" } + ] + }, + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "2.0.0" } + ] + } + ], + "require": { + "foo/a": "2.*" + } +} +--RUN-- +update +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[1.0.0] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. + +--EXPECT-- +--EXPECT-EXIT-CODE-- +2 diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities2.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities2.test new file mode 100644 index 000000000..598079d80 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities2.test @@ -0,0 +1,26 @@ +--TEST-- +Packages found in a higher priority repository take precedence +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "1.0.0" } + ] + }, + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "1.1.0" } + ] + } + ], + "require": { + "foo/a": "1.*" + } +} +--RUN-- +update +--EXPECT-- +Installing foo/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 15d1b4ef5..202767c1f 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -23,7 +23,7 @@ The locked version will not get overwritten by an install { "name": "foo/bar", "version": "1.0.0" }, { "name": "foo/baz", "version": "2.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -38,4 +38,4 @@ The locked version will not get overwritten by an install --RUN-- install --EXPECT-- -Updating foo/baz (1.0.0) to foo/baz (2.0.0) +Upgrading foo/baz (1.0.0 => 2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index cab45f9dc..9c3d0e534 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -6,50 +6,151 @@ Test the error output of solver problems. { "type": "package", "package": [ + { "name": "package/found", "version": "2.0.0", "require": { + "unstable/package2": "2.*" + } }, + { "name": "package/found2", "version": "2.0.0", "require": { + "invalid/💩package": "*" + } }, + { "name": "package/found3", "version": "2.0.0", "require": { + "unstable/package2": "2.*" + } }, + { "name": "package/found4", "version": "2.0.0", "require": { + "non-existent/pkg2": "1.*" + } }, + { "name": "package/found5", "version": "2.0.0", "require": { + "requirer/pkg": "1.*" + } }, + { "name": "package/found6", "version": "2.0.0", "require": { + "stable-requiree-excluded/pkg2": "1.0.1" + } }, + { "name": "package/found7", "version": "2.0.0", "require": { + "php-64bit": "1.0.1" + } }, + { "name": "conflict/requirer", "version": "2.0.0", "require": { + "conflict/dep": "1.0.0" + } }, + { "name": "conflict/requirer2", "version": "2.0.0", "require": { + "conflict/dep": "2.0.0" + } }, + { "name": "conflict/dep", "version": "1.0.0" }, + { "name": "conflict/dep", "version": "2.0.0" }, { "name": "unstable/package", "version": "2.0.0-alpha" }, { "name": "unstable/package", "version": "1.0.0" }, - { "name": "requirer/pkg", "version": "1.0.0", "require": {"dependency/pkg": "1.0.0" } }, + { "name": "unstable/package2", "version": "2.0.0-alpha" }, + { "name": "unstable/package2", "version": "1.0.0" }, + { "name": "requirer/pkg", "version": "1.0.0", "require": { + "dependency/pkg": "1.0.0", + "dependency/unstable-pkg": "1.0.0-dev" + } }, { "name": "dependency/pkg", "version": "2.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "dependency/unstable-pkg", "version": "1.0.0-dev" }, { "name": "stable-requiree-excluded/pkg", "version": "1.0.1" }, - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "api/provider", "description": "Provides the missing API", "version": "1.0.0", "provide": { "missing/provided-api": "1.*" } } ] } ], "require": { + "package/found": "2.*", + "package/found2": "2.*", + "package/found3": "2.*", + "package/found4": "2.*", + "package/found5": "2.*", + "package/found6": "2.*", + "package/found7": "2.*", + "missing/provided-api": "2.*", + "conflict/requirer": "2.*", + "conflict/requirer2": "2.*", "unstable/package": "2.*", - "bogus/pkg": "1.*", + "non-existent/pkg": "1.*", "requirer/pkg": "1.*", "dependency/pkg": "2.*", - "stable-requiree-excluded/pkg": "1.0.1" + "stable-requiree-excluded/pkg": "1.0.1", + "lib-xml": "1002.*", + "lib-icu": "1001.*", + "ext-xml": "1002.*", + "php": "1" } } --INSTALLED-- [ - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} + --RUN-- -update unstable/package requirer/pkg dependency/pkg +update unstable/package requirer/pkg dependency/pkg conflict/requirer --EXPECT-EXIT-CODE-- 2 --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package unstable/package 2.* exists as unstable/package[1.0.0] but these are rejected by your constraint. + - Root composer.json requires missing/provided-api 2.*, it could not be found in any version, but the following packages provide it: + - api/provider Provides the missing API + Consider requiring one of these to satisfy the missing/provided-api requirement. Problem 2 - - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name. + - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. Problem 3 - - The requested package stable-requiree-excluded/pkg (installed at 1.0.0, required as 1.0.1) is satisfiable by stable-requiree-excluded/pkg[1.0.0] but these conflict with your requirements or minimum-stability. + - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 4 - - Installation request for requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> satisfiable by dependency/pkg[1.0.0] but these conflict with your requirements or minimum-stability. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. + Problem 5 + - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. + Problem 6 + - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. + Problem 7 + - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension. + Problem 8 + - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. + Problem 9 + - Root composer.json requires package/found 2.* -> satisfiable by package/found[2.0.0]. + - package/found 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. + Problem 10 + - Root composer.json requires package/found2 2.* -> satisfiable by package/found2[2.0.0]. + - package/found2 2.0.0 requires invalid/💩package * -> could not be found, it looks like its name is invalid, "💩" is not allowed in package names. + Problem 11 + - Root composer.json requires package/found3 2.* -> satisfiable by package/found3[2.0.0]. + - package/found3 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. + Problem 12 + - Root composer.json requires package/found4 2.* -> satisfiable by package/found4[2.0.0]. + - package/found4 2.0.0 requires non-existent/pkg2 1.* -> could not be found in any version, there may be a typo in the package name. + Problem 13 + - Root composer.json requires package/found6 2.* -> satisfiable by package/found6[2.0.0]. + - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match your constraint. + Problem 14 + - Root composer.json requires package/found7 2.* -> satisfiable by package/found7[2.0.0]. + - package/found7 2.0.0 requires php-64bit 1.0.1 -> your php-64bit version (%s) does not satisfy that requirement. + Problem 15 + - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + Problem 16 + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - Root composer.json requires package/found5 2.* -> satisfiable by package/found5[2.0.0]. Potential causes: - A typo in the package name @@ -58,6 +159,9 @@ Potential causes: - It's a private package and you forgot to add a custom repository to find it Read for further common problems. + To enable extensions, verify that they are enabled in your .ini files: +__inilist__ + You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 198203ce9..413ac8665 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -17,12 +17,16 @@ Suggestions are not displayed for installed packages } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 2 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) + - Locking b/b (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test b/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test new file mode 100644 index 000000000..b4d1fbb09 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test @@ -0,0 +1,32 @@ +--TEST-- +Suggestions are displayed even in non-dev mode for new suggesters installed when updating the lock file +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } } + ] + } + ], + "require": { + "a/a": "1.0.0" + } +} +--RUN-- +install --no-dev +--EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. +Loading composer repositories with package information +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking a/a (1.0.0) +Writing lock file +Installing dependencies from lock file +Package operations: 1 install, 0 updates, 0 removals +1 package suggestions were added by new dependencies, use `composer suggest` to see details. +Generating autoload files + +--EXPECT-- +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 40546f8d0..ed2023504 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -1,5 +1,5 @@ --TEST-- -Suggestions are not displayed in non-dev mode +Suggestions are not displayed for when not updating the lock file --COMPOSER-- { "repositories": [ @@ -14,13 +14,26 @@ Suggestions are not displayed in non-dev mode "a/a": "1.0.0" } } +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- -install --no-dev +install --EXPECT-OUTPUT-- -Loading composer repositories with package information -Updating dependencies +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. Package operations: 1 install, 0 updates, 0 removals -Writing lock file Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index f18054d74..a0e90332e 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -17,12 +17,16 @@ Suggestions are not displayed for packages if they are replaced } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 2 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) + - Locking c/c (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index ae5ff36e3..fff3e19d6 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -17,11 +17,15 @@ Suggestions are displayed --RUN-- install --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 1 install, 0 updates, 0 removals -a/a suggests installing b/b (an obscure reason) +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking a/a (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 0 updates, 0 removals +1 package suggestions were added by new dependencies, use `composer suggest` to see details. Generating autoload files --EXPECT-- 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 new file mode 100644 index 000000000..6997e5a77 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test @@ -0,0 +1,31 @@ +--TEST-- +Test that a conflict against >=5 does not include dev-master or other dev-x +--COMPOSER-- +{ + "repositories": [ + { + "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/pkg2", "version": "dev-foo" } + ] + } + ], + "require": { + "conflicter/pkg": "1.0.0", + "victim/pkg": "*", + "victim/pkg2": "*" + }, + "minimum-stability": "dev" +} + + +--RUN-- +update + +--EXPECT-- +Installing conflicter/pkg (1.0.0) +Installing victim/pkg (dev-master) +Marking victim/pkg (9999999-dev) as installed, alias of victim/pkg (dev-master) +Installing victim/pkg2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index f4f5e98eb..5aceda8ea 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -29,20 +29,6 @@ Update aliased package does not mess up the lock file }, "minimum-stability": "dev" } ---LOCK-- -{ - "_": "outdated lock file, should not have to be loaded in an update", - "packages": [ - { "package": "a/a", "version": "dev-master", "source-reference": "1234" }, - { "package": "a/a", "version": "dev-master", "alias-pretty-version": "1.0.x-dev", "alias-version": "1.0.9999999.9999999-dev" } - ], - "packages-dev": null, - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false -} --INSTALLED-- [ { @@ -73,4 +59,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master 1234) to a/a (dev-master master) +Upgrading a/a (dev-master 1234 => dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias.test b/tests/Composer/Test/Fixtures/installer/update-alias.test index c1020e33c..8da3d4d23 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias.test @@ -33,5 +33,5 @@ Update aliased package to non-aliased version --RUN-- update --EXPECT-- -Updating a/a (dev-master master) to a/a (dev-foo foo) -Marking a/a (1.0.x-dev master) as uninstalled, alias of a/a (dev-master master) \ No newline at end of file +Upgrading a/a (dev-master master => dev-foo foo) +Marking a/a (1.0.x-dev master) as uninstalled, alias of a/a (dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test index cca859e9f..3d93d23bd 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test +++ b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test @@ -35,6 +35,15 @@ Updates updateable packages in dry-run mode ] --RUN-- update --dry-run +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Lock file operations: 3 installs, 0 updates, 0 removals + - Locking a/a (1.0.1) + - Locking a/b (2.0.0) + - Locking a/c (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals + - Upgrading a/a (1.0.0 => 1.0.1) + - Upgrading a/b (1.0.0 => 2.0.0) --EXPECT-- -Updating a/a (1.0.0) to a/a (1.0.1) -Updating a/b (1.0.0) to a/b (2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-all.test b/tests/Composer/Test/Fixtures/installer/update-all.test index a9bb435a1..893f9717c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all.test +++ b/tests/Composer/Test/Fixtures/installer/update-all.test @@ -36,5 +36,5 @@ Updates updateable packages --RUN-- update --EXPECT-- -Updating a/a (1.0.0) to a/a (1.0.1) -Updating a/b (1.0.0) to a/b (2.0.0) +Upgrading a/a (1.0.0 => 1.0.1) +Upgrading a/b (1.0.0 => 2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test new file mode 100644 index 000000000..0f009ae6f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test @@ -0,0 +1,53 @@ +--TEST-- +Update with a package whitelist only updates those packages if they are not present in composer.json +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "fixed/dependency": "1.*" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "fixed/dependency", "version": "1.1.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/sub-dependency", "version": "1.1.0" }, + { "name": "fixed/sub-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "whitelisted/pkg": "1.*", + "fixed/dependency": "1.*" + } +} +--INSTALLED-- +[ + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/sub-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/sub-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update whitelisted/pkg dependency/pkg +--EXPECT-- +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test new file mode 100644 index 000000000..95fd639f2 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test @@ -0,0 +1,65 @@ +--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 +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "dependency/pkg": "1.*", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update whitelisted/pkg-* --with-all-dependencies +--EXPECT-- +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test new file mode 100644 index 000000000..d40a924ab --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test @@ -0,0 +1,67 @@ +--TEST-- +Update with a package whitelist only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "root/pkg-dependency", "version": "1.1.0" }, + { "name": "root/pkg-dependency", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "root/pkg-dependency": "1.*", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "root/pkg-dependency", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "root/pkg-dependency", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} +--RUN-- +update whitelisted/pkg-* --with-dependencies +--EXPECT-- +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test new file mode 100644 index 000000000..55a07b118 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test @@ -0,0 +1,77 @@ +--TEST-- +Update with a package whitelist only updates those packages and their dependencies matching the pattern +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0", "require": { "whitelisted/pkg-component2": "1.1.0" } }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "whitelisted/pkg-component5": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.1.0", "require": { "whitelisted/pkg-component4": "1.1.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, + { "name": "whitelisted/pkg-component4", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "whitelisted/pkg-component3": "1.0.0", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, + { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, + { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update whitelisted/pkg-* foobar --with-dependencies +--EXPECT-- +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test new file mode 100644 index 000000000..6cd1d7778 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test @@ -0,0 +1,61 @@ +--TEST-- +Update with a package whitelist only updates those packages matching the pattern +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} +--RUN-- +update whitelisted/pkg-* +--EXPECT-- +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test new file mode 100644 index 000000000..738f0af74 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test @@ -0,0 +1,69 @@ +--TEST-- +Update with a package whitelist only updates those corresponding to the pattern +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "vendor/test-package", "version": "2.0" }, + { "name": "vendor/not-me", "version": "2.0" }, + { "name": "exact/test-package", "version": "2.0" }, + { "name": "notexact/testpackage", "version": "2.0" }, + { "name": "all/package1", "version": "2.0" }, + { "name": "all/package2", "version": "2.0" }, + { "name": "another/another", "version": "2.0" }, + { "name": "no/regexp", "version": "2.0" } + ] + } + ], + "require": { + "vendor/test-package": "*.*", + "vendor/not-me": "*.*", + "exact/test-package": "*.*", + "notexact/testpackage": "*.*", + "all/package1": "*.*", + "all/package2": "*.*", + "another/another": "*.*", + "no/regexp": "*.*" + } +} +--INSTALLED-- +[ + { "name": "vendor/test-package", "version": "1.0" }, + { "name": "vendor/not-me", "version": "1.0" }, + { "name": "exact/test-package", "version": "1.0" }, + { "name": "notexact/testpackage", "version": "1.0" }, + { "name": "all/package1", "version": "1.0" }, + { "name": "all/package2", "version": "1.0" }, + { "name": "another/another", "version": "1.0" }, + { "name": "no/regexp", "version": "1.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "vendor/test-package", "version": "1.0" }, + { "name": "vendor/not-me", "version": "1.0" }, + { "name": "exact/test-package", "version": "1.0" }, + { "name": "notexact/testpackage", "version": "1.0" }, + { "name": "all/package1", "version": "1.0" }, + { "name": "all/package2", "version": "1.0" }, + { "name": "another/another", "version": "1.0" }, + { "name": "no/regexp", "version": "1.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update vendor/Test* exact/test-package notexact/Test all/* no/reg.?xp +--EXPECT-- +Upgrading all/package1 (1.0 => 2.0) +Upgrading all/package2 (1.0 => 2.0) +Upgrading exact/test-package (1.0 => 2.0) +Upgrading vendor/test-package (1.0 => 2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test similarity index 90% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test index c84f0e65d..ef7ca56c0 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test @@ -28,7 +28,7 @@ Limited update takes rules from lock if available, and not from the installed re { "name": "toupdate/installed", "version": "1.0.0" }, { "name": "toupdate/notinstalled", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -43,6 +43,6 @@ Limited update takes rules from lock if available, and not from the installed re --RUN-- update toupdate/installed --EXPECT-- -Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0) -Updating old/installed (0.9.0) to old/installed (1.0.0) +Upgrading old/installed (0.9.0 => 1.0.0) +Upgrading toupdate/installed (1.0.0 => 1.1.0) Installing toupdate/notinstalled (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test new file mode 100644 index 000000000..9360bc2f6 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test @@ -0,0 +1,48 @@ +--TEST-- +Update with a package whitelist removes unused packages +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.1.0" }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "whitelisted/pkg": "1.*", + "fixed/dependency": "1.*" + } +} +--INSTALLED-- +[ + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update --with-dependencies whitelisted/pkg +--EXPECT-- +Removing old/dependency (1.0.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test new file mode 100644 index 000000000..dc6e9aa5f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -0,0 +1,55 @@ +--TEST-- +If a new requirement cannot be installed on a partial update due to replace, there should be a suggestion to use --with-all-dependencies +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "current/dep": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg +--EXPECT-EXIT-CODE-- +2 +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - current/dep is locked to version 1.0.0 and an update of this package was not requested. + - new/pkg 1.0.0 cannot be installed as that would require removing current/dep 1.0.0. new/pkg replaces current/dep and thus cannot coexist with it. + - Root composer.json requires new/pkg 1.* -> satisfiable by new/pkg[1.0.0]. + +Use the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions. +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test new file mode 100644 index 000000000..d4d258112 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test @@ -0,0 +1,58 @@ +--TEST-- +Verify that partial updates warn about using patterns in the argument which have no matches +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" }, + { "name": "b/b", "version": "1.1.0" } + ] + } + ], + "require": { + "a/a": "~1.0", + "b/b": "~1.0" + } +} + +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } +] + +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update b/b foo/bar baz/* --with-dependencies + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Package "foo/bar" listed for update is not locked. +Pattern "baz/*" listed for update does not match any locked packages. +Lock file operations: 0 installs, 1 update, 0 removals + - Upgrading b/b (1.0.0 => 1.1.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 1 update, 0 removals +Generating autoload files + +--EXPECT-- +Upgrading b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test new file mode 100644 index 000000000..2b68c6c69 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -0,0 +1,99 @@ +--TEST-- +Verify that a partial update with deps correctly keeps track of all aliases. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.1.0", "require": {"current/dep2": "*"} }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "2.x-dev"}}}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "^1.1"} }, + { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "current/dep": "dev-master as 1.1.0", + "current/dep2": "dev-master as 1.1.2", + "current/pkg": "1.0.0 as 2.0.0", + "new/pkg": "1.*" + }, + "minimum-stability": "dev" +} +--INSTALLED-- +[ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } +] +--LOCK-- +{ + "packages": [ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } + ], + "packages-dev": [], + "aliases": [ + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep" + } + ], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-all-dependencies +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "2.x-dev"}}, "type": "library"}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" }, "type": "library"}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "^1.1"}, "type": "library"} + ], + "packages-dev": [], + "aliases": [ + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep" + }, + { + "alias": "1.1.2", + "alias_normalized": "1.1.2.0", + "version": "dev-master", + "package": "current/dep2" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "current/dep": 20, + "current/dep2": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Marking current/dep (1.1.0) as installed, alias of current/dep (dev-master) +Upgrading current/dep2 (dev-foo => dev-master) +Marking current/dep2 (1.1.2) as installed, alias of current/dep2 (dev-master) +Marking current/dep2 (2.x-dev) as installed, alias of current/dep2 (dev-master) +Installing new/pkg (1.0.0) +Marking current/dep2 (1.0.x-dev) as uninstalled, alias of current/dep2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test new file mode 100644 index 000000000..ff1257498 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test @@ -0,0 +1,49 @@ +--TEST-- +When partially updating a package to a newer version and the new version has a new requirement for a package we already have installed, mark it for update +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "root/pkg1", "version": "1.2.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "root/pkg2", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.2.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "root/pkg1": "1.*", + "root/pkg2": "1.*" + } +} +--INSTALLED-- +[ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update root/pkg2 --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.2.0) +Upgrading root/pkg2 (1.0.0 => 1.2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test new file mode 100644 index 000000000..0cb5ad97f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test @@ -0,0 +1,50 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*", "mutual/target-provide": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } }, + { "name": "new/pkg-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*", + "new/pkg-provide": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Removing current/dep (1.0.0) +Installing new/pkg (1.0.0) +Installing new/pkg-provide (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test new file mode 100644 index 000000000..8bb676e76 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test @@ -0,0 +1,44 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "current/dep": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Removing current/dep (1.0.0) +Installing new/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test new file mode 100644 index 000000000..24eb95538 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test @@ -0,0 +1,48 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should update locked dependencies as far as possible +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/pkg", "version": "1.1.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.1.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1" } }, + { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.1.0) +Installing new/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test new file mode 100644 index 000000000..079ad9d2b --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test @@ -0,0 +1,56 @@ +--TEST-- +Update with a package whitelist only updates those packages and their dependencies listed as command arguments +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg": "1.*", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} +--RUN-- +update whitelisted/pkg --with-dependencies +--EXPECT-- +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test new file mode 100644 index 000000000..299c505cb --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test @@ -0,0 +1,54 @@ +--TEST-- +Update with a package whitelist only updates whitelisted packages if no dependency conflicts +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg": "1.*", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} +--RUN-- +update whitelisted/pkg +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list.test b/tests/Composer/Test/Fixtures/installer/update-allow-list.test new file mode 100644 index 000000000..a02e00c4b --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list.test @@ -0,0 +1,57 @@ +--TEST-- +Update with a package whitelist only updates those packages listed as command arguments +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed/pkg": "1.*", + "whitelisted/pkg": "1.*", + "unrelated/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update whitelisted/pkg +--EXPECT-- +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 0a0d47507..4831c7705 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 whitelisted and remains unchanged d/d is dev but with a #ref so it should get URL updated but not the reference e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref -e/e is dev but not whitelisted and gets the new URL but keeps its old ref +f/f is dev but not whitelisted and remains unchanged g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref --COMPOSER-- { @@ -98,6 +98,57 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "transport-options": { "foo": "bar" } } ] +--LOCK-- +{ + "packages": [ + { + "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" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, + "type": "library" + }, + { + "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" + }, + { + "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" } + }, + { + "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" } + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --EXPECT-LOCK-- { "packages": [ @@ -115,8 +166,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "c/c", "version": "1.0.0", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" }, "type": "library" }, { @@ -133,10 +184,10 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "f/f", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" }, + "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": "bar2" } + "transport-options": { "foo": "bar" } }, { "name": "g/g", "version": "dev-master", @@ -163,6 +214,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an --RUN-- update a/a b/b d/d g/g --EXPECT-- +Upgrading a/a (dev-master 1111111 => dev-master 2222222) +Upgrading b/b (2.0.3 1111111 => 2.0.3 2222222) Installing e/e (dev-master 1111111) -Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) -Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) +Marking e/e (9999999-dev 1111111) as installed, alias of e/e (dev-master 1111111) +Upgrading g/g (dev-master 0000000 => dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test b/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test index e0858e054..68c02006d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test @@ -35,4 +35,4 @@ Updating a dev package selects its newest version but no providers --RUN-- update --EXPECT-- -Updating a/installed (dev-master oldref) to a/installed (dev-master newref) +Upgrading a/installed (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test index f91a67cea..0a918b183 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test @@ -93,4 +93,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster) to a/a (dev-master newmaster) +Upgrading a/a (dev-master oldmaster => dev-master newmaster) 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 fa2146345..181e039ea 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 @@ -38,4 +38,5 @@ Updating a dev package to its latest ref should pick up new dependencies update --EXPECT-- Installing a/dependency (dev-master ref) -Updating a/devpackage (dev-master oldref) to a/devpackage (dev-master newref) +Marking a/dependency (9999999-dev ref) as installed, alias of a/dependency (dev-master ref) +Upgrading a/devpackage (dev-master oldref => dev-master 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 0cb42e6fa..f0755d0d0 100644 --- a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test +++ b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test @@ -46,4 +46,5 @@ Downgrading from unstable to more stable package should work even if already ins --RUN-- update --EXPECT-- -Downgrading a/a (dev-master abcd) to a/a (1.0.0) +Downgrading a/a (dev-master abcd => 1.0.0) +Marking a/a (9999999-dev abcd) as uninstalled, alias of a/a (dev-master abcd) diff --git a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test index 02f94cd6e..30e0e6112 100644 --- a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test +++ b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test @@ -23,4 +23,4 @@ Update in ignore-platform-reqs mode --RUN-- update --ignore-platform-reqs --EXPECT-- -Updating a/a (1.0.0) to a/a (1.0.1) +Upgrading a/a (1.0.0 => 1.0.1) diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test index e6814ccfe..ae0b2d537 100644 --- a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test @@ -28,3 +28,4 @@ Updating a dev package forcing it's reference should not do anything if the refe --RUN-- update --EXPECT-- +Upgrading a/a (dev-master def000 => dev-master ) diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test new file mode 100644 index 000000000..9bfca4c85 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test @@ -0,0 +1,204 @@ +--TEST-- +Update mirrors updates URLs for all packages if they have changed without updating versions + +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 +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 +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-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "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/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/c/newc", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/c/newc/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "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/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "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/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "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/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "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/zipball/2222222222222222222222222222222222222222", "type": "zip" } + } + ] + } + ], + "require": { + "a/a": "dev-master", + "b/b": "2.0.3", + "c/c": "1.0.0", + "d/d": "dev-master#1111111111111111111111111111111111111111", + "e/e": "dev-master#1111111111111111111111111111111111111111", + "f/f": "dev-master", + "g/g": "dev-master#1111111111111111111111111111111111111111" + } +} +--INSTALLED-- +[ + { + "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" } + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "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" } + }, + { + "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" } + }, + { + "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" } + } +] +--LOCK-- +{ + "packages": [ + { + "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" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "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" }, + "type": "library" + }, + { + "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" }, + "type": "library" + }, + { + "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" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/newa/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/newb/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "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/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/newg/zipball/0000000000000000000000000000000000000000", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "a/a": 20, + "d/d": 20, + "e/e": 20, + "f/f": 20, + "g/g": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update mirrors +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index 3b90755e2..06cbc27c7 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -60,9 +60,9 @@ Updates with --no-dev but we still end up with a complete lock file including de --RUN-- update --no-dev --EXPECT-- -Uninstalling a/b (1.0.0) -Updating a/a (1.0.0) to a/a (1.0.1) -Updating dev/pkg (dev-master old) to dev/pkg (dev-master new) +Removing a/b (1.0.0) +Upgrading a/a (1.0.0 => 1.0.1) Installing a/c (1.0.0) +Upgrading dev/pkg (dev-master old => dev-master new) Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new) Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index 1e528d047..c1e746356 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -31,10 +31,18 @@ Converting from one VCS type to another (including an URL change) should update "name": "a/a", "version": "1.0.0", "source": { "reference": "old-hg-ref", "type": "hg", "url": "old-hg-url" } } - ] + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] } --RUN-- -update +update mirrors --EXPECT-LOCK-- { "packages": [ @@ -54,4 +62,4 @@ update "platform-dev": [] } --EXPECT-- - +Upgrading a/a (1.0.0 old-hg-ref => 1.0.0 new-git-ref) diff --git a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test index 68e3effe2..58935c8d7 100644 --- a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test +++ b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test @@ -36,5 +36,5 @@ Updates packages to their lowest stable version --RUN-- update --prefer-lowest --prefer-stable --EXPECT-- -Updating a/a (1.0.0-rc1) to a/a (1.0.1) -Downgrading a/b (1.0.1) to a/b (1.0.0) +Upgrading a/a (1.0.0-rc1 => 1.0.1) +Downgrading a/b (1.0.1 => 1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test b/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test new file mode 100644 index 000000000..734315eb5 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test @@ -0,0 +1,31 @@ +--TEST-- +Updating a dev package should update to the latest available reference +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "abc123", "url": "", "type": "git" } + } + ] + } + ], + "require": { + "a/a": "dev-master" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "def000", "url": "", "type": "git" }, + "dist": { "reference": "def000", "url": "", "type": "zip", "shasum": "" } + } +] +--RUN-- +update +--EXPECT-- +Upgrading a/a (dev-master def000 => dev-master abc123) diff --git a/tests/Composer/Test/Fixtures/installer/update-reference.test b/tests/Composer/Test/Fixtures/installer/update-reference.test index 9dca245ee..7836cbf5a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-reference.test @@ -27,4 +27,4 @@ Updates a dev package forcing it's reference --RUN-- install --EXPECT-- -Updating a/a (dev-master abc123) to a/a (dev-master def000) +Upgrading a/a (dev-master abc123 => dev-master def000) diff --git a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test new file mode 100644 index 000000000..a47afb7ff --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test @@ -0,0 +1,67 @@ +--TEST-- +A composer update should remove unused locked dependencies from the lock file and remove unused installed deps from disk +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ] + } + ], + "require": { + "a/a": "*" + } +} +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" }, + { "name": "c/c", "version": "1.0.0" } +] +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Lock file operations: 0 installs, 0 updates, 1 removal + - Removing b/b (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 0 updates, 2 removals +Generating autoload files + +--EXPECT-- +Removing c/c (1.0.0) +Removing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test new file mode 100644 index 000000000..e9892c3b6 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test @@ -0,0 +1,20 @@ +--TEST-- +Update to a state without dependency works well from a blank slate +--COMPOSER-- +{ +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- 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 new file mode 100644 index 000000000..89e94d781 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test @@ -0,0 +1,48 @@ +--TEST-- +Update to a state without dependency works well from locked with dependency +--COMPOSER-- +{ + "minimum-stability": "dev" +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1234", "type": "git", "url": "" } + } +] +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1234", "type": "git", "url": "" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Removing a/a (dev-master 1234) +Marking a/a (9999999-dev 1234) as uninstalled, alias of a/a (dev-master 1234) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test deleted file mode 100644 index 381416af1..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ /dev/null @@ -1,36 +0,0 @@ ---TEST-- -Update with a package whitelist 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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "fixed-dependency", "version": "1.1.0", "require": { "fixed-sub-dependency": "1.*" } }, - { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, - { "name": "fixed-sub-dependency", "version": "1.1.0" }, - { "name": "fixed-sub-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "whitelisted": "1.*", - "fixed-dependency": "1.*" - } -} ---INSTALLED-- -[ - { "name": "whitelisted", "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 ---EXPECT-- -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (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 deleted file mode 100644 index 8ea177cad..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ /dev/null @@ -1,46 +0,0 @@ ---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 ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "dependency": "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": "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 ---EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-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) 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 deleted file mode 100644 index c685f14ce..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ /dev/null @@ -1,49 +0,0 @@ ---TEST-- -Update with a package whitelist only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "root-dependency", "version": "1.1.0" }, - { "name": "root-dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "root-dependency": "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": "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 ---EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-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) 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 deleted file mode 100644 index a24bafb91..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ /dev/null @@ -1,55 +0,0 @@ ---TEST-- -Update with a package whitelist only updates those packages and their dependencies matching the pattern ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "whitelisted-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": "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 ---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) 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 deleted file mode 100644 index e5551b43f..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ /dev/null @@ -1,44 +0,0 @@ ---TEST-- -Update with a package whitelist only updates those packages matching the pattern ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-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": "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-* ---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) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test deleted file mode 100644 index de1fb1b73..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ /dev/null @@ -1,48 +0,0 @@ ---TEST-- -Update with a package whitelist only updates those corresponding to the pattern ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "package": [ - { "name": "vendor/Test-Package", "version": "2.0" }, - { "name": "vendor/NotMe", "version": "2.0" }, - { "name": "exact/Test-Package", "version": "2.0" }, - { "name": "notexact/TestPackage", "version": "2.0" }, - { "name": "all/Package1", "version": "2.0" }, - { "name": "all/Package2", "version": "2.0" }, - { "name": "another/another", "version": "2.0" }, - { "name": "no/regexp", "version": "2.0" } - ] - } - ], - "require": { - "vendor/Test-Package": "*.*", - "vendor/NotMe": "*.*", - "exact/Test-Package": "*.*", - "notexact/TestPackage": "*.*", - "all/Package1": "*.*", - "all/Package2": "*.*", - "another/another": "*.*", - "no/regexp": "*.*" - } -} ---INSTALLED-- -[ - { "name": "vendor/Test-Package", "version": "1.0" }, - { "name": "vendor/NotMe", "version": "1.0" }, - { "name": "exact/Test-Package", "version": "1.0" }, - { "name": "notexact/TestPackage", "version": "1.0" }, - { "name": "all/Package1", "version": "1.0" }, - { "name": "all/Package2", "version": "1.0" }, - { "name": "another/another", "version": "1.0" }, - { "name": "no/regexp", "version": "1.0" } -] ---RUN-- -update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp ---EXPECT-- -Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) -Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) -Updating all/Package1 (1.0) to all/Package1 (2.0) -Updating all/Package2 (1.0) to all/Package2 (2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test deleted file mode 100644 index e658e8c06..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ /dev/null @@ -1,32 +0,0 @@ ---TEST-- -Update with a package whitelist 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": "fixed-dependency", "version": "1.1.0" }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "whitelisted": "1.*", - "fixed-dependency": "1.*" - } -} ---INSTALLED-- -[ - { "name": "whitelisted", "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 ---EXPECT-- -Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (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 deleted file mode 100644 index bb2e04193..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ /dev/null @@ -1,40 +0,0 @@ ---TEST-- -Update with a package whitelist only updates those packages and their dependencies listed as command arguments ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted": "1.*", - "unrelated": "1.*" - } -} ---INSTALLED-- -[ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "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 ---EXPECT-- -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (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 deleted file mode 100644 index f63229fbc..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test +++ /dev/null @@ -1,38 +0,0 @@ ---TEST-- -Update with a package whitelist only updates whitelisted packages if no dependency conflicts ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted": "1.*", - "unrelated": "1.*" - } -} ---INSTALLED-- -[ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "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 ---EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test deleted file mode 100644 index 751d79e70..000000000 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ /dev/null @@ -1,39 +0,0 @@ ---TEST-- -Update with a package whitelist only updates those packages listed as command arguments ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "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": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } - ] - } - ], - "require": { - "fixed": "1.*", - "whitelisted": "1.*", - "unrelated": "1.*" - } -} ---INSTALLED-- -[ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "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 ---EXPECT-- -Updating whitelisted (1.0.0) to whitelisted (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..a950b247a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -28,17 +28,35 @@ When `--with-all-dependencies` is used, Composer\Installer::whitelistUpdateDepen { "name": "a/a", "version": "1.0.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] - +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/b --with-all-dependencies --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 0 installs, 2 updates, 0 removals +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Upgrading a/a (1.0.0 => 1.1.0) + - Upgrading b/b (1.0.0 => 1.1.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- -Updating a/a (1.0.0) to a/a (1.1.0) -Updating b/b (1.0.0) to b/b (1.1.0) +Upgrading a/a (1.0.0 => 1.1.0) +Upgrading b/b (1.0.0 => 1.1.0) 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 04624561d..3fb6654ab 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 @@ -16,7 +16,7 @@ Installing locked dev packages should remove old dependencies "require": {} } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], @@ -41,5 +41,6 @@ Installing locked dev packages should remove old dependencies --RUN-- install --EXPECT-- -Uninstalling a/dependency (dev-master ref) -Updating a/devpackage (dev-master oldref) to a/devpackage (dev-master newref) +Removing a/dependency (dev-master ref) +Upgrading a/devpackage (dev-master oldref => dev-master newref) +Marking a/dependency (9999999-dev ref) as uninstalled, alias of a/dependency (dev-master ref) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index c5c838517..c84ab3525 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -28,7 +28,7 @@ Updating a dev package for new reference updates the url and reference "dist": { "reference": "oldref", "url": "oldurl", "type": "zip", "shasum": "" } } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": {"a/a":20}, @@ -65,4 +65,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldref) to a/a (dev-master newref) +Upgrading a/a (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Installer/InstallationManagerTest.php b/tests/Composer/Test/Installer/InstallationManagerTest.php index 477cbbe41..9f9bf5572 100644 --- a/tests/Composer/Test/Installer/InstallationManagerTest.php +++ b/tests/Composer/Test/Installer/InstallationManagerTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Installer; use Composer\Installer\InstallationManager; +use Composer\Installer\NoopInstaller; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -21,10 +22,14 @@ use Composer\Test\TestCase; class InstallationManagerTest extends TestCase { protected $repository; + protected $loop; + protected $io; public function setUp() { + $this->loop = $this->getMockBuilder('Composer\Util\Loop')->disableOriginalConstructor()->getMock(); $this->repository = $this->getMockBuilder('Composer\Repository\InstalledRepositoryInterface')->getMock(); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); } public function testAddGetInstaller() @@ -38,7 +43,7 @@ class InstallationManagerTest extends TestCase return $arg === 'vendor'; })); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $this->assertSame($installer, $manager->getInstaller('vendor')); @@ -67,7 +72,7 @@ class InstallationManagerTest extends TestCase return $arg === 'vendor'; })); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $this->assertSame($installer, $manager->getInstaller('vendor')); @@ -80,16 +85,21 @@ class InstallationManagerTest extends TestCase public function testExecute() { $manager = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->setConstructorArgs(array($this->loop, $this->io)) ->setMethods(array('install', 'update', 'uninstall')) ->getMock(); - $installOperation = new InstallOperation($this->createPackageMock()); - $removeOperation = new UninstallOperation($this->createPackageMock()); + $installOperation = new InstallOperation($package = $this->createPackageMock()); + $removeOperation = new UninstallOperation($package); $updateOperation = new UpdateOperation( - $this->createPackageMock(), - $this->createPackageMock() + $package, + $package ); + $package->expects($this->any()) + ->method('getType') + ->will($this->returnValue('library')); + $manager ->expects($this->once()) ->method('install') @@ -103,15 +113,14 @@ class InstallationManagerTest extends TestCase ->method('update') ->with($this->repository, $updateOperation); - $manager->execute($this->repository, $installOperation); - $manager->execute($this->repository, $removeOperation); - $manager->execute($this->repository, $updateOperation); + $manager->addInstaller(new NoopInstaller()); + $manager->execute($this->repository, array($installOperation, $removeOperation, $updateOperation)); } public function testInstall() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -139,7 +148,7 @@ class InstallationManagerTest extends TestCase public function testUpdateWithEqualTypes() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $initial = $this->createPackageMock(); @@ -173,18 +182,17 @@ class InstallationManagerTest extends TestCase { $libInstaller = $this->createInstallerMock(); $bundleInstaller = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($libInstaller); $manager->addInstaller($bundleInstaller); $initial = $this->createPackageMock(); - $target = $this->createPackageMock(); - $operation = new UpdateOperation($initial, $target, 'test'); - $initial ->expects($this->once()) ->method('getType') ->will($this->returnValue('library')); + + $target = $this->createPackageMock(); $target ->expects($this->once()) ->method('getType') @@ -213,13 +221,14 @@ class InstallationManagerTest extends TestCase ->method('install') ->with($this->repository, $target); + $operation = new UpdateOperation($initial, $target, 'test'); $manager->update($this->repository, $operation); } public function testUninstall() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -249,7 +258,7 @@ class InstallationManagerTest extends TestCase $installer = $this->getMockBuilder('Composer\Installer\LibraryInstaller') ->disableOriginalConstructor() ->getMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -281,7 +290,9 @@ class InstallationManagerTest extends TestCase private function createPackageMock() { - return $this->getMockBuilder('Composer\Package\PackageInterface') + $mock = $this->getMockBuilder('Composer\Package\PackageInterface') ->getMock(); + + return $mock; } } diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php index 531697b62..6c0689844 100644 --- a/tests/Composer/Test/Installer/InstallerEventTest.php +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -21,21 +21,14 @@ class InstallerEventTest extends TestCase { $composer = $this->getMockBuilder('Composer\Composer')->getMock(); $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); - $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); - $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $operations = array($this->getMockBuilder('Composer\DependencyResolver\Operation\OperationInterface')->getMock()); - $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $policy, $pool, $installedRepo, $request, $operations); + $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); + $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, true, $transaction); $this->assertSame('EVENT_NAME', $event->getName()); $this->assertInstanceOf('Composer\Composer', $event->getComposer()); $this->assertInstanceOf('Composer\IO\IOInterface', $event->getIO()); $this->assertTrue($event->isDevMode()); - $this->assertInstanceOf('Composer\DependencyResolver\PolicyInterface', $event->getPolicy()); - $this->assertInstanceOf('Composer\DependencyResolver\Pool', $event->getPool()); - $this->assertInstanceOf('Composer\Repository\CompositeRepository', $event->getInstalledRepo()); - $this->assertInstanceOf('Composer\DependencyResolver\Request', $event->getRequest()); - $this->assertCount(1, $event->getOperations()); + $this->assertTrue($event->isExecutingOperations()); + $this->assertInstanceOf('Composer\DependencyResolver\Transaction', $event->getTransaction()); } } diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index d92ed3309..f2f2e89e6 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -113,7 +113,7 @@ class LibraryInstallerTest extends TestCase $this->dm ->expects($this->once()) - ->method('download') + ->method('install') ->with($package, $this->vendorDir.'/some/package'); $this->repository diff --git a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php index 03bb0b5fb..286b386d9 100644 --- a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php +++ b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php @@ -33,14 +33,13 @@ class SuggestedPackagesReporterTest extends TestCase /** * @covers ::__construct */ - public function testContrsuctor() + public function testConstructor() { $this->io->expects($this->once()) - ->method('writeError'); + ->method('write'); - $suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); - $suggestedPackagesReporter->addPackage('a', 'b', 'c'); - $suggestedPackagesReporter->output(); + $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_LIST); } /** @@ -135,25 +134,33 @@ class SuggestedPackagesReporterTest extends TestCase { $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); - $this->io->expects($this->once()) - ->method('writeError') - ->with('a suggests installing b (c)'); + $this->io->expects($this->at(0)) + ->method('write') + ->with('a suggests:'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(1)) + ->method('write') + ->with(' - b: c'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** * @covers ::output */ - public function testOutputWithNoSuggestedPackage() + public function testOutputWithNoSuggestionReason() { $this->suggestedPackagesReporter->addPackage('a', 'b', ''); - $this->io->expects($this->once()) - ->method('writeError') - ->with('a suggests installing b'); + $this->io->expects($this->at(0)) + ->method('write') + ->with('a suggests:'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(1)) + ->method('write') + ->with(' - b'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** @@ -165,14 +172,18 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('source', 'target2', "Like us on Facebook"); $this->io->expects($this->at(0)) - ->method('writeError') - ->with("source suggests installing target1 ([1;37;42m Like us on Facebook [0m)"); + ->method('write') + ->with('source suggests:'); $this->io->expects($this->at(1)) - ->method('writeError') - ->with('source suggests installing target2 (\\Like us on Facebook\\)'); + ->method('write') + ->with(' - target1: [1;37;42m Like us on Facebook [0m'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(2)) + ->method('write') + ->with(' - target2: \\Like us on Facebook\\'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** @@ -184,14 +195,26 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); $this->io->expects($this->at(0)) - ->method('writeError') - ->with('a suggests installing b (c)'); + ->method('write') + ->with('a suggests:'); $this->io->expects($this->at(1)) - ->method('writeError') - ->with('source package suggests installing target (because reasons)'); + ->method('write') + ->with(' - b: c'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(2)) + ->method('write') + ->with(''); + + $this->io->expects($this->at(3)) + ->method('write') + ->with('source package suggests:'); + + $this->io->expects($this->at(4)) + ->method('write') + ->with(' - target: because reasons'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** @@ -221,11 +244,15 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); - $this->io->expects($this->once()) - ->method('writeError') - ->with('source package suggests installing target (because reasons)'); + $this->io->expects($this->at(0)) + ->method('write') + ->with('source package suggests:'); - $this->suggestedPackagesReporter->output($repository); + $this->io->expects($this->at(1)) + ->method('write') + ->with(' - target: because reasons'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository); } /** @@ -237,7 +264,7 @@ class SuggestedPackagesReporterTest extends TestCase $repository->expects($this->exactly(0)) ->method('getPackages'); - $this->suggestedPackagesReporter->output($repository); + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository); } private function getSuggestedPackageArray() diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 067baf17a..3c6c35de6 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -12,9 +12,12 @@ namespace Composer\Test; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\Console\Application; +use Composer\IO\BufferIO; use Composer\Json\JsonFile; +use Composer\Package\Dumper\ArrayDumper; use Composer\Util\Filesystem; use Composer\Repository\ArrayRepository; use Composer\Repository\RepositoryManager; @@ -29,8 +32,6 @@ use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatter; -use Composer\Test\TestCase; -use Composer\IO\BufferIO; class InstallerTest extends TestCase { @@ -57,14 +58,16 @@ class InstallerTest extends TestCase */ public function testInstaller(RootPackageInterface $rootPackage, $repositories, array $options) { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false)); $downloadManager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($io)) ->getMock(); $config = $this->getMockBuilder('Composer\Config')->getMock(); - $repositoryManager = new RepositoryManager($io, $config); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); + $repositoryManager = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher); $repositoryManager->setLocalRepository(new InstalledArrayRepository()); if (!is_array($repositories)) { @@ -73,23 +76,44 @@ class InstallerTest extends TestCase foreach ($repositories as $repository) { $repositoryManager->addRepository($repository); } - - $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $installationManager = new InstallationManagerMock(); - $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + // emulate a writable lock file + $lockData = null; + $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $lockJsonMock->expects($this->any()) + ->method('read') + ->will($this->returnCallback(function() use (&$lockData) { + return json_decode($lockData, true); + })); + $lockJsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnCallback(function () use (&$lockData) { + return $lockData !== null; + })); + $lockJsonMock->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { + $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT); + })); + + $tempLockData = null; + $locker = new Locker($io, $lockJsonMock, $installationManager, '{}'); + $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); $result = $installer->run(); - $this->assertSame(0, $result); + + $output = str_replace("\r", '', $io->getOutput()); + $this->assertEquals(0, $result, $output); $expectedInstalled = isset($options['install']) ? $options['install'] : array(); $expectedUpdated = isset($options['update']) ? $options['update'] : array(); $expectedUninstalled = isset($options['uninstall']) ? $options['uninstall'] : array(); $installed = $installationManager->getInstalledPackages(); - $this->assertSame($expectedInstalled, $installed); + $this->assertEquals($this->makePackagesComparable($expectedInstalled), $this->makePackagesComparable($installed)); $updated = $installationManager->getUpdatedPackages(); $this->assertSame($expectedUpdated, $updated); @@ -98,6 +122,17 @@ class InstallerTest extends TestCase $this->assertSame($expectedUninstalled, $uninstalled); } + protected function makePackagesComparable($packages) + { + $dumper = new ArrayDumper(); + + $comparable = array(); + foreach ($packages as $package) { + $comparable[] = $dumper->dump($package); + } + return $comparable; + } + public function provideInstaller() { $cases = array(); @@ -107,11 +142,11 @@ class InstallerTest extends TestCase $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage'); $a->setRequires(array( - new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')), + 'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $b = $this->getPackage('B', '1.0.0'); $b->setRequires(array( - new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')), + 'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $cases[] = array( @@ -127,11 +162,11 @@ class InstallerTest extends TestCase $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage'); $a->setRequires(array( - new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')), + 'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $b = $this->getPackage('B', '1.0.0'); $b->setRequires(array( - new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')), + 'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $cases[] = array( @@ -142,6 +177,7 @@ class InstallerTest extends TestCase ), ); + // TODO why are there not more cases with uninstall/update? return $cases; } @@ -180,13 +216,24 @@ class InstallerTest extends TestCase $repositoryManager = $composer->getRepositoryManager(); $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); + // emulate a writable lock file + $lockData = $lock ? json_encode($lock, JsonFile::JSON_PRETTY_PRINT): null; $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $lockJsonMock->expects($this->any()) ->method('read') - ->will($this->returnValue($lock)); + ->will($this->returnCallback(function() use (&$lockData) { + return json_decode($lockData, true); + })); $lockJsonMock->expects($this->any()) ->method('exists') - ->will($this->returnValue(true)); + ->will($this->returnCallback(function () use (&$lockData) { + return $lockData !== null; + })); + $lockJsonMock->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { + $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT); + })); if ($expectLock) { $actualLock = array(); @@ -203,7 +250,7 @@ class InstallerTest extends TestCase } $contents = json_encode($composerConfig); - $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents); + $locker = new Locker($io, $lockJsonMock, $composer->getInstallationManager(), $contents); $composer->setLocker($locker); $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); @@ -226,13 +273,27 @@ class InstallerTest extends TestCase }); $application->get('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); + }); + $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); + $packages = $filteredPackages; + + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + } elseif ($input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + } + $installer ->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')) + ->setUpdateMirrors($updateMirrors) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); @@ -246,7 +307,7 @@ class InstallerTest extends TestCase $application->setAutoExit(false); $appOutput = fopen('php://memory', 'w+'); - $input = new StringInput($run); + $input = new StringInput($run.' -vvv'); $input->setInteractive(false); $result = $application->run($input, new StreamOutput($appOutput)); fseek($appOutput, 0); @@ -270,6 +331,9 @@ class InstallerTest extends TestCase $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); if ($expectOutput) { + $output = preg_replace('{^ - .*?\.ini$}m', '__inilist__', $output); + $output = preg_replace('{(__inilist__\r?\n)+}', "__inilist__\n", $output); + $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output)); } } diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index 47683afcd..d4dc444a0 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -18,8 +18,10 @@ use Composer\Factory; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; use Composer\Installer; +use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Test\TestCase; +use Composer\Util\Loop; class FactoryMock extends Factory { @@ -39,9 +41,9 @@ class FactoryMock extends Factory { } - protected function createInstallationManager() + public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $dispatcher = null) { - return new InstallationManagerMock; + return new InstallationManagerMock(); } protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io) diff --git a/tests/Composer/Test/Mock/RemoteFilesystemMock.php b/tests/Composer/Test/Mock/HttpDownloaderMock.php similarity index 73% rename from tests/Composer/Test/Mock/RemoteFilesystemMock.php rename to tests/Composer/Test/Mock/HttpDownloaderMock.php index 5d4f52e54..1e2774af0 100644 --- a/tests/Composer/Test/Mock/RemoteFilesystemMock.php +++ b/tests/Composer/Test/Mock/HttpDownloaderMock.php @@ -12,13 +12,11 @@ namespace Composer\Test\Mock; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; +use Composer\Util\Http\Response; use Composer\Downloader\TransportException; -/** - * Remote filesystem mock - */ -class RemoteFilesystemMock extends RemoteFilesystem +class HttpDownloaderMock extends HttpDownloader { protected $contentMap; @@ -30,10 +28,10 @@ class RemoteFilesystemMock extends RemoteFilesystem $this->contentMap = $contentMap; } - public function getContents($originUrl, $fileUrl, $progress = true, $options = array()) + public function get($fileUrl, $options = array()) { if (!empty($this->contentMap[$fileUrl])) { - return $this->contentMap[$fileUrl]; + return new Response(array('url' => $fileUrl), 200, array(), $this->contentMap[$fileUrl]); } throw new TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404); diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index de1de514b..220313daa 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -17,6 +17,7 @@ use Composer\Repository\RepositoryInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; @@ -29,6 +30,20 @@ class InstallationManagerMock extends InstallationManager private $uninstalled = array(); private $trace = array(); + public function __construct() + { + + } + + public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) + { + foreach ($operations as $operation) { + $method = $operation->getOperationType(); + // skipping download() step here for tests + $this->$method($repo, $operation); + } + } + public function getInstallPath(PackageInterface $package) { return ''; diff --git a/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php b/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php index 9c11dc307..574cfbd83 100644 --- a/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php +++ b/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php @@ -13,6 +13,7 @@ namespace Composer\Test\Mock; use Composer\Repository\InstalledFilesystemRepository; +use Composer\Installer\InstallationManager; class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository { @@ -20,7 +21,7 @@ class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository { } - public function write() + public function write($devMode, InstallationManager $installationManager) { } } diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php index f6afe10f1..b8f110937 100644 --- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -189,6 +189,7 @@ class ArchivableFilesFinderTest extends TestCase 'git init && '. 'git config user.email "you@example.com" && '. 'git config user.name "Your Name" && '. + 'git config commit.gpgsign false && '. 'git add .git* && '. 'git commit -m "ignore rules" && '. 'git add . && '. @@ -308,7 +309,11 @@ class ArchivableFilesFinderTest extends TestCase protected function getArchivedFiles($command) { - $process = new Process($command, $this->sources); + if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { + $process = Process::fromShellCommandline($command, $this->sources); + } else { + $process = new Process($command, $this->sources); + } $process->run(); $archive = new \PharData($this->sources.'/archive.zip'); diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index f9fe308fa..45a635437 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -12,9 +12,12 @@ namespace Composer\Test\Package\Archiver; +use Composer\IO\NullIO; use Composer\Factory; use Composer\Package\Archiver\ArchiveManager; use Composer\Package\PackageInterface; +use Composer\Util\Loop; +use Composer\Test\Mock\FactoryMock; class ArchiveManagerTest extends ArchiverTest { @@ -30,7 +33,13 @@ class ArchiveManagerTest extends ArchiverTest parent::setUp(); $factory = new Factory(); - $this->manager = $factory->createArchiveManager($factory->createConfig()); + $dm = $factory->createDownloadManager( + $io = new NullIO, + $config = FactoryMock::createConfig(), + $httpDownloader = $factory->createHttpDownloader($io, $config) + ); + $loop = new Loop($httpDownloader); + $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop); $this->targetDir = $this->testDir.'/composer_archiver_tests'; } @@ -116,6 +125,12 @@ class ArchiveManagerTest extends ArchiverTest throw new \RuntimeException('Could not config: '.$this->process->getErrorOutput()); } + $result = $this->process->execute('git config commit.gpgsign false', $output, $this->testDir); + if ($result > 0) { + chdir($currentWorkDir); + throw new \RuntimeException('Could not config: '.$this->process->getErrorOutput()); + } + $result = $this->process->execute('git config user.name "Your Name"', $output, $this->testDir); if ($result > 0) { chdir($currentWorkDir); diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index a94539279..93cde83f5 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -46,8 +46,8 @@ class RootPackageLoaderTest extends TestCase 'zux/complex' => '~1.0,>=1.0.2@dev', 'or/op' => '^2.0@dev || ^2.0@dev', 'multi/lowest-wins' => '^2.0@rc || >=3.0@dev , ~3.5@alpha', - 'or/op/without-flags' => 'dev-master || 2.0 , ~3.5-alpha', - 'or/op/without-flags2' => '3.0-beta || 2.0 , ~3.5-alpha', + 'or/op-without-flags' => 'dev-master || 2.0 , ~3.5-alpha', + 'or/op-without-flags2' => '3.0-beta || 2.0 , ~3.5-alpha', ), 'minimum-stability' => 'alpha', )); @@ -59,8 +59,8 @@ class RootPackageLoaderTest extends TestCase 'zux/complex' => BasePackage::STABILITY_DEV, 'or/op' => BasePackage::STABILITY_DEV, 'multi/lowest-wins' => BasePackage::STABILITY_DEV, - 'or/op/without-flags' => BasePackage::STABILITY_DEV, - 'or/op/without-flags2' => BasePackage::STABILITY_ALPHA, + 'or/op-without-flags' => BasePackage::STABILITY_DEV, + 'or/op-without-flags2' => BasePackage::STABILITY_ALPHA, ), $package->getStabilityFlags()); } @@ -160,6 +160,8 @@ class RootPackageLoaderTest extends TestCase $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($config, $executor, new VersionParser())); $package = $loader->load(array('require' => array('foo/bar' => 'self.version'))); + $this->assertEquals("9999999-dev", $package->getPrettyVersion()); + $package = $package->getAliasOf(); $this->assertEquals("dev-master", $package->getPrettyVersion()); } diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index 2cde001ac..1b7565992 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -356,7 +356,6 @@ class ValidatingArrayLoaderTest extends TestCase 'require' => array( 'foo/baz' => '*', 'bar/baz' => '>=1.0', - 'bar/foo' => 'dev-master', 'bar/hacked' => '@stable', 'bar/woo' => '1.0.0', ), @@ -364,7 +363,6 @@ class ValidatingArrayLoaderTest extends TestCase array( 'require.foo/baz : unbound version constraints (*) should be avoided', 'require.bar/baz : unbound version constraints (>=1.0) should be avoided', - 'require.bar/foo : unbound version constraints (dev-master) should be avoided', 'require.bar/hacked : unbound version constraints (@stable) should be avoided', 'require.bar/woo : exact version constraints (1.0.0) should be avoided if the package follows semantic versioning', ), diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 04e8e6c84..29f3e7c2a 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -25,7 +25,6 @@ class LockerTest extends TestCase $locker = new Locker( new NullIO, $json, - $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), $this->getJsonContent() ); @@ -45,10 +44,9 @@ class LockerTest extends TestCase public function testGetNotLockedPackages() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $json ->expects($this->once()) @@ -63,10 +61,9 @@ class LockerTest extends TestCase public function testGetLockedPackages() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $json ->expects($this->once()) @@ -90,11 +87,10 @@ class LockerTest extends TestCase public function testSetLockData() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent() . ' '; - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $package1 = $this->createPackageMock(); $package2 = $this->createPackageMock(); @@ -164,10 +160,9 @@ class LockerTest extends TestCase public function testLockBadPackages() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $package1 = $this->createPackageMock(); $package1 @@ -183,11 +178,10 @@ class LockerTest extends TestCase public function testIsFresh() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $json ->expects($this->once()) @@ -200,10 +194,9 @@ class LockerTest extends TestCase public function testIsFreshFalse() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $json ->expects($this->once()) @@ -216,11 +209,10 @@ class LockerTest extends TestCase public function testIsFreshWithContentHash() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $json ->expects($this->once()) @@ -233,11 +225,10 @@ class LockerTest extends TestCase public function testIsFreshWithContentHashAndNoHash() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $json ->expects($this->once()) @@ -250,10 +241,9 @@ class LockerTest extends TestCase public function testIsFreshFalseWithContentHash() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $differentHash = md5($this->getJsonContent(array('name' => 'test2'))); @@ -272,19 +262,6 @@ class LockerTest extends TestCase ->getMock(); } - private function createRepositoryManagerMock() - { - $mock = $this->getMockBuilder('Composer\Repository\RepositoryManager') - ->disableOriginalConstructor() - ->getMock(); - - $mock->expects($this->any()) - ->method('getLocalRepository') - ->will($this->returnValue($this->getMockBuilder('Composer\Repository\ArrayRepository')->getMock())); - - return $mock; - } - private function createInstallationManagerMock() { $mock = $this->getMockBuilder('Composer\Installer\InstallationManager') diff --git a/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 31c47d72b..9527b628f 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -89,7 +89,7 @@ class VersionGuesserTest extends TestCase $guesser = new VersionGuesser($config, $executor, new VersionParser()); $versionArray = $guesser->guessVersion(array(), 'dummy/path'); - $this->assertEquals("9999999-dev", $versionArray['version']); + $this->assertEquals("dev-".$branch, $versionArray['version']); $this->assertEquals("dev-".$branch, $versionArray['pretty_version']); $this->assertEmpty($versionArray['commit']); } @@ -124,7 +124,7 @@ class VersionGuesserTest extends TestCase $guesser = new VersionGuesser($config, $executor, new VersionParser()); $versionArray = $guesser->guessVersion(array(), 'dummy/path'); - $this->assertEquals("9999999-dev", $versionArray['version']); + $this->assertEquals("dev-master", $versionArray['version']); $this->assertEquals("dev-master", $versionArray['pretty_version']); $this->assertArrayNotHasKey('feature_version', $versionArray); $this->assertArrayNotHasKey('feature_pretty_version', $versionArray); diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index 5a76720e8..66d07d267 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -21,7 +21,7 @@ use Composer\Test\TestCase; class VersionSelectorTest extends TestCase { // A) multiple versions, get the latest one - // B) targetPackageVersion will pass to pool + // B) targetPackageVersion will pass to repo set // C) No results, throw exception public function testLatestVersionIsReturned() @@ -33,13 +33,13 @@ class VersionSelectorTest extends TestCase $package3 = $this->createPackage('1.2.0'); $packages = array($package1, $package2, $package3); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName); // 1.2.2 should be returned because it's the latest of the returned versions @@ -57,13 +57,13 @@ class VersionSelectorTest extends TestCase $package2->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.6'), 'requires', '>=5.6'))); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, '5.5.0'); $this->assertSame($package1, $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); @@ -77,13 +77,13 @@ class VersionSelectorTest extends TestCase $package2 = $this->createPackage('1.1.0-beta'); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package1, $best, 'Latest most stable version should be returned (1.0.0)'); @@ -97,18 +97,18 @@ class VersionSelectorTest extends TestCase $package2 = $this->createPackage('2.0.0-beta3'); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->at(0)) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->at(0)) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $pool->expects($this->at(1)) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet->expects($this->at(1)) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue(array_reverse($packages))); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null); $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); @@ -124,13 +124,13 @@ class VersionSelectorTest extends TestCase $package2 = $this->createPackage('1.1.0-beta'); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null, 'dev'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); @@ -145,13 +145,13 @@ class VersionSelectorTest extends TestCase $package3 = $this->createPackage('1.2.0-alpha'); $packages = array($package1, $package2, $package3); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null, 'beta'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); @@ -165,13 +165,13 @@ class VersionSelectorTest extends TestCase $package3 = $this->createPackage('1.2.0-alpha'); $packages = array($package2, $package3); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null, 'stable'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); @@ -179,12 +179,12 @@ class VersionSelectorTest extends TestCase public function testFalseReturnedOnNoPackages() { - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') ->will($this->returnValue(array())); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate('foobaz'); $this->assertFalse($best, 'No versions are available returns false'); } @@ -194,8 +194,8 @@ class VersionSelectorTest extends TestCase */ public function testFindRecommendedRequireVersion($prettyVersion, $isDev, $stability, $expectedVersion, $branchAlias = null) { - $pool = $this->createMockPool(); - $versionSelector = new VersionSelector($pool); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet); $versionParser = new VersionParser(); $package = $this->getMockBuilder('\Composer\Package\PackageInterface')->getMock(); @@ -273,8 +273,10 @@ class VersionSelectorTest extends TestCase return new Package('foo', $parser->normalize($version), $version); } - private function createMockPool() + private function createMockRepositorySet() { - return $this->getMockBuilder('Composer\DependencyResolver\Pool')->getMock(); + return $this->getMockBuilder('Composer\Repository\RepositorySet') + ->disableOriginalConstructor() + ->getMock(); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php index f80acd325..c757d4b09 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php @@ -12,5 +12,16 @@ class Plugin implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v1'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v1'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v1'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json index 574c4402f..335f772c9 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php index db5a4462e..32090b66d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php @@ -12,5 +12,16 @@ class Plugin2 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v2'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v2'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v2'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json index 27432acfa..4104f4be6 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php index 861c1679b..034388162 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php @@ -12,5 +12,16 @@ class Plugin2 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v3'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v3'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v3'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json index 881eb5cae..ee087e2d7 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php index 93bcabc98..2eaee6a3f 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php @@ -13,5 +13,16 @@ class Plugin1 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v4-plugin1'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v4-plugin1'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v4-plugin1'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php index d946deb89..3c5311a82 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php @@ -13,5 +13,16 @@ class Plugin2 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v4-plugin2'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v4-plugin2'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v4-plugin2'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json index f61cb3fbd..a349ccc2c 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json @@ -10,6 +10,6 @@ ] }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php index a2ac37bc5..fb9f08a6d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php @@ -10,5 +10,16 @@ class Plugin5 implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v5'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v5'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v5'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php index e46c0fcb0..acce1f972 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php @@ -10,5 +10,16 @@ class Plugin6 implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v6'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v6'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v6'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php index 5560a6047..84734ce3b 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php @@ -10,5 +10,16 @@ class Plugin7 implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v7'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v7'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v7'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php index 7e9a0aab1..4534e13ef 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php @@ -13,6 +13,17 @@ class Plugin8 implements PluginInterface, Capable public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v8'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v8'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v8'); } public function getCapabilities() diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json index 799df2e61..aa44b5a3d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json @@ -9,6 +9,6 @@ ] }, "require": { - "composer-plugin-api": "1.1.0" + "composer-plugin-api": "2.0.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php index 74e1beb8b..870f11cd1 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php @@ -14,5 +14,16 @@ class Plugin implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v9'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v9'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v9'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json index f3ccb9397..45d8d794b 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index d56e054fb..b73907b48 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -19,6 +19,9 @@ use Composer\Package\CompletePackage; use Composer\Package\Loader\JsonLoader; use Composer\Package\Loader\ArrayLoader; use Composer\Plugin\PluginManager; +use Symfony\Component\Console\Output\OutputInterface; +use Composer\IO\BufferIO; +use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; use Composer\Test\TestCase; use Composer\Util\Filesystem; @@ -89,14 +92,14 @@ class PluginInstallerTest extends TestCase ->method('getLocalRepository') ->will($this->returnValue($this->repository)); - $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock(); + $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); $im->expects($this->any()) ->method('getInstallPath') ->will($this->returnCallback(function ($package) { return __DIR__.'/Fixtures/'.$package->getPrettyName(); })); - $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->io = new BufferIO(); $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $this->autoloadGenerator = new AutoloadGenerator($dispatcher); @@ -108,6 +111,7 @@ class PluginInstallerTest extends TestCase $this->composer->setRepositoryManager($rm); $this->composer->setInstallationManager($im); $this->composer->setAutoloadGenerator($this->autoloadGenerator); + $this->composer->setEventDispatcher(new EventDispatcher($this->composer, $this->io)); $this->pm = new PluginManager($this->io, $this->composer); $this->composer->setPluginManager($this->pm); @@ -130,7 +134,7 @@ class PluginInstallerTest extends TestCase public function testInstallNewPlugin() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array())); $installer = new PluginInstaller($this->io, $this->composer); @@ -140,12 +144,13 @@ class PluginInstallerTest extends TestCase $plugins = $this->pm->getPlugins(); $this->assertEquals('installer-v1', $plugins[0]->version); + $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput()); } public function testInstallMultiplePlugins() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[3]))); $installer = new PluginInstaller($this->io, $this->composer); @@ -158,12 +163,13 @@ class PluginInstallerTest extends TestCase $this->assertEquals('installer-v4', $plugins[0]->version); $this->assertEquals('plugin2', $plugins[1]->name); $this->assertEquals('installer-v4', $plugins[1]->version); + $this->assertEquals('activate v4-plugin1'.PHP_EOL.'activate v4-plugin2'.PHP_EOL, $this->io->getOutput()); } public function testUpgradeWithNewClassName() { $this->repository - ->expects($this->exactly(3)) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[0]))); $this->repository @@ -176,13 +182,35 @@ class PluginInstallerTest extends TestCase $installer->update($this->repository, $this->packages[0], $this->packages[1]); $plugins = $this->pm->getPlugins(); + $this->assertCount(1, $plugins); $this->assertEquals('installer-v2', $plugins[1]->version); + $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'activate v2'.PHP_EOL, $this->io->getOutput()); + } + + public function testUninstall() + { + $this->repository + ->expects($this->any()) + ->method('getPackages') + ->will($this->returnValue(array($this->packages[0]))); + $this->repository + ->expects($this->exactly(1)) + ->method('hasPackage') + ->will($this->onConsecutiveCalls(true, false)); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); + + $installer->uninstall($this->repository, $this->packages[0]); + + $plugins = $this->pm->getPlugins(); + $this->assertCount(0, $plugins); + $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'uninstall v1'.PHP_EOL, $this->io->getOutput()); } public function testUpgradeWithSameClassName() { $this->repository - ->expects($this->exactly(3)) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[1]))); $this->repository @@ -196,12 +224,13 @@ class PluginInstallerTest extends TestCase $plugins = $this->pm->getPlugins(); $this->assertEquals('installer-v3', $plugins[1]->version); + $this->assertEquals('activate v2'.PHP_EOL.'deactivate v2'.PHP_EOL.'activate v3'.PHP_EOL, $this->io->getOutput()); } public function testRegisterPluginOnlyOneTime() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array())); $installer = new PluginInstaller($this->io, $this->composer); @@ -213,6 +242,7 @@ class PluginInstallerTest extends TestCase $plugins = $this->pm->getPlugins(); $this->assertCount(1, $plugins); $this->assertEquals('installer-v1', $plugins[0]->version); + $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput()); } /** @@ -240,11 +270,11 @@ class PluginInstallerTest extends TestCase // Add the plugins to the repo along with the internal Plugin package on which they all rely. $this->repository - ->expects($this->any()) - ->method('getPackages') - ->will($this->returnCallback(function () use ($plugApiInternalPackage, $plugins) { - return array_merge(array($plugApiInternalPackage), $plugins); - })); + ->expects($this->any()) + ->method('getPackages') + ->will($this->returnCallback(function () use ($plugApiInternalPackage, $plugins) { + return array_merge(array($plugApiInternalPackage), $plugins); + })); $this->pm->loadInstalledPlugins(); } @@ -300,7 +330,7 @@ class PluginInstallerTest extends TestCase public function testCommandProviderCapability() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[7]))); $installer = new PluginInstaller($this->io, $this->composer); diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 4ec826334..1915389c4 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -18,7 +18,7 @@ use Composer\Repository\RepositoryInterface; use Composer\Test\Mock\FactoryMock; use Composer\Test\TestCase; use Composer\Package\Loader\ArrayLoader; -use Composer\Semver\VersionParser; +use Composer\Package\Version\VersionParser; class ComposerRepositoryTest extends TestCase { @@ -32,11 +32,13 @@ class ComposerRepositoryTest extends TestCase ); $repository = $this->getMockBuilder('Composer\Repository\ComposerRepository') - ->setMethods(array('loadRootServerFile', 'createPackage')) + ->setMethods(array('loadRootServerFile', 'createPackages')) ->setConstructorArgs(array( $repoConfig, new NullIO, FactoryMock::createConfig(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() )) ->getMock(); @@ -45,16 +47,17 @@ class ComposerRepositoryTest extends TestCase ->method('loadRootServerFile') ->will($this->returnValue($repoPackages)); + $stubs = array(); foreach ($expected as $at => $arg) { - $stubPackage = $this->getPackage('stub/stub', '1.0.0'); - - $repository - ->expects($this->at($at + 2)) - ->method('createPackage') - ->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage')) - ->will($this->returnValue($stubPackage)); + $stubs[] = $this->getPackage('stub/stub', '1.0.0'); } + $repository + ->expects($this->at(2)) + ->method('createPackages') + ->with($this->identicalTo($expected), $this->equalTo('Composer\Package\CompletePackage')) + ->will($this->returnValue($stubs)); + // Triggers initialization $packages = $repository->getPackages(); @@ -96,7 +99,13 @@ class ComposerRepositoryTest extends TestCase public function testWhatProvides() { $repo = $this->getMockBuilder('Composer\Repository\ComposerRepository') - ->disableOriginalConstructor() + ->setConstructorArgs(array( + array('url' => 'https://dummy.test.link'), + new NullIO, + FactoryMock::createConfig(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() + )) ->setMethods(array('fetchFile')) ->getMock(); @@ -142,28 +151,21 @@ class ComposerRepositoryTest extends TestCase ), ))); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->getMock(); - $pool->expects($this->any()) - ->method('isPackageAcceptable') - ->will($this->returnValue(true)); - $versionParser = new VersionParser(); - $repo->setRootAliases(array( - 'a' => array( - $versionParser->normalize('0.6') => array('alias' => 'dev-feature', 'alias_normalized' => $versionParser->normalize('dev-feature')), - $versionParser->normalize('1.1.x-dev') => array('alias' => '1.0', 'alias_normalized' => $versionParser->normalize('1.0')), - ), - )); + $reflMethod = new \ReflectionMethod($repo, 'whatProvides'); + $reflMethod->setAccessible(true); + $packages = $reflMethod->invoke($repo, 'a', array($this, 'isPackageAcceptableReturnTrue')); - $packages = $repo->whatProvides($pool, 'a'); - - $this->assertCount(7, $packages); - $this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages)); - $this->assertInstanceOf('Composer\Package\AliasPackage', $packages['2-root']); - $this->assertSame($packages['2'], $packages['2-root']->getAliasOf()); + $this->assertCount(5, $packages); + $this->assertEquals(array('1', '1-alias', '2', '2-alias', '3'), array_keys($packages)); $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf()); } + public function isPackageAcceptableReturnTrue() + { + return true; + } + public function testSearchWithType() { $repoConfig = array( @@ -179,21 +181,29 @@ class ComposerRepositoryTest extends TestCase ), ); - $rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->disableOriginalConstructor() + ->getMock(); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->disableOriginalConstructor() ->getMock(); - $rfs->expects($this->at(0)) - ->method('getContents') - ->with('example.org', 'http://example.org/packages.json', false) - ->willReturn(json_encode(array('search' => '/search.json?q=%query%&type=%type%'))); + $httpDownloader->expects($this->at(0)) + ->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%')))); - $rfs->expects($this->at(1)) - ->method('getContents') - ->with('example.org', 'http://example.org/search.json?q=foo&type=composer-plugin', false) - ->willReturn(json_encode($result)); + $httpDownloader->expects($this->at(1)) + ->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))); - $repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), null, $rfs); + $httpDownloader->expects($this->at(2)) + ->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()))); + + $repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), $httpDownloader, $eventDispatcher); $this->assertSame( array(array('name' => 'foo', 'description' => null)), @@ -217,7 +227,9 @@ class ComposerRepositoryTest extends TestCase $repository = new ComposerRepository( array('url' => $repositoryUrl), new NullIO(), - FactoryMock::createConfig() + FactoryMock::createConfig(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); $object = new \ReflectionObject($repository); @@ -274,30 +286,28 @@ class ComposerRepositoryTest extends TestCase public function testGetProviderNamesWillReturnPartialPackageNames() { - $rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock(); - $rfs->expects($this->at(0)) - ->method('getContents') - ->with('example.org', 'http://example.org/packages.json', false) - ->willReturn(json_encode(array( + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($url = 'http://example.org/packages.json') + ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array( 'providers-lazy-url' => '/foo/p/%package%.json', 'packages' => array('foo/bar' => array( - 'dev-branch' => array(), - 'v1.0.0' => array(), + 'dev-branch' => array('name' => 'foo/bar'), + 'v1.0.0' => array('name' => 'foo/bar'), )) - ))); + )))); $repository = new ComposerRepository( array('url' => 'http://example.org/packages.json'), new NullIO(), FactoryMock::createConfig(), - null, - $rfs + $httpDownloader ); - $this->assertTrue($repository->hasProviders()); - $this->assertEquals(array('foo/bar'), $repository->getProviderNames()); + $this->assertEquals(array('foo/bar'), $repository->getPackageNames()); } } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index be8b0d0a9..97747ebc5 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -82,11 +82,21 @@ class FilesystemRepositoryTest extends TestCase $json = $this->createJsonFileMock(); $repository = new FilesystemRepository($json); + $im = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->getMock(); + $im->expects($this->once()) + ->method('getInstallPath') + ->will($this->returnValue('/foo/bar/vendor/woop/woop')); $json ->expects($this->once()) ->method('read') ->will($this->returnValue(array())); + $json + ->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('/foo/bar/vendor/composer/installed.json')); $json ->expects($this->once()) ->method('exists') @@ -95,11 +105,12 @@ class FilesystemRepositoryTest extends TestCase ->expects($this->once()) ->method('write') ->with(array( - array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0'), + 'packages' => array(array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0', 'install-path' => '../woop/woop')), + 'dev' => true, )); $repository->addPackage($this->getPackage('mypkg', '0.1.10')); - $repository->write(); + $repository->write(true, $im); } private function createJsonFileMock() diff --git a/tests/Composer/Test/Repository/InstalledRepositoryTest.php b/tests/Composer/Test/Repository/InstalledRepositoryTest.php new file mode 100644 index 000000000..a37adb058 --- /dev/null +++ b/tests/Composer/Test/Repository/InstalledRepositoryTest.php @@ -0,0 +1,51 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Repository; + +use Composer\Repository\InstalledRepository; +use Composer\Repository\ArrayRepository; +use Composer\Repository\InstalledArrayRepository; +use Composer\Package\Link; +use Composer\Test\TestCase; + +class InstalledRepositoryTest extends TestCase +{ + public function testFindPackagesWithReplacersAndProviders() + { + $arrayRepoOne = new InstalledArrayRepository; + $arrayRepoOne->addPackage($foo = $this->getPackage('foo', '1')); + $arrayRepoOne->addPackage($foo2 = $this->getPackage('foo', '2')); + + $arrayRepoTwo = new InstalledArrayRepository; + $arrayRepoTwo->addPackage($bar = $this->getPackage('bar', '1')); + $arrayRepoTwo->addPackage($bar2 = $this->getPackage('bar', '2')); + + $foo->setReplaces(array(new Link('foo', 'provided'))); + $bar2->setProvides(array(new Link('bar', 'provided'))); + + $repo = new InstalledRepository(array($arrayRepoOne, $arrayRepoTwo)); + + $this->assertEquals(array($foo2), $repo->findPackagesWithReplacersAndProviders('foo', '2')); + $this->assertEquals(array($bar), $repo->findPackagesWithReplacersAndProviders('bar', '1')); + $this->assertEquals(array($foo, $bar2), $repo->findPackagesWithReplacersAndProviders('provided')); + } + + public function testAddRepository() + { + $arrayRepoOne = new ArrayRepository; + + $this->setExpectedException('LogicException'); + + new InstalledRepository(array($arrayRepoOne)); + } +} diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index 159d8cc97..f1c394156 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -14,8 +14,8 @@ namespace Composer\Test\Repository; use Composer\Package\Loader\ArrayLoader; use Composer\Repository\PathRepository; -use Composer\Semver\VersionParser; use Composer\Test\TestCase; +use Composer\Package\Version\VersionParser; class PathRepositoryTest extends TestCase { @@ -47,7 +47,7 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); $repository->getPackages(); - $this->assertEquals(1, $repository->count()); + $this->assertSame(1, $repository->count()); $this->assertTrue($repository->hasPackage($this->getPackage('test/path-versioned', '0.0.2'))); } @@ -63,10 +63,10 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); $packages = $repository->getPackages(); - $this->assertEquals(1, $repository->count()); + $this->assertGreaterThanOrEqual(1, $repository->count()); $package = $packages[0]; - $this->assertEquals('test/path-unversioned', $package->getName()); + $this->assertSame('test/path-unversioned', $package->getName()); $packageVersion = $package->getVersion(); $this->assertNotEmpty($packageVersion); @@ -85,16 +85,16 @@ class PathRepositoryTest extends TestCase $packages = $repository->getPackages(); $names = array(); - $this->assertEquals(2, $repository->count()); + $this->assertGreaterThanOrEqual(2, $repository->count()); $package = $packages[0]; $names[] = $package->getName(); - $package = $packages[1]; + $package = $packages[count($packages) - 1]; $names[] = $package->getName(); sort($names); - $this->assertEquals(array('test/path-unversioned', 'test/path-versioned'), $names); + $this->assertSame(array('test/path-unversioned', 'test/path-versioned'), $names); } /** @@ -118,13 +118,13 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $relativeUrl), $ioInterface, $config); $packages = $repository->getPackages(); - $this->assertEquals(1, $repository->count()); + $this->assertSame(1, $repository->count()); $package = $packages[0]; - $this->assertEquals('test/path-versioned', $package->getName()); + $this->assertSame('test/path-versioned', $package->getName()); // Convert platform specific separators back to generic URL slashes $relativeUrl = str_replace(DIRECTORY_SEPARATOR, '/', $relativeUrl); - $this->assertEquals($relativeUrl, $package->getDistUrl()); + $this->assertSame($relativeUrl, $package->getDistUrl()); } } diff --git a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php index 74e3c0c25..2e2fe6933 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php @@ -22,19 +22,19 @@ use Composer\Semver\VersionParser; use Composer\Semver\Constraint\Constraint; use Composer\Package\Link; use Composer\Package\CompletePackage; -use Composer\Test\Mock\RemoteFilesystemMock; +use Composer\Test\Mock\HttpDownloaderMock; class ChannelReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new RemoteFilesystemMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://pear.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), )); - $reader = new \Composer\Repository\Pear\ChannelReader($rfs); + $reader = new \Composer\Repository\Pear\ChannelReader($httpDownloader); $channelInfo = $reader->read('http://pear.net/'); $packages = $channelInfo->getPackages(); @@ -50,17 +50,21 @@ class ChannelReaderTest extends TestCase public function testShouldSelectCorrectReader() { - $rfs = new RemoteFilesystemMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://pear.1.0.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.0.xml'), 'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'), 'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'), + 'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'), + 'http://test.loc/rest10/r/http_client/deps.1.2.1.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_deps.1.2.1.txt'), 'http://test.loc/rest10/p/http_request/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_info.xml'), + 'http://test.loc/rest10/r/http_request/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_allreleases.xml'), + 'http://test.loc/rest10/r/http_request/deps.1.4.0.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_deps.1.4.0.txt'), 'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), )); - $reader = new \Composer\Repository\Pear\ChannelReader($rfs); + $reader = new \Composer\Repository\Pear\ChannelReader($httpDownloader); $pear10 = $reader->read('http://pear.1.0.net/'); $this->assertCount(2, $pear10->getPackages()); diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php index 4aa7bbba2..3960c7858 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php @@ -13,13 +13,13 @@ namespace Composer\Test\Repository\Pear; use Composer\Test\TestCase; -use Composer\Test\Mock\RemoteFilesystemMock; +use Composer\Test\Mock\HttpDownloaderMock; class ChannelRest10ReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new RemoteFilesystemMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'), 'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'), 'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'), @@ -29,7 +29,7 @@ class ChannelRest10ReaderTest extends TestCase 'http://test.loc/rest10/r/http_request/deps.1.4.0.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_deps.1.4.0.txt'), )); - $reader = new \Composer\Repository\Pear\ChannelRest10Reader($rfs); + $reader = new \Composer\Repository\Pear\ChannelRest10Reader($httpDownloader); /** @var \Composer\Package\PackageInterface[] $packages */ $packages = $reader->read('http://test.loc/rest10'); diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php index 04e48426e..684c59155 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php @@ -13,19 +13,19 @@ namespace Composer\Test\Repository\Pear; use Composer\Test\TestCase; -use Composer\Test\Mock\RemoteFilesystemMock; +use Composer\Test\Mock\HttpDownloaderMock; class ChannelRest11ReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new RemoteFilesystemMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), )); - $reader = new \Composer\Repository\Pear\ChannelRest11Reader($rfs); + $reader = new \Composer\Repository\Pear\ChannelRest11Reader($httpDownloader); /** @var \Composer\Package\PackageInterface[] $packages */ $packages = $reader->read('http://test.loc/rest11'); diff --git a/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php b/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php index 0ca9259d9..f8cf3efca 100644 --- a/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php +++ b/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php @@ -53,7 +53,7 @@ class PackageDependencyParserTest extends TestCase { $data = json_decode(file_get_contents(__DIR__.'/Fixtures/DependencyParserTestData.json'), true); if (0 !== json_last_error()) { - throw new \PHPUnit_Framework_Exception('Invalid json file.'); + throw new \PHPUnit\Framework\Exception('Invalid json file.'); } return $data; diff --git a/tests/Composer/Test/Repository/PearRepositoryTest.php b/tests/Composer/Test/Repository/PearRepositoryTest.php index b1a3c0b5e..867d4978d 100644 --- a/tests/Composer/Test/Repository/PearRepositoryTest.php +++ b/tests/Composer/Test/Repository/PearRepositoryTest.php @@ -28,7 +28,7 @@ class PearRepositoryTest extends TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $remoteFilesystem; + private $httpDownloader; public function testComposerShouldSetIncludePath() { @@ -133,7 +133,7 @@ class PearRepositoryTest extends TestCase $config = new \Composer\Config(); - $this->remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $this->httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock(); @@ -143,6 +143,6 @@ class PearRepositoryTest extends TestCase protected function tearDown() { $this->repository = null; - $this->remoteFilesystem = null; + $this->httpDownloader = null; } } diff --git a/tests/Composer/Test/Repository/RepositoryFactoryTest.php b/tests/Composer/Test/Repository/RepositoryFactoryTest.php index acd666430..20b1fad28 100644 --- a/tests/Composer/Test/Repository/RepositoryFactoryTest.php +++ b/tests/Composer/Test/Repository/RepositoryFactoryTest.php @@ -21,7 +21,9 @@ class RepositoryFactoryTest extends TestCase { $manager = RepositoryFactory::manager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), - $this->getMockBuilder('Composer\Config')->getMock() + $this->getMockBuilder('Composer\Config')->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); $ref = new \ReflectionProperty($manager, 'repositoryClasses'); diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php index f09b55ad8..dad0bd346 100644 --- a/tests/Composer/Test/Repository/RepositoryManagerTest.php +++ b/tests/Composer/Test/Repository/RepositoryManagerTest.php @@ -38,6 +38,7 @@ class RepositoryManagerTest extends TestCase $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getMockBuilder('Composer\Config')->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); @@ -61,6 +62,7 @@ class RepositoryManagerTest extends TestCase $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $config = $this->getMockBuilder('Composer\Config')->setMethods(array('get'))->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php index 8d711e8f0..f0139970b 100644 --- a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -16,6 +16,8 @@ use Composer\Config; use Composer\Repository\Vcs\GitBitbucketDriver; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; +use Composer\Util\Http\Response; /** * @group bitbucket @@ -26,8 +28,8 @@ class GitBitbucketDriverTest extends TestCase private $io; /** @type \Composer\Config */ private $config; - /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ - private $rfs; + /** @type \Composer\Util\HttpDownloader|\PHPUnit_Framework_MockObject_MockObject */ + private $httpDownloader; /** @type string */ private $home; /** @type string */ @@ -46,7 +48,7 @@ class GitBitbucketDriverTest extends TestCase ), )); - $this->rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $this->httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock(); } @@ -67,8 +69,8 @@ class GitBitbucketDriverTest extends TestCase $repoConfig, $this->io, $this->config, - null, - $this->rfs + $this->httpDownloader, + new ProcessExecutor($this->io) ); $driver->initialize(); @@ -83,15 +85,14 @@ class GitBitbucketDriverTest extends TestCase 'https://bitbucket.org/user/repo.git does not appear to be a git repository, use https://bitbucket.org/user/repo if this is a mercurial bitbucket repository' ); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', - false + $url = 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', + array() ) ->willReturn( - '{"scm":"hg","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo","name":"https"},{"href":"ssh:\/\/hg@bitbucket.org\/user\/repo","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}' + new Response(array('url' => $url), 200, array(), '{"scm":"hg","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo","name":"https"},{"href":"ssh:\/\/hg@bitbucket.org\/user\/repo","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}') ); $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); @@ -103,47 +104,43 @@ class GitBitbucketDriverTest extends TestCase { $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); - $this->rfs->expects($this->any()) - ->method('getContents') + $urls = array( + 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', + 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=mainbranch', + 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/tags?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cnext&sort=-target.date', + 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/branches?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cvalues.heads%2Cnext&sort=-target.date', + 'https://api.bitbucket.org/2.0/repositories/user/repo/src/master/composer.json', + 'https://api.bitbucket.org/2.0/repositories/user/repo/commit/master?fields=date', + ); + $this->httpDownloader->expects($this->any()) + ->method('get') ->withConsecutive( array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', - false, + $urls[0], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=mainbranch', - false, + $urls[1], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/tags?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cnext&sort=-target.date', - false, + $urls[2], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/branches?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cvalues.heads%2Cnext&sort=-target.date', - false, + $urls[3], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/src/master/composer.json', - false, + $urls[4], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/commit/master?fields=date', - false, + $urls[5], array() ) ) ->willReturnOnConsecutiveCalls( - '{"scm":"git","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo.git","name":"https"},{"href":"ssh:\/\/git@bitbucket.org\/user\/repo.git","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}', - '{"mainbranch": {"name": "master"}}', - '{"values":[{"name":"1.0.1","target":{"hash":"9b78a3932143497c519e49b8241083838c8ff8a1"}},{"name":"1.0.0","target":{"hash":"d3393d514318a9267d2f8ebbf463a9aaa389f8eb"}}]}', - '{"values":[{"name":"master","target":{"hash":"937992d19d72b5116c3e8c4a04f960e5fa270b22"}}]}', - '{"name": "user/repo","description": "test repo","license": "GPL","authors": [{"name": "Name","email": "local@domain.tld"}],"require": {"creator/package": "^1.0"},"require-dev": {"phpunit/phpunit": "~4.8"}}', - '{"date": "2016-05-17T13:19:52+00:00"}' + new Response(array('url' => $urls[0]), 200, array(), '{"scm":"git","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo.git","name":"https"},{"href":"ssh:\/\/git@bitbucket.org\/user\/repo.git","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}'), + new Response(array('url' => $urls[1]), 200, array(), '{"mainbranch": {"name": "master"}}'), + new Response(array('url' => $urls[2]), 200, array(), '{"values":[{"name":"1.0.1","target":{"hash":"9b78a3932143497c519e49b8241083838c8ff8a1"}},{"name":"1.0.0","target":{"hash":"d3393d514318a9267d2f8ebbf463a9aaa389f8eb"}}]}'), + new Response(array('url' => $urls[3]), 200, array(), '{"values":[{"name":"master","target":{"hash":"937992d19d72b5116c3e8c4a04f960e5fa270b22"}}]}'), + new Response(array('url' => $urls[4]), 200, array(), '{"name": "user/repo","description": "test repo","license": "GPL","authors": [{"name": "Name","email": "local@domain.tld"}],"require": {"creator/package": "^1.0"},"require-dev": {"phpunit/phpunit": "~4.8"}}'), + new Response(array('url' => $urls[5]), 200, array(), '{"date": "2016-05-17T13:19:52+00:00"}') ); $this->assertEquals( diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index 45df0ce7f..29c153d4b 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -16,6 +16,7 @@ use Composer\Downloader\TransportException; use Composer\Repository\Vcs\GitHubDriver; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; use Composer\Config; class GitHubDriverTest extends TestCase @@ -53,8 +54,8 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); @@ -62,9 +63,9 @@ class GitHubDriverTest extends TestCase ->method('execute') ->will($this->returnValue(1)); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) ->will($this->throwException(new TransportException('HTTP/1.1 404 Not Found', 404))); $io->expects($this->once()) @@ -76,15 +77,15 @@ class GitHubDriverTest extends TestCase ->method('setAuthentication') ->with($this->equalTo('github.com'), $this->matchesRegularExpression('{sometoken}'), $this->matchesRegularExpression('{x-oauth-basic}')); - $remoteFilesystem->expects($this->at(1)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/'), $this->equalTo(false)) - ->will($this->returnValue('{}')); + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{}'))); - $remoteFilesystem->expects($this->at(2)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}')); + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($this->equalTo($url = $repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}'))); $configSource = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); $authConfigSource = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); @@ -95,7 +96,7 @@ class GitHubDriverTest extends TestCase 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -124,21 +125,25 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}')); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $repoApiUrl), 200, array(), '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}'))); $repoConfig = array( 'url' => $repoUrl, ); $repoUrl = 'https://github.com/composer/packagist.git'; - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor') + ->disableOriginalConstructor() + ->getMock(); + + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -167,31 +172,40 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}')); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($url = $repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}'))); - $remoteFilesystem->expects($this->at(1)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo'), $this->equalTo(false)) - ->will($this->returnValue('{"encoding":"base64","content":"'.base64_encode('{"support": {"source": "'.$repoUrl.'" }}').'"}')); + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"encoding":"base64","content":"'.base64_encode('{"support": {"source": "'.$repoUrl.'" }}').'"}'))); - $remoteFilesystem->expects($this->at(2)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo'), $this->equalTo(false)) - ->will($this->returnValue('{"commit": {"committer":{ "date": "2012-09-10"}}}')); + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"commit": {"committer":{ "date": "2012-09-10"}}}'))); + + $httpDownloader->expects($this->at(3)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"encoding": "base64", "content": "'.base64_encode("custom: https://example.com").'"}'))); $repoConfig = array( 'url' => $repoUrl, ); $repoUrl = 'https://github.com/composer/packagist.git'; - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor') + ->disableOriginalConstructor() + ->getMock(); + + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config,$httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -225,30 +239,39 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor') + ->disableOriginalConstructor() ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}')); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) + ->getMock(); - $remoteFilesystem->expects($this->at(1)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($composerJsonUrl), $this->equalTo(false)) - ->will($this->returnValue('{"encoding": "base64", "content": "' . base64_encode('{"name": "composer/packagist"}') . '"}')); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $repoApiUrl), 200, array(), '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}'))); - $remoteFilesystem->expects($this->at(2)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/repos/composer/packagist/commits/SOMESHA'), $this->equalTo(false)) - ->will($this->returnValue('{"commit": {"committer":{ "date": "2012-09-10"}}}')); + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($this->equalTo($composerJsonUrl)) + ->will($this->returnValue(new Response(array('url' => $composerJsonUrl), 200, array(), '{"encoding": "base64", "content": "' . base64_encode('{"name": "composer/packagist"}') . '"}'))); + + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/commits/'.$sha)) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"commit": {"committer":{ "date": "2012-09-10"}}}'))); + + $httpDownloader->expects($this->at(3)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"encoding": "base64", "content": "'.base64_encode("custom: https://example.com").'"}'))); $repoConfig = array( 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -274,13 +297,13 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(false)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) ->will($this->throwException(new TransportException('HTTP/1.1 404 Not Found', 404))); // clean local clone if present @@ -325,7 +348,7 @@ class GitHubDriverTest extends TestCase 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php index 81e623182..0fd2fa956 100644 --- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Prophecy\Argument; +use Composer\Util\Http\Response; /** * @author Jérôme Tamarelle @@ -27,7 +28,7 @@ class GitLabDriverTest extends TestCase private $config; private $io; private $process; - private $remoteFilesystem; + private $httpDownloader; public function setUp() { @@ -47,7 +48,7 @@ class GitLabDriverTest extends TestCase $this->io = $this->prophesize('Composer\IO\IOInterface'); $this->process = $this->prophesize('Composer\Util\ProcessExecutor'); - $this->remoteFilesystem = $this->prophesize('Composer\Util\RemoteFilesystem'); + $this->httpDownloader = $this->prophesize('Composer\Util\HttpDownloader'); } public function tearDown() @@ -87,13 +88,11 @@ class GitLabDriverTest extends TestCase } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -126,13 +125,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -164,13 +161,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -206,12 +201,10 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents($domain.':'.$port, $apiUrl, false, array()) - ->willReturn(sprintf($projectData, $domain, $port, $namespace)) + $this->mockResponse($apiUrl, array(), sprintf($projectData, $domain, $port, $namespace)) ->shouldBeCalledTimes(1); - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -289,15 +282,11 @@ JSON; ] JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($tagData) + $this->mockResponse($apiUrl, array(), $tagData) ->shouldBeCalledTimes(1) ; - $this->remoteFilesystem->getLastHeaders() - ->willReturn(array()); - $driver->setRemoteFilesystem($this->remoteFilesystem->reveal()); + $driver->setHttpDownloader($this->httpDownloader->reveal()); $expected = array( 'v1.0.0' => '092ed2c762bbae331e3f51d4a17f67310bf99a81', @@ -344,26 +333,20 @@ JSON; $branchData = json_encode($branchData); - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($branchData) - ->shouldBeCalledTimes(1) - ; + $headers = array('Link: ; rel="next", ; rel="first", ; rel="last"'); + $this->httpDownloader + ->get($apiUrl, array()) + ->willReturn(new Response(array('url' => $apiUrl), 200, $headers, $branchData)) + ->shouldBeCalledTimes(1); - $this->remoteFilesystem - ->getContents('gitlab.com', "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20", false, array()) - ->willReturn($branchData) - ->shouldBeCalledTimes(1) - ; + $apiUrl = "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20"; + $headers = array('Link: ; rel="prev", ; rel="first", ; rel="last"'); + $this->httpDownloader + ->get($apiUrl, array()) + ->willReturn(new Response(array('url' => $apiUrl), 200, $headers, $branchData)) + ->shouldBeCalledTimes(1); - $this->remoteFilesystem->getLastHeaders() - ->willReturn( - array('Link: ; rel="next", ; rel="first", ; rel="last"'), - array('Link: ; rel="prev", ; rel="first", ; rel="last"') - ) - ->shouldBeCalledTimes(2); - - $driver->setRemoteFilesystem($this->remoteFilesystem->reveal()); + $driver->setHttpDownloader($this->httpDownloader->reveal()); $expected = array( 'mymaster' => '97eda36b5c1dd953a3792865c222d4e85e5f302e', @@ -401,15 +384,11 @@ JSON; ] JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($branchData) + $this->mockResponse($apiUrl, array(), $branchData) ->shouldBeCalledTimes(1) ; - $this->remoteFilesystem->getLastHeaders() - ->willReturn(array()); - $driver->setRemoteFilesystem($this->remoteFilesystem->reveal()); + $driver->setHttpDownloader($this->httpDownloader->reveal()); $expected = array( 'mymaster' => '97eda36b5c1dd953a3792865c222d4e85e5f302e', @@ -474,13 +453,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('mycompany.com/gitlab', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -507,13 +484,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -540,13 +515,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('mycompany.com/gitlab', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -575,18 +548,23 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents(Argument::cetera(), $options) - ->willReturn($projectData) + $this->mockResponse(Argument::cetera(), $options, $projectData) ->shouldBeCalled(); $driver = new GitLabDriver( array('url' => 'https://gitlab.mycompany.local/mygroup/myproject', 'options' => $options), $this->io->reveal(), $this->config, - $this->process->reveal(), - $this->remoteFilesystem->reveal() + $this->httpDownloader->reveal(), + $this->process->reveal() ); $driver->initialize(); } + + private function mockResponse($url, $options, $return) + { + return $this->httpDownloader + ->get($url, $options) + ->willReturn(new Response(array('url' => $url), 200, array(), $return)); + } } diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index a5e5d4b4c..1c44e82dd 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -26,7 +26,7 @@ class PerforceDriverTest extends TestCase protected $config; protected $io; protected $process; - protected $remoteFileSystem; + protected $httpDownloader; protected $testPath; protected $driver; protected $repoConfig; @@ -43,9 +43,9 @@ class PerforceDriverTest extends TestCase $this->repoConfig = $this->getTestRepoConfig(); $this->io = $this->getMockIOInterface(); $this->process = $this->getMockProcessExecutor(); - $this->remoteFileSystem = $this->getMockRemoteFilesystem(); + $this->httpDownloader = $this->getMockHttpDownloader(); $this->perforce = $this->getMockPerforce(); - $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); + $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->process); $this->overrideDriverInternalPerforce($this->perforce); } @@ -56,7 +56,7 @@ class PerforceDriverTest extends TestCase $fs->removeDirectory($this->testPath); $this->driver = null; $this->perforce = null; - $this->remoteFileSystem = null; + $this->httpDownloader = null; $this->process = null; $this->io = null; $this->repoConfig = null; @@ -99,21 +99,21 @@ class PerforceDriverTest extends TestCase return $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); } - protected function getMockRemoteFilesystem() + protected function getMockHttpDownloader() { - return $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); + return $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); } protected function getMockPerforce() { $methods = array('p4login', 'checkStream', 'writeP4ClientSpec', 'connectClient', 'getComposerInformation', 'cleanupClientSpec'); - return $this->getMockBuilder('Composer\Util\Perforce', $methods)->disableOriginalConstructor()->getMock(); + return $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); } public function testInitializeCapturesVariablesFromRepoConfig() { - $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); + $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); $this->assertEquals(self::TEST_URL, $driver->getUrl()); $this->assertEquals(self::TEST_DEPOT, $driver->getDepot()); diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index 4ef0d9bcc..b43cbbc2a 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -46,6 +46,7 @@ class SvnDriverTest extends TestCase public function testWrongCredentialsInUrl() { $console = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); $output = "svn: OPTIONS of 'https://corp.svn.local/repo':"; $output .= " authorization failed: Could not authenticate to server:"; @@ -66,7 +67,7 @@ class SvnDriverTest extends TestCase 'url' => 'https://till:secret@corp.svn.local/repo', ); - $svn = new SvnDriver($repoConfig, $console, $this->config, $process); + $svn = new SvnDriver($repoConfig, $console, $this->config, $httpDownloader, $process); $svn->initialize(); } diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php index 0cab2f8bb..65bf52409 100644 --- a/tests/Composer/Test/Repository/VcsRepositoryTest.php +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -32,17 +32,18 @@ class VcsRepositoryTest extends TestCase protected function initialize() { - $oldCwd = getcwd(); - self::$composerHome = $this->getUniqueTmpDirectory(); - self::$gitRepo = $this->getUniqueTmpDirectory(); - $locator = new ExecutableFinder(); if (!$locator->find('git')) { $this->skipped = 'This test needs a git binary in the PATH to be able to run'; return; } - if (!@mkdir(self::$gitRepo) || !@chdir(self::$gitRepo)) { + + $oldCwd = getcwd(); + self::$composerHome = $this->getUniqueTmpDirectory(); + self::$gitRepo = $this->getUniqueTmpDirectory(); + + if (!@chdir(self::$gitRepo)) { $this->skipped = 'Could not create and move into the temp git repo '.self::$gitRepo; return; @@ -60,6 +61,7 @@ class VcsRepositoryTest extends TestCase $exec('git init'); $exec('git config user.email composertest@example.org'); $exec('git config user.name ComposerTest'); + $exec('git config commit.gpgsign false'); touch('foo'); $exec('git add foo'); $exec('git commit -m init'); @@ -150,7 +152,8 @@ class VcsRepositoryTest extends TestCase 'home' => self::$composerHome, ), )); - $repo = new VcsRepository(array('url' => self::$gitRepo, 'type' => 'vcs'), new NullIO, $config); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); + $repo = new VcsRepository(array('url' => self::$gitRepo, 'type' => 'vcs'), new NullIO, $config, $httpDownloader); $packages = $repo->getPackages(); $dumper = new ArrayDumper(); diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index c89b393e1..5837d1996 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\Bitbucket; +use Composer\Util\Http\Response; use Composer\Test\TestCase; /** @@ -30,8 +31,8 @@ class BitbucketTest extends TestCase /** @type \Composer\IO\ConsoleIO|\PHPUnit_Framework_MockObject_MockObject */ private $io; - /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ - private $rfs; + /** @type \Composer\Util\HttpDownloader|\PHPUnit_Framework_MockObject_MockObject */ + private $httpDownloader; /** @type \Composer\Config|\PHPUnit_Framework_MockObject_MockObject */ private $config; /** @type Bitbucket */ @@ -47,8 +48,8 @@ class BitbucketTest extends TestCase ->getMock() ; - $this->rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') + $this->httpDownloader = $this + ->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock() ; @@ -57,7 +58,7 @@ class BitbucketTest extends TestCase $this->time = time(); - $this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->rfs, $this->time); + $this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->httpDownloader, $this->time); } public function testRequestAccessTokenWithValidOAuthConsumer() @@ -66,12 +67,10 @@ class BitbucketTest extends TestCase ->method('setAuthentication') ->with($this->origin, $this->consumer_key, $this->consumer_secret); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->origin, Bitbucket::OAUTH2_ACCESS_TOKEN_URL, - false, array( 'retry-auth-failure' => false, 'http' => array( @@ -81,9 +80,14 @@ class BitbucketTest extends TestCase ) ) ->willReturn( - sprintf( - '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', - $this->token + new Response( + array('url' => Bitbucket::OAUTH2_ACCESS_TOKEN_URL), + 200, + array(), + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', + $this->token + ) ) ); @@ -142,12 +146,10 @@ class BitbucketTest extends TestCase ->method('setAuthentication') ->with($this->origin, $this->consumer_key, $this->consumer_secret); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->origin, Bitbucket::OAUTH2_ACCESS_TOKEN_URL, - false, array( 'retry-auth-failure' => false, 'http' => array( @@ -157,9 +159,14 @@ class BitbucketTest extends TestCase ) ) ->willReturn( - sprintf( - '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', - $this->token + new Response( + array('url' => Bitbucket::OAUTH2_ACCESS_TOKEN_URL), + 200, + array(), + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', + $this->token + ) ) ); @@ -186,12 +193,10 @@ class BitbucketTest extends TestCase array('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url') ); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->origin, Bitbucket::OAUTH2_ACCESS_TOKEN_URL, - false, array( 'retry-auth-failure' => false, 'http' => array( @@ -234,21 +239,24 @@ class BitbucketTest extends TestCase ) ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); - $this->rfs + $this->httpDownloader ->expects($this->once()) - ->method('getContents') + ->method('get') ->with( - $this->equalTo($this->origin), - $this->equalTo(sprintf('https://%s/site/oauth2/access_token', $this->origin)), - $this->isFalse(), + $this->equalTo($url = sprintf('https://%s/site/oauth2/access_token', $this->origin)), $this->anything() ) ->willReturn( - sprintf( - '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refresh_token", "token_type": "bearer"}', - $this->token + new Response( + array('url' => $url), + 200, + array(), + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refresh_token", "token_type": "bearer"}', + $this->token + ) ) - ) + ); ; $this->setExpectationsForStoringAccessToken(true); diff --git a/tests/Composer/Test/Util/GitHubTest.php b/tests/Composer/Test/Util/GitHubTest.php index 375279652..37683395f 100644 --- a/tests/Composer/Test/Util/GitHubTest.php +++ b/tests/Composer/Test/Util/GitHubTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Util; use Composer\Downloader\TransportException; use Composer\Util\GitHub; +use Composer\Util\Http\Response; use Composer\Test\TestCase; use RecursiveArrayIterator; use RecursiveIteratorIterator; @@ -42,17 +43,15 @@ class GitHubTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->once()) - ->method('getContents') + ->method('get') ->with( - $this->equalTo($this->origin), - $this->equalTo(sprintf('https://api.%s/', $this->origin)), - $this->isFalse(), + $this->equalTo($url = sprintf('https://api.%s/', $this->origin)), $this->anything() ) - ->willReturn('{}') + ->willReturn(new Response(array('url' => $url), 200, array(), '{}')); ; $config = $this->getConfigMock(); @@ -67,7 +66,7 @@ class GitHubTest extends TestCase ->willReturn($this->getConfJsonMock()) ; - $github = new GitHub($io, $config, null, $rfs); + $github = new GitHub($io, $config, null, $httpDownloader); $this->assertTrue($github->authorizeOAuthInteractively($this->origin, $this->message)); } @@ -82,10 +81,10 @@ class GitHubTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->exactly(1)) - ->method('getContents') + ->method('get') ->will($this->throwException(new TransportException('', 401))) ; @@ -96,7 +95,7 @@ class GitHubTest extends TestCase ->willReturn($this->getAuthJsonMock()) ; - $github = new GitHub($io, $config, null, $rfs); + $github = new GitHub($io, $config, null, $httpDownloader); $this->assertFalse($github->authorizeOAuthInteractively($this->origin)); } @@ -117,15 +116,15 @@ class GitHubTest extends TestCase return $this->getMockBuilder('Composer\Config')->getMock(); } - private function getRemoteFilesystemMock() + private function getHttpDownloaderMock() { - $rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this + ->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock() ; - return $rfs; + return $httpDownloader; } private function getAuthJsonMock() diff --git a/tests/Composer/Test/Util/GitLabTest.php b/tests/Composer/Test/Util/GitLabTest.php index 541e94c61..c9ac9dbf6 100644 --- a/tests/Composer/Test/Util/GitLabTest.php +++ b/tests/Composer/Test/Util/GitLabTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Util; use Composer\Downloader\TransportException; use Composer\Util\GitLab; +use Composer\Util\Http\Response; use Composer\Test\TestCase; /** @@ -48,17 +49,15 @@ class GitLabTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->once()) - ->method('getContents') + ->method('get') ->with( - $this->equalTo($this->origin), - $this->equalTo(sprintf('http://%s/oauth/token', $this->origin)), - $this->isFalse(), + $this->equalTo($url = sprintf('http://%s/oauth/token', $this->origin)), $this->anything() ) - ->willReturn(sprintf('{"access_token": "%s", "token_type": "bearer", "expires_in": 7200}', $this->token)) + ->willReturn(new Response(array('url' => $url), 200, array(), sprintf('{"access_token": "%s", "token_type": "bearer", "expires_in": 7200}', $this->token))); ; $config = $this->getConfigMock(); @@ -68,7 +67,7 @@ class GitLabTest extends TestCase ->willReturn($this->getAuthJsonMock()) ; - $gitLab = new GitLab($io, $config, null, $rfs); + $gitLab = new GitLab($io, $config, null, $httpDownloader); $this->assertTrue($gitLab->authorizeOAuthInteractively('http', $this->origin, $this->message)); } @@ -93,10 +92,10 @@ class GitLabTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->exactly(5)) - ->method('getContents') + ->method('get') ->will($this->throwException(new TransportException('', 401))) ; @@ -107,7 +106,7 @@ class GitLabTest extends TestCase ->willReturn($this->getAuthJsonMock()) ; - $gitLab = new GitLab($io, $config, null, $rfs); + $gitLab = new GitLab($io, $config, null, $httpDownloader); $gitLab->authorizeOAuthInteractively('https', $this->origin); } @@ -128,15 +127,15 @@ class GitLabTest extends TestCase return $this->getMockBuilder('Composer\Config')->getMock(); } - private function getRemoteFilesystemMock() + private function getHttpDownloaderMock() { - $rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this + ->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock() ; - return $rfs; + return $httpDownloader; } private function getAuthJsonMock() diff --git a/tests/Composer/Test/Util/HttpDownloaderTest.php b/tests/Composer/Test/Util/HttpDownloaderTest.php new file mode 100644 index 000000000..b65aa760a --- /dev/null +++ b/tests/Composer/Test/Util/HttpDownloaderTest.php @@ -0,0 +1,51 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\HttpDownloader; +use PHPUnit\Framework\TestCase; + +class HttpDownloaderTest 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; + } + + /** + * @group slow + */ + public function testCaptureAuthenticationParamsFromUrl() + { + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io->expects($this->once()) + ->method('setAuthentication') + ->with($this->equalTo('github.com'), $this->equalTo('user'), $this->equalTo('pass')); + + $fs = new HttpDownloader($io, $this->getConfigMock()); + try { + $fs->get('https://user:pass@github.com/composer/composer/404'); + } catch (\Composer\Downloader\TransportException $e) { + $this->assertNotEquals(200, $e->getCode()); + } + } +} diff --git a/tests/Composer/Test/Util/MetadataMinifierTest.php b/tests/Composer/Test/Util/MetadataMinifierTest.php new file mode 100644 index 000000000..ad8d3abad --- /dev/null +++ b/tests/Composer/Test/Util/MetadataMinifierTest.php @@ -0,0 +1,45 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\MetadataMinifier; +use Composer\Package\CompletePackage; +use Composer\Package\Dumper\ArrayDumper; +use PHPUnit\Framework\TestCase; + +class MetadataMinifierTest extends TestCase +{ + public function testMinifyExpand() + { + $package1 = new CompletePackage('foo/bar', '2.0.0.0', '2.0.0'); + $package1->setScripts(array('foo' => 'bar')); + $package1->setLicense(array('MIT')); + $package2 = new CompletePackage('foo/bar', '1.2.0.0', '1.2.0'); + $package2->setLicense(array('GPL')); + $package2->setHomepage('https://example.org'); + $package3 = new CompletePackage('foo/bar', '1.0.0.0', '1.0.0'); + $package3->setLicense(array('GPL')); + $dumper = new ArrayDumper(); + + $minified = array( + array('name' => 'foo/bar', 'version' => '2.0.0', 'version_normalized' => '2.0.0.0', 'type' => 'library', 'scripts' => array('foo' => 'bar'), 'license' => array('MIT')), + array('version' => '1.2.0', 'version_normalized' => '1.2.0.0', 'license' => array('GPL'), 'homepage' => 'https://example.org', 'scripts' => '__unset'), + array('version' => '1.0.0', 'version_normalized' => '1.0.0.0', 'homepage' => '__unset'), + ); + + $source = array($dumper->dump($package1), $dumper->dump($package2), $dumper->dump($package3)); + + $this->assertSame($minified, MetadataMinifier::minify($source)); + $this->assertSame($source, MetadataMinifier::expand($minified)); + } +} diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index 4ac0d570f..87c2d07cc 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -44,7 +44,7 @@ class ProcessExecutorTest extends TestCase { $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io->expects($this->once()) - ->method('write') + ->method('writeRaw') ->with($this->equalTo('foo'.PHP_EOL), false); $process = new ProcessExecutor($io); diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 486ad9a3b..fe4f213c6 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -17,6 +17,20 @@ use Composer\Test\TestCase; class RemoteFilesystemTest extends TestCase { + private function getConfigMock() + { + $config = $this->getMockBuilder('Composer\Config')->getMock(); + $config->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($key) { + if ($key === 'github-domains' || $key === 'gitlab-domains') { + return array(); + } + })); + + return $config; + } + public function testGetOptionsForUrl() { $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); @@ -113,7 +127,7 @@ class RemoteFilesystemTest extends TestCase public function testCallbackGetFileSize() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20); $this->assertAttributeEquals(20, 'bytesMax', $fs); } @@ -126,7 +140,7 @@ class RemoteFilesystemTest extends TestCase ->method('overwriteError') ; - $fs = new RemoteFilesystem($io); + $fs = new RemoteFilesystem($io, $this->getConfigMock()); $this->setAttribute($fs, 'bytesMax', 20); $this->setAttribute($fs, 'progress', true); @@ -136,40 +150,21 @@ class RemoteFilesystemTest extends TestCase public function testCallbackGetPassesThrough404() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); } - /** - * @group slow - */ - public function testCaptureAuthenticationParamsFromUrl() - { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $io->expects($this->once()) - ->method('setAuthentication') - ->with($this->equalTo('github.com'), $this->equalTo('user'), $this->equalTo('pass')); - - $fs = new RemoteFilesystem($io); - try { - $fs->getContents('github.com', 'https://user:pass@github.com/composer/composer/404'); - } catch (\Exception $e) { - $this->assertInstanceOf('Composer\Downloader\TransportException', $e); - $this->assertNotEquals(200, $e->getCode()); - } - } - public function testGetContents() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $this->assertContains('testGetContents', $fs->getContents('http://example.org', 'file://'.__FILE__)); } public function testCopy() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $file = tempnam(sys_get_temp_dir(), 'c'); $this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file)); @@ -230,7 +225,7 @@ class RemoteFilesystemTest extends TestCase ->disableOriginalConstructor() ->getMock(); - $rfs = new RemoteFilesystem($io); + $rfs = new RemoteFilesystem($io, $this->getConfigMock()); $hostname = parse_url($url, PHP_URL_HOST); $result = $rfs->getContents($hostname, $url, false); @@ -252,14 +247,6 @@ class RemoteFilesystemTest extends TestCase ->disableOriginalConstructor() ->getMock(); - $config = $this - ->getMockBuilder('Composer\Config') - ->getMock(); - $config - ->method('get') - ->withAnyParameters() - ->willReturn(array()); - $domains = array(); $io ->expects($this->any()) @@ -279,7 +266,7 @@ class RemoteFilesystemTest extends TestCase 'password' => '1A0yeK5Po3ZEeiiRiMWLivS0jirLdoGuaSGq9NvESFx1Fsdn493wUDXC8rz_1iKVRTl1GINHEUCsDxGh5lZ=', )); - $rfs = new RemoteFilesystem($io, $config); + $rfs = new RemoteFilesystem($io, $this->getConfigMock()); $hostname = parse_url($url, PHP_URL_HOST); $result = $rfs->getContents($hostname, $url, false); @@ -290,7 +277,7 @@ class RemoteFilesystemTest extends TestCase protected function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '') { - $fs = new RemoteFilesystem($io, null, $options); + $fs = new RemoteFilesystem($io, $this->getConfigMock(), $options); $ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); $prop = new \ReflectionProperty($fs, 'fileUrl'); $ref->setAccessible(true); diff --git a/tests/Composer/Test/Util/StreamContextFactoryTest.php b/tests/Composer/Test/Util/StreamContextFactoryTest.php index 34de2fa33..bd4935cf2 100644 --- a/tests/Composer/Test/Util/StreamContextFactoryTest.php +++ b/tests/Composer/Test/Util/StreamContextFactoryTest.php @@ -142,7 +142,6 @@ class StreamContextFactoryTest extends TestCase $expected = array( 'http' => array( 'proxy' => 'tcp://proxyserver.net:80', - 'request_fulluri' => true, 'method' => 'GET', 'header' => array('User-Agent: foo', "Proxy-Authorization: Basic " . base64_encode('username:password')), 'max_redirects' => 20, @@ -173,7 +172,6 @@ class StreamContextFactoryTest extends TestCase $expected = array( 'http' => array( 'proxy' => 'ssl://woopproxy.net:443', - 'request_fulluri' => true, 'method' => 'GET', 'max_redirects' => 20, 'follow_location' => 1, diff --git a/tests/Composer/Test/Util/UrlTest.php b/tests/Composer/Test/Util/UrlTest.php index e57c0ba9f..8eb33f851 100644 --- a/tests/Composer/Test/Util/UrlTest.php +++ b/tests/Composer/Test/Util/UrlTest.php @@ -58,4 +58,25 @@ class UrlTest extends TestCase array('https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=abcd', 'https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=65', array('gitlab-domains' => array('mygitlab.com')), '65'), ); } + + /** + * @dataProvider sanitizeProvider + */ + public function testSanitize($expected, $url) + { + $this->assertSame($expected, Url::sanitize($url)); + } + + public static function sanitizeProvider() + { + return array( + array('https://foo:***@example.org/', 'https://foo:bar@example.org/'), + array('https://foo@example.org/', 'https://foo@example.org/'), + array('https://example.org/', 'https://example.org/'), + array('http://***:***@example.org', 'http://10a8f08e8d7b7b9:foo@example.org'), + array('https://foo:***@example.org:123/', 'https://foo:bar@example.org:123/'), + array('https://example.org/foo/bar?access_token=***', 'https://example.org/foo/bar?access_token=abcdef'), + array('https://example.org/foo/bar?foo=bar&access_token=***', 'https://example.org/foo/bar?foo=bar&access_token=abcdef'), + ); + } }