diff --git a/doc/03-cli.md b/doc/03-cli.md index 92d78b8d7..161f2ee69 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -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. @@ -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 diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index 56ee9f7f4..8c45899cf 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -19,6 +19,7 @@ use Composer\Factory; 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; @@ -180,4 +181,25 @@ abstract class BaseCommand extends Command 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); + } } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index ecbbbf1e0..7f00daa5f 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -585,17 +585,6 @@ EOT 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) { @@ -660,13 +649,6 @@ EOT return false; } - protected function normalizeRequirements(array $requirements) - { - $parser = new VersionParser(); - - return $parser->parseNameVersionPairs($requirements); - } - protected function addVendorIgnore($ignoreFile, $vendor = '/vendor/') { $contents = ""; diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 4bb15fc9b..3ca704620 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -18,6 +18,9 @@ use Composer\Installer; 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; @@ -39,6 +42,7 @@ class UpdateCommand extends BaseCommand ->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).'), @@ -80,6 +84,14 @@ from a specific vendor: php composer.phar update vendor/package1 foo/* [...] +To run an update with more restrictive constraints you can use: + +php composer.phar update --with vendor/package:1.0.* + +To run a partial update with more restrictive constraints you can use the shorthand: + +php composer.phar update vendor/package:1.0.* + To select packages names interactively with auto-completion use -i. Read more at https://getcomposer.org/doc/03-cli.md#update-u @@ -101,22 +113,54 @@ EOT $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; } } @@ -242,4 +286,19 @@ EOT 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 + ); + } }