1
0
Fork 0

Add --minimal-changes mode to perform partial updates --with-dependencies while changing only what is necessary in other dependencies (#11665)

pull/11699/head
Jordi Boggiano 2023-10-26 10:25:04 +02:00 committed by GitHub
parent 7a09e05560
commit 899dcedf66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 193 additions and 9 deletions

View File

@ -229,6 +229,8 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.*
* **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal
versions of requirements, generally used with `--prefer-stable`. Can also be set via the
COMPOSER_PREFER_LOWEST=1 env var.
* **--minimal-changes:** 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.
* **--interactive:** Interactive interface with autocompletion to select the packages to update.
* **--root-reqs:** Restricts the update to your first degree dependencies.
@ -288,6 +290,8 @@ If you do not specify a package, Composer will prompt you to search for a packag
* **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal
versions of requirements, generally used with `--prefer-stable`. Can also be set via the
COMPOSER_PREFER_LOWEST=1 env var.
* **--minimal-changes:** During an update with `-w`/`-W`, only perform absolutely necessary
changes to transitive dependencies. Can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var.
* **--sort-packages:** Keep packages sorted in `composer.json`.
* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to
get a faster autoloader. This is recommended especially for production, but
@ -326,6 +330,8 @@ uninstalled.
(Deprecated, is now default behavior)
* **--update-with-all-dependencies (-W):** Allows all inherited dependencies to be updated,
including those that are root requirements.
* **--minimal-changes:** During an update with `-w`/`-W`, only perform absolutely necessary
changes to transitive dependencies. Can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var.
* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`,
`lib-*` and `ext-*`) and force the installation even if the local machine does
not fulfill these.
@ -1290,6 +1296,11 @@ If set to `1`, it is the equivalent of passing the `--prefer-stable` option to
If set to `1`, it is the equivalent of passing the `--prefer-lowest` option to
`update` or `require`.
### COMPOSER_MINIMAL_CHANGES
If set to `1`, it is the equivalent of passing the `--minimal-changes` option to
`update`, `require` or `remove`.
### COMPOSER_IGNORE_PLATFORM_REQ or COMPOSER_IGNORE_PLATFORM_REQS
If `COMPOSER_IGNORE_PLATFORM_REQS` set to `1`, it is the equivalent of passing the `--ignore-platform-reqs` argument.

View File

@ -248,6 +248,7 @@ abstract class BaseCommand extends Command
'COMPOSER_NO_DEV' => ['no-dev', 'update-no-dev'],
'COMPOSER_PREFER_STABLE' => ['prefer-stable'],
'COMPOSER_PREFER_LOWEST' => ['prefer-lowest'],
'COMPOSER_MINIMAL_CHANGES' => ['minimal-changes'],
];
foreach ($envOptions as $envName => $optionNames) {
foreach ($optionNames as $optionName) {

View File

@ -59,6 +59,7 @@ class RemoveCommand extends BaseCommand
new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'),
new InputOption('no-update-with-dependencies', null, InputOption::VALUE_NONE, 'Does not allow inherited dependencies to be updated with explicit dependencies.'),
new InputOption('minimal-changes', 'm', InputOption::VALUE_NONE, 'During an 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('unused', null, InputOption::VALUE_NONE, 'Remove all packages which are locked but not required by any other package.'),
new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'),
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'),
@ -286,6 +287,7 @@ EOT
->setDryRun($dryRun)
->setAudit(!$input->getOption('no-audit'))
->setAuditFormat($this->getAuditFormat($input))
->setMinimalUpdate($input->getOption('minimal-changes'))
;
// if no lock is present, we do not do a partial update as

View File

@ -103,6 +103,7 @@ class RequireCommand extends BaseCommand
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'),
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('minimal-changes', 'm', InputOption::VALUE_NONE, 'During an 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('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages when adding/updating a new dependency'),
new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'),
@ -479,6 +480,7 @@ EOT
->setPreferLowest($input->getOption('prefer-lowest'))
->setAudit(!$input->getOption('no-audit'))
->setAuditFormat($this->getAuditFormat($input))
->setMinimalUpdate($input->getOption('minimal-changes'))
;
// if no lock is present, or the file is brand new, we do not do a

View File

@ -75,6 +75,7 @@ class UpdateCommand extends BaseCommand
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'),
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('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.'),
])
@ -238,6 +239,7 @@ EOT
->setTemporaryConstraints($temporaryConstraints)
->setAudit(!$input->getOption('no-audit'))
->setAuditFormat($this->getAuditFormat($input))
->setMinimalUpdate($input->getOption('minimal-changes'))
;
if ($input->getOption('no-plugins')) {

View File

@ -28,15 +28,21 @@ class DefaultPolicy implements PolicyInterface
private $preferStable;
/** @var bool */
private $preferLowest;
/** @var array<string, string>|null */
private $preferredVersions;
/** @var array<int, array<string, array<int, int>>> */
private $preferredPackageResultCachePerPool;
/** @var array<int, array<string, int>> */
private $sortingCachePerPool;
public function __construct(bool $preferStable = false, bool $preferLowest = false)
/**
* @param array<string, string>|null $preferredVersions Must be an array of package name => normalized version
*/
public function __construct(bool $preferStable = false, bool $preferLowest = false, ?array $preferredVersions = null)
{
$this->preferStable = $preferStable;
$this->preferLowest = $preferLowest;
$this->preferredVersions = $preferredVersions;
}
/**
@ -204,6 +210,22 @@ class DefaultPolicy implements PolicyInterface
*/
protected function pruneToBestVersion(Pool $pool, array $literals): array
{
if ($this->preferredVersions !== null) {
$name = $pool->literalToPackage($literals[0])->getName();
if (isset($this->preferredVersions[$name])) {
$preferredVersion = $this->preferredVersions[$name];
$bestLiterals = [];
foreach ($literals as $literal) {
if ($pool->literalToPackage($literal)->getVersion() === $preferredVersion) {
$bestLiterals[] = $literal;
}
}
if (\count($bestLiterals) > 0) {
return $bestLiterals;
}
}
}
$operator = $this->preferLowest ? '<' : '>';
$bestLiterals = [$literals[0]];
$bestPackage = $pool->literalToPackage($literals[0]);

View File

@ -165,6 +165,8 @@ class Installer
/** @var bool */
protected $preferLowest = false;
/** @var bool */
protected $minimalUpdate = false;
/** @var bool */
protected $writeLock;
/** @var bool */
protected $executeOperations = true;
@ -464,7 +466,7 @@ class Installer
$this->io->writeError('<info>Loading composer repositories with package information</info>');
// creating repository set
$policy = $this->createPolicy(true);
$policy = $this->createPolicy(true, $lockedRepository);
$repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases);
$repositories = $this->repositoryManager->getRepositories();
foreach ($repositories as $repository) {
@ -904,7 +906,7 @@ class Installer
return $repositorySet;
}
private function createPolicy(bool $forUpdate): DefaultPolicy
private function createPolicy(bool $forUpdate, ?LockArrayRepository $lockedRepo = null): DefaultPolicy
{
$preferStable = null;
$preferLowest = null;
@ -921,7 +923,18 @@ class Installer
$preferLowest = $this->preferLowest;
}
return new DefaultPolicy($preferStable, $preferLowest);
$preferredVersions = null;
if ($forUpdate && $this->minimalUpdate && $this->updateAllowList !== null && $lockedRepo !== null) {
$preferredVersions = [];
foreach ($lockedRepo->getPackages() as $pkg) {
if ($pkg instanceof AliasPackage || in_array($pkg->getName(), $this->updateAllowList, true)) {
continue;
}
$preferredVersions[$pkg->getName()] = $pkg->getVersion();
}
}
return new DefaultPolicy($preferStable, $preferLowest, $preferredVersions);
}
/**
@ -1384,7 +1397,7 @@ class Installer
*/
public function setPreferStable(bool $preferStable = true): self
{
$this->preferStable = (bool) $preferStable;
$this->preferStable = $preferStable;
return $this;
}
@ -1396,7 +1409,21 @@ class Installer
*/
public function setPreferLowest(bool $preferLowest = true): self
{
$this->preferLowest = (bool) $preferLowest;
$this->preferLowest = $preferLowest;
return $this;
}
/**
* Only relevant for partial updates (with setUpdateAllowList), if this is enabled currently locked versions will be preferred for packages which are not in the allowlist
*
* This reduces the update to
*
* @return Installer
*/
public function setMinimalUpdate(bool $minimalUpdate = true): self
{
$this->minimalUpdate = $minimalUpdate;
return $this;
}
@ -1410,7 +1437,7 @@ class Installer
*/
public function setWriteLock(bool $writeLock = true): self
{
$this->writeLock = (bool) $writeLock;
$this->writeLock = $writeLock;
return $this;
}
@ -1424,7 +1451,7 @@ class Installer
*/
public function setExecuteOperations(bool $executeOperations = true): self
{
$this->executeOperations = (bool) $executeOperations;
$this->executeOperations = $executeOperations;
return $this;
}

View File

@ -121,6 +121,59 @@ class DefaultPolicyTest extends TestCase
$this->assertSame($expected, $selected);
}
public function testSelectNewestWithPreferredVersionPicksPreferredVersionIfAvailable(): void
{
$this->repo->addPackage($packageA1 = self::getPackage('A', '1.0.0'));
$this->repo->addPackage($packageA2 = self::getPackage('A', '1.1.0'));
$this->repo->addPackage($packageA2b = self::getPackage('A', '1.1.0'));
$this->repo->addPackage($packageA3 = self::getPackage('A', '1.2.0'));
$this->repositorySet->addRepository($this->repo);
$pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked);
$literals = [$packageA1->getId(), $packageA2->getId(), $packageA2b->getId(), $packageA3->getId()];
$expected = [$packageA2->getId(), $packageA2b->getId()];
$policy = new DefaultPolicy(false, false, ['a' => '1.1.0.0']);
$selected = $policy->selectPreferredPackages($pool, $literals);
$this->assertSame($expected, $selected);
}
public function testSelectNewestWithPreferredVersionPicksNewestOtherwise(): void
{
$this->repo->addPackage($packageA1 = self::getPackage('A', '1.0.0'));
$this->repo->addPackage($packageA2 = self::getPackage('A', '1.2.0'));
$this->repositorySet->addRepository($this->repo);
$pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked);
$literals = [$packageA1->getId(), $packageA2->getId()];
$expected = [$packageA2->getId()];
$policy = new DefaultPolicy(false, false, ['a' => '1.1.0.0']);
$selected = $policy->selectPreferredPackages($pool, $literals);
$this->assertSame($expected, $selected);
}
public function testSelectNewestWithPreferredVersionPicksLowestIfPreferLowest(): void
{
$this->repo->addPackage($packageA1 = self::getPackage('A', '1.0.0'));
$this->repo->addPackage($packageA2 = self::getPackage('A', '1.2.0'));
$this->repositorySet->addRepository($this->repo);
$pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked);
$literals = [$packageA1->getId(), $packageA2->getId()];
$expected = [$packageA1->getId()];
$policy = new DefaultPolicy(false, true, ['a' => '1.1.0.0']);
$selected = $policy->selectPreferredPackages($pool, $literals);
$this->assertSame($expected, $selected);
}
public function testRepositoryOrderingAffectsPriority(): void
{
$repo1 = new ArrayRepository;

View File

@ -0,0 +1,62 @@
--TEST--
Updating transitive dependencies only updates what is really required when a minimal update is requested
* dependency/pkg has to upgrade to 2.x
* dependency/pkg2 remains at 1.0.0 and does not upgrade to 1.1.0 even though it would without minimal update
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{ "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "2.*", "dependency/pkg2": "1.*" } },
{ "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "dependency/pkg2": "1.*" } },
{ "name": "dependency/pkg", "version": "2.0.0" },
{ "name": "dependency/pkg", "version": "1.1.0" },
{ "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "dependency/pkg2", "version": "2.0.0" },
{ "name": "dependency/pkg2", "version": "1.1.0" },
{ "name": "dependency/pkg2", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.1.0" },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" }
]
}
],
"require": {
"allowed/pkg": "1.*",
"unrelated/pkg": "1.*"
}
}
--INSTALLED--
[
{ "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "dependency/pkg2": "1.*" } },
{ "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "dependency/pkg2", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" }
]
--LOCK--
{
"packages": [
{ "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "dependency/pkg2": "1.*" } },
{ "name": "dependency/pkg", "version": "1.0.0" },
{ "name": "dependency/pkg2", "version": "1.0.0" },
{ "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } },
{ "name": "unrelated/pkg-dependency", "version": "1.0.0" }
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}
--RUN--
update allowed/pkg --with-all-dependencies --minimal-changes
--EXPECT--
Upgrading dependency/pkg (1.0.0 => 2.0.0)
Upgrading allowed/pkg (1.0.0 => 1.1.0)

View File

@ -381,6 +381,7 @@ class InstallerTest extends TestCase
$update->addOption('lock', null, InputOption::VALUE_NONE);
$update->addOption('with-all-dependencies', null, InputOption::VALUE_NONE);
$update->addOption('with-dependencies', null, InputOption::VALUE_NONE);
$update->addOption('minimal-changes', null, InputOption::VALUE_NONE);
$update->addOption('prefer-stable', null, InputOption::VALUE_NONE);
$update->addOption('prefer-lowest', null, InputOption::VALUE_NONE);
$update->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL);
@ -412,7 +413,8 @@ class InstallerTest extends TestCase
->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest'))
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs))
->setAudit(false);
->setAudit(false)
->setMinimalUpdate($input->getOption('minimal-changes'));
return $installer->run();
});