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 --with to update command to allow downgrading to a specific version / applying custom temporary constraints #8860

Merged
merged 1 commit into from
May 5, 2020
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
21 changes: 21 additions & 0 deletions doc/03-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,26 @@ You can also use wildcards to update a bunch of packages at once:
php composer.phar update "vendor/*"
```


If you want to downgrade a package to a specific version without changing your
composer.json you can use `--with` and provide a custom version constraint:

```sh
php composer.phar update --with vendor/package:2.0.1
```

The custom constraint has to be a subset of the existing constraint you have,
and this feature is only available for your root package dependencies.

If you only want to update the package(s) for which you provide custom constraints
using `--with`, you can skip `--with` and just use constraints with the partial
update syntax:

```sh
php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.*
```


### Options

* **--prefer-source:** Install packages from `source` when available.
Expand All @@ -152,6 +172,7 @@ php composer.phar update "vendor/*"
* **--no-install:** Does not run the install step after updating the composer.lock file.
* **--lock:** Only updates the lock file hash to suppress warning about the
lock file being out of date.
* **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0
* **--no-autoloader:** Skips autoloader generation.
* **--no-scripts:** Skips execution of scripts defined in `composer.json`.
* **--no-progress:** Removes the progress display that can mess with some
Expand Down
22 changes: 22 additions & 0 deletions src/Composer/Command/BaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Composer\IO\IOInterface;
use Composer\IO\NullIO;
use Composer\Plugin\PreCommandRunEvent;
use Composer\Package\Version\VersionParser;
use Composer\Plugin\PluginEvents;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand Down Expand Up @@ -180,4 +181,25 @@ protected function getPreferredInstallOptions(Config $config, InputInterface $in

return array($preferSource, $preferDist);
}

protected function formatRequirements(array $requirements)
{
$requires = array();
$requirements = $this->normalizeRequirements($requirements);
foreach ($requirements as $requirement) {
if (!isset($requirement['version'])) {
throw new \UnexpectedValueException('Option '.$requirement['name'] .' is missing a version constraint, use e.g. '.$requirement['name'].':^1.0');
}
$requires[$requirement['name']] = $requirement['version'];
}

return $requires;
}

protected function normalizeRequirements(array $requirements)
{
$parser = new VersionParser();

return $parser->parseNameVersionPairs($requirements);
}
}
18 changes: 0 additions & 18 deletions src/Composer/Command/InitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -577,17 +577,6 @@ protected function formatAuthors($author)
return array($this->parseAuthorString($author));
}

protected function formatRequirements(array $requirements)
{
$requires = array();
$requirements = $this->normalizeRequirements($requirements);
foreach ($requirements as $requirement) {
$requires[$requirement['name']] = $requirement['version'];
}

return $requires;
}

protected function getGitConfig()
{
if (null !== $this->gitConfig) {
Expand Down Expand Up @@ -652,13 +641,6 @@ protected function hasVendorIgnore($ignoreFile, $vendor = 'vendor')
return false;
}

protected function normalizeRequirements(array $requirements)
{
$parser = new VersionParser();

return $parser->parseNameVersionPairs($requirements);
}

protected function addVendorIgnore($ignoreFile, $vendor = '/vendor/')
{
$contents = "";
Expand Down
69 changes: 64 additions & 5 deletions src/Composer/Command/UpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
use Composer\IO\IOInterface;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Package\Version\VersionParser;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Package\Link;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -39,6 +42,7 @@ protected function configure()
->setDescription('Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file.')
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.'),
new InputOption('with', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
Expand Down Expand Up @@ -80,6 +84,14 @@ protected function configure()

<info>php composer.phar update vendor/package1 foo/* [...]</info>

To run an update with more restrictive constraints you can use:

<info>php composer.phar update --with vendor/package:1.0.*</info>

To run a partial update with more restrictive constraints you can use the shorthand:

<info>php composer.phar update vendor/package:1.0.*</info>

To select packages names interactively with auto-completion use <info>-i</info>.

Read more at https://getcomposer.org/doc/03-cli.md#update-u
Expand All @@ -101,22 +113,54 @@ protected function execute(InputInterface $input, OutputInterface $output)
$composer = $this->getComposer(true, $input->getOption('no-plugins'));

$packages = $input->getArgument('packages');
$reqs = $this->formatRequirements($input->getOption('with'));

// extract --with shorthands from the allowlist
if ($packages) {
$allowlistPackagesWithRequirements = array_filter($packages, function ($pkg) {
return preg_match('{\S+[ =:]\S+}', $pkg) > 0;
});
foreach ($this->formatRequirements($allowlistPackagesWithRequirements) as $package => $constraint) {
var_Dump($package, $constraint);
$reqs[$package] = $constraint;
}

// replace the foo/bar:req by foo/bar in the allowlist
foreach ($allowlistPackagesWithRequirements as $package) {
$packageName = preg_replace('{^([^ =:]+)[ =:].*$}', '$1', $package);
$index = array_search($package, $packages);
$packages[$index] = $packageName;
}
}

$rootRequires = $composer->getPackage()->getRequires();
$rootDevRequires = $composer->getPackage()->getDevRequires();
foreach ($reqs as $package => $constraint) {
if (isset($rootRequires[$package])) {
$rootRequires[$package] = $this->appendConstraintToLink($rootRequires[$package], $constraint);
} elseif (isset($rootDevRequires[$package])) {
$rootDevRequires[$package] = $this->appendConstraintToLink($rootDevRequires[$package], $constraint);
} else {
throw new \UnexpectedValueException('Only root package requirements can receive temporary constraints and '.$package.' is not one');
}
}
$composer->getPackage()->setRequires($rootRequires);
$composer->getPackage()->setDevRequires($rootDevRequires);

if ($input->getOption('interactive')) {
$packages = $this->getPackagesInteractively($io, $input, $output, $composer, $packages);
}

if ($input->getOption('root-reqs')) {
$require = array_keys($composer->getPackage()->getRequires());
$requires = array_keys($rootRequires);
if (!$input->getOption('no-dev')) {
$requireDev = array_keys($composer->getPackage()->getDevRequires());
$require = array_merge($require, $requireDev);
$requires = array_merge($requires, array_keys($rootDevRequires));
}

if (!empty($packages)) {
$packages = array_intersect($packages, $require);
$packages = array_intersect($packages, $requires);
} else {
$packages = $require;
$packages = $requires;
}
}

Expand Down Expand Up @@ -242,4 +286,19 @@ private function getPackagesInteractively(IOInterface $io, InputInterface $input

throw new \RuntimeException('Installation aborted.');
}

private function appendConstraintToLink(Link $link, $constraint)
{
$parser = new VersionParser;
$oldPrettyString = $link->getConstraint()->getPrettyString();
$newConstraint = MultiConstraint::create(array($link->getConstraint(), $parser->parseConstraints($constraint)));
$newConstraint->setPrettyString($oldPrettyString.' && '.$constraint);
return new Link(
$link->getSource(),
$link->getTarget(),
$newConstraint,
$link->getDescription(),
$link->getPrettyConstraint() . ' && ' . $constraint
);
}
}