diff --git a/doc/03-cli.md b/doc/03-cli.md index 396c24f63..8c1c56e16 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -234,6 +234,7 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.* changes to transitive dependencies. Can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var. * **--interactive:** Interactive interface with autocompletion to select the packages to update. * **--root-reqs:** Restricts the update to your first degree dependencies. +* **--bump-after-update:** Runs `bump` after performing the update. Set to `dev` or `no-dev` to only bump those dependencies. Specifying one of the words `mirrors`, `lock`, or `nothing` as an argument has the same effect as specifying the option `--lock`, for example `composer update mirrors` is exactly the same as `composer update --lock`. diff --git a/doc/06-config.md b/doc/06-config.md index 19a0df714..bff70d03d 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -481,6 +481,12 @@ throw, but you can set this config option to `["example.org"]` to allow using sv URLs on that hostname. This is a better/safer alternative to disabling `secure-http` altogether. +## bump-after-update + +Defaults to `false` and can be any of `true`, `false`, `"dev"` or `"no-dev"`. If +set to true, Composer will run the `bump` command after running the `update` command. +If set to `"dev"` or `"no-dev"` then only the corresponding dependencies will be bumped. + ## allow-missing-requirements Defaults to `false`. Ignores error during `install` if there are any missing diff --git a/res/composer-schema.json b/res/composer-schema.json index 8a3d82b1b..e6f626dcb 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -663,6 +663,10 @@ "type": ["boolean", "string"], "description": "Defaults to \"php-only\" which checks only the PHP version. Setting to true will also check the presence of required PHP extensions. If set to false, Composer will not create and require a platform_check.php file as part of the autoloader bootstrap." }, + "bump-after-update": { + "type": ["string", "boolean"], + "description": "Defaults to false and can be any of true, false, \"dev\"` or \"no-dev\"`. If set to true, Composer will run the bump command after running the update command. If set to \"dev\" or \"no-dev\" then only the corresponding dependencies will be bumped." + }, "allow-missing-requirements": { "type": ["boolean"], "description": "Defaults to false. If set to true, Composer will allow install when lock file is not up to date with the latest changes in composer.json." diff --git a/src/Composer/Command/BumpCommand.php b/src/Composer/Command/BumpCommand.php index db5b9464a..5cd7a0f20 100644 --- a/src/Composer/Command/BumpCommand.php +++ b/src/Composer/Command/BumpCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Locker; @@ -72,9 +73,28 @@ EOT */ protected function execute(InputInterface $input, OutputInterface $output): int { + return $this->doBump( + $this->getIO(), + $input->getOption('dev-only'), + $input->getOption('no-dev-only'), + $input->getOption('dry-run'), + $input->getArgument('packages') + ); + } + + /** + * @param string[] $packagesFilter + * @throws \Seld\JsonLint\ParsingException + */ + public function doBump( + IOInterface $io, + bool $devOnly, + bool $noDevOnly, + bool $dryRun, + array $packagesFilter + ): int { /** @readonly */ $composerJsonPath = Factory::getComposerFile(); - $io = $this->getIO(); if (!Filesystem::isReadable($composerJsonPath)) { $io->writeError(''.$composerJsonPath.' is not readable.'); @@ -112,7 +132,7 @@ EOT $repo = $composer->getRepositoryManager()->getLocalRepository(); } - if ($composer->getPackage()->getType() !== 'project' && !$input->getOption('dev-only')) { + if ($composer->getPackage()->getType() !== 'project' && !$devOnly) { $io->writeError('Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.'); $contents = $composerJson->read(); @@ -125,14 +145,13 @@ EOT $bumper = new VersionBumper(); $tasks = []; - if (!$input->getOption('dev-only')) { + if (!$devOnly) { $tasks['require'] = $composer->getPackage()->getRequires(); } - if (!$input->getOption('no-dev-only')) { + if (!$noDevOnly) { $tasks['require-dev'] = $composer->getPackage()->getDevRequires(); } - $packagesFilter = $input->getArgument('packages'); if (count($packagesFilter) > 0) { $pattern = BasePackage::packageNamesToRegexp(array_unique(array_map('strtolower', $packagesFilter))); foreach ($tasks as $key => $reqs) { @@ -171,8 +190,6 @@ EOT } } - $dryRun = $input->getOption('dry-run'); - if (!$dryRun && !$this->updateFileCleanly($composerJson, $updates)) { $composerDefinition = $composerJson->read(); foreach ($updates as $key => $packages) { diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 02e77df60..6dc487574 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -469,6 +469,18 @@ EOT 'prepend-autoloader' => [$booleanValidator, $booleanNormalizer], 'disable-tls' => [$booleanValidator, $booleanNormalizer], 'secure-http' => [$booleanValidator, $booleanNormalizer], + 'bump-after-update' => [ + static function ($val): bool { + return in_array($val, ['dev', 'no-dev', 'true', 'false', '1', '0'], true); + }, + static function ($val) { + if ('dev' === $val || 'no-dev' === $val) { + return $val; + } + + return $val !== 'false' && (bool) $val; + }, + ], 'cafile' => [ static function ($val): bool { return file_exists($val) && Filesystem::isReadable($val); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 1dee17972..756aa00d9 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -85,6 +85,7 @@ class UpdateCommand extends BaseCommand new InputOption('minimal-changes', 'm', InputOption::VALUE_NONE, 'During a partial update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).'), new InputOption('interactive', 'i', InputOption::VALUE_NONE, 'Interactive interface with autocompletion to select the packages to update.'), new InputOption('root-reqs', null, InputOption::VALUE_NONE, 'Restricts the update to your first degree dependencies.'), + new InputOption('bump-after-update', null, InputOption::VALUE_OPTIONAL, 'Runs bump after performing the update.', false, ['dev', 'no-dev', 'all']), ]) ->setHelp( <<disablePlugins(); } - return $install->run(); + $result = $install->run(); + + if ($result === 0) { + $bumpAfterUpdate = $input->getOption('bump-after-update'); + if (false === $bumpAfterUpdate) { + $bumpAfterUpdate = $composer->getConfig()->get('bump-after-update'); + } + + if (false !== $bumpAfterUpdate) { + $io->writeError('Bumping dependencies'); + $bumpCommand = new BumpCommand(); + $bumpCommand->setComposer($composer); + $result = $bumpCommand->doBump( + $io, + $bumpAfterUpdate === 'dev', + $bumpAfterUpdate === 'no-dev', + $input->getOption('dry-run'), + $input->getArgument('packages') + ); + } + } + return $result; } /** diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 34737c09c..6844eb1e2 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -84,6 +84,7 @@ class Config 'gitlab-token' => [], 'http-basic' => [], 'bearer' => [], + 'bump-after-update' => false, 'allow-missing-requirements' => false, ]; diff --git a/tests/Composer/Test/Command/UpdateCommandTest.php b/tests/Composer/Test/Command/UpdateCommandTest.php index a4d34626e..dd8427d29 100644 --- a/tests/Composer/Test/Command/UpdateCommandTest.php +++ b/tests/Composer/Test/Command/UpdateCommandTest.php @@ -24,10 +24,14 @@ class UpdateCommandTest extends TestCase * @param array $composerJson * @param array $command */ - public function testUpdate(array $composerJson, array $command, string $expected): void + public function testUpdate(array $composerJson, array $command, string $expected, bool $createLock = false): void { $this->initTempComposer($composerJson); + if ($createLock) { + $this->createComposerLock(); + } + $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command)); @@ -126,6 +130,62 @@ The temporary constraint "^2" for "root/req" must be a subset of the constraint Run `composer require root/req` or `composer require root/req:^2` instead to replace the constraint OUTPUT ]; + + yield 'update & bump' => [ + $rootDepAndTransitiveDep, + ['--bump-after-update' => true], + <<Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users. +If your package is not a library, you can explicitly specify the "type" by using "composer config type project". +Alternatively you can use --dev-only to only bump dependencies within "require-dev". +No requirements to update in ./composer.json. +OUTPUT + , true + ]; + + yield 'update & bump dev only' => [ + $rootDepAndTransitiveDep, + ['--bump-after-update' => 'dev'], + << [ + $rootDepAndTransitiveDep, + ['--with' => ['dep/pkg:^2'], '--bump-after-update' => true], + << satisfiable by root/req[1.0.0]. + - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2). +OUTPUT + ]; + } public function testInteractiveModeThrowsIfNoPackageToUpdate(): void