1
0
Fork 0

Merge branch 'composer:main' into add-tests-for-GlobalCommand

pull/11769/head
Franck Ranaivo-Harisoa 2024-01-19 15:12:10 +01:00 committed by GitHub
commit 50c2a969d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 735 additions and 137 deletions

98
composer.lock generated
View File

@ -8,16 +8,16 @@
"packages": [ "packages": [
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
"version": "1.3.7", "version": "1.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/ca-bundle.git", "url": "https://github.com/composer/ca-bundle.git",
"reference": "76e46335014860eec1aa5a724799a00a2e47cc85" "reference": "b66d11b7479109ab547f9405b97205640b17d385"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/76e46335014860eec1aa5a724799a00a2e47cc85", "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385",
"reference": "76e46335014860eec1aa5a724799a00a2e47cc85", "reference": "b66d11b7479109ab547f9405b97205640b17d385",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -29,7 +29,7 @@
"phpstan/phpstan": "^0.12.55", "phpstan/phpstan": "^0.12.55",
"psr/log": "^1.0", "psr/log": "^1.0",
"symfony/phpunit-bridge": "^4.2 || ^5", "symfony/phpunit-bridge": "^4.2 || ^5",
"symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -64,7 +64,7 @@
"support": { "support": {
"irc": "irc://irc.freenode.org/composer", "irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues", "issues": "https://github.com/composer/ca-bundle/issues",
"source": "https://github.com/composer/ca-bundle/tree/1.3.7" "source": "https://github.com/composer/ca-bundle/tree/1.4.0"
}, },
"funding": [ "funding": [
{ {
@ -80,7 +80,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-08-30T09:31:38+00:00" "time": "2023-12-18T12:05:55+00:00"
}, },
{ {
"name": "composer/class-map-generator", "name": "composer/class-map-generator",
@ -765,16 +765,16 @@
}, },
{ {
"name": "seld/jsonlint", "name": "seld/jsonlint",
"version": "1.10.0", "version": "1.10.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Seldaek/jsonlint.git", "url": "https://github.com/Seldaek/jsonlint.git",
"reference": "594fd6462aad8ecee0b45ca5045acea4776667f1" "reference": "76d449a358ece77d6f1d6331c68453e657172202"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/594fd6462aad8ecee0b45ca5045acea4776667f1", "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/76d449a358ece77d6f1d6331c68453e657172202",
"reference": "594fd6462aad8ecee0b45ca5045acea4776667f1", "reference": "76d449a358ece77d6f1d6331c68453e657172202",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -801,7 +801,7 @@
{ {
"name": "Jordi Boggiano", "name": "Jordi Boggiano",
"email": "j.boggiano@seld.be", "email": "j.boggiano@seld.be",
"homepage": "http://seld.be" "homepage": "https://seld.be"
} }
], ],
"description": "JSON Linter", "description": "JSON Linter",
@ -813,7 +813,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Seldaek/jsonlint/issues", "issues": "https://github.com/Seldaek/jsonlint/issues",
"source": "https://github.com/Seldaek/jsonlint/tree/1.10.0" "source": "https://github.com/Seldaek/jsonlint/tree/1.10.1"
}, },
"funding": [ "funding": [
{ {
@ -825,7 +825,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-05-11T13:16:46+00:00" "time": "2023-12-18T13:03:25+00:00"
}, },
{ {
"name": "seld/phar-utils", "name": "seld/phar-utils",
@ -938,16 +938,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v5.4.32", "version": "v5.4.34",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "c70df1ffaf23a8d340bded3cfab1b86752ad6ed7" "reference": "4b4d8cd118484aa604ec519062113dd87abde18c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c70df1ffaf23a8d340bded3cfab1b86752ad6ed7", "url": "https://api.github.com/repos/symfony/console/zipball/4b4d8cd118484aa604ec519062113dd87abde18c",
"reference": "c70df1ffaf23a8d340bded3cfab1b86752ad6ed7", "reference": "4b4d8cd118484aa604ec519062113dd87abde18c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1017,7 +1017,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v5.4.32" "source": "https://github.com/symfony/console/tree/v5.4.34"
}, },
"funding": [ "funding": [
{ {
@ -1033,7 +1033,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-11-18T18:23:04+00:00" "time": "2023-12-08T13:33:03+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@ -1802,16 +1802,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v5.4.28", "version": "v5.4.34",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", "url": "https://api.github.com/repos/symfony/process/zipball/8fa22178dfc368911dbd513b431cd9b06f9afe7a",
"reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", "reference": "8fa22178dfc368911dbd513b431cd9b06f9afe7a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1844,7 +1844,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v5.4.28" "source": "https://github.com/symfony/process/tree/v5.4.34"
}, },
"funding": [ "funding": [
{ {
@ -1860,7 +1860,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-08-07T10:36:04+00:00" "time": "2023-12-02T08:41:43+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@ -1947,16 +1947,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v5.4.32", "version": "v5.4.34",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "91bf4453d65d8231688a04376c3a40efe0770f04" "reference": "e3f98bfc7885c957488f443df82d97814a3ce061"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/91bf4453d65d8231688a04376c3a40efe0770f04", "url": "https://api.github.com/repos/symfony/string/zipball/e3f98bfc7885c957488f443df82d97814a3ce061",
"reference": "91bf4453d65d8231688a04376c3a40efe0770f04", "reference": "e3f98bfc7885c957488f443df82d97814a3ce061",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2013,7 +2013,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v5.4.32" "source": "https://github.com/symfony/string/tree/v5.4.34"
}, },
"funding": [ "funding": [
{ {
@ -2029,22 +2029,22 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-11-26T13:43:46+00:00" "time": "2023-12-09T13:20:28+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.10.50", "version": "1.10.55",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9a88f9d18ddf4cf54c922fbeac16c4cb164c5949",
"reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2093,7 +2093,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-12-13T10:59:42+00:00" "time": "2024-01-08T12:32:40+00:00"
}, },
{ {
"name": "phpstan/phpstan-deprecation-rules", "name": "phpstan/phpstan-deprecation-rules",
@ -2246,16 +2246,16 @@
}, },
{ {
"name": "phpstan/phpstan-symfony", "name": "phpstan/phpstan-symfony",
"version": "1.3.5", "version": "1.3.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git", "url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "27ff6339f83796a7e0dd963cf445cd3c456fc620" "reference": "34b3c43684834f6a20aa51af8d455480d9de8b88"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/27ff6339f83796a7e0dd963cf445cd3c456fc620", "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/34b3c43684834f6a20aa51af8d455480d9de8b88",
"reference": "27ff6339f83796a7e0dd963cf445cd3c456fc620", "reference": "34b3c43684834f6a20aa51af8d455480d9de8b88",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2312,22 +2312,22 @@
"description": "Symfony Framework extensions and rules for PHPStan", "description": "Symfony Framework extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues", "issues": "https://github.com/phpstan/phpstan-symfony/issues",
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.5" "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.6"
}, },
"time": "2023-10-30T14:52:15+00:00" "time": "2023-12-22T11:22:34+00:00"
}, },
{ {
"name": "symfony/phpunit-bridge", "name": "symfony/phpunit-bridge",
"version": "v7.0.1", "version": "v7.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/phpunit-bridge.git", "url": "https://github.com/symfony/phpunit-bridge.git",
"reference": "c2d059b25e31274157dd7727131cd1cf33650207" "reference": "92df075808c9437beca9540e25ae0c40eea1c061"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c2d059b25e31274157dd7727131cd1cf33650207", "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/92df075808c9437beca9540e25ae0c40eea1c061",
"reference": "c2d059b25e31274157dd7727131cd1cf33650207", "reference": "92df075808c9437beca9540e25ae0c40eea1c061",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2379,7 +2379,7 @@
"description": "Provides utilities for PHPUnit, especially user deprecation notices management", "description": "Provides utilities for PHPUnit, especially user deprecation notices management",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.1" "source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.2"
}, },
"funding": [ "funding": [
{ {
@ -2395,7 +2395,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-12-01T09:26:31+00:00" "time": "2023-12-19T11:23:03+00:00"
} }
], ],
"aliases": [], "aliases": [],

View File

@ -122,8 +122,8 @@ versions of the dependencies that you are using. Your CI server, production
machines, other developers in your team, everything and everyone runs on the machines, other developers in your team, everything and everyone runs on the
same dependencies, which mitigates the potential for bugs affecting only some same dependencies, which mitigates the potential for bugs affecting only some
parts of the deployments. Even if you develop alone, in six months when parts of the deployments. Even if you develop alone, in six months when
reinstalling the project you can feel confident the dependencies installed are reinstalling the project you can feel confident that the dependencies installed are
still working even if your dependencies released many new versions since then. still working, even if the dependencies have released many new versions since then.
(See note below about using the `update` command.) (See note below about using the `update` command.)
> **Note:** For libraries it is not necessary to commit the lock > **Note:** For libraries it is not necessary to commit the lock
@ -141,7 +141,7 @@ in `composer.lock` to ensure that the package versions are consistent for everyo
working on your project. As a result you will have all dependencies requested by your working on your project. As a result you will have all dependencies requested by your
`composer.json` file, but they may not all be at the very latest available versions `composer.json` file, but they may not all be at the very latest available versions
(some of the dependencies listed in the `composer.lock` file may have released newer versions since (some of the dependencies listed in the `composer.lock` file may have released newer versions since
the file was created). This is by design, it ensures that your project does not break because of the file was created). This is by design, ensuring that your project does not break because of
unexpected changes in dependencies. unexpected changes in dependencies.
So after fetching new changes from your VCS repository it is recommended to run So after fetching new changes from your VCS repository it is recommended to run

View File

@ -198,8 +198,9 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.*
* **--no-install:** Does not run the install step after updating the composer.lock file. * **--no-install:** Does not run the install step after updating the composer.lock file.
* **--no-audit:** Does not run the audit steps after updating the composer.lock file. Also see [COMPOSER_NO_AUDIT](#composer-no-audit). * **--no-audit:** Does not run the audit steps after updating the composer.lock file. Also see [COMPOSER_NO_AUDIT](#composer-no-audit).
* **--audit-format:** Audit output format. Must be "table", "plain", "json", or "summary" (default). * **--audit-format:** Audit output format. Must be "table", "plain", "json", or "summary" (default).
* **--lock:** Only updates the lock file hash to suppress warning about the * **--lock:** Overwrites the lock file hash to suppress warning about the lock file being out of
lock file being out of date. date without updating package versions. Package metadata like mirrors and URLs are updated if
they changed.
* **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 * **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0
* **--no-autoloader:** Skips autoloader generation. * **--no-autoloader:** Skips autoloader generation.
* **--no-progress:** Removes the progress display that can mess with some * **--no-progress:** Removes the progress display that can mess with some
@ -556,6 +557,7 @@ php composer.phar show monolog/monolog 1.0.2
* **--major-only (-M):** Use with --latest or --outdated. Only shows packages that have major SemVer-compatible updates. * **--major-only (-M):** Use with --latest or --outdated. Only shows packages that have major SemVer-compatible updates.
* **--minor-only (-m):** Use with --latest or --outdated. Only shows packages that have minor SemVer-compatible updates. * **--minor-only (-m):** Use with --latest or --outdated. Only shows packages that have minor SemVer-compatible updates.
* **--patch-only:** Use with --latest or --outdated. Only shows packages that have patch-level SemVer-compatible updates. * **--patch-only:** Use with --latest or --outdated. Only shows packages that have patch-level SemVer-compatible updates.
* **--sort-by-age (-A):** Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option.
* **--direct (-D):** Restricts the list of packages to your direct dependencies. * **--direct (-D):** Restricts the list of packages to your direct dependencies.
* **--strict:** Return a non-zero exit code when there are outdated packages. * **--strict:** Return a non-zero exit code when there are outdated packages.
* **--format (-f):** Lets you pick between text (default) or json output format. * **--format (-f):** Lets you pick between text (default) or json output format.
@ -589,6 +591,7 @@ The color coding is as such:
* **--major-only (-M):** Only shows packages that have major SemVer-compatible updates. * **--major-only (-M):** Only shows packages that have major SemVer-compatible updates.
* **--minor-only (-m):** Only shows packages that have minor SemVer-compatible updates. * **--minor-only (-m):** Only shows packages that have minor SemVer-compatible updates.
* **--patch-only (-p):** Only shows packages that have patch-level SemVer-compatible updates. * **--patch-only (-p):** Only shows packages that have patch-level SemVer-compatible updates.
* **--sort-by-age (-A):** Displays the installed version's age, and sorts packages oldest first.
* **--format (-f):** Lets you pick between text (default) or json output format. * **--format (-f):** Lets you pick between text (default) or json output format.
* **--no-dev:** Do not show outdated dev dependencies. * **--no-dev:** Do not show outdated dev dependencies.
* **--locked:** Shows updates for packages from the lock file, regardless of what is currently in vendor dir. * **--locked:** Shows updates for packages from the lock file, regardless of what is currently in vendor dir.
@ -1143,6 +1146,10 @@ If set to 1, this env suppresses a warning when Composer is running with the Xde
This env var controls the [`discard-changes`](06-config.md#discard-changes) config option. This env var controls the [`discard-changes`](06-config.md#discard-changes) config option.
### COMPOSER_FUND
If set to 0, this env suppresses funding notices when installing.
### COMPOSER_HOME ### COMPOSER_HOME
The `COMPOSER_HOME` var allows you to change the Composer home directory. This The `COMPOSER_HOME` var allows you to change the Composer home directory. This
@ -1237,6 +1244,11 @@ defaults to 12 and must be between 1 and 50. If your proxy has issues with
concurrency maybe you want to lower this. Increasing it should generally not result concurrency maybe you want to lower this. Increasing it should generally not result
in performance gains. in performance gains.
### COMPOSER_IPRESOLVE
Set to `4` or `6` to force IPv4 or IPv6 DNS resolution. This only works when the
curl extension is used for downloads.
### HTTP_PROXY_REQUEST_FULLURI ### HTTP_PROXY_REQUEST_FULLURI
If you use a proxy, but it does not support the request_fulluri flag, then you If you use a proxy, but it does not support the request_fulluri flag, then you

View File

@ -738,7 +738,7 @@ monolithic repository.
"repositories": [ "repositories": [
{ {
"type": "path", "type": "path",
"url": "../../packages/my-package", "url": "../../packages/*",
"options": { "options": {
"symlink": false "symlink": false
} }
@ -772,7 +772,7 @@ The following modes exist:
"repositories": [ "repositories": [
{ {
"type": "path", "type": "path",
"url": "../../packages/my-package", "url": "../../packages/*",
"options": { "options": {
"reference": "config" "reference": "config"
} }

View File

@ -61,7 +61,7 @@ autoloader are considered the application "runtime".
Starting with version 2.0, Composer makes [additional features](../07-runtime.md) (besides registering the class autoloader) available to the application runtime environment. Starting with version 2.0, Composer makes [additional features](../07-runtime.md) (besides registering the class autoloader) available to the application runtime environment.
Similar to `composer-plugin-api`, not every Composer release adds new runtime features, Similar to `composer-plugin-api`, not every Composer release adds new runtime features,
thus the version of `composer-runtimeapi` is also increased independently from Composer's version. thus the version of `composer-runtime-api` is also increased independently from Composer's version.
## Composer package `composer` ## Composer package `composer`

View File

@ -304,6 +304,11 @@ open stream: Operation timed out
We recommend you fix your IPv6 setup. If that is not possible, you can try the We recommend you fix your IPv6 setup. If that is not possible, you can try the
following workarounds: following workarounds:
**Generic Workaround:**
Set the [`COMPOSER_IPRESOLVE=4`](../03-cli.md#composer-ipresolve) environment variable which will force curl to resolve
domains using IPv4. This only works when the curl extension is used for downloads.
**Workaround Linux:** **Workaround Linux:**
On linux, it seems that running this command helps to make ipv4 traffic have a On linux, it seems that running this command helps to make ipv4 traffic have a

View File

@ -55,6 +55,11 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Config/JsonConfigSource.php path: ../src/Composer/Config/JsonConfigSource.php
-
message: "#^Call to function method_exists\\(\\) with \\$this\\(Composer\\\\Console\\\\Application\\) and 'setCatchErrors' will always evaluate to true\\.$#"
count: 2
path: ../src/Composer/Console/Application.php
- -
message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(string, string\\)\\: int, 'version_compare' given\\.$#" message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(string, string\\)\\: int, 'version_compare' given\\.$#"
count: 2 count: 2
@ -85,6 +90,11 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Downloader/GzipDownloader.php path: ../src/Composer/Downloader/GzipDownloader.php
-
message: "#^Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#"
count: 1
path: ../src/Composer/EventDispatcher/EventDispatcher.php
- -
message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#" message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#"
count: 1 count: 1
@ -270,11 +280,21 @@ parameters:
count: 2 count: 2
path: ../tests/Composer/Test/ConfigTest.php path: ../tests/Composer/Test/ConfigTest.php
-
message: "#^Call to function method_exists\\(\\) with Composer\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#"
count: 1
path: ../tests/Composer/Test/DocumentationTest.php
- -
message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{Composer\\\\Repository\\\\CompositeRepository, string\\} given\\.$#" message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{Composer\\\\Repository\\\\CompositeRepository, string\\} given\\.$#"
count: 1 count: 1
path: ../tests/Composer/Test/Repository/CompositeRepositoryTest.php path: ../tests/Composer/Test/Repository/CompositeRepositoryTest.php
-
message: "#^Call to function method_exists\\(\\) with Composer\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#"
count: 1
path: ../tests/Composer/Test/TestCase.php
- -
message: "#^Parameter \\#1 \\$object of method ReflectionProperty\\:\\:getValue\\(\\) expects object\\|null, object\\|string given\\.$#" message: "#^Parameter \\#1 \\$object of method ReflectionProperty\\:\\:getValue\\(\\) expects object\\|null, object\\|string given\\.$#"
count: 1 count: 1

View File

@ -765,16 +765,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Command/ShowCommand.php path: ../src/Composer/Command/ShowCommand.php
-
message: "#^Only booleans are allowed in \\|\\|, array\\<string, Composer\\\\Package\\\\Link\\> given on the left side\\.$#"
count: 1
path: ../src/Composer/Command/ShowCommand.php
-
message: "#^Only booleans are allowed in \\|\\|, array\\<string, Composer\\\\Package\\\\Link\\> given on the right side\\.$#"
count: 1
path: ../src/Composer/Command/ShowCommand.php
- -
message: "#^Parameter \\#1 \\$arrayTree of method Composer\\\\Command\\\\ShowCommand\\:\\:displayPackageTree\\(\\) expects array\\<int, array\\<string, array\\|string\\>\\>, array\\<int, array\\<string, array\\<int, array\\<string, array\\|string\\>\\>\\|string\\|null\\>\\> given\\.$#" message: "#^Parameter \\#1 \\$arrayTree of method Composer\\\\Command\\\\ShowCommand\\:\\:displayPackageTree\\(\\) expects array\\<int, array\\<string, array\\|string\\>\\>, array\\<int, array\\<string, array\\<int, array\\<string, array\\|string\\>\\>\\|string\\|null\\>\\> given\\.$#"
count: 2 count: 2
@ -4314,7 +4304,7 @@ parameters:
path: ../src/Composer/Util/Http/CurlDownloader.php path: ../src/Composer/Util/Http/CurlDownloader.php
- -
message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#" message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\<array\\{url\\: non\\-empty\\-string, origin\\: string, attributes\\: array\\{retryAuthFailure\\: bool, redirects\\: int\\<0, max\\>, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#"
count: 1 count: 1
path: ../src/Composer/Util/Http/CurlDownloader.php path: ../src/Composer/Util/Http/CurlDownloader.php

View File

@ -247,6 +247,7 @@ class Auditor
foreach ($packageAdvisories as $advisory) { foreach ($packageAdvisories as $advisory) {
$headers = [ $headers = [
'Package', 'Package',
'Severity',
'CVE', 'CVE',
'Title', 'Title',
'URL', 'URL',
@ -255,6 +256,7 @@ class Auditor
]; ];
$row = [ $row = [
$advisory->packageName, $advisory->packageName,
$this->getSeverity($advisory),
$this->getCVE($advisory), $this->getCVE($advisory),
$advisory->title, $advisory->title,
$this->getURL($advisory), $this->getURL($advisory),
@ -289,6 +291,7 @@ class Auditor
$error[] = '--------'; $error[] = '--------';
} }
$error[] = "Package: ".$advisory->packageName; $error[] = "Package: ".$advisory->packageName;
$error[] = "Severity: ".$this->getSeverity($advisory);
$error[] = "CVE: ".$this->getCVE($advisory); $error[] = "CVE: ".$this->getCVE($advisory);
$error[] = "Title: ".OutputFormatter::escape($advisory->title); $error[] = "Title: ".OutputFormatter::escape($advisory->title);
$error[] = "URL: ".$this->getURL($advisory); $error[] = "URL: ".$this->getURL($advisory);
@ -350,6 +353,15 @@ class Auditor
return $packageUrl !== null ? '<href=' . OutputFormatter::escape($packageUrl) . '>' . $package->getPrettyName() . '</>' : $package->getPrettyName(); return $packageUrl !== null ? '<href=' . OutputFormatter::escape($packageUrl) . '>' . $package->getPrettyName() . '</>' : $package->getPrettyName();
} }
private function getSeverity(SecurityAdvisory $advisory): string
{
if ($advisory->severity === null) {
return '';
}
return $advisory->severity;
}
private function getCVE(SecurityAdvisory $advisory): string private function getCVE(SecurityAdvisory $advisory): string
{ {
if ($advisory->cve === null) { if ($advisory->cve === null) {

View File

@ -26,9 +26,9 @@ class IgnoredSecurityAdvisory extends SecurityAdvisory
/** /**
* @param non-empty-array<array{name: string, remoteId: string}> $sources * @param non-empty-array<array{name: string, remoteId: string}> $sources
*/ */
public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $ignoreReason = null) public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $ignoreReason = null, ?string $severity = null)
{ {
parent::__construct($packageName, $advisoryId, $affectedVersions, $title, $sources, $reportedAt, $cve, $link); parent::__construct($packageName, $advisoryId, $affectedVersions, $title, $sources, $reportedAt, $cve, $link, $severity);
$this->ignoreReason = $ignoreReason; $this->ignoreReason = $ignoreReason;
} }

View File

@ -44,7 +44,7 @@ class PartialSecurityAdvisory implements JsonSerializable
{ {
$constraint = $parser->parseConstraints($data['affectedVersions']); $constraint = $parser->parseConstraints($data['affectedVersions']);
if (isset($data['title'], $data['sources'], $data['reportedAt'])) { if (isset($data['title'], $data['sources'], $data['reportedAt'])) {
return new SecurityAdvisory($packageName, $data['advisoryId'], $constraint, $data['title'], $data['sources'], new \DateTimeImmutable($data['reportedAt'], new \DateTimeZone('UTC')), $data['cve'] ?? null, $data['link'] ?? null); return new SecurityAdvisory($packageName, $data['advisoryId'], $constraint, $data['title'], $data['sources'], new \DateTimeImmutable($data['reportedAt'], new \DateTimeZone('UTC')), $data['cve'] ?? null, $data['link'] ?? null, $data['severity'] ?? null);
} }
return new self($packageName, $data['advisoryId'], $constraint); return new self($packageName, $data['advisoryId'], $constraint);

View File

@ -47,10 +47,16 @@ class SecurityAdvisory extends PartialSecurityAdvisory
*/ */
public $sources; public $sources;
/**
* @var string|null
* @readonly
*/
public $severity;
/** /**
* @param non-empty-array<array{name: string, remoteId: string}> $sources * @param non-empty-array<array{name: string, remoteId: string}> $sources
*/ */
public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null) public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $severity = null)
{ {
parent::__construct($packageName, $advisoryId, $affectedVersions); parent::__construct($packageName, $advisoryId, $affectedVersions);
@ -59,6 +65,7 @@ class SecurityAdvisory extends PartialSecurityAdvisory
$this->reportedAt = $reportedAt; $this->reportedAt = $reportedAt;
$this->cve = $cve; $this->cve = $cve;
$this->link = $link; $this->link = $link;
$this->severity = $severity;
} }
/** /**
@ -75,7 +82,8 @@ class SecurityAdvisory extends PartialSecurityAdvisory
$this->reportedAt, $this->reportedAt,
$this->cve, $this->cve,
$this->link, $this->link,
$ignoreReason $ignoreReason,
$this->severity
); );
} }

View File

@ -12,14 +12,23 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Advisory\Auditor;
use Composer\Composer; use Composer\Composer;
use Composer\Factory; use Composer\Factory;
use Composer\Config; use Composer\Config;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\IO\BufferIO;
use Composer\Json\JsonFile;
use Composer\Package\RootPackage;
use Composer\Package\Version\VersionParser;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Repository\ComposerRepository;
use Composer\Repository\FilesystemRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Repository\RepositorySet;
use Composer\Repository\RootPackageRepository;
use Composer\Util\ConfigValidator; use Composer\Util\ConfigValidator;
use Composer\Util\Git; use Composer\Util\Git;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
@ -153,10 +162,13 @@ EOT
$io->write('Checking pubkeys: ', false); $io->write('Checking pubkeys: ', false);
$this->outputResult($this->checkPubKeys($config)); $this->outputResult($this->checkPubKeys($config));
$io->write('Checking composer version: ', false); $io->write('Checking Composer version: ', false);
$this->outputResult($this->checkVersion($config)); $this->outputResult($this->checkVersion($config));
} }
$io->write('Checking Composer and its dependencies for vulnerabilities: ', false);
$this->outputResult($this->checkComposerAudit($config));
$io->write(sprintf('Composer version: <comment>%s</comment>', Composer::getVersion())); $io->write(sprintf('Composer version: <comment>%s</comment>', Composer::getVersion()));
$platformOverrides = $config->get('platform') ?: []; $platformOverrides = $config->get('platform') ?: [];
@ -438,6 +450,48 @@ EOT
return true; return true;
} }
/**
* @return string|true
*/
private function checkComposerAudit(Config $config)
{
$result = $this->checkConnectivityAndComposerNetworkHttpEnablement();
if ($result !== true) {
return $result;
}
$auditor = new Auditor();
$repoSet = new RepositorySet();
$installedJson = new JsonFile(__DIR__ . '/../../../vendor/composer/installed.json');
if (!$installedJson->exists()) {
return '<warning>Could not find Composer\'s installed.json, this must be a non-standard Composer installation.</>';
}
$localRepo = new FilesystemRepository($installedJson);
$version = Composer::getVersion();
$packages = $localRepo->getCanonicalPackages();
if ($version !== '@package_version@') {
$versionParser = new VersionParser();
$normalizedVersion = $versionParser->normalize($version);
$rootPkg = new RootPackage('composer/composer', $normalizedVersion, $version);
$packages[] = $rootPkg;
}
$repoSet->addRepository(new ComposerRepository(['type' => 'composer', 'url' => 'https://packagist.org'], new NullIO(), $config, $this->httpDownloader));
try {
$io = new BufferIO();
$result = $auditor->audit($io, $repoSet, $packages, Auditor::FORMAT_TABLE, true, [], Auditor::ABANDONED_IGNORE);
} catch (\Throwable $e) {
return '<warning>Failed performing audit: '.$e->getMessage().'</>';
}
if ($result > 0) {
return '<error>Audit found some issues:</>' . PHP_EOL . $io->getOutput();
}
return true;
}
private function getCurlVersion(): string private function getCurlVersion(): string
{ {
if (extension_loaded('curl')) { if (extension_loaded('curl')) {
@ -499,7 +553,7 @@ EOT
if ($result) { if ($result) {
foreach ($result as $message) { foreach ($result as $message) {
$io->write($message); $io->write(trim($message));
} }
} }
} }
@ -722,6 +776,11 @@ EOT
$out($iniMessage, 'comment'); $out($iniMessage, 'comment');
} }
if (in_array(Platform::getEnv('COMPOSER_IPRESOLVE'), ['4', '6'], true)) {
$warnings['ipresolve'] = true;
$out('The COMPOSER_IPRESOLVE env var is set to ' . Platform::getEnv('COMPOSER_IPRESOLVE') .' which may result in network failures below.', 'comment');
}
return count($warnings) === 0 && count($errors) === 0 ? true : $output; return count($warnings) === 0 && count($errors) === 0 ? true : $output;
} }

View File

@ -40,6 +40,7 @@ class OutdatedCommand extends BaseCommand
new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates.'), new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates.'),
new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates.'),
new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates.'), new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates.'),
new InputOption('sort-by-age', 'A', InputOption::VALUE_NONE, 'Displays the installed version\'s age, and sorts packages oldest first.'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage(false)), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage(false)),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'),
@ -97,6 +98,9 @@ EOT
if ($input->getOption('no-dev')) { if ($input->getOption('no-dev')) {
$args['--no-dev'] = true; $args['--no-dev'] = true;
} }
if ($input->getOption('sort-by-age')) {
$args['--sort-by-age'] = true;
}
$args['--ignore-platform-req'] = $input->getOption('ignore-platform-req'); $args['--ignore-platform-req'] = $input->getOption('ignore-platform-req');
if ($input->getOption('ignore-platform-reqs')) { if ($input->getOption('ignore-platform-reqs')) {
$args['--ignore-platform-reqs'] = true; $args['--ignore-platform-reqs'] = true;

View File

@ -349,6 +349,10 @@ EOT
} }
throw $e; throw $e;
} finally { } finally {
if ($input->getOption('dry-run') && $this->newlyCreated) {
@unlink($this->json->getPath());
}
$signalHandler->unregister(); $signalHandler->unregister();
} }
} }

View File

@ -156,6 +156,10 @@ EOT
return $this->rollback($output, $rollbackDir, $localFilename); return $this->rollback($output, $rollbackDir, $localFilename);
} }
if ($input->getArgument('command') === 'self' && $input->getArgument('version') === 'update') {
$input->setArgument('version', null);
}
$latest = $versionsUtil->getLatest(); $latest = $versionsUtil->getLatest();
$latestStable = $versionsUtil->getLatest('stable'); $latestStable = $versionsUtil->getLatest('stable');
try { try {

View File

@ -27,6 +27,7 @@ use Composer\Package\Version\VersionSelector;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Repository\ArrayRepository;
use Composer\Repository\InstalledArrayRepository; use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\ComposerRepository; use Composer\Repository\ComposerRepository;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
@ -98,6 +99,7 @@ class ShowCommand extends BaseCommand
new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.'), new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.'),
new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.'),
new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.'), new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.'),
new InputOption('sort-by-age', 'A', InputOption::VALUE_NONE, 'Displays the installed version\'s age, and sorts packages oldest first. Use with the --latest or --outdated option.'),
new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'),
new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
@ -141,7 +143,7 @@ EOT
$composer = $this->tryComposer(); $composer = $this->tryComposer();
$io = $this->getIO(); $io = $this->getIO();
if ($input->getOption('installed')) { if ($input->getOption('installed') && !$input->getOption('self')) {
$io->writeError('<warning>You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>'); $io->writeError('<warning>You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>');
} }
@ -198,7 +200,7 @@ EOT
$platformRepo = new PlatformRepository([], $platformOverrides); $platformRepo = new PlatformRepository([], $platformOverrides);
$lockedRepo = null; $lockedRepo = null;
if ($input->getOption('self')) { if ($input->getOption('self') && !$input->getOption('installed') && !$input->getOption('locked')) {
$package = clone $this->requireComposer()->getPackage(); $package = clone $this->requireComposer()->getPackage();
if ($input->getOption('name-only')) { if ($input->getOption('name-only')) {
$io->write($package->getName()); $io->write($package->getName());
@ -242,6 +244,9 @@ EOT
} }
$locker = $composer->getLocker(); $locker = $composer->getLocker();
$lockedRepo = $locker->getLockedRepository(!$input->getOption('no-dev')); $lockedRepo = $locker->getLockedRepository(!$input->getOption('no-dev'));
if ($input->getOption('self')) {
$lockedRepo->addPackage(clone $composer->getPackage());
}
$repos = $installedRepo = new InstalledRepository([$lockedRepo]); $repos = $installedRepo = new InstalledRepository([$lockedRepo]);
} else { } else {
// --installed / default case // --installed / default case
@ -249,17 +254,30 @@ EOT
$composer = $this->requireComposer(); $composer = $this->requireComposer();
} }
$rootPkg = $composer->getPackage(); $rootPkg = $composer->getPackage();
$repos = $installedRepo = new InstalledRepository([$composer->getRepositoryManager()->getLocalRepository()]);
$rootRepo = new InstalledArrayRepository();
if ($input->getOption('self')) {
$rootRepo = new RootPackageRepository(clone $rootPkg);
}
if ($input->getOption('no-dev')) { if ($input->getOption('no-dev')) {
$packages = RepositoryUtils::filterRequiredPackages($installedRepo->getPackages(), $rootPkg); $packages = RepositoryUtils::filterRequiredPackages($composer->getRepositoryManager()->getLocalRepository()->getPackages(), $rootPkg);
$repos = $installedRepo = new InstalledRepository([new InstalledArrayRepository(array_map(static function ($pkg): PackageInterface { $repos = $installedRepo = new InstalledRepository([$rootRepo, new InstalledArrayRepository(array_map(static function ($pkg): PackageInterface {
return clone $pkg; return clone $pkg;
}, $packages))]); }, $packages))]);
} else {
$repos = $installedRepo = new InstalledRepository([$rootRepo, $composer->getRepositoryManager()->getLocalRepository()]);
} }
if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) { if (!$installedRepo->getPackages()) {
$io->writeError('<warning>No dependencies installed. Try running composer install or update.</warning>'); $hasNonPlatformReqs = static function (array $reqs): bool {
return (bool) array_filter(array_keys($reqs), function (string $name) {
return !PlatformRepository::isPlatformPackage($name);
});
};
if ($hasNonPlatformReqs($rootPkg->getRequires()) || $hasNonPlatformReqs($rootPkg->getDevRequires())) {
$io->writeError('<warning>No dependencies installed. Try running composer install or update.</warning>');
}
} }
} }
@ -281,6 +299,12 @@ EOT
} elseif (null !== $packageFilter && !str_contains($packageFilter, '*')) { } elseif (null !== $packageFilter && !str_contains($packageFilter, '*')) {
[$package, $versions] = $this->getPackage($installedRepo, $repos, $packageFilter, $input->getArgument('version')); [$package, $versions] = $this->getPackage($installedRepo, $repos, $packageFilter, $input->getArgument('version'));
if (isset($package) && $input->getOption('direct')) {
if (!in_array($package->getName(), $this->getRootRequires(), true)) {
throw new \InvalidArgumentException('Package "' . $package->getName() . '" is installed but not a direct dependent of the root package.');
}
}
if (!isset($package)) { if (!isset($package)) {
$options = $input->getOptions(); $options = $input->getOptions();
$hint = ''; $hint = '';
@ -293,7 +317,7 @@ EOT
if (PlatformRepository::isPlatformPackage($packageFilter) && !$input->getOption('platform')) { if (PlatformRepository::isPlatformPackage($packageFilter) && !$input->getOption('platform')) {
$hint .= ', try using --platform (-p) to show platform packages'; $hint .= ', try using --platform (-p) to show platform packages';
} }
if (!$input->getOption('all')) { if (!$input->getOption('all') && !$input->getOption('available')) {
$hint .= ', try using --available (-a) to show all available packages'; $hint .= ', try using --available (-a) to show all available packages';
} }
@ -450,7 +474,7 @@ EOT
if (isset($packages[$type])) { if (isset($packages[$type])) {
ksort($packages[$type]); ksort($packages[$type]);
$nameLength = $versionLength = $latestLength = 0; $nameLength = $versionLength = $latestLength = $releaseDateLength = 0;
if ($showLatest && $showVersion) { if ($showLatest && $showVersion) {
foreach ($packages[$type] as $package) { foreach ($packages[$type] as $package) {
@ -469,9 +493,20 @@ EOT
$writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion; $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion;
$writeLatest = $writeVersion && $showLatest; $writeLatest = $writeVersion && $showLatest;
$writeDescription = !$input->getOption('name-only') && !$input->getOption('path'); $writeDescription = !$input->getOption('name-only') && !$input->getOption('path');
$writeReleaseDate = $writeLatest && $input->getOption('sort-by-age');
$hasOutdatedPackages = false; $hasOutdatedPackages = false;
if ($input->getOption('sort-by-age')) {
usort($packages[$type], function ($a, $b) {
if (is_object($a) && is_object($b)) {
return $a->getReleaseDate() <=> $b->getReleaseDate();
}
return 0;
});
}
$viewData[$type] = []; $viewData[$type] = [];
foreach ($packages[$type] as $package) { foreach ($packages[$type] as $package) {
$packageViewData = []; $packageViewData = [];
@ -505,6 +540,17 @@ EOT
$packageViewData['version'] = $package->getFullPrettyVersion(); $packageViewData['version'] = $package->getFullPrettyVersion();
$versionLength = max($versionLength, strlen($package->getFullPrettyVersion())); $versionLength = max($versionLength, strlen($package->getFullPrettyVersion()));
} }
if ($writeReleaseDate) {
if ($package->getReleaseDate() !== null) {
$packageViewData['release-age'] = str_replace(' ago', ' old', $this->getRelativeTime($package->getReleaseDate()));
if (!str_contains($packageViewData['release-age'], ' old')) {
$packageViewData['release-age'] = 'from '.$packageViewData['release-age'];
}
$releaseDateLength = max($releaseDateLength, strlen($packageViewData['release-age']));
} else {
$packageViewData['release-age'] = '';
}
}
if ($writeLatest && $latestPackage) { if ($writeLatest && $latestPackage) {
$packageViewData['latest'] = $latestPackage->getFullPrettyVersion(); $packageViewData['latest'] = $latestPackage->getFullPrettyVersion();
$packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package); $packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package);
@ -552,7 +598,9 @@ EOT
'nameLength' => $nameLength, 'nameLength' => $nameLength,
'versionLength' => $versionLength, 'versionLength' => $versionLength,
'latestLength' => $latestLength, 'latestLength' => $latestLength,
'releaseDateLength' => $releaseDateLength,
'writeLatest' => $writeLatest, 'writeLatest' => $writeLatest,
'writeReleaseDate' => $writeReleaseDate,
]; ];
if ($input->getOption('strict') && $hasOutdatedPackages) { if ($input->getOption('strict') && $hasOutdatedPackages) {
$exitCode = 1; $exitCode = 1;
@ -588,11 +636,14 @@ EOT
$nameLength = $viewMetaData[$type]['nameLength']; $nameLength = $viewMetaData[$type]['nameLength'];
$versionLength = $viewMetaData[$type]['versionLength']; $versionLength = $viewMetaData[$type]['versionLength'];
$latestLength = $viewMetaData[$type]['latestLength']; $latestLength = $viewMetaData[$type]['latestLength'];
$releaseDateLength = $viewMetaData[$type]['releaseDateLength'];
$writeLatest = $viewMetaData[$type]['writeLatest']; $writeLatest = $viewMetaData[$type]['writeLatest'];
$writeReleaseDate = $viewMetaData[$type]['writeReleaseDate'];
$versionFits = $nameLength + $versionLength + 3 <= $width; $versionFits = $nameLength + $versionLength + 3 <= $width;
$latestFits = $nameLength + $versionLength + $latestLength + 3 <= $width; $latestFits = $nameLength + $versionLength + $latestLength + 3 <= $width;
$descriptionFits = $nameLength + $versionLength + $latestLength + 24 <= $width; $releaseDateFits = $nameLength + $versionLength + $latestLength + $releaseDateLength + 3 <= $width;
$descriptionFits = $nameLength + $versionLength + $latestLength + $releaseDateLength + 24 <= $width;
if ($latestFits && !$io->isDecorated()) { if ($latestFits && !$io->isDecorated()) {
$latestLength += 2; $latestLength += 2;
@ -620,14 +671,14 @@ EOT
$io->writeError(''); $io->writeError('');
$io->writeError('<info>Direct dependencies required in composer.json:</>'); $io->writeError('<info>Direct dependencies required in composer.json:</>');
if (\count($directDeps) > 0) { if (\count($directDeps) > 0) {
$this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); $this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength);
} else { } else {
$io->writeError('Everything up to date'); $io->writeError('Everything up to date');
} }
$io->writeError(''); $io->writeError('');
$io->writeError('<info>Transitive dependencies not required in composer.json:</>'); $io->writeError('<info>Transitive dependencies not required in composer.json:</>');
if (\count($transitiveDeps) > 0) { if (\count($transitiveDeps) > 0) {
$this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); $this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength);
} else { } else {
$io->writeError('Everything up to date'); $io->writeError('Everything up to date');
} }
@ -635,7 +686,7 @@ EOT
if ($writeLatest && \count($packages) === 0) { if ($writeLatest && \count($packages) === 0) {
$io->writeError('All your direct dependencies are up to date'); $io->writeError('All your direct dependencies are up to date');
} else { } else {
$this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength); $this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength);
} }
} }
@ -651,11 +702,12 @@ EOT
/** /**
* @param array<array{name: string, direct-dependency?: bool, version?: string, latest?: string, latest-status?: string, description?: string|null, path?: string|null, source?: string|null, homepage?: string|null, warning?: string, abandoned?: bool|string}> $packages * @param array<array{name: string, direct-dependency?: bool, version?: string, latest?: string, latest-status?: string, description?: string|null, path?: string|null, source?: string|null, homepage?: string|null, warning?: string, abandoned?: bool|string}> $packages
*/ */
private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength): void private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength, bool $writeReleaseDate, int $releaseDateLength): void
{ {
$padName = $writeVersion || $writeLatest || $writeDescription; $padName = $writeVersion || $writeLatest || $writeReleaseDate || $writeDescription;
$padVersion = $writeLatest || $writeDescription; $padVersion = $writeLatest || $writeReleaseDate || $writeDescription;
$padLatest = $writeDescription; $padLatest = $writeDescription || $writeReleaseDate;
$padReleaseDate = $writeDescription;
foreach ($packages as $package) { foreach ($packages as $package) {
$link = $package['source'] ?? $package['homepage'] ?? ''; $link = $package['source'] ?? $package['homepage'] ?? '';
if ($link !== '') { if ($link !== '') {
@ -674,10 +726,13 @@ EOT
$latestVersion = str_replace(['up-to-date', 'semver-safe-update', 'update-possible'], ['=', '!', '~'], $updateStatus) . ' ' . $latestVersion; $latestVersion = str_replace(['up-to-date', 'semver-safe-update', 'update-possible'], ['=', '!', '~'], $updateStatus) . ' ' . $latestVersion;
} }
$io->write(' <' . $style . '>' . str_pad($latestVersion, ($padLatest ? $latestLength : 0), ' ') . '</' . $style . '>', false); $io->write(' <' . $style . '>' . str_pad($latestVersion, ($padLatest ? $latestLength : 0), ' ') . '</' . $style . '>', false);
if ($writeReleaseDate && isset($package['release-age'])) {
$io->write(' '.str_pad($package['release-age'], ($padReleaseDate ? $releaseDateLength : 0), ' '), false);
}
} }
if (isset($package['description']) && $writeDescription) { if (isset($package['description']) && $writeDescription) {
$description = strtok($package['description'], "\r\n"); $description = strtok($package['description'], "\r\n");
$remaining = $width - $nameLength - $versionLength - 4; $remaining = $width - $nameLength - $versionLength - $releaseDateLength - 4;
if ($writeLatest) { if ($writeLatest) {
$remaining -= $latestLength; $remaining -= $latestLength;
} }
@ -806,14 +861,20 @@ EOT
*/ */
protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, ?PackageInterface $latestPackage = null): void protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, ?PackageInterface $latestPackage = null): void
{ {
$isInstalledPackage = !PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package);
$io = $this->getIO(); $io = $this->getIO();
$io->write('<info>name</info> : ' . $package->getPrettyName()); $io->write('<info>name</info> : ' . $package->getPrettyName());
$io->write('<info>descrip.</info> : ' . $package->getDescription()); $io->write('<info>descrip.</info> : ' . $package->getDescription());
$io->write('<info>keywords</info> : ' . implode(', ', $package->getKeywords() ?: [])); $io->write('<info>keywords</info> : ' . implode(', ', $package->getKeywords() ?: []));
$this->printVersions($package, $versions, $installedRepo); $this->printVersions($package, $versions, $installedRepo);
if ($isInstalledPackage && $package->getReleaseDate() !== null) {
$io->write('<info>released</info> : ' . $package->getReleaseDate()->format('Y-m-d') . ', ' . $this->getRelativeTime($package->getReleaseDate()));
}
if ($latestPackage) { if ($latestPackage) {
$style = $this->getVersionStyle($latestPackage, $package); $style = $this->getVersionStyle($latestPackage, $package);
$io->write('<info>latest</info> : <'.$style.'>' . $latestPackage->getPrettyVersion() . '</'.$style.'>'); $releasedTime = $latestPackage->getReleaseDate() === null ? '' : ' released ' . $latestPackage->getReleaseDate()->format('Y-m-d') . ', ' . $this->getRelativeTime($latestPackage->getReleaseDate());
$io->write('<info>latest</info> : <'.$style.'>' . $latestPackage->getPrettyVersion() . '</'.$style.'>' . $releasedTime);
} else { } else {
$latestPackage = $package; $latestPackage = $package;
} }
@ -822,7 +883,7 @@ EOT
$io->write('<info>homepage</info> : ' . $package->getHomepage()); $io->write('<info>homepage</info> : ' . $package->getHomepage());
$io->write('<info>source</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference())); $io->write('<info>source</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference()));
$io->write('<info>dist</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); $io->write('<info>dist</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference()));
if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) { if ($isInstalledPackage) {
$path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package);
if (is_string($path)) { if (is_string($path)) {
$io->write('<info>path</info> : ' . realpath($path)); $io->write('<info>path</info> : ' . realpath($path));
@ -993,6 +1054,10 @@ EOT
} else { } else {
$json['path'] = null; $json['path'] = null;
} }
if ($package->getReleaseDate() !== null) {
$json['released'] = $package->getReleaseDate()->format(DATE_ATOM);
}
} }
if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) { if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) {
@ -1447,4 +1512,30 @@ EOT
return $this->repositorySet; return $this->repositorySet;
} }
private function getRelativeTime(\DateTimeInterface $releaseDate): string
{
if ($releaseDate->format('Ymd') === date('Ymd')) {
return 'today';
}
$diff = $releaseDate->diff(new \DateTimeImmutable());
if ($diff->days < 7) {
return 'this week';
}
if ($diff->days < 14) {
return 'last week';
}
if ($diff->m < 1 && $diff->days < 31) {
return floor($diff->days / 7) . ' weeks ago';
}
if ($diff->y < 1) {
return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago';
}
return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago';
}
} }

View File

@ -15,6 +15,7 @@ namespace Composer\DependencyResolver;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
use Composer\Package\Package; use Composer\Package\Package;
use Composer\Pcre\Preg;
/** /**
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
@ -111,6 +112,14 @@ class LockTransaction extends Transaction
if ($package->getName() === $presentPackage->getName() && $package->getVersion() === $presentPackage->getVersion()) { if ($package->getName() === $presentPackage->getName() && $package->getVersion() === $presentPackage->getVersion()) {
if ($presentPackage->getSourceReference() && $presentPackage->getSourceType() === $package->getSourceType()) { if ($presentPackage->getSourceReference() && $presentPackage->getSourceType() === $package->getSourceType()) {
$package->setSourceDistReferences($presentPackage->getSourceReference()); $package->setSourceDistReferences($presentPackage->getSourceReference());
// if the dist url is not one of those handled gracefully by setSourceDistReferences then we should overwrite it with the old one
if ($package->getDistUrl() !== null && !Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) {
$package->setDistUrl($presentPackage->getDistUrl());
}
$package->setDistType($presentPackage->getDistType());
if ($package instanceof Package) {
$package->setDistSha1Checksum($presentPackage->getDistSha1Checksum());
}
} }
if ($presentPackage->getReleaseDate() !== null && $package instanceof Package) { if ($presentPackage->getReleaseDate() !== null && $package instanceof Package) {
$package->setReleaseDate($presentPackage->getReleaseDate()); $package->setReleaseDate($presentPackage->getReleaseDate());

View File

@ -24,7 +24,7 @@ interface DvcsDownloaderInterface
/** /**
* Checks for unpushed changes to a current branch * Checks for unpushed changes to a current branch
* *
* @param PackageInterface $package package directory * @param PackageInterface $package package instance
* @param string $path package directory * @param string $path package directory
* @return string|null changes or null * @return string|null changes or null
*/ */

View File

@ -458,7 +458,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
/** /**
* Process the download url * Process the download url
* *
* @param PackageInterface $package package the url is coming from * @param PackageInterface $package package instance
* @param non-empty-string $url download url * @param non-empty-string $url download url
* @throws \RuntimeException If any problem with the url * @throws \RuntimeException If any problem with the url
* @return non-empty-string url * @return non-empty-string url

View File

@ -24,7 +24,7 @@ interface VcsCapableDownloaderInterface
/** /**
* Gets the VCS Reference for the package at path * Gets the VCS Reference for the package at path
* *
* @param PackageInterface $package package directory * @param PackageInterface $package package instance
* @param string $path package directory * @param string $path package directory
* @return string|null reference or null * @return string|null reference or null
*/ */

View File

@ -20,4 +20,9 @@ final class IgnoreAllPlatformRequirementFilter implements PlatformRequirementFil
{ {
return PlatformRepository::isPlatformPackage($req); return PlatformRepository::isPlatformPackage($req);
} }
public function isUpperBoundIgnored(string $req): bool
{
return $this->isIgnored($req);
}
} }

View File

@ -60,6 +60,15 @@ final class IgnoreListPlatformRequirementFilter implements PlatformRequirementFi
return Preg::isMatch($this->ignoreRegex, $req); return Preg::isMatch($this->ignoreRegex, $req);
} }
public function isUpperBoundIgnored(string $req): bool
{
if (!PlatformRepository::isPlatformPackage($req)) {
return false;
}
return $this->isIgnored($req) || Preg::isMatch($this->ignoreUpperBoundRegex, $req);
}
/** /**
* @param bool $allowUpperBoundOverride For conflicts we do not want the upper bound to be skipped * @param bool $allowUpperBoundOverride For conflicts we do not want the upper bound to be skipped
*/ */

View File

@ -21,4 +21,12 @@ final class IgnoreNothingPlatformRequirementFilter implements PlatformRequiremen
{ {
return false; return false;
} }
/**
* @return false
*/
public function isUpperBoundIgnored(string $req): bool
{
return false;
}
} }

View File

@ -15,4 +15,6 @@ namespace Composer\Filter\PlatformRequirementFilter;
interface PlatformRequirementFilterInterface interface PlatformRequirementFilterInterface
{ {
public function isIgnored(string $req): bool; public function isIgnored(string $req): bool;
public function isUpperBoundIgnored(string $req): bool;
} }

View File

@ -372,22 +372,30 @@ class Installer
} }
} }
$fundingCount = 0; $fundEnv = Platform::getEnv('COMPOSER_FUND');
foreach ($localRepo->getPackages() as $package) { $showFunding = true;
if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) { if (is_numeric($fundEnv)) {
$fundingCount++; $showFunding = intval($fundEnv) !== 0;
}
} }
if ($fundingCount > 0) {
$this->io->writeError([ if ($showFunding) {
sprintf( $fundingCount = 0;
"<info>%d package%s you are using %s looking for funding.</info>", foreach ($localRepo->getPackages() as $package) {
$fundingCount, if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) {
1 === $fundingCount ? '' : 's', $fundingCount++;
1 === $fundingCount ? 'is' : 'are' }
), }
'<info>Use the `composer fund` command to find out more!</info>', if ($fundingCount > 0) {
]); $this->io->writeError([
sprintf(
"<info>%d package%s you are using %s looking for funding.</info>",
$fundingCount,
1 === $fundingCount ? '' : 's',
1 === $fundingCount ? 'is' : 'are'
),
'<info>Use the `composer fund` command to find out more!</info>',
]);
}
} }
if ($this->runScripts) { if ($this->runScripts) {
@ -605,7 +613,14 @@ class Installer
// output op if lock file is enabled, but alias op only in debug verbosity // output op if lock file is enabled, but alias op only in debug verbosity
if ($this->config->get('lock') && (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug())) { if ($this->config->get('lock') && (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug())) {
$this->io->writeError(' - ' . $operation->show(true)); $sourceRepo = '';
if ($this->io->isVeryVerbose() && false === strpos($operation->getOperationType(), 'Alias')) {
$operationPkg = ($operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage());
if ($operationPkg->getRepository() !== null) {
$sourceRepo = ' from ' . $operationPkg->getRepository()->getRepoName();
}
}
$this->io->writeError(' - ' . $operation->show(true) . $sourceRepo);
} }
} }

View File

@ -83,10 +83,10 @@ class VersionBumper
$pattern = '{ $pattern = '{
(?<=,|\ |\||^) # leading separator (?<=,|\ |\||^) # leading separator
(?P<constraint> (?P<constraint>
\^'.$major.'(?:\.\d+)* # e.g. ^2.anything \^v?'.$major.'(?:\.\d+)* # e.g. ^2.anything
| ~'.$major.'(?:\.\d+){0,2} # e.g. ~2 or ~2.2 or ~2.2.2 but no more | ~v?'.$major.'(?:\.\d+){0,2} # e.g. ~2 or ~2.2 or ~2.2.2 but no more
| '.$major.'(?:\.[*x])+ # e.g. 2.* or 2.*.* or 2.x.x.x etc | v?'.$major.'(?:\.[*x])+ # e.g. 2.* or 2.*.* or 2.x.x.x etc
| >=\d(?:\.\d+)* # e.g. >=2 or >=1.2 etc | >=v?\d(?:\.\d+)* # e.g. >=2 or >=1.2 etc
| \* # full wildcard | \* # full wildcard
) )
(?=,|$|\ |\||@) # trailing separator (?=,|$|\ |\||@) # trailing separator

View File

@ -13,6 +13,7 @@
namespace Composer\Package\Version; namespace Composer\Package\Version;
use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter;
use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
@ -130,6 +131,13 @@ class VersionSelector
// constraint satisfied, go to next require // constraint satisfied, go to next require
continue 2; continue 2;
} }
if ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter && $platformRequirementFilter->isUpperBoundIgnored($name)) {
$filteredConstraint = $platformRequirementFilter->filterConstraint($name, $link->getConstraint());
if ($filteredConstraint->matches($providedConstraint)) {
// constraint satisfied with the upper bound ignored, go to next require
continue 2;
}
}
} }
// constraint not satisfied // constraint not satisfied
@ -182,6 +190,7 @@ class VersionSelector
* *
* For example: * For example:
* * 1.2.1 -> ^1.2 * * 1.2.1 -> ^1.2
* * 1.2.1.2 -> ^1.2
* * 1.2 -> ^1.2 * * 1.2 -> ^1.2
* * v3.2.1 -> ^3.2 * * v3.2.1 -> ^3.2
* * 2.0-beta.1 -> ^2.0@beta * * 2.0-beta.1 -> ^2.0@beta
@ -227,7 +236,7 @@ class VersionSelector
$semanticVersionParts = explode('.', $version); $semanticVersionParts = explode('.', $version);
// check to see if we have a semver-looking version // check to see if we have a semver-looking version
if (count($semanticVersionParts) === 4 && Preg::isMatch('{^0\D?}', $semanticVersionParts[3])) { if (count($semanticVersionParts) === 4 && Preg::isMatch('{^\d+\D?}', $semanticVersionParts[3])) {
// remove the last parts (i.e. the patch version number and any extra) // remove the last parts (i.e. the patch version number and any extra)
if ($semanticVersionParts[0] === '0') { if ($semanticVersionParts[0] === '0') {
unset($semanticVersionParts[3]); unset($semanticVersionParts[3]);

View File

@ -109,7 +109,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
private $partialPackagesByName = null; private $partialPackagesByName = null;
/** @var bool */ /** @var bool */
private $displayedWarningAboutNonMatchingPackageIndex = false; private $displayedWarningAboutNonMatchingPackageIndex = false;
/** @var array{metadata: bool, query-all: bool, api-url: string|null}|null */ /** @var array{metadata: bool, api-url: string|null}|null */
private $securityAdvisoryConfig = null; private $securityAdvisoryConfig = null;
/** /**
@ -637,6 +637,15 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$apiUrl = $this->securityAdvisoryConfig['api-url']; $apiUrl = $this->securityAdvisoryConfig['api-url'];
// respect available-package-patterns / available-packages directives from the repo
if ($this->hasAvailablePackageList) {
foreach ($packageConstraintMap as $name => $constraint) {
if (!$this->lazyProvidersRepoContains(strtolower($name))) {
unset($packageConstraintMap[$name]);
}
}
}
$parser = new VersionParser(); $parser = new VersionParser();
/** /**
* @param array<mixed> $data * @param array<mixed> $data
@ -700,8 +709,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$options['http']['content'] = http_build_query(['packages' => array_keys($packageConstraintMap)]); $options['http']['content'] = http_build_query(['packages' => array_keys($packageConstraintMap)]);
$response = $this->httpDownloader->get($apiUrl, $options); $response = $this->httpDownloader->get($apiUrl, $options);
$warned = false;
/** @var string $name */ /** @var string $name */
foreach ($response->decodeJson()['advisories'] as $name => $list) { foreach ($response->decodeJson()['advisories'] as $name => $list) {
if (!isset($packageConstraintMap[$name])) {
if (!$warned) {
$this->io->writeError('<warning>'.$this->getRepoName().' returned names which were not requested in response to the security-advisories API. '.$name.' was not requested but is present in the response. Requested names were: '.implode(', ', array_keys($packageConstraintMap)).'</warning>');
$warned = true;
}
continue;
}
if (count($list) > 0) { if (count($list) > 0) {
$advisories[$name] = array_filter(array_map( $advisories[$name] = array_filter(array_map(
static function ($data) use ($name, $create) { static function ($data) use ($name, $create) {
@ -1248,9 +1265,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
if (isset($data['security-advisories']) && is_array($data['security-advisories'])) { if (isset($data['security-advisories']) && is_array($data['security-advisories'])) {
$this->securityAdvisoryConfig = [ $this->securityAdvisoryConfig = [
'metadata' => $data['security-advisories']['metadata'] ?? false, 'metadata' => $data['security-advisories']['metadata'] ?? false,
'api-url' => $data['security-advisories']['api-url'] ?? null, 'api-url' => isset($data['security-advisories']['api-url']) && is_string($data['security-advisories']['api-url']) ? $this->canonicalizeUrl($data['security-advisories']['api-url']) : null,
'query-all' => $data['security-advisories']['query-all'] ?? false,
]; ];
if ($this->securityAdvisoryConfig['api-url'] === null && !$this->hasAvailablePackageList) {
throw new \UnexpectedValueException('Invalid security advisory configuration on '.$this->getRepoName().': If the repository does not provide a security-advisories.api-url then available-packages or available-package-patterns are required to be provided for performance reason.');
}
} }
} }
@ -1280,12 +1299,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
} }
/** /**
* @param non-empty-string $url * @param string $url
* @return non-empty-string * @return non-empty-string
*/ */
private function canonicalizeUrl(string $url): string private function canonicalizeUrl(string $url): string
{ {
if ('/' === $url[0]) { if (strlen($url) === 0) {
throw new \InvalidArgumentException('Expected a string with a value and not an empty string');
}
if (str_starts_with($url, '/')) {
if (Preg::isMatch('{^[^:]++://[^/]*+}', $this->url, $matches)) { if (Preg::isMatch('{^[^:]++://[^/]*+}', $this->url, $matches)) {
return $matches[0] . $url; return $matches[0] . $url;
} }

View File

@ -43,7 +43,7 @@ class GitLabDriver extends VcsDriver
/** /**
* @var mixed[] Project data returned by GitLab API * @var mixed[] Project data returned by GitLab API
*/ */
private $project; private $project = null;
/** /**
* @var array<string|int, mixed[]> Keeps commits returned by GitLab API as commit id => info * @var array<string|int, mixed[]> Keeps commits returned by GitLab API as commit id => info
@ -381,6 +381,10 @@ class GitLabDriver extends VcsDriver
protected function fetchProject(): void protected function fetchProject(): void
{ {
if (!is_null($this->project)) {
return;
}
// we need to fetch the default branch from the api // we need to fetch the default branch from the api
$resource = $this->getApiUrl(); $resource = $this->getApiUrl();
$this->project = $this->getContents($resource, true)->decodeJson(); $this->project = $this->getContents($resource, true)->decodeJson();
@ -581,6 +585,18 @@ class GitLabDriver extends VcsDriver
return true; return true;
} }
/**
* Gives back the loaded <gitlab-api>/projects/<owner>/<repo> result
*
* @return mixed[]|null
*/
public function getRepoData(): ?array
{
$this->fetchProject();
return $this->project;
}
protected function getNextPage(Response $response): ?string protected function getNextPage(Response $response): ?string
{ {
$header = $response->getHeader('link'); $header = $response->getHeader('link');

View File

@ -28,7 +28,7 @@ use React\Promise\Promise;
* @internal * @internal
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nicolas Grekas <p@tchwork.com> * @author Nicolas Grekas <p@tchwork.com>
* @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool} * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null}
* @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable} * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable}
*/ */
class CurlDownloader class CurlDownloader
@ -143,7 +143,7 @@ class CurlDownloader
/** /**
* @param mixed[] $options * @param mixed[] $options
* *
* @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool} $attributes * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool, ipResolve?: 4|6|null} $attributes
* @param non-empty-string $url * @param non-empty-string $url
* *
* @return int internal job id * @return int internal job id
@ -155,8 +155,15 @@ class CurlDownloader
'redirects' => 0, 'redirects' => 0,
'retries' => 0, 'retries' => 0,
'storeAuth' => false, 'storeAuth' => false,
'ipResolve' => null,
], $attributes); ], $attributes);
if ($attributes['ipResolve'] === null && Platform::getEnv('COMPOSER_IPRESOLVE') === '4') {
$attributes['ipResolve'] = 4;
} elseif ($attributes['ipResolve'] === null && Platform::getEnv('COMPOSER_IPRESOLVE') === '6') {
$attributes['ipResolve'] = 6;
}
$originalOptions = $options; $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 // 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
@ -199,6 +206,12 @@ class CurlDownloader
curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
if ($attributes['ipResolve'] === 4) {
curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
} elseif ($attributes['ipResolve'] === 6) {
curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
}
if (function_exists('curl_share_init')) { if (function_exists('curl_share_init')) {
curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle); curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle);
} }
@ -352,8 +365,12 @@ class CurlDownloader
|| (in_array($errno, [56 /* CURLE_RECV_ERROR */, 35 /* CURLE_SSL_CONNECT_ERROR */], true) && str_contains((string) $error, 'Connection reset by peer')) || (in_array($errno, [56 /* CURLE_RECV_ERROR */, 35 /* CURLE_SSL_CONNECT_ERROR */], true) && str_contains((string) $error, 'Connection reset by peer'))
) && $job['attributes']['retries'] < $this->maxRetries ) && $job['attributes']['retries'] < $this->maxRetries
) { ) {
$attributes = ['retries' => $job['attributes']['retries'] + 1];
if ($errno === 7 && !isset($job['attributes']['ipResolve'])) { // CURLE_COULDNT_CONNECT, retry forcing IPv4 if no IP stack was selected
$attributes['ipResolve'] = 4;
}
$this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG); $this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG);
$this->restartJobWithDelay($job, $job['url'], ['retries' => $job['attributes']['retries'] + 1]); $this->restartJobWithDelay($job, $job['url'], $attributes);
continue; continue;
} }
@ -582,7 +599,7 @@ class CurlDownloader
* @param Job $job * @param Job $job
* @param non-empty-string $url * @param non-empty-string $url
* *
* @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>} $attributes * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>, ipResolve?: 4|6} $attributes
*/ */
private function restartJob(array $job, string $url, array $attributes = []): void private function restartJob(array $job, string $url, array $attributes = []): void
{ {
@ -600,7 +617,7 @@ class CurlDownloader
* @param Job $job * @param Job $job
* @param non-empty-string $url * @param non-empty-string $url
* *
* @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>} $attributes * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>, ipResolve?: 4|6} $attributes
*/ */
private function restartJobWithDelay(array $job, string $url, array $attributes): void private function restartJobWithDelay(array $job, string $url, array $attributes): void
{ {

View File

@ -54,6 +54,7 @@ class AuditorTest extends TestCase
'expected' => 1, 'expected' => 1,
'output' => '<warning>Found 2 security vulnerability advisories affecting 1 package:</warning> 'output' => '<warning>Found 2 security vulnerability advisories affecting 1 package:</warning>
Package: vendor1/package1 Package: vendor1/package1
Severity: high
CVE: CVE3 CVE: CVE3
Title: advisory4 Title: advisory4
URL: https://advisory.example.com/advisory4 URL: https://advisory.example.com/advisory4
@ -61,6 +62,7 @@ Affected versions: >=8,<8.2.2|>=1,<2.5.6
Reported at: 2022-05-25T13:21:00+00:00 Reported at: 2022-05-25T13:21:00+00:00
-------- --------
Package: vendor1/package1 Package: vendor1/package1
Severity: medium
CVE: '.' CVE: '.'
Title: advisory5 Title: advisory5
URL: https://advisory.example.com/advisory5 URL: https://advisory.example.com/advisory5
@ -169,6 +171,7 @@ Found 2 abandoned packages:
[ [
['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendor1/package1'], ['text' => 'Package: vendor1/package1'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE1'], ['text' => 'CVE: CVE1'],
['text' => 'Title: advisory1'], ['text' => 'Title: advisory1'],
['text' => 'URL: https://advisory.example.com/advisory1'], ['text' => 'URL: https://advisory.example.com/advisory1'],
@ -185,6 +188,7 @@ Found 2 abandoned packages:
[ [
['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendor1/package1'], ['text' => 'Package: vendor1/package1'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE1'], ['text' => 'CVE: CVE1'],
['text' => 'Title: advisory1'], ['text' => 'Title: advisory1'],
['text' => 'URL: https://advisory.example.com/advisory1'], ['text' => 'URL: https://advisory.example.com/advisory1'],
@ -202,6 +206,7 @@ Found 2 abandoned packages:
[ [
['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendor1/package2'], ['text' => 'Package: vendor1/package2'],
['text' => 'Severity: medium'],
['text' => 'CVE: '], ['text' => 'CVE: '],
['text' => 'Title: advisory2'], ['text' => 'Title: advisory2'],
['text' => 'URL: https://advisory.example.com/advisory2'], ['text' => 'URL: https://advisory.example.com/advisory2'],
@ -218,6 +223,7 @@ Found 2 abandoned packages:
[ [
['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendorx/packagex'], ['text' => 'Package: vendorx/packagex'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE5'], ['text' => 'CVE: CVE5'],
['text' => 'Title: advisory17'], ['text' => 'Title: advisory17'],
['text' => 'URL: https://advisory.example.com/advisory17'], ['text' => 'URL: https://advisory.example.com/advisory17'],
@ -234,6 +240,7 @@ Found 2 abandoned packages:
[ [
['text' => 'Found 1 security vulnerability advisory affecting 1 package:'], ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendor1/package1'], ['text' => 'Package: vendor1/package1'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE1'], ['text' => 'CVE: CVE1'],
['text' => 'Title: advisory1'], ['text' => 'Title: advisory1'],
['text' => 'URL: https://advisory.example.com/advisory1'], ['text' => 'URL: https://advisory.example.com/advisory1'],
@ -254,6 +261,7 @@ Found 2 abandoned packages:
[ [
['text' => 'Found 3 ignored security vulnerability advisories affecting 2 packages:'], ['text' => 'Found 3 ignored security vulnerability advisories affecting 2 packages:'],
['text' => 'Package: vendor2/package1'], ['text' => 'Package: vendor2/package1'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE2'], ['text' => 'CVE: CVE2'],
['text' => 'Title: advisory3'], ['text' => 'Title: advisory3'],
['text' => 'URL: https://advisory.example.com/advisory3'], ['text' => 'URL: https://advisory.example.com/advisory3'],
@ -262,6 +270,7 @@ Found 2 abandoned packages:
['text' => 'Ignore reason: None specified'], ['text' => 'Ignore reason: None specified'],
['text' => '--------'], ['text' => '--------'],
['text' => 'Package: vendor2/package1'], ['text' => 'Package: vendor2/package1'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE4'], ['text' => 'CVE: CVE4'],
['text' => 'Title: advisory6'], ['text' => 'Title: advisory6'],
['text' => 'URL: https://advisory.example.com/advisory6'], ['text' => 'URL: https://advisory.example.com/advisory6'],
@ -270,6 +279,7 @@ Found 2 abandoned packages:
['text' => 'Ignore reason: None specified'], ['text' => 'Ignore reason: None specified'],
['text' => '--------'], ['text' => '--------'],
['text' => 'Package: vendorx/packagex'], ['text' => 'Package: vendorx/packagex'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE5'], ['text' => 'CVE: CVE5'],
['text' => 'Title: advisory17'], ['text' => 'Title: advisory17'],
['text' => 'URL: https://advisory.example.com/advisory17'], ['text' => 'URL: https://advisory.example.com/advisory17'],
@ -278,6 +288,7 @@ Found 2 abandoned packages:
['text' => 'Ignore reason: None specified'], ['text' => 'Ignore reason: None specified'],
['text' => 'Found 1 security vulnerability advisory affecting 1 package:'], ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendor3/package1'], ['text' => 'Package: vendor3/package1'],
['text' => 'Severity: medium'],
['text' => 'CVE: CVE5'], ['text' => 'CVE: CVE5'],
['text' => 'Title: advisory7'], ['text' => 'Title: advisory7'],
['text' => 'URL: https://advisory.example.com/advisory7'], ['text' => 'URL: https://advisory.example.com/advisory7'],
@ -380,6 +391,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2022-05-25 13:21:00', 'reportedAt' => '2022-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
[ [
'advisoryId' => 'ID4', 'advisoryId' => 'ID4',
@ -396,6 +408,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2022-05-25 13:21:00', 'reportedAt' => '2022-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'high',
], ],
[ [
'advisoryId' => 'ID5', 'advisoryId' => 'ID5',
@ -412,6 +425,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2022-05-25 13:21:00', 'reportedAt' => '2022-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
], ],
'vendor1/package2' => [ 'vendor1/package2' => [
@ -430,6 +444,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2022-05-25 13:21:00', 'reportedAt' => '2022-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
], ],
'vendorx/packagex' => [ 'vendorx/packagex' => [
@ -448,6 +463,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2015-05-25 13:21:00', 'reportedAt' => '2015-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
], ],
'vendor2/package1' => [ 'vendor2/package1' => [
@ -466,6 +482,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2022-05-25 13:21:00', 'reportedAt' => '2022-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
[ [
'advisoryId' => 'ID6', 'advisoryId' => 'ID6',
@ -482,6 +499,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2015-05-25 13:21:00', 'reportedAt' => '2015-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
], ],
'vendory/packagey' => [ 'vendory/packagey' => [
@ -500,6 +518,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2015-05-25 13:21:00', 'reportedAt' => '2015-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
], ],
'vendor3/package1' => [ 'vendor3/package1' => [
@ -518,6 +537,7 @@ Found 2 abandoned packages:
], ],
'reportedAt' => '2015-05-25 13:21:00', 'reportedAt' => '2015-05-25 13:21:00',
'composerRepository' => 'https://packagist.org', 'composerRepository' => 'https://packagist.org',
'severity' => 'medium',
], ],
], ],
]; ];

View File

@ -76,7 +76,7 @@ class BumpCommandTest extends TestCase
yield 'bump all by default' => [ yield 'bump all by default' => [
[ [
'require' => [ 'require' => [
'first/pkg' => '^2.0', 'first/pkg' => '^v2.0',
'second/pkg' => '3.*', 'second/pkg' => '3.*',
], ],
'require-dev' => [ 'require-dev' => [

View File

@ -13,6 +13,7 @@
namespace Composer\Test\Command; namespace Composer\Test\Command;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use RuntimeException;
class ConfigCommandTest extends TestCase class ConfigCommandTest extends TestCase
{ {
@ -139,4 +140,13 @@ class ConfigCommandTest extends TestCase
'{"foo":{"type":"vcs","url":"https://example.org"}}', '{"foo":{"type":"vcs","url":"https://example.org"}}',
]; ];
} }
public function testConfigThrowsForInvalidArgCombination(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('--file and --global can not be combined');
$appTester = $this->getApplicationTester();
$appTester->run(['command' => 'config', '--file' => 'alt.composer.json', '--global' => true]);
}
} }

View File

@ -17,6 +17,8 @@ use Composer\Pcre\Preg;
use Composer\Pcre\Regex; use Composer\Pcre\Regex;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use DateTimeImmutable;
use InvalidArgumentException;
class ShowCommandTest extends TestCase class ShowCommandTest extends TestCase
{ {
@ -28,6 +30,8 @@ class ShowCommandTest extends TestCase
public function testShow(array $command, string $expected, array $requires = []): void public function testShow(array $command, string $expected, array $requires = []): void
{ {
$this->initTempComposer([ $this->initTempComposer([
'name' => 'root/pkg',
'version' => '1.2.3',
'repositories' => [ 'repositories' => [
'packages' => [ 'packages' => [
'type' => 'package', 'type' => 'package',
@ -55,12 +59,19 @@ class ShowCommandTest extends TestCase
$pkg = self::getPackage('vendor/package', '1.0.0'); $pkg = self::getPackage('vendor/package', '1.0.0');
$pkg->setDescription('description of installed package'); $pkg->setDescription('description of installed package');
$major = self::getPackage('outdated/major', '1.0.0');
$major->setReleaseDate(new DateTimeImmutable());
$minor = self::getPackage('outdated/minor', '1.0.0');
$minor->setReleaseDate(new DateTimeImmutable('-2 years'));
$patch = self::getPackage('outdated/patch', '1.0.0');
$patch->setReleaseDate(new DateTimeImmutable('-2 weeks'));
$this->createInstalledJson([ $this->createInstalledJson([$pkg, $major, $minor, $patch]);
$pkg = self::getPackage('vendor/locked', '3.0.0');
$pkg->setDescription('description of locked package');
$this->createComposerLock([
$pkg, $pkg,
self::getPackage('outdated/major', '1.0.0'),
self::getPackage('outdated/minor', '1.0.0'),
self::getPackage('outdated/patch', '1.0.0'),
]); ]);
$appTester = $this->getApplicationTester(); $appTester = $this->getApplicationTester();
@ -78,6 +89,21 @@ outdated/patch 1.0.0
vendor/package 1.0.0 description of installed package', vendor/package 1.0.0 description of installed package',
]; ];
yield 'with -s and --installed shows list of installed + self package' => [
['--installed' => true, '--self' => true],
'outdated/major 1.0.0
outdated/minor 1.0.0
outdated/patch 1.0.0
root/pkg 1.2.3
vendor/package 1.0.0 description of installed package',
];
yield 'with -s and --locked shows list of installed + self package' => [
['--locked' => true, '--self' => true],
'root/pkg 1.2.3
vendor/locked 3.0.0 description of locked package',
];
yield 'with -a show available packages with description but no version' => [ yield 'with -a show available packages with description but no version' => [
['-a' => true], ['-a' => true],
'outdated/major outdated/major v2.0.0 description 'outdated/major outdated/major v2.0.0 description
@ -112,6 +138,21 @@ outdated/minor 1.0.0 <highlight>! 1.1.1</highlight>
outdated/patch 1.0.0 <highlight>! 1.0.1</highlight>', outdated/patch 1.0.0 <highlight>! 1.0.1</highlight>',
]; ];
yield 'outdated deps sorting by age' => [
['command' => 'outdated', '--sort-by-age' => true],
'Legend:
! patch or minor release available - update recommended
~ major release available - update possible
Direct dependencies required in composer.json:
Everything up to date
Transitive dependencies not required in composer.json:
outdated/minor 1.0.0 <highlight>! 1.1.1</highlight> 2 years old
outdated/patch 1.0.0 <highlight>! 1.0.1</highlight> 2 weeks old
outdated/major 1.0.0 ~ 2.0.0 from today',
];
yield 'outdated deps with --direct only show direct deps with updated' => [ yield 'outdated deps with --direct only show direct deps with updated' => [
['command' => 'outdated', '--direct' => true], ['command' => 'outdated', '--direct' => true],
'Legend: 'Legend:
@ -258,6 +299,23 @@ Transitive dependencies not required in composer.json:
vendor/package 1.1.0 <highlight>! 1.2.0</highlight>", trim($appTester->getDisplay(true))); vendor/package 1.1.0 <highlight>! 1.2.0</highlight>", trim($appTester->getDisplay(true)));
} }
public function testShowDirectWithNameOnlyShowsDirectDependents(): void
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('Package "vendor/package" is installed but not a direct dependent of the root package.');
$this->initTempComposer([
'repositories' => [],
]);
$this->createInstalledJson([
self::getPackage('vendor/package', '1.0.0'),
]);
$appTester = $this->getApplicationTester();
$appTester->run(['command' => 'show', '--direct' => true, 'package' => 'vendor/package']);
}
public function testShowPlatformOnlyShowsPlatformPackages(): void public function testShowPlatformOnlyShowsPlatformPackages(): void
{ {
$this->initTempComposer([ $this->initTempComposer([
@ -533,7 +591,7 @@ OUTPUT;
public function testSelf(): void public function testSelf(): void
{ {
$this->initTempComposer(['name' => 'vendor/package']); $this->initTempComposer(['name' => 'vendor/package', 'time' => date('Y-m-d')]);
$appTester = $this->getApplicationTester(); $appTester = $this->getApplicationTester();
$appTester->run(['command' => 'show', '--self' => true]); $appTester->run(['command' => 'show', '--self' => true]);
@ -542,6 +600,7 @@ OUTPUT;
'descrip.' => '', 'descrip.' => '',
'keywords' => '', 'keywords' => '',
'versions' => '* 1.0.0+no-version-set', 'versions' => '* 1.0.0+no-version-set',
'released' => date('Y-m-d'). ', today',
'type' => 'library', 'type' => 'library',
'homepage' => '', 'homepage' => '',
'source' => '[] ', 'source' => '[] ',

View File

@ -29,7 +29,7 @@ class UpdateCommandTest extends TestCase
$appTester = $this->getApplicationTester(); $appTester = $this->getApplicationTester();
$appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command)); $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command));
$this->assertSame(trim($expected), trim($appTester->getDisplay(true))); $this->assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true)));
} }
public static function provideUpdates(): \Generator public static function provideUpdates(): \Generator
@ -67,6 +67,29 @@ Package operations: 2 installs, 0 updates, 0 removals
OUTPUT OUTPUT
]; ];
yield 'simple update with very verbose output' => [
$rootDepAndTransitiveDep,
['-vv' => true],
<<<OUTPUT
Loading composer repositories with package information
Pool optimizer completed in %f seconds
Found %d package versions referenced in your dependency graph. %d (%d%%) were optimized away.
Updating dependencies
Dependency resolution completed in %f seconds
Analyzed %d packages to resolve dependencies
Analyzed %d rules to resolve dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
Installs: dep/pkg:1.0.2, root/req:1.0.0
- Locking dep/pkg (1.0.2) from package repo (defining 4 packages)
- Locking root/req (1.0.0) from package repo (defining 4 packages)
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
Installs: dep/pkg:1.0.2, root/req:1.0.0
- Installing dep/pkg (1.0.2)
- Installing root/req (1.0.0)
OUTPUT
];
yield 'update with temporary constraint + --no-install' => [ yield 'update with temporary constraint + --no-install' => [
$rootDepAndTransitiveDep, $rootDepAndTransitiveDep,
['--with' => ['dep/pkg:1.0.0'], '--no-install' => true], ['--with' => ['dep/pkg:1.0.0'], '--no-install' => true],

View File

@ -25,6 +25,7 @@ final class IgnoreAllPlatformRequirementFilterTest extends TestCase
$platformRequirementFilter = new IgnoreAllPlatformRequirementFilter(); $platformRequirementFilter = new IgnoreAllPlatformRequirementFilter();
$this->assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req)); $this->assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req));
$this->assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req));
} }
/** /**

View File

@ -48,4 +48,37 @@ final class IgnoreListPlatformRequirementFilterTest extends TestCase
'list entries are not completing each other' => [['ext-', 'foo'], 'ext-foo', false], 'list entries are not completing each other' => [['ext-', 'foo'], 'ext-foo', false],
]; ];
} }
/**
* @dataProvider dataIsUpperBoundIgnored
*
* @param string[] $reqList
*/
public function testIsUpperBoundIgnored(array $reqList, string $req, bool $expectIgnored): void
{
$platformRequirementFilter = new IgnoreListPlatformRequirementFilter($reqList);
$this->assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req));
}
/**
* @return array<string, mixed[]>
*/
public static function dataIsUpperBoundIgnored(): array
{
return [
'ext-json is ignored if listed and fully ignored' => [['ext-json', 'monolog/monolog'], 'ext-json', true],
'ext-json is ignored if listed and upper bound ignored' => [['ext-json+', 'monolog/monolog'], 'ext-json', true],
'php is not ignored if not listed' => [['ext-json+', 'monolog/monolog'], 'php', false],
'monolog/monolog is not ignored even if listed' => [['monolog/monolog'], 'monolog/monolog', false],
'ext-json is ignored if ext-* is listed' => [['ext-*+'], 'ext-json', true],
'php is ignored if php* is listed' => [['ext-*+', 'php*+'], 'php', true],
'ext-json is ignored if * is listed' => [['foo', '*+'], 'ext-json', true],
'php is ignored if * is listed' => [['*+', 'foo'], 'php', true],
'monolog/monolog is not ignored even if * or monolog/* are listed' => [['*+', 'monolog/*+'], 'monolog/monolog', false],
'empty list entry does not ignore' => [[''], 'ext-foo', false],
'empty array does not ignore' => [[], 'ext-foo', false],
'list entries are not completing each other' => [['ext-', 'foo'], 'ext-foo', false],
];
}
} }

View File

@ -25,6 +25,7 @@ final class IgnoreNothingPlatformRequirementFilterTest extends TestCase
$platformRequirementFilter = new IgnoreNothingPlatformRequirementFilter(); $platformRequirementFilter = new IgnoreNothingPlatformRequirementFilter();
$this->assertFalse($platformRequirementFilter->isIgnored($req)); // @phpstan-ignore-line $this->assertFalse($platformRequirementFilter->isIgnored($req)); // @phpstan-ignore-line
$this->assertFalse($platformRequirementFilter->isUpperBoundIgnored($req)); // @phpstan-ignore-line
} }
/** /**

View File

@ -0,0 +1,62 @@
--TEST--
Installs a simple package with exact match requirement
--CONDITION--
putenv('COMPOSER_FUND=1')
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{
"name": "a/a",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }],
"require": {
"d/d": "^1.0"
}
},
{
"name": "b/b",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
},
{
"name": "c/c",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
},
{
"name": "d/d",
"version": "1.0.0",
"require": {
"b/b": "^1.0"
}
}
]
}
],
"require": {
"a/a": "1.0.0"
}
}
--RUN--
install
--EXPECT-OUTPUT--
<warning>No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.</warning>
Loading composer repositories with package information
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!
--EXPECT--
Installing b/b (1.0.0)
Installing d/d (1.0.0)
Installing a/a (1.0.0)

View File

@ -0,0 +1,60 @@
--TEST--
Installs a simple package with exact match requirement
--CONDITION--
putenv('COMPOSER_FUND=0')
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{
"name": "a/a",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }],
"require": {
"d/d": "^1.0"
}
},
{
"name": "b/b",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
},
{
"name": "c/c",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
},
{
"name": "d/d",
"version": "1.0.0",
"require": {
"b/b": "^1.0"
}
}
]
}
],
"require": {
"a/a": "1.0.0"
}
}
--RUN--
install
--EXPECT-OUTPUT--
<warning>No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.</warning>
Loading composer repositories with package information
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
--EXPECT--
Installing b/b (1.0.0)
Installing d/d (1.0.0)
Installing a/a (1.0.0)

View File

@ -59,6 +59,7 @@ class InstallerTest extends TestCase
{ {
parent::tearDown(); parent::tearDown();
Platform::clearEnv('COMPOSER_POOL_OPTIMIZER'); Platform::clearEnv('COMPOSER_POOL_OPTIMIZER');
Platform::clearEnv('COMPOSER_FUND');
chdir($this->prevCwd); chdir($this->prevCwd);
if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) { if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) {

View File

@ -44,6 +44,7 @@ class VersionBumperTest extends TestCase
{ {
// constraint, version, expected recommendation, [branch-alias] // constraint, version, expected recommendation, [branch-alias]
yield 'upgrade caret' => ['^1.0', '1.2.1', '^1.2.1']; yield 'upgrade caret' => ['^1.0', '1.2.1', '^1.2.1'];
yield 'upgrade caret with v' => ['^v1.0', '1.2.1', '^1.2.1'];
yield 'skip trailing .0s' => ['^1.0', '1.0.0', '^1.0']; yield 'skip trailing .0s' => ['^1.0', '1.0.0', '^1.0'];
yield 'skip trailing .0s/2' => ['^1.2', '1.2.0', '^1.2']; yield 'skip trailing .0s/2' => ['^1.2', '1.2.0', '^1.2'];
yield 'preserve major.minor.patch format when installed minor is 0' => ['^1.0.0', '1.2.0', '^1.2.0']; yield 'preserve major.minor.patch format when installed minor is 0' => ['^1.0.0', '1.2.0', '^1.2.0'];
@ -58,6 +59,7 @@ class VersionBumperTest extends TestCase
yield 'dev version does not upgrade' => ['^3.2', 'dev-main', '^3.2']; yield 'dev version does not upgrade' => ['^3.2', 'dev-main', '^3.2'];
yield 'upgrade dev version if aliased' => ['^3.2', 'dev-main', '^3.3', '3.3.x-dev']; yield 'upgrade dev version if aliased' => ['^3.2', 'dev-main', '^3.3', '3.3.x-dev'];
yield 'upgrade major wildcard to caret' => ['2.*', '2.4.0', '^2.4']; yield 'upgrade major wildcard to caret' => ['2.*', '2.4.0', '^2.4'];
yield 'upgrade major wildcard to caret with v' => ['v2.*', '2.4.0', '^2.4'];
yield 'upgrade major wildcard as x to caret' => ['2.x', '2.4.0', '^2.4']; yield 'upgrade major wildcard as x to caret' => ['2.x', '2.4.0', '^2.4'];
yield 'upgrade major wildcard as x to caret/2' => ['2.x.x', '2.4.0', '^2.4.0']; yield 'upgrade major wildcard as x to caret/2' => ['2.x.x', '2.4.0', '^2.4.0'];
yield 'leave minor wildcard alone' => ['2.4.*', '2.4.3', '2.4.*']; yield 'leave minor wildcard alone' => ['2.4.*', '2.4.3', '2.4.*'];
@ -66,6 +68,7 @@ class VersionBumperTest extends TestCase
yield 'update patch-only-tilde alone' => ['~2.2.3', '2.2.6', '~2.2.6']; yield 'update patch-only-tilde alone' => ['~2.2.3', '2.2.6', '~2.2.6'];
yield 'leave extra-only-tilde alone' => ['~2.2.3.1', '2.2.4.5', '~2.2.3.1']; yield 'leave extra-only-tilde alone' => ['~2.2.3.1', '2.2.4.5', '~2.2.3.1'];
yield 'upgrade bigger-or-eq to latest' => ['>=3.0', '3.4.5', '>=3.4.5']; yield 'upgrade bigger-or-eq to latest' => ['>=3.0', '3.4.5', '>=3.4.5'];
yield 'upgrade bigger-or-eq to latest with v' => ['>=v3.0', '3.4.5', '>=3.4.5'];
yield 'leave bigger-than untouched' => ['>2.2.3', '2.2.6', '>2.2.3']; yield 'leave bigger-than untouched' => ['>2.2.3', '2.2.6', '>2.2.3'];
yield 'upgrade full wildcard to bigger-or-eq' => ['*', '1.2.3', '>=1.2.3']; yield 'upgrade full wildcard to bigger-or-eq' => ['*', '1.2.3', '>=1.2.3'];
} }

View File

@ -346,6 +346,9 @@ class VersionSelectorTest extends TestCase
['0.1.3', '^0.1.3'], ['0.1.3', '^0.1.3'],
['0.0.3', '^0.0.3'], ['0.0.3', '^0.0.3'],
['0.0.3-alpha', '^0.0.3@alpha'], ['0.0.3-alpha', '^0.0.3@alpha'],
['0.0.3.4-alpha', '^0.0.3@alpha'],
['3.0.0.2-RC2', '^3.0@RC'],
['1.2.1.1020402', '^1.2'],
// date-based versions are not touched at all // date-based versions are not touched at all
['v20121020', 'v20121020'], ['v20121020', 'v20121020'],
['v20121020.2', 'v20121020.2'], ['v20121020.2', 'v20121020.2'],