Skip to content

Commit

Permalink
Add bump command to bump requirements to the currently installed vers…
Browse files Browse the repository at this point in the history
…ion, fixes #7273
  • Loading branch information
Seldaek committed Jun 9, 2022
1 parent 73fd0f2 commit f293173
Show file tree
Hide file tree
Showing 11 changed files with 612 additions and 13 deletions.
18 changes: 18 additions & 0 deletions doc/03-cli.md
Expand Up @@ -322,6 +322,24 @@ uninstalled.
* **--apcu-autoloader-prefix:** Use a custom prefix for the APCu autoloader cache.
Implicitly enables `--apcu-autoloader`.

## bump

The `bump` command increases the lower limit of your composer.json requirements
to the currently installed versions. This helps to ensure your dependencies do not
accidentally get downgraded due to some other conflict, and can slightly improve
dependency resolution performance as it limits the amount of package versions
Composer has to look at.

Running this blindly on libraries is **NOT** recommended as it will narrow down
your allowed dependencies, which may cause dependency hell for your users.
Running it with `--dev-only` on libraries may be fine however as dev requirements
are local to the library and do not affect consumers of the package.

### Options

* **--dev-only:** Only bump requirements in "require-dev".
* **--no-dev-only:** Only bump requirements in "require".

## reinstall

The `reinstall` command looks up installed packages by name,
Expand Down
227 changes: 227 additions & 0 deletions src/Composer/Command/BumpCommand.php
@@ -0,0 +1,227 @@
<?php declare(strict_types=1);

/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Composer\Command;

use Composer\DependencyResolver\Request;
use Composer\Package\AliasPackage;
use Composer\Package\Locker;
use Composer\Package\Version\VersionBumper;
use Composer\Package\Version\VersionSelector;
use Composer\Util\Filesystem;
use Symfony\Component\Console\Input\InputInterface;
use Composer\Console\Input\InputArgument;
use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Installer\InstallerEvents;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\Version\VersionParser;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\BasePackage;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\IO\IOInterface;
use Composer\Util\Silencer;

/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
final class BumpCommand extends BaseCommand
{
private const ERROR_GENERIC = 1;
private const ERROR_LOCK_OUTDATED = 2;

use CompletionTrait;

protected function configure(): void
{
$this
->setName('bump')
->setDescription('Increases the lower limit of your composer.json requirements to the currently installed versions.')
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name(s) to restrict which packages are bumped.', null, $this->suggestRootRequirement()),
new InputOption('dev-only', 'D', InputOption::VALUE_NONE, 'Only bump requirements in "require-dev".'),
new InputOption('no-dev-only', 'R', InputOption::VALUE_NONE, 'Only bump requirements in "require".'),
))
->setHelp(
<<<EOT
The <info>bump</info> command increases the lower limit of your composer.json requirements
to the currently installed versions. This helps to ensure your dependencies do not
accidentally get downgraded due to some other conflict, and can slightly improve
dependency resolution performance as it limits the amount of package versions
Composer has to look at.
Running this blindly on libraries is **NOT** recommended as it will narrow down
your allowed dependencies, which may cause dependency hell for your users.
Running it with <info>--dev-only</info> on libraries may be fine however as dev requirements
are local to the library and do not affect consumers of the package.
EOT
)
;
}

/**
* @throws \Seld\JsonLint\ParsingException
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
/** @readonly */
$composerJsonPath = Factory::getComposerFile();
$io = $this->getIO();

if (!Filesystem::isReadable($composerJsonPath)) {
$io->writeError('<error>'.$composerJsonPath.' is not readable.</error>');

return self::ERROR_GENERIC;
}

$composerJson = new JsonFile($composerJsonPath);
$contents = file_get_contents($composerJson->getPath());
if (false === $contents) {
$io->writeError('<error>'.$composerJsonPath.' is not readable.</error>');

return self::ERROR_GENERIC;
}

// check for writability by writing to the file as is_writable can not be trusted on network-mounts
// see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
if (!is_writable($composerJsonPath) && false === Silencer::call('file_put_contents', $composerJsonPath, $contents)) {
$io->writeError('<error>'.$composerJsonPath.' is not writable.</error>');

return self::ERROR_GENERIC;
}
unset($contents);

$composer = $this->requireComposer();
if ($composer->getLocker()->isLocked()) {
if (!$composer->getLocker()->isFresh()) {
$io->writeError('<error>The lock file is not up to date with the latest changes in composer.json. Run the appropriate `update` to fix that before you use the `bump` command.</error>');

return self::ERROR_LOCK_OUTDATED;
}

$repo = $composer->getLocker()->getLockedRepository(true);
} else {
$repo = $composer->getRepositoryManager()->getLocalRepository();
}

if ($composer->getPackage()->getType() !== 'project' && !$input->getOption('dev-only')) {
$io->writeError('<warning>Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.</warning>');

$contents = $composerJson->read();
if (!isset($contents['type'])) {
$io->writeError('<warning>If your package is not a library, you can explicitly specify the "type" by using "composer config type project".</warning>');
$io->writeError('<warning>Alternatively you can use --dev to only bump dependencies within "require-dev".</warning>');
}
unset($contents);
}

$bumper = new VersionBumper();
$tasks = [];
if (!$input->getOption('no-dev-only')) {
$tasks['require-dev'] = $composer->getPackage()->getDevRequires();
};
if (!$input->getOption('dev-only')) {
$tasks['require'] = $composer->getPackage()->getRequires();
}

$updates = [];
foreach ($tasks as $key => $reqs) {
foreach ($reqs as $pkgName => $link) {
if (PlatformRepository::isPlatformPackage($pkgName)) {
continue;
}
$currentConstraint = $link->getPrettyConstraint();

$package = $repo->findPackage($pkgName, '*');
// name must be provided or replaced
if (null === $package) {
continue;
}
while ($package instanceof AliasPackage) {
$package = $package->getAliasOf();
}

$bumped = $bumper->bumpRequirement($link->getConstraint(), $package);

if ($bumped === $currentConstraint) {
continue;
}

$updates[$key][$pkgName] = $bumped;
}
}

if (!$this->updateFileCleanly($composerJson, $updates)) {
$composerDefinition = $composerJson->read();
foreach ($updates as $key => $packages) {
foreach ($packages as $package => $version) {
$composerDefinition[$key][$package] = $version;
}
}
$composerJson->write($composerDefinition);
}

$changeCount = array_sum(array_map('count', $updates));
if ($changeCount > 0) {
$io->write('<info>'.$composerJsonPath.' has been updated ('.$changeCount.' changes).</info>');
} else {
$io->write('<info>No requirements to update in '.$composerJsonPath.'.</info>');
}

if ($composer->getLocker()->isLocked() && $changeCount > 0) {
$contents = file_get_contents($composerJson->getPath());
if (false === $contents) {
throw new \RuntimeException('Unable to read '.$composerJson->getPath().' contents to update the lock file hash.');
}
$lock = new JsonFile(Factory::getLockFile($composerJsonPath));
$lockData = $lock->read();
$lockData['content-hash'] = Locker::getContentHash($contents);
$lock->write($lockData);
}

return 0;
}

/**
* @param array<'require'|'require-dev', array<string, string>> $updates
*/
private function updateFileCleanly(JsonFile $json, array $updates): bool
{
$contents = file_get_contents($json->getPath());
if (false === $contents) {
throw new \RuntimeException('Unable to read '.$json->getPath().' contents.');
}

$manipulator = new JsonManipulator($contents);

foreach ($updates as $key => $packages) {
foreach ($packages as $package => $version) {
if (!$manipulator->addLink($key, $package, $version)) {
return false;
}
}
}

if (false === file_put_contents($json->getPath(), $manipulator->getContents())) {
throw new \RuntimeException('Unable to write new '.$json->getPath().' contents.');
}

return true;
}
}
12 changes: 12 additions & 0 deletions src/Composer/Command/CompletionTrait.php
Expand Up @@ -45,6 +45,18 @@ private function suggestPreferInstall(): array
return ['dist', 'source', 'auto'];
}

/**
* Suggest package names from root requirements.
*/
private function suggestRootRequirement(): \Closure
{
return function (CompletionInput $input): array {
$composer = $this->requireComposer();

return array_merge(array_keys($composer->getPackage()->getRequires()), array_keys($composer->getPackage()->getDevRequires()));
};
}

/**
* Suggest package names from installed.
*/
Expand Down
22 changes: 11 additions & 11 deletions src/Composer/Command/RequireCommand.php
Expand Up @@ -116,13 +116,6 @@ protected function configure()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () { $this->revertComposerFile(); });
pcntl_signal(SIGTERM, function () { $this->revertComposerFile(); });
pcntl_signal(SIGHUP, function () { $this->revertComposerFile(); });
}

$this->file = Factory::getComposerFile();
$io = $this->getIO();

Expand Down Expand Up @@ -151,9 +144,16 @@ protected function execute(InputInterface $input, OutputInterface $output)
$this->composerBackup = file_get_contents($this->json->getPath());
$this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null;

if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () { $this->revertComposerFile(); });
pcntl_signal(SIGTERM, function () { $this->revertComposerFile(); });
pcntl_signal(SIGHUP, function () { $this->revertComposerFile(); });
}

// check for writability by writing to the file as is_writable can not be trusted on network-mounts
// see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
if (!is_writable($this->file) && !Silencer::call('file_put_contents', $this->file, $this->composerBackup)) {
if (!is_writable($this->file) && false === Silencer::call('file_put_contents', $this->file, $this->composerBackup)) {
$io->writeError('<error>'.$this->file.' is not writable.</error>');

return 1;
Expand All @@ -168,10 +168,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
* @see https://github.com/composer/composer/pull/8313#issuecomment-532637955
*/
if ($packageType !== 'project') {
$io->writeError('<error>"--fixed" option is allowed for "project" package types only to prevent possible misuses.</error>');
$io->writeError('<error>The "--fixed" option is only allowed for packages with a "project" type to prevent possible misuses.</error>');

if (empty($config['type'])) {
$io->writeError('<error>If your package is not library, you should explicitly specify "type" parameter in composer.json.</error>');
if (!isset($config['type'])) {
$io->writeError('<error>If your package is not a library, you can explicitly specify the "type" by using "composer config type project".</error>');
}

return 1;
Expand Down
1 change: 1 addition & 0 deletions src/Composer/Console/Application.php
Expand Up @@ -547,6 +547,7 @@ protected function getDefaultCommands(): array
new Command\CheckPlatformReqsCommand(),
new Command\FundCommand(),
new Command\ReinstallCommand(),
new Command\BumpCommand(),
));

if (strpos(__FILE__, 'phar:') === 0 || '1' === Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING')) {
Expand Down

0 comments on commit f293173

Please sign in to comment.