Browse Source

Add --dry-run to require and remove commands, fixes #7793

Jordi Boggiano 5 years ago
parent
commit
d14d411fa4
3 changed files with 64 additions and 7 deletions
  1. 2 0
      doc/03-cli.md
  2. 39 4
      src/Composer/Command/RemoveCommand.php
  3. 23 3
      src/Composer/Command/RequireCommand.php

+ 2 - 0
doc/03-cli.md

@@ -198,6 +198,7 @@ If you do not specify a package, composer will prompt you to search for a packag
 ### Options
 
 * **--dev:** Add packages to `require-dev`.
+* **--dry-run:** Simulate the command without actually doing anything.
 * **--prefer-source:** Install packages from `source` when available.
 * **--prefer-dist:** Install packages from `dist` when available.
 * **--no-progress:** Removes the progress display that can mess with some
@@ -236,6 +237,7 @@ uninstalled.
 
 ### Options
 * **--dev:** Remove packages from `require-dev`.
+* **--dry-run:** Simulate the command without actually doing anything.
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
 * **--no-update:** Disables the automatic update of the dependencies.

+ 39 - 4
src/Composer/Command/RemoveCommand.php

@@ -38,6 +38,7 @@ class RemoveCommand extends BaseCommand
             ->setDefinition(array(
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'),
+                new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
@@ -92,26 +93,44 @@ EOT
             }
         }
 
+        $dryRun = $input->getOption('dry-run');
+        $toRemove = array();
         foreach ($packages as $package) {
             if (isset($composer[$type][$package])) {
-                $json->removeLink($type, $composer[$type][$package]);
+                if ($dryRun) {
+                    $toRemove[$type][] = $composer[$type][$package];
+                } else {
+                    $json->removeLink($type, $composer[$type][$package]);
+                }
             } elseif (isset($composer[$altType][$package])) {
                 $io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                 if ($io->isInteractive()) {
                     if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
-                        $json->removeLink($altType, $composer[$altType][$package]);
+                        if ($dryRun) {
+                            $toRemove[$altType][] = $composer[$altType][$package];
+                        } else {
+                            $json->removeLink($altType, $composer[$altType][$package]);
+                        }
                     }
                 }
             } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) {
                 foreach ($matches as $matchedPackage) {
-                    $json->removeLink($type, $matchedPackage);
+                    if ($dryRun) {
+                        $toRemove[$type][] = $matchedPackage;
+                    } else {
+                        $json->removeLink($type, $matchedPackage);
+                    }
                 }
             } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) {
                 foreach ($matches as $matchedPackage) {
                     $io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
                     if ($io->isInteractive()) {
                         if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
-                            $json->removeLink($altType, $matchedPackage);
+                            if ($dryRun) {
+                                $toRemove[$altType][] = $matchedPackage;
+                            } else {
+                                $json->removeLink($altType, $matchedPackage);
+                            }
                         }
                     }
                 }
@@ -128,6 +147,21 @@ EOT
         $this->resetComposer();
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
 
+        if ($dryRun) {
+            $rootPackage = $composer->getPackage();
+            $links = array(
+                'require' => $rootPackage->getRequires(),
+                'require-dev' => $rootPackage->getDevRequires(),
+            );
+            foreach ($toRemove as $type => $packages) {
+                foreach ($packages as $package) {
+                    unset($links[$type][$package]);
+                }
+            }
+            $rootPackage->setRequires($links['require']);
+            $rootPackage->setDevRequires($links['require-dev']);
+        }
+
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
 
@@ -149,6 +183,7 @@ EOT
             ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies'))
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
             ->setRunScripts(!$input->getOption('no-scripts'))
+            ->setDryRun($dryRun)
         ;
 
         $status = $install->run();

+ 23 - 3
src/Composer/Command/RequireCommand.php

@@ -21,6 +21,8 @@ use Composer\Installer;
 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;
@@ -48,6 +50,7 @@ class RequireCommand extends InitCommand
             ->setDefinition(array(
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
+                new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
                 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('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'),
@@ -195,7 +198,7 @@ EOT
             }
         }
 
-        if (!$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) {
+        if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) {
             $composerDefinition = $this->json->read();
             foreach ($requirements as $package => $version) {
                 $composerDefinition[$requireKey][$package] = $version;
@@ -211,19 +214,35 @@ EOT
         }
 
         try {
-            return $this->doUpdate($input, $output, $io, $requirements);
+            return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey);
         } catch (\Exception $e) {
             $this->revertComposerFile(false);
             throw $e;
         }
     }
 
-    private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements)
+    private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, $requireKey, $removeKey)
     {
         // Update packages
         $this->resetComposer();
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
 
+        if ($input->getOption('dry-run')) {
+            $rootPackage = $composer->getPackage();
+            $links = array(
+                'require' => $rootPackage->getRequires(),
+                'require-dev' => $rootPackage->getDevRequires(),
+            );
+            $loader = new ArrayLoader();
+            $newLinks = $loader->parseLinks($rootPackage->getName(), $rootPackage->getPrettyVersion(), BasePackage::$supportedLinkTypes[$requireKey]['description'], $requirements);
+            $links[$requireKey] = array_merge($links[$requireKey], $newLinks);
+            foreach ($requirements as $package => $constraint) {
+                unset($links[$removeKey][$package]);
+            }
+            $rootPackage->setRequires($links['require']);
+            $rootPackage->setDevRequires($links['require-dev']);
+        }
+
         $updateDevMode = !$input->getOption('update-no-dev');
         $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader');
         $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative');
@@ -250,6 +269,7 @@ EOT
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
             ->setPreferStable($input->getOption('prefer-stable'))
             ->setPreferLowest($input->getOption('prefer-lowest'))
+            ->setDryRun($input->getOption('dry-run'))
         ;
 
         // if no lock is present, or the file is brand new, we do not do a