Browse Source

Simplify suggester output when updating, refactor suggest command to reuse SuggestedPackagesReporter and make smarter defaults, fixes #6267

Jordi Boggiano 5 years ago
parent
commit
44d1e15294

+ 4 - 7
doc/03-cli.md

@@ -106,7 +106,6 @@ resolution.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-progress:** Removes the progress display that can mess with some
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   terminals or scripts which don't handle backspace characters.
-* **--no-suggest:** Skips suggested packages in the output.
 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
   autoloader. This is recommended especially for production, but can take
   autoloader. This is recommended especially for production, but can take
   a bit of time to run so it is currently not done by default.
   a bit of time to run so it is currently not done by default.
@@ -156,7 +155,6 @@ php composer.phar update "vendor/*"
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-progress:** Removes the progress display that can mess with some
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   terminals or scripts which don't handle backspace characters.
-* **--no-suggest:** Skips suggested packages in the output.
 * **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements.
 * **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements.
 * **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements.
 * **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements.
 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
@@ -203,7 +201,6 @@ If you do not specify a package, composer will prompt you to search for a packag
 * **--prefer-dist:** Install packages from `dist` when available.
 * **--prefer-dist:** Install packages from `dist` when available.
 * **--no-progress:** Removes the progress display that can mess with some
 * **--no-progress:** Removes the progress display that can mess with some
   terminals or scripts which don't handle backspace characters.
   terminals or scripts which don't handle backspace characters.
-* **--no-suggest:** Skips suggested packages in the output.
 * **--no-update:** Disables the automatic update of the dependencies.
 * **--no-update:** Disables the automatic update of the dependencies.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
 * **--update-no-dev:** Run the dependency update with the `--no-dev` option.
 * **--update-no-dev:** Run the dependency update with the `--no-dev` option.
@@ -410,16 +407,16 @@ Lists all packages suggested by currently installed set of packages. You can
 optionally pass one or multiple package names in the format of `vendor/package`
 optionally pass one or multiple package names in the format of `vendor/package`
 to limit output to suggestions made by those packages only.
 to limit output to suggestions made by those packages only.
 
 
-Use the `--by-package` or `--by-suggestion` flags to group the output by
+Use the `--by-package` (default) or `--by-suggestion` flags to group the output by
 the package offering the suggestions or the suggested packages respectively.
 the package offering the suggestions or the suggested packages respectively.
 
 
-Use the `--verbose (-v)` flag to display the suggesting package and the suggestion reason.
-This implies `--by-package --by-suggestion`, showing both lists.
+If you only want a list of suggested package names, use `--list`.
 
 
 ### Options
 ### Options
 
 
-* **--by-package:** Groups output by suggesting package.
+* **--by-package:** Groups output by suggesting package (default).
 * **--by-suggestion:** Groups output by suggested package.
 * **--by-suggestion:** Groups output by suggested package.
+* **--list:** Show only list of suggested package names.
 * **--no-dev:** Excludes suggestions from `require-dev` packages.
 * **--no-dev:** Excludes suggestions from `require-dev` packages.
 
 
 ## depends (why)
 ## depends (why)

+ 0 - 2
src/Composer/Command/InstallCommand.php

@@ -44,7 +44,6 @@ class InstallCommand extends BaseCommand
                 new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
                 new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
-                new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
                 new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
                 new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
                 new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
                 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`.'),
                 new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'),
@@ -107,7 +106,6 @@ EOT
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)

+ 0 - 2
src/Composer/Command/RequireCommand.php

@@ -55,7 +55,6 @@ class RequireCommand extends InitCommand
                 new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
                 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.'),
                 new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
-                new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
                 new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
                 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.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
                 new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
@@ -259,7 +258,6 @@ EOT
             ->setPreferDist($input->getOption('prefer-dist'))
             ->setPreferDist($input->getOption('prefer-dist'))
             ->setDevMode($updateDevMode)
             ->setDevMode($updateDevMode)
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)

+ 35 - 91
src/Composer/Command/SuggestsCommand.php

@@ -13,6 +13,9 @@
 namespace Composer\Command;
 namespace Composer\Command;
 
 
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
+use Composer\Repository\RootPackageRepository;
+use Composer\Repository\CompositeRepository;
+use Composer\Installer\SuggestedPackagesReporter;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
@@ -26,8 +29,9 @@ class SuggestsCommand extends BaseCommand
             ->setName('suggests')
             ->setName('suggests')
             ->setDescription('Shows package suggestions.')
             ->setDescription('Shows package suggestions.')
             ->setDefinition(array(
             ->setDefinition(array(
-                new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package'),
+                new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'),
                 new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'),
                 new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'),
+                new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'),
                 new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'),
                 new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'),
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'),
                 new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'),
             ))
             ))
@@ -36,8 +40,6 @@ class SuggestsCommand extends BaseCommand
 
 
 The <info>%command.name%</info> command shows a sorted list of suggested packages.
 The <info>%command.name%</info> command shows a sorted list of suggested packages.
 
 
-Enabling <info>-v</info> implies <info>--by-package --by-suggestion</info>, showing both lists.
-
 Read more at https://getcomposer.org/doc/03-cli.md#suggests
 Read more at https://getcomposer.org/doc/03-cli.md#suggests
 EOT
 EOT
             )
             )
@@ -49,108 +51,50 @@ EOT
      */
      */
     protected function execute(InputInterface $input, OutputInterface $output)
     protected function execute(InputInterface $input, OutputInterface $output)
     {
     {
-        $lock = $this->getComposer()->getLocker()->getLockData();
-
-        if (empty($lock)) {
-            throw new \RuntimeException('Lockfile seems to be empty?');
+        $composer = $this->getComposer();
+
+        $installedRepos = array(
+            new RootPackageRepository(array(clone $composer->getPackage())),
+        );
+
+        $locker = $composer->getLocker();
+        if ($locker->isLocked()) {
+            $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides());
+            $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev'));
+        } else {
+            $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array());
+            $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository();
         }
         }
 
 
-        $packages = $lock['packages'];
-
-        if (!$input->getOption('no-dev')) {
-            $packages += $lock['packages-dev'];
-        }
+        $installedRepo = new CompositeRepository($installedRepos);
+        $reporter = new SuggestedPackagesReporter($this->getIO());
 
 
         $filter = $input->getArgument('packages');
         $filter = $input->getArgument('packages');
-
-        // First assemble lookup list of packages that are installed, replaced or provided
-        $installed = array();
-        foreach ($packages as $package) {
-            $installed[] = $package['name'];
-
-            if (!empty($package['provide'])) {
-                $installed = array_merge($installed, array_keys($package['provide']));
-            }
-
-            if (!empty($package['replace'])) {
-                $installed = array_merge($installed, array_keys($package['replace']));
-            }
-        }
-
-        // Undub and sort the install list into a sorted lookup array
-        $installed = array_flip($installed);
-        ksort($installed);
-
-        // Init platform repo
-        $platform = new PlatformRepository(array(), $this->getComposer()->getConfig()->get('platform') ?: array());
-
-        // Next gather all suggestions that are not in that list
-        $suggesters = array();
-        $suggested = array();
-        foreach ($packages as $package) {
-            $packageName = $package['name'];
-            if ((!empty($filter) && !in_array($packageName, $filter)) || empty($package['suggest'])) {
+        foreach ($installedRepo->getPackages() as $package) {
+            if (!empty($filter) && !in_array($package->getName(), $filter)) {
                 continue;
                 continue;
             }
             }
-            foreach ($package['suggest'] as $suggestion => $reason) {
-                if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $suggestion) && null !== $platform->findPackage($suggestion, '*')) {
-                    continue;
-                }
-                if (!isset($installed[$suggestion])) {
-                    $suggesters[$packageName][$suggestion] = $reason;
-                    $suggested[$suggestion][$packageName] = $reason;
-                }
-            }
+
+            $reporter->addSuggestionsFromPackage($package);
         }
         }
-        ksort($suggesters);
-        ksort($suggested);
 
 
-        // Determine output mode
-        $mode = 0;
+        // Determine output mode, default is by-package
+        $mode = SuggestedPackagesReporter::MODE_BY_PACKAGE;
         $io = $this->getIO();
         $io = $this->getIO();
-        if ($input->getOption('by-package') || $io->isVerbose()) {
-            $mode |= 1;
-        }
+        // if by-suggestion is given we override the default
         if ($input->getOption('by-suggestion')) {
         if ($input->getOption('by-suggestion')) {
-            $mode |= 2;
+            $mode = SuggestedPackagesReporter::MODE_BY_SUGGESTION;
         }
         }
-
-        // Simple mode
-        if ($mode === 0) {
-            foreach (array_keys($suggested) as $suggestion) {
-                $io->write(sprintf('<info>%s</info>', $suggestion));
-            }
-
-            return 0;
+        // unless by-package is also present then we enable both
+        if ($input->getOption('by-package')) {
+            $mode |= SuggestedPackagesReporter::MODE_BY_PACKAGE;
         }
         }
-
-        // Grouped by package
-        if ($mode & 1) {
-            foreach ($suggesters as $suggester => $suggestions) {
-                $io->write(sprintf('<comment>%s</comment> suggests:', $suggester));
-
-                foreach ($suggestions as $suggestion => $reason) {
-                    $io->write(sprintf(' - <info>%s</info>: %s', $suggestion, $reason ?: '*'));
-                }
-                $io->write('');
-            }
+        // list is exclusive and overrides everything else
+        if ($input->getOption('list')) {
+            $mode = SuggestedPackagesReporter::MODE_LIST;
         }
         }
 
 
-        // Grouped by suggestion
-        if ($mode & 2) {
-            // Improve readability in full mode
-            if ($mode & 1) {
-                $io->write(str_repeat('-', 78));
-            }
-            foreach ($suggested as $suggestion => $suggesters) {
-                $io->write(sprintf('<comment>%s</comment> is suggested by:', $suggestion));
-
-                foreach ($suggesters as $suggester => $reason) {
-                    $io->write(sprintf(' - <info>%s</info>: %s', $suggester, $reason ?: '*'));
-                }
-                $io->write('');
-            }
-        }
+        $reporter->output($mode, $installedRepo);
 
 
         return 0;
         return 0;
     }
     }

+ 0 - 2
src/Composer/Command/UpdateCommand.php

@@ -48,7 +48,6 @@ class UpdateCommand extends BaseCommand
                 new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
                 new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
-                new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
                 new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'),
                 new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'),
                 new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'),
                 new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'),
                 new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
                 new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
@@ -154,7 +153,6 @@ EOT
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDevMode(!$input->getOption('no-dev'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setDumpAutoloader(!$input->getOption('no-autoloader'))
             ->setRunScripts(!$input->getOption('no-scripts'))
             ->setRunScripts(!$input->getOption('no-scripts'))
-            ->setSkipSuggest($input->getOption('no-suggest'))
             ->setOptimizeAutoloader($optimize)
             ->setOptimizeAutoloader($optimize)
             ->setClassMapAuthoritative($authoritative)
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setApcuAutoloader($apcu)

+ 7 - 17
src/Composer/Installer.php

@@ -131,7 +131,6 @@ class Installer
     protected $ignorePlatformReqs = false;
     protected $ignorePlatformReqs = false;
     protected $preferStable = false;
     protected $preferStable = false;
     protected $preferLowest = false;
     protected $preferLowest = false;
-    protected $skipSuggest = false;
     protected $writeLock;
     protected $writeLock;
     protected $executeOperations = true;
     protected $executeOperations = true;
 
 
@@ -257,9 +256,13 @@ class Installer
             $this->installationManager->notifyInstalls($this->io);
             $this->installationManager->notifyInstalls($this->io);
         }
         }
 
 
-        // output suggestions if we're in dev mode
-        if ($this->update && $this->devMode && !$this->skipSuggest) {
-            $this->suggestedPackagesReporter->output($this->locker->getLockedRepository($this->devMode));
+        if ($this->update) {
+            $installedRepos = array(
+                $this->locker->getLockedRepository($this->devMode),
+                $this->createPlatformRepo(false),
+                new RootPackageRepository(array(clone $this->package)),
+            );
+            $this->suggestedPackagesReporter->outputMinimalistic(new CompositeRepository($installedRepos));
         }
         }
 
 
         // Find abandoned packages and warn user
         // Find abandoned packages and warn user
@@ -1310,19 +1313,6 @@ class Installer
         return $this;
         return $this;
     }
     }
 
 
-    /**
-     * Should suggestions be skipped?
-     *
-     * @param  bool      $skipSuggest
-     * @return Installer
-     */
-    public function setSkipSuggest($skipSuggest = true)
-    {
-        $this->skipSuggest = (bool) $skipSuggest;
-
-        return $this;
-    }
-
     /**
     /**
      * Disables plugins.
      * Disables plugins.
      *
      *

+ 86 - 15
src/Composer/Installer/SuggestedPackagesReporter.php

@@ -24,6 +24,10 @@ use Symfony\Component\Console\Formatter\OutputFormatter;
  */
  */
 class SuggestedPackagesReporter
 class SuggestedPackagesReporter
 {
 {
+    const MODE_LIST = 1;
+    const MODE_BY_PACKAGE = 2;
+    const MODE_BY_SUGGESTION = 4;
+
     /**
     /**
      * @var array
      * @var array
      */
      */
@@ -91,38 +95,105 @@ class SuggestedPackagesReporter
 
 
     /**
     /**
      * Output suggested packages.
      * Output suggested packages.
+     *
      * Do not list the ones already installed if installed repository provided.
      * Do not list the ones already installed if installed repository provided.
      *
      *
-     * @param  RepositoryInterface       $installedRepo Installed packages
+     * @param  int                       $mode One of the MODE_* constants from this class
      * @return SuggestedPackagesReporter
      * @return SuggestedPackagesReporter
      */
      */
-    public function output(RepositoryInterface $lockedRepo = null)
+    public function output($mode, RepositoryInterface $installedRepo = null)
+    {
+        $suggestedPackages = $this->getFilteredSuggestions($installedRepo);
+
+        $suggesters = array();
+        $suggested = array();
+        foreach ($suggestedPackages as $suggestion) {
+            $suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason'];
+            $suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason'];
+        }
+        ksort($suggesters);
+        ksort($suggested);
+
+        // Simple mode
+        if ($mode & self::MODE_LIST) {
+            foreach (array_keys($suggested) as $name) {
+                $this->io->write(sprintf('<info>%s</info>', $name));
+            }
+
+            return 0;
+        }
+
+        // Grouped by package
+        if ($mode & self::MODE_BY_PACKAGE) {
+            foreach ($suggesters as $suggester => $suggestions) {
+                $this->io->write(sprintf('<comment>%s</comment> suggests:', $suggester));
+
+                foreach ($suggestions as $suggestion => $reason) {
+                    $this->io->write(sprintf(' - <info>%s</info>' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason)));
+                }
+                $this->io->write('');
+            }
+        }
+
+        // Grouped by suggestion
+        if ($mode & self::MODE_BY_SUGGESTION) {
+            // Improve readability in full mode
+            if ($mode & self::MODE_BY_PACKAGE) {
+                $this->io->write(str_repeat('-', 78));
+            }
+            foreach ($suggested as $suggestion => $suggesters) {
+                $this->io->write(sprintf('<comment>%s</comment> is suggested by:', $suggestion));
+
+                foreach ($suggesters as $suggester => $reason) {
+                    $this->io->write(sprintf(' - <info>%s</info>' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason)));
+                }
+                $this->io->write('');
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Output number of new suggested packages and a hint to use suggest command.
+     **
+     * Do not list the ones already installed if installed repository provided.
+     *
+     * @return SuggestedPackagesReporter
+     */
+    public function outputMinimalistic(RepositoryInterface $installedRepo = null)
+    {
+        $suggestedPackages = $this->getFilteredSuggestions($installedRepo);
+        if ($suggestedPackages) {
+            $this->io->writeError(count($suggestedPackages).' package suggestions were added by new dependencies, use <info>composer suggest</info> to see details.');
+        }
+
+        return $this;
+    }
+
+    private function getFilteredSuggestions(RepositoryInterface $installedRepo = null)
     {
     {
         $suggestedPackages = $this->getPackages();
         $suggestedPackages = $this->getPackages();
-        $lockedPackages = array();
-        if (null !== $lockedRepo && ! empty($suggestedPackages)) {
-            foreach ($lockedRepo->getPackages() as $package) {
-                $lockedPackages = array_merge(
-                    $lockedPackages,
+        $installedNames = array();
+        if (null !== $installedRepo && !empty($suggestedPackages)) {
+            foreach ($installedRepo->getPackages() as $package) {
+                $installedNames = array_merge(
+                    $installedNames,
                     $package->getNames()
                     $package->getNames()
                 );
                 );
             }
             }
         }
         }
 
 
+        $suggestions = array();
         foreach ($suggestedPackages as $suggestion) {
         foreach ($suggestedPackages as $suggestion) {
-            if (in_array($suggestion['target'], $lockedPackages)) {
+            if (in_array($suggestion['target'], $installedNames)) {
                 continue;
                 continue;
             }
             }
 
 
-            $this->io->writeError(sprintf(
-                '%s suggests installing %s%s',
-                $suggestion['source'],
-                $this->escapeOutput($suggestion['target']),
-                $this->escapeOutput('' !== $suggestion['reason'] ? ' ('.$suggestion['reason'].')' : '')
-            ));
+            $suggestions[] = $suggestion;
         }
         }
 
 
-        return $this;
+        return $suggestions;
     }
     }
 
 
     /**
     /**

+ 32 - 0
tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test

@@ -0,0 +1,32 @@
+--TEST--
+Suggestions are displayed even in non-dev mode for new suggesters installed when updating the lock file
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } }
+            ]
+        }
+    ],
+    "require": {
+        "a/a": "1.0.0"
+    }
+}
+--RUN--
+install --no-dev
+--EXPECT-OUTPUT--
+<warning>No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.</warning>
+Loading composer repositories with package information
+Updating dependencies
+Lock file operations: 1 install, 0 updates, 0 removals
+  - Locking a/a (1.0.0)
+Writing lock file
+Installing dependencies from lock file
+Package operations: 1 install, 0 updates, 0 removals
+1 package suggestions were added by new dependencies, use composer suggest to see details.
+Generating autoload files
+
+--EXPECT--
+Installing a/a (1.0.0)

+ 18 - 9
tests/Composer/Test/Fixtures/installer/suggest-prod.test

@@ -1,5 +1,5 @@
 --TEST--
 --TEST--
-Suggestions are not displayed in non-dev mode
+Suggestions are not displayed for when not updating the lock file
 --COMPOSER--
 --COMPOSER--
 {
 {
     "repositories": [
     "repositories": [
@@ -14,16 +14,25 @@ Suggestions are not displayed in non-dev mode
         "a/a": "1.0.0"
         "a/a": "1.0.0"
     }
     }
 }
 }
+--LOCK--
+{
+    "packages": [
+        { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 --RUN--
-install --no-dev
+install
 --EXPECT-OUTPUT--
 --EXPECT-OUTPUT--
-<warning>No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.</warning>
-Loading composer repositories with package information
-Updating dependencies
-Lock file operations: 1 install, 0 updates, 0 removals
-  - Locking a/a (1.0.0)
-Writing lock file
-Installing dependencies from lock file
+Installing dependencies from lock file (including require-dev)
+Verifying lock file contents can be installed on current platform.
 Package operations: 1 install, 0 updates, 0 removals
 Package operations: 1 install, 0 updates, 0 removals
 Generating autoload files
 Generating autoload files
 
 

+ 1 - 1
tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test

@@ -25,7 +25,7 @@ Lock file operations: 1 install, 0 updates, 0 removals
 Writing lock file
 Writing lock file
 Installing dependencies from lock file (including require-dev)
 Installing dependencies from lock file (including require-dev)
 Package operations: 1 install, 0 updates, 0 removals
 Package operations: 1 install, 0 updates, 0 removals
-a/a suggests installing b/b (an obscure reason)
+1 package suggestions were added by new dependencies, use composer suggest to see details.
 Generating autoload files
 Generating autoload files
 
 
 --EXPECT--
 --EXPECT--

+ 56 - 29
tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php

@@ -33,14 +33,13 @@ class SuggestedPackagesReporterTest extends TestCase
     /**
     /**
      * @covers ::__construct
      * @covers ::__construct
      */
      */
-    public function testContrsuctor()
+    public function testConstructor()
     {
     {
         $this->io->expects($this->once())
         $this->io->expects($this->once())
-            ->method('writeError');
+            ->method('write');
 
 
-        $suggestedPackagesReporter = new SuggestedPackagesReporter($this->io);
-        $suggestedPackagesReporter->addPackage('a', 'b', 'c');
-        $suggestedPackagesReporter->output();
+        $this->suggestedPackagesReporter->addPackage('a', 'b', 'c');
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_LIST);
     }
     }
 
 
     /**
     /**
@@ -135,25 +134,33 @@ class SuggestedPackagesReporterTest extends TestCase
     {
     {
         $this->suggestedPackagesReporter->addPackage('a', 'b', 'c');
         $this->suggestedPackagesReporter->addPackage('a', 'b', 'c');
 
 
-        $this->io->expects($this->once())
-            ->method('writeError')
-            ->with('a suggests installing b (c)');
+        $this->io->expects($this->at(0))
+            ->method('write')
+            ->with('<comment>a</comment> suggests:');
+
+        $this->io->expects($this->at(1))
+            ->method('write')
+            ->with(' - <info>b</info>: c');
 
 
-        $this->suggestedPackagesReporter->output();
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE);
     }
     }
 
 
     /**
     /**
      * @covers ::output
      * @covers ::output
      */
      */
-    public function testOutputWithNoSuggestedPackage()
+    public function testOutputWithNoSuggestionReason()
     {
     {
         $this->suggestedPackagesReporter->addPackage('a', 'b', '');
         $this->suggestedPackagesReporter->addPackage('a', 'b', '');
 
 
-        $this->io->expects($this->once())
-            ->method('writeError')
-            ->with('a suggests installing b');
+        $this->io->expects($this->at(0))
+            ->method('write')
+            ->with('<comment>a</comment> suggests:');
+
+        $this->io->expects($this->at(1))
+            ->method('write')
+            ->with(' - <info>b</info>');
 
 
-        $this->suggestedPackagesReporter->output();
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE);
     }
     }
 
 
     /**
     /**
@@ -165,14 +172,18 @@ class SuggestedPackagesReporterTest extends TestCase
         $this->suggestedPackagesReporter->addPackage('source', 'target2', "<bg=green>Like us on Facebook</>");
         $this->suggestedPackagesReporter->addPackage('source', 'target2', "<bg=green>Like us on Facebook</>");
 
 
         $this->io->expects($this->at(0))
         $this->io->expects($this->at(0))
-            ->method('writeError')
-            ->with("source suggests installing target1 ([1;37;42m Like us on Facebook [0m)");
+            ->method('write')
+            ->with('<comment>source</comment> suggests:');
 
 
         $this->io->expects($this->at(1))
         $this->io->expects($this->at(1))
-            ->method('writeError')
-            ->with('source suggests installing target2 (\\<bg=green>Like us on Facebook\\</>)');
+            ->method('write')
+            ->with(' - <info>target1</info>: [1;37;42m Like us on Facebook [0m');
 
 
-        $this->suggestedPackagesReporter->output();
+        $this->io->expects($this->at(2))
+            ->method('write')
+            ->with(' - <info>target2</info>: \\<bg=green>Like us on Facebook\\</>');
+
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE);
     }
     }
 
 
     /**
     /**
@@ -184,14 +195,26 @@ class SuggestedPackagesReporterTest extends TestCase
         $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons');
         $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons');
 
 
         $this->io->expects($this->at(0))
         $this->io->expects($this->at(0))
-            ->method('writeError')
-            ->with('a suggests installing b (c)');
+            ->method('write')
+            ->with('<comment>a</comment> suggests:');
 
 
         $this->io->expects($this->at(1))
         $this->io->expects($this->at(1))
-            ->method('writeError')
-            ->with('source package suggests installing target (because reasons)');
+            ->method('write')
+            ->with(' - <info>b</info>: c');
+
+        $this->io->expects($this->at(2))
+            ->method('write')
+            ->with('');
+
+        $this->io->expects($this->at(3))
+            ->method('write')
+            ->with('<comment>source package</comment> suggests:');
+
+        $this->io->expects($this->at(4))
+            ->method('write')
+            ->with(' - <info>target</info>: because reasons');
 
 
-        $this->suggestedPackagesReporter->output();
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE);
     }
     }
 
 
     /**
     /**
@@ -221,11 +244,15 @@ class SuggestedPackagesReporterTest extends TestCase
         $this->suggestedPackagesReporter->addPackage('a', 'b', 'c');
         $this->suggestedPackagesReporter->addPackage('a', 'b', 'c');
         $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons');
         $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons');
 
 
-        $this->io->expects($this->once())
-            ->method('writeError')
-            ->with('source package suggests installing target (because reasons)');
+        $this->io->expects($this->at(0))
+            ->method('write')
+            ->with('<comment>source package</comment> suggests:');
+
+        $this->io->expects($this->at(1))
+            ->method('write')
+            ->with(' - <info>target</info>: because reasons');
 
 
-        $this->suggestedPackagesReporter->output($repository);
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository);
     }
     }
 
 
     /**
     /**
@@ -237,7 +264,7 @@ class SuggestedPackagesReporterTest extends TestCase
         $repository->expects($this->exactly(0))
         $repository->expects($this->exactly(0))
             ->method('getPackages');
             ->method('getPackages');
 
 
-        $this->suggestedPackagesReporter->output($repository);
+        $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository);
     }
     }
 
 
     private function getSuggestedPackageArray()
     private function getSuggestedPackageArray()