1
0
Fork 0

Add --patch-only flag to update command to restrict updates to patch versions and make them safer (#12122)

Fixes #11446
pull/12129/head
Jordi Boggiano 2024-09-21 13:54:03 +02:00 committed by GitHub
parent 6b81140f81
commit c8bd0e6278
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 0 deletions

View File

@ -232,6 +232,7 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.*
COMPOSER_PREFER_LOWEST=1 env var. COMPOSER_PREFER_LOWEST=1 env var.
* **--minimal-changes (-m):** During a partial update with `-w`/`-W`, only perform absolutely necessary * **--minimal-changes (-m):** 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. changes to transitive dependencies. Can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var.
* **--patch-only:** Only allow patch version updates for currently installed dependencies.
* **--interactive:** Interactive interface with autocompletion to select the packages to update. * **--interactive:** Interactive interface with autocompletion to select the packages to update.
* **--root-reqs:** Restricts the update to your first degree dependencies. * **--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. * **--bump-after-update:** Runs `bump` after performing the update. Set to `dev` or `no-dev` to only bump those dependencies.

View File

@ -28,6 +28,7 @@ use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositorySet; use Composer\Repository\RepositorySet;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Semver\Intervals; use Composer\Semver\Intervals;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Advisory\Auditor; use Composer\Advisory\Auditor;
@ -83,6 +84,7 @@ class UpdateCommand extends BaseCommand
new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).'),
new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).'),
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('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('patch-only', null, InputOption::VALUE_NONE, 'Only allow patch version updates for currently installed dependencies.'),
new InputOption('interactive', 'i', InputOption::VALUE_NONE, 'Interactive interface with autocompletion to select the packages to update.'), 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('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']), new InputOption('bump-after-update', null, InputOption::VALUE_OPTIONAL, 'Runs bump after performing the update.', false, ['dev', 'no-dev', 'all']),
@ -175,6 +177,26 @@ EOT
} }
} }
if ($input->getOption('patch-only')) {
if (!$composer->getLocker()->isLocked()) {
throw new \InvalidArgumentException('patch-only can only be used with a lock file present');
}
foreach ($composer->getLocker()->getLockedRepository(true)->getCanonicalPackages() as $package) {
if ($package->isDev()) {
continue;
}
if (!Preg::isMatch('{^(\d+\.\d+\.\d+)}', $package->getVersion(), $match)) {
continue;
}
$constraint = $parser->parseConstraints('~'.$match[1]);
if (isset($temporaryConstraints[$package->getName()])) {
$temporaryConstraints[$package->getName()] = MultiConstraint::create([$temporaryConstraints[$package->getName()], $constraint], true);
} else {
$temporaryConstraints[$package->getName()] = $constraint;
}
}
}
if ($input->getOption('interactive')) { if ($input->getOption('interactive')) {
$packages = $this->getPackagesInteractively($io, $input, $output, $composer, $packages); $packages = $this->getPackagesInteractively($io, $input, $output, $composer, $packages);
} }

View File

@ -185,7 +185,69 @@ Your requirements could not be resolved to an installable set of packages.
- 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). - 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 OUTPUT
]; ];
}
public function testUpdateWithPatchOnly(): void
{
$this->initTempComposer([
'repositories' => [
'packages' => [
'type' => 'package',
'package' => [
['name' => 'root/req', 'version' => '1.0.0'],
['name' => 'root/req', 'version' => '1.0.1'],
['name' => 'root/req', 'version' => '1.1.0'],
['name' => 'root/req2', 'version' => '1.0.0'],
['name' => 'root/req2', 'version' => '1.0.1'],
['name' => 'root/req2', 'version' => '1.1.0'],
['name' => 'root/req3', 'version' => '1.0.0'],
['name' => 'root/req3', 'version' => '1.0.1'],
['name' => 'root/req3', 'version' => '1.1.0'],
],
],
],
'require' => [
'root/req' => '1.*',
'root/req2' => '1.*',
'root/req3' => '1.*',
],
]);
$package = self::getPackage('root/req', '1.0.0');
$package2 = self::getPackage('root/req2', '1.0.0');
$package3 = self::getPackage('root/req3', '1.0.0');
$this->createComposerLock([$package, $package2, $package3]);
$appTester = $this->getApplicationTester();
// root/req fails because of incompatible --with requirement
$appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.1']]));
$expected = <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires root/req 1.*, found root/req[1.0.0, 1.0.1, 1.1.0] but it conflicts with your temporary update constraint (root/req:[[>= 1.1.0.0-dev < 2.0.0.0-dev] [>= 1.0.0.0-dev < 1.1.0.0-dev]]).
OUTPUT;
self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true)));
$appTester = $this->getApplicationTester();
// root/req upgrades to 1.0.1 as that is compatible with the --with requirement now
// root/req2 upgrades to 1.0.1 only due to --patch-only
// root/req3 does not update as it is not in the allowlist
$appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.0.1'], 'packages' => ['root/req', 'root/req2']]));
$expected = <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 0 installs, 2 updates, 0 removals
- Upgrading root/req (1.0.0 => 1.0.1)
- Upgrading root/req2 (1.0.0 => 1.0.1)
OUTPUT;
self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true)));
} }
public function testInteractiveModeThrowsIfNoPackageToUpdate(): void public function testInteractiveModeThrowsIfNoPackageToUpdate(): void