Add --minimal-changes mode to perform partial updates --with-dependencies while changing only what is necessary in other dependencies (#11665)
parent
7a09e05560
commit
899dcedf66
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
|
@ -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();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue