Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --minimal-update mode to perform partial updates --with-dependencies while upgrading only what is necessary #11665

Merged
merged 2 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/03-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/Composer/Command/BaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ protected function initialize(InputInterface $input, OutputInterface $output)
'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) {
Expand Down
2 changes: 2 additions & 0 deletions src/Composer/Command/RemoveCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ protected function configure()
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).'),
Expand Down Expand Up @@ -286,6 +287,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
->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
Expand Down
2 changes: 2 additions & 0 deletions src/Composer/Command/RequireCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ protected function configure()
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`.'),
Expand Down Expand Up @@ -479,6 +480,7 @@ private function doUpdate(InputInterface $input, OutputInterface $output, IOInte
->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
Expand Down
2 changes: 2 additions & 0 deletions src/Composer/Command/UpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ protected function configure()
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.'),
])
Expand Down Expand Up @@ -238,6 +239,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
->setTemporaryConstraints($temporaryConstraints)
->setAudit(!$input->getOption('no-audit'))
->setAuditFormat($this->getAuditFormat($input))
->setMinimalUpdate($input->getOption('minimal-changes'))
;

if ($input->getOption('no-plugins')) {
Expand Down
24 changes: 23 additions & 1 deletion src/Composer/DependencyResolver/DefaultPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -204,6 +210,22 @@ protected function replaces(BasePackage $source, BasePackage $target): bool
*/
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]);
Expand Down
41 changes: 34 additions & 7 deletions src/Composer/Installer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -464,7 +466,7 @@ protected function doUpdate(InstalledRepositoryInterface $localRepo, bool $doIns
$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) {
Expand Down Expand Up @@ -904,7 +906,7 @@ private function createRepositorySet(bool $forUpdate, PlatformRepository $platfo
return $repositorySet;
}

private function createPolicy(bool $forUpdate): DefaultPolicy
private function createPolicy(bool $forUpdate, ?LockArrayRepository $lockedRepo = null): DefaultPolicy
{
$preferStable = null;
$preferLowest = null;
Expand All @@ -921,7 +923,18 @@ private function createPolicy(bool $forUpdate): DefaultPolicy
$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);
}

/**
Expand Down Expand Up @@ -1384,7 +1397,7 @@ public function setUpdateAllowTransitiveDependencies(int $updateAllowTransitiveD
*/
public function setPreferStable(bool $preferStable = true): self
{
$this->preferStable = (bool) $preferStable;
$this->preferStable = $preferStable;

return $this;
}
Expand All @@ -1396,7 +1409,21 @@ public function setPreferStable(bool $preferStable = true): self
*/
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;
}
Expand All @@ -1410,7 +1437,7 @@ public function setPreferLowest(bool $preferLowest = true): self
*/
public function setWriteLock(bool $writeLock = true): self
{
$this->writeLock = (bool) $writeLock;
$this->writeLock = $writeLock;

return $this;
}
Expand All @@ -1424,7 +1451,7 @@ public function setWriteLock(bool $writeLock = true): self
*/
public function setExecuteOperations(bool $executeOperations = true): self
{
$this->executeOperations = (bool) $executeOperations;
$this->executeOperations = $executeOperations;

return $this;
}
Expand Down
53 changes: 53 additions & 0 deletions tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,59 @@ public function testSelectNewestWithDevPicksNonDev(): void
$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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 3 additions & 1 deletion tests/Composer/Test/InstallerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ private function doTestIntegration(string $file, string $message, ?string $condi
$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);
Expand Down Expand Up @@ -412,7 +413,8 @@ private function doTestIntegration(string $file, string $message, ?string $condi
->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();
});
Expand Down