Эх сурвалжийг харах

Merge pull request #8558 from Seldaek/error-reporting

Improve error reporting of solver issues
Nils Adermann 5 жил өмнө
parent
commit
65dfb26c77
47 өөрчлөгдсөн 842 нэмэгдсэн , 291 устгасан
  1. 1 1
      src/Composer/Command/BaseDependencyCommand.php
  2. 1 3
      src/Composer/DependencyResolver/PoolBuilder.php
  3. 168 82
      src/Composer/DependencyResolver/Problem.php
  4. 87 65
      src/Composer/DependencyResolver/Rule.php
  5. 5 3
      src/Composer/DependencyResolver/RuleSet.php
  6. 6 8
      src/Composer/DependencyResolver/Solver.php
  7. 6 6
      src/Composer/DependencyResolver/SolverProblemsException.php
  8. 2 1
      src/Composer/Downloader/GitDownloader.php
  9. 9 9
      src/Composer/Installer.php
  10. 1 1
      src/Composer/Plugin/PluginManager.php
  11. 8 0
      src/Composer/Repository/ArrayRepository.php
  12. 5 0
      src/Composer/Repository/ArtifactRepository.php
  13. 23 0
      src/Composer/Repository/ComposerRepository.php
  14. 5 0
      src/Composer/Repository/CompositeRepository.php
  15. 4 0
      src/Composer/Repository/InstalledArrayRepository.php
  16. 4 0
      src/Composer/Repository/InstalledFilesystemRepository.php
  17. 4 0
      src/Composer/Repository/LockArrayRepository.php
  18. 5 0
      src/Composer/Repository/PackageRepository.php
  19. 6 0
      src/Composer/Repository/PathRepository.php
  20. 5 0
      src/Composer/Repository/PearRepository.php
  21. 7 2
      src/Composer/Repository/PlatformRepository.php
  22. 9 0
      src/Composer/Repository/RepositoryInterface.php
  23. 61 20
      src/Composer/Repository/RepositorySet.php
  24. 4 0
      src/Composer/Repository/RootPackageRepository.php
  25. 12 0
      src/Composer/Repository/VcsRepository.php
  26. 0 11
      src/Composer/Util/AuthHelper.php
  27. 2 13
      src/Composer/Util/Git.php
  28. 2 13
      src/Composer/Util/Hg.php
  29. 4 4
      src/Composer/Util/Http/CurlDownloader.php
  30. 2 2
      src/Composer/Util/RemoteFilesystem.php
  31. 17 0
      src/Composer/Util/Url.php
  32. 4 1
      tests/Composer/Test/DependencyResolver/RuleSetTest.php
  33. 4 1
      tests/Composer/Test/DependencyResolver/RuleTest.php
  34. 10 14
      tests/Composer/Test/DependencyResolver/SolverTest.php
  35. 1 1
      tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
  36. 38 0
      tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test
  37. 40 0
      tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test
  38. 1 1
      tests/Composer/Test/Fixtures/installer/github-issues-4319.test
  39. 2 10
      tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test
  40. 49 0
      tests/Composer/Test/Fixtures/installer/provider-conflicts.test
  41. 45 0
      tests/Composer/Test/Fixtures/installer/provider-conflicts2.test
  42. 54 0
      tests/Composer/Test/Fixtures/installer/provider-conflicts3.test
  43. 1 1
      tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test
  44. 1 8
      tests/Composer/Test/Fixtures/installer/repositories-priorities.test
  45. 93 10
      tests/Composer/Test/Fixtures/installer/solver-problems.test
  46. 3 0
      tests/Composer/Test/InstallerTest.php
  47. 21 0
      tests/Composer/Test/Util/UrlTest.php

+ 1 - 1
src/Composer/Command/BaseDependencyCommand.php

@@ -89,7 +89,7 @@ class BaseDependencyCommand extends BaseCommand
         );
         );
 
 
         // Find packages that are or provide the requested package first
         // Find packages that are or provide the requested package first
-        $packages = $repositorySet->findPackages(strtolower($needle), null, false);
+        $packages = $repositorySet->findPackages(strtolower($needle), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS);
         if (empty($packages)) {
         if (empty($packages)) {
             throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle));
             throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle));
         }
         }

+ 1 - 3
src/Composer/DependencyResolver/PoolBuilder.php

@@ -31,7 +31,6 @@ class PoolBuilder
     private $stabilityFlags;
     private $stabilityFlags;
     private $rootAliases;
     private $rootAliases;
     private $rootReferences;
     private $rootReferences;
-    private $rootRequires;
 
 
     private $aliasMap = array();
     private $aliasMap = array();
     private $nameConstraints = array();
     private $nameConstraints = array();
@@ -39,13 +38,12 @@ class PoolBuilder
     private $packages = array();
     private $packages = array();
     private $unacceptableFixedPackages = array();
     private $unacceptableFixedPackages = array();
 
 
-    public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $rootRequires = array())
+    public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences)
     {
     {
         $this->acceptableStabilities = $acceptableStabilities;
         $this->acceptableStabilities = $acceptableStabilities;
         $this->stabilityFlags = $stabilityFlags;
         $this->stabilityFlags = $stabilityFlags;
         $this->rootAliases = $rootAliases;
         $this->rootAliases = $rootAliases;
         $this->rootReferences = $rootReferences;
         $this->rootReferences = $rootReferences;
-        $this->rootRequires = $rootRequires;
     }
     }
 
 
     public function buildPool(array $repositories, Request $request)
     public function buildPool(array $repositories, Request $request)

+ 168 - 82
src/Composer/DependencyResolver/Problem.php

@@ -13,6 +13,8 @@
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
 use Composer\Package\CompletePackageInterface;
 use Composer\Package\CompletePackageInterface;
+use Composer\Repository\RepositorySet;
+use Composer\Semver\Constraint\Constraint;
 
 
 /**
 /**
  * Represents a problem detected while solving dependencies
  * Represents a problem detected while solving dependencies
@@ -35,13 +37,6 @@ class Problem
 
 
     protected $section = 0;
     protected $section = 0;
 
 
-    protected $pool;
-
-    public function __construct(Pool $pool)
-    {
-        $this->pool = $pool;
-    }
-
     /**
     /**
      * Add a rule as a reason
      * Add a rule as a reason
      *
      *
@@ -68,7 +63,7 @@ class Problem
      * @param  array  $installedMap A map of all present packages
      * @param  array  $installedMap A map of all present packages
      * @return string
      * @return string
      */
      */
-    public function getPrettyString(array $installedMap = array(), array $learnedPool = array())
+    public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
     {
     {
         // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections?
         // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections?
         $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
         $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
@@ -81,91 +76,25 @@ class Problem
                 throw new \LogicException("Single reason problems must contain a request rule.");
                 throw new \LogicException("Single reason problems must contain a request rule.");
             }
             }
 
 
-            $request = $rule->getReasonData();
-            $packageName = $request['packageName'];
-            $constraint = $request['constraint'];
+            $reasonData = $rule->getReasonData();
+            $packageName = $reasonData['packageName'];
+            $constraint = $reasonData['constraint'];
 
 
             if (isset($constraint)) {
             if (isset($constraint)) {
-                $packages = $this->pool->whatProvides($packageName, $constraint);
+                $packages = $pool->whatProvides($packageName, $constraint);
             } else {
             } else {
                 $packages = array();
                 $packages = array();
             }
             }
 
 
             if (empty($packages)) {
             if (empty($packages)) {
-                // handle php/hhvm
-                if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
-                    $version = phpversion();
-                    $available = $this->pool->whatProvides($packageName);
-
-                    if (count($available)) {
-                        $firstAvailable = reset($available);
-                        $version = $firstAvailable->getPrettyVersion();
-                        $extra = $firstAvailable->getExtra();
-                        if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
-                            $version .= '; ' . $firstAvailable->getDescription();
-                        }
-                    }
-
-                    $msg = "\n    - This package requires ".$packageName.$this->constraintToText($constraint).' but ';
-
-                    if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
-                        return $msg . 'your HHVM version does not satisfy that requirement.';
-                    }
-
-                    if ($packageName === 'hhvm') {
-                        return $msg . 'you are running this with PHP and not HHVM.';
-                    }
-
-                    return $msg . 'your PHP version ('. $version .') does not satisfy that requirement.';
-                }
-
-                // handle php extensions
-                if (0 === stripos($packageName, 'ext-')) {
-                    if (false !== strpos($packageName, ' ')) {
-                        return "\n    - The requested PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.';
-                    }
-
-                    $ext = substr($packageName, 4);
-                    $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
-
-                    return "\n    - The requested PHP extension ".$packageName.$this->constraintToText($constraint).' '.$error.'. Install or enable PHP\'s '.$ext.' extension.';
-                }
-
-                // handle linked libs
-                if (0 === stripos($packageName, 'lib-')) {
-                    if (strtolower($packageName) === 'lib-icu') {
-                        $error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.';
-
-                        return "\n    - The requested linked library ".$packageName.$this->constraintToText($constraint).' '.$error;
-                    }
-
-                    return "\n    - The requested linked library ".$packageName.$this->constraintToText($constraint).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.';
-                }
-
-                if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
-                    $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
-
-                    return "\n    - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.';
-                }
-
-                // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet
-                /*if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) {
-                    return "\n    - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.';
-                }*/
-
-                // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet
-                /*if ($providers = $this->pool->whatProvides($packageName, null, true, true)) {
-                    return "\n    - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.';
-                }*/
-
-                return "\n    - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.';
+                return "\n    ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint));
             }
             }
         }
         }
 
 
         $messages = array();
         $messages = array();
 
 
         foreach ($reasons as $rule) {
         foreach ($reasons as $rule) {
-            $messages[] = $rule->getPrettyString($this->pool, $installedMap, $learnedPool);
+            $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
         }
         }
 
 
         return "\n    - ".implode("\n    - ", $messages);
         return "\n    - ".implode("\n    - ", $messages);
@@ -193,7 +122,150 @@ class Problem
         $this->section++;
         $this->section++;
     }
     }
 
 
-    protected function getPackageList($packages)
+    /**
+     * @internal
+     */
+    public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null)
+    {
+        // handle php/hhvm
+        if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
+            $version = phpversion();
+            $available = $pool->whatProvides($packageName);
+
+            if (count($available)) {
+                $firstAvailable = reset($available);
+                $version = $firstAvailable->getPrettyVersion();
+                $extra = $firstAvailable->getExtra();
+                if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
+                    $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription());
+                }
+            }
+
+            $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but ';
+
+            if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
+                return array($msg, 'your HHVM version does not satisfy that requirement.');
+            }
+
+            if ($packageName === 'hhvm') {
+                return array($msg, 'you are running this with PHP and not HHVM.');
+            }
+
+            return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.');
+        }
+
+        // handle php extensions
+        if (0 === stripos($packageName, 'ext-')) {
+            if (false !== strpos($packageName, ' ')) {
+                return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.');
+            }
+
+            $ext = substr($packageName, 4);
+            $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system';
+
+            return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.');
+        }
+
+        // handle linked libs
+        if (0 === stripos($packageName, 'lib-')) {
+            if (strtolower($packageName) === 'lib-icu') {
+                $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.';
+
+                return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error);
+            }
+
+            return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.');
+        }
+
+        $fixedPackage = null;
+        foreach ($request->getFixedPackages() as $package) {
+            if ($package->getName() === $packageName) {
+                $fixedPackage = $package;
+                if ($pool->isUnacceptableFixedPackage($package)) {
+                    return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update.');
+                }
+                break;
+            }
+        }
+
+        // first check if the actual requested package is found in normal conditions
+        // if so it must mean it is rejected by another constraint than the one given here
+        if ($packages = $repositorySet->findPackages($packageName, $constraint)) {
+            $rootReqs = $repositorySet->getRootRequires();
+            if (isset($rootReqs[$packageName])) {
+                $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) {
+                    return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
+                });
+                if (0 === count($filtered)) {
+                    return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').');
+                }
+            }
+
+            if ($fixedPackage) {
+                $fixedConstraint = new Constraint('==', $fixedPackage->getVersion());
+                $filtered = array_filter($packages, function ($p) use ($fixedConstraint) {
+                    return $fixedConstraint->matches(new Constraint('==', $p->getVersion()));
+                });
+                if (0 === count($filtered)) {
+                    return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update.');
+                }
+            }
+
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.');
+        }
+
+        // check if the package is found when bypassing stability checks
+        if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.');
+        }
+
+        // check if the package is found when bypassing the constraint check
+        if ($packages = $repositorySet->findPackages($packageName, null)) {
+            // we must first verify if a valid package would be found in a lower priority repository
+            if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
+                $higherRepoPackages = $repositorySet->findPackages($packageName, null);
+                $nextRepoPackages = array();
+                $nextRepo = null;
+
+                foreach ($allReposPackages as $package) {
+                    if ($nextRepo === null || $nextRepo === $package->getRepository()) {
+                        $nextRepoPackages[] = $package;
+                        $nextRepo = $package->getRepository();
+                    } else {
+                        break;
+                    }
+                }
+
+                return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.');
+            }
+
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.');
+        }
+
+        if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
+            $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
+
+            return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.');
+        }
+
+        if ($providers = $repositorySet->getProviders($packageName)) {
+            $maxProviders = 20;
+            $providersStr = implode(array_map(function ($p) {
+                return "      - ${p['name']} ".substr($p['description'], 0, 100)."\n";
+            }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers));
+            if (count($providers) > $maxProviders+1) {
+                $providersStr .= '      ... and '.(count($providers)-$maxProviders).' more.'."\n";
+            }
+            return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it: \n".$providersStr."      Consider requiring one of these to satisfy the $packageName requirement.");
+        }
+
+        return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name.");
+    }
+
+    /**
+     * @internal
+     */
+    public static function getPackageList(array $packages)
     {
     {
         $prepared = array();
         $prepared = array();
         foreach ($packages as $package) {
         foreach ($packages as $package) {
@@ -207,13 +279,27 @@ class Problem
         return implode(', ', $prepared);
         return implode(', ', $prepared);
     }
     }
 
 
+    private static function hasMultipleNames(array $packages)
+    {
+        $name = null;
+        foreach ($packages as $package) {
+            if ($name === null || $name === $package->getName()) {
+                $name = $package->getName();
+            } else {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
     /**
      * Turns a constraint into text usable in a sentence describing a request
      * Turns a constraint into text usable in a sentence describing a request
      *
      *
      * @param  \Composer\Semver\Constraint\ConstraintInterface $constraint
      * @param  \Composer\Semver\Constraint\ConstraintInterface $constraint
      * @return string
      * @return string
      */
      */
-    protected function constraintToText($constraint)
+    protected static function constraintToText($constraint)
     {
     {
         return $constraint ? ' '.$constraint->getPrettyString() : '';
         return $constraint ? ' '.$constraint->getPrettyString() : '';
     }
     }

+ 87 - 65
src/Composer/DependencyResolver/Rule.php

@@ -15,6 +15,7 @@ namespace Composer\DependencyResolver;
 use Composer\Package\CompletePackage;
 use Composer\Package\CompletePackage;
 use Composer\Package\Link;
 use Composer\Package\Link;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
+use Composer\Repository\RepositorySet;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
@@ -122,7 +123,7 @@ abstract class Rule
 
 
     abstract public function isAssertion();
     abstract public function isAssertion();
 
 
-    public function getPrettyString(Pool $pool, array $installedMap = array(), array $learnedPool = array())
+    public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
     {
     {
         $literals = $this->getLiterals();
         $literals = $this->getLiterals();
 
 
@@ -161,7 +162,7 @@ abstract class Rule
                 $package1 = $pool->literalToPackage($literals[0]);
                 $package1 = $pool->literalToPackage($literals[0]);
                 $package2 = $pool->literalToPackage($literals[1]);
                 $package2 = $pool->literalToPackage($literals[1]);
 
 
-                return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.';
+                return $package2->getPrettyString().' conflicts with '.$package1->getPrettyString().'.';
 
 
             case self::RULE_PACKAGE_REQUIRES:
             case self::RULE_PACKAGE_REQUIRES:
                 $sourceLiteral = array_shift($literals);
                 $sourceLiteral = array_shift($literals);
@@ -178,85 +179,103 @@ abstract class Rule
                 } else {
                 } else {
                     $targetName = $this->reasonData->getTarget();
                     $targetName = $this->reasonData->getTarget();
 
 
-                    if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') {
-                        // handle php/hhvm
-                        if (defined('HHVM_VERSION')) {
-                            return $text . ' -> your HHVM version does not satisfy that requirement.';
-                        }
+                    $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint());
 
 
-                        $packages = $pool->whatProvides($targetName);
-                        $package = count($packages) ? current($packages) : phpversion();
+                    return $text . ' -> ' . $reason[1];
+                }
 
 
-                        if ($targetName === 'hhvm') {
-                            if ($package instanceof CompletePackage) {
-                                return $text . ' -> your HHVM version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
-                            } else {
-                                return $text . ' -> you are running this with PHP and not HHVM.';
-                            }
-                        }
+                return $text;
 
 
+            case self::RULE_PACKAGE_OBSOLETES:
+                if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) {
+                    $package1 = $pool->literalToPackage($literals[0]);
+                    $package2 = $pool->literalToPackage($literals[1]);
+
+                    $replaces1 = $this->getReplacedNames($package1);
+                    $replaces2 = $this->getReplacedNames($package2);
+
+                    $reason = null;
+                    if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) {
+                        $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and can thus not coexist.';
+                    } elseif (in_array($package1->getName(), $replaces2, true)) {
+                        $reason = $package2->getName().' replaces '.$package1->getName().' and can thus not coexist with it.';
+                    } elseif (in_array($package2->getName(), $replaces1, true)) {
+                        $reason = $package1->getName().' replaces '.$package2->getName().' and can thus not coexist with it.';
+                    }
 
 
-                        if (!($package instanceof CompletePackage)) {
-                            return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.';
+                    if ($reason) {
+                        if (isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) {
+                            // swap vars so the if below passes
+                            $tmp = $package2;
+                            $package2 = $package1;
+                            $package1 = $tmp;
                         }
                         }
-
-                        $extra = $package->getExtra();
-
-                        if (!empty($extra['config.platform'])) {
-                            $text .= ' -> your PHP version ('.phpversion().') overridden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
-                        } else {
-                            $text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
+                        if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) {
+                            return $package1->getPrettyString().' can not be installed as that would require removing '.$package2->getPrettyString().'. '.$reason;
                         }
                         }
 
 
-                        return $text;
+                        if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) {
+                            return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. '.$reason;
+                        }
                     }
                     }
 
 
-                    if (0 === strpos($targetName, 'ext-')) {
-                        // handle php extensions
-                        $ext = substr($targetName, 4);
-                        $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
+                    return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.';
+                }
 
 
-                        return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.';
+                return $ruleText;
+            case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
+                return $ruleText;
+            case self::RULE_PACKAGE_SAME_NAME:
+                $replacedNames = null;
+                $packageNames = array();
+                foreach ($literals as $literal) {
+                    $package = $pool->literalToPackage($literal);
+                    $pkgReplaces = $this->getReplacedNames($package);
+                    if ($pkgReplaces) {
+                        if ($replacedNames === null) {
+                            $replacedNames = $this->getReplacedNames($package);
+                        } else {
+                            $replacedNames = array_intersect($replacedNames, $this->getReplacedNames($package));
+                        }
                     }
                     }
+                    $packageNames[$package->getName()] = true;
+                }
 
 
-                    if (0 === strpos($targetName, 'lib-')) {
-                        // handle linked libs
-                        $lib = substr($targetName, 4);
-
-                        return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.';
+                if ($replacedNames) {
+                    $replacedNames = array_values(array_intersect(array_keys($packageNames), $replacedNames));
+                }
+                if ($replacedNames && count($packageNames) > 1) {
+                    $replacer = null;
+                    foreach ($literals as $literal) {
+                        $package = $pool->literalToPackage($literal);
+                        if (array_intersect($replacedNames, $this->getReplacedNames($package))) {
+                            $replacer = $package;
+                            break;
+                        }
                     }
                     }
+                    $replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0];
 
 
-                    // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet
-                    /*if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) {
-                        return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.';
-                    }*/
-
-                    return $text . ' -> no matching package found.';
+                    if ($replacer) {
+                        return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and can thus not coexist with it.';
+                    }
                 }
                 }
 
 
-                return $text;
-
-            case self::RULE_PACKAGE_OBSOLETES:
-                return $ruleText;
-            case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
-                return $ruleText;
-            case self::RULE_PACKAGE_SAME_NAME:
-                return 'Same name, can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.';
+                return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.';
             case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
             case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
                 return $ruleText;
                 return $ruleText;
             case self::RULE_LEARNED:
             case self::RULE_LEARNED:
-                // TODO not sure this is a good idea, most of these rules should be listed in the problem anyway
-                $learnedString = '(learned rule, ';
                 if (isset($learnedPool[$this->reasonData])) {
                 if (isset($learnedPool[$this->reasonData])) {
+                    $learnedString = ', learned rules:'."\n        - ";
+                    $reasons = array();
                     foreach ($learnedPool[$this->reasonData] as $learnedRule) {
                     foreach ($learnedPool[$this->reasonData] as $learnedRule) {
-                        $learnedString .= $learnedRule->getPrettyString($pool, $installedMap, $learnedPool);
+                        $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
                     }
                     }
+                    $learnedString .= implode("\n        - ", array_unique($reasons));
                 } else {
                 } else {
-                    $learnedString .= 'reasoning unavailable';
+                    $learnedString = ' (reasoning unavailable)';
                 }
                 }
-                $learnedString .= ')';
 
 
-                return 'Conclusion: '.$ruleText.' '.$learnedString;
+                return 'Conclusion: '.$ruleText.$learnedString;
             case self::RULE_PACKAGE_ALIAS:
             case self::RULE_PACKAGE_ALIAS:
                 return $ruleText;
                 return $ruleText;
             default:
             default:
@@ -272,20 +291,23 @@ abstract class Rule
      */
      */
     protected function formatPackagesUnique($pool, array $packages)
     protected function formatPackagesUnique($pool, array $packages)
     {
     {
-        // TODO this is essentially a duplicate of Problem: getPackageList, maintain in one place only?
-
         $prepared = array();
         $prepared = array();
-        foreach ($packages as $package) {
+        foreach ($packages as $index => $package) {
             if (!is_object($package)) {
             if (!is_object($package)) {
-                $package = $pool->literalToPackage($package);
+                $packages[$index] = $pool->literalToPackage($package);
             }
             }
-            $prepared[$package->getName()]['name'] = $package->getPrettyName();
-            $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
         }
         }
-        foreach ($prepared as $name => $package) {
-            $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
+
+        return Problem::getPackageList($packages);
+    }
+
+    private function getReplacedNames(PackageInterface $package)
+    {
+        $names = array();
+        foreach ($package->getReplaces() as $link) {
+            $names[] = $link->getTarget();
         }
         }
 
 
-        return implode(', ', $prepared);
+        return $names;
     }
     }
 }
 }

+ 5 - 3
src/Composer/DependencyResolver/RuleSet.php

@@ -12,6 +12,8 @@
 
 
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
+use Composer\Repository\RepositorySet;
+
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
  */
  */
@@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable
         return array_keys($types);
         return array_keys($types);
     }
     }
 
 
-    public function getPrettyString(Pool $pool = null)
+    public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null)
     {
     {
         $string = "\n";
         $string = "\n";
         foreach ($this->rules as $type => $rules) {
         foreach ($this->rules as $type => $rules) {
             $string .= str_pad(self::$types[$type], 8, ' ') . ": ";
             $string .= str_pad(self::$types[$type], 8, ' ') . ": ";
             foreach ($rules as $rule) {
             foreach ($rules as $rule) {
-                $string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n";
+                $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n";
             }
             }
             $string .= "\n\n";
             $string .= "\n\n";
         }
         }
@@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable
 
 
     public function __toString()
     public function __toString()
     {
     {
-        return $this->getPrettyString(null);
+        return $this->getPrettyString(null, null, null);
     }
     }
 }
 }

+ 6 - 8
src/Composer/DependencyResolver/Solver.php

@@ -14,9 +14,7 @@ namespace Composer\DependencyResolver;
 
 
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
-use Composer\Repository\RepositoryInterface;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
-use Composer\Repository\RepositorySet;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
@@ -29,7 +27,7 @@ class Solver
     /** @var PolicyInterface */
     /** @var PolicyInterface */
     protected $policy;
     protected $policy;
     /** @var Pool */
     /** @var Pool */
-    protected $pool = null;
+    protected $pool;
 
 
     /** @var RuleSet */
     /** @var RuleSet */
     protected $rules;
     protected $rules;
@@ -120,7 +118,7 @@ class Solver
             $conflict = $this->decisions->decisionRule($literal);
             $conflict = $this->decisions->decisionRule($literal);
 
 
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
-                $problem = new Problem($this->pool);
+                $problem = new Problem();
 
 
                 $problem->addRule($rule);
                 $problem->addRule($rule);
                 $problem->addRule($conflict);
                 $problem->addRule($conflict);
@@ -130,7 +128,7 @@ class Solver
             }
             }
 
 
             // conflict with another root require/fixed package
             // conflict with another root require/fixed package
-            $problem = new Problem($this->pool);
+            $problem = new Problem();
             $problem->addRule($rule);
             $problem->addRule($rule);
             $problem->addRule($conflict);
             $problem->addRule($conflict);
 
 
@@ -177,7 +175,7 @@ class Solver
             }
             }
 
 
             if (!$this->pool->whatProvides($packageName, $constraint)) {
             if (!$this->pool->whatProvides($packageName, $constraint)) {
-                $problem = new Problem($this->pool);
+                $problem = new Problem();
                 $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint)));
                 $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint)));
                 $this->problems[] = $problem;
                 $this->problems[] = $problem;
             }
             }
@@ -214,7 +212,7 @@ class Solver
         $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE);
         $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE);
 
 
         if ($this->problems) {
         if ($this->problems) {
-            throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool);
+            throw new SolverProblemsException($this->problems, $this->learnedPool);
         }
         }
 
 
         return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
         return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
@@ -513,7 +511,7 @@ class Solver
      */
      */
     private function analyzeUnsolvable(Rule $conflictRule)
     private function analyzeUnsolvable(Rule $conflictRule)
     {
     {
-        $problem = new Problem($this->pool);
+        $problem = new Problem();
         $problem->addRule($conflictRule);
         $problem->addRule($conflictRule);
 
 
         $this->analyzeUnsolvableRule($problem, $conflictRule);
         $this->analyzeUnsolvableRule($problem, $conflictRule);

+ 6 - 6
src/Composer/DependencyResolver/SolverProblemsException.php

@@ -13,6 +13,7 @@
 namespace Composer\DependencyResolver;
 namespace Composer\DependencyResolver;
 
 
 use Composer\Util\IniHelper;
 use Composer\Util\IniHelper;
+use Composer\Repository\RepositorySet;
 
 
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
@@ -20,24 +21,23 @@ use Composer\Util\IniHelper;
 class SolverProblemsException extends \RuntimeException
 class SolverProblemsException extends \RuntimeException
 {
 {
     protected $problems;
     protected $problems;
-    protected $installedMap;
     protected $learnedPool;
     protected $learnedPool;
 
 
-    public function __construct(array $problems, array $installedMap, array $learnedPool)
+    public function __construct(array $problems, array $learnedPool)
     {
     {
         $this->problems = $problems;
         $this->problems = $problems;
-        $this->installedMap = $installedMap;
         $this->learnedPool = $learnedPool;
         $this->learnedPool = $learnedPool;
 
 
-        parent::__construct($this->createMessage(), 2);
+        parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2);
     }
     }
 
 
-    protected function createMessage()
+    public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool)
     {
     {
+        $installedMap = $request->getPresentMap(true);
         $text = "\n";
         $text = "\n";
         $hasExtensionProblems = false;
         $hasExtensionProblems = false;
         foreach ($this->problems as $i => $problem) {
         foreach ($this->problems as $i => $problem) {
-            $text .= "  Problem ".($i + 1).$problem->getPrettyString($this->installedMap, $this->learnedPool)."\n";
+            $text .= "  Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n";
 
 
             if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
             if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
                 $hasExtensionProblems = true;
                 $hasExtensionProblems = true;

+ 2 - 1
src/Composer/Downloader/GitDownloader.php

@@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
 use Composer\Util\Git as GitUtil;
 use Composer\Util\Git as GitUtil;
+use Composer\Util\Url;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 use Composer\Cache;
 use Composer\Cache;
@@ -434,7 +435,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
             $this->io->writeError('    <warning>'.$reference.' is gone (history was rewritten?)</warning>');
             $this->io->writeError('    <warning>'.$reference.' is gone (history was rewritten?)</warning>');
         }
         }
 
 
-        throw new \RuntimeException(GitUtil::sanitizeUrl('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()));
+        throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()));
     }
     }
 
 
     protected function updateOriginUrl($path, $url)
     protected function updateOriginUrl($path, $url)

+ 9 - 9
src/Composer/Installer.php

@@ -389,14 +389,14 @@ class Installer
         $pool = $repositorySet->createPool($request);
         $pool = $repositorySet->createPool($request);
 
 
         // solve dependencies
         // solve dependencies
-        $solver = new Solver($policy, $pool, $this->io);
+        $solver = new Solver($policy, $pool, $this->io, $repositorySet);
         try {
         try {
             $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs);
             $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs);
             $ruleSetSize = $solver->getRuleSetSize();
             $ruleSetSize = $solver->getRuleSetSize();
             $solver = null;
             $solver = null;
         } catch (SolverProblemsException $e) {
         } catch (SolverProblemsException $e) {
             $this->io->writeError('<error>Your requirements could not be resolved to an installable set of packages.</error>', true, IOInterface::QUIET);
             $this->io->writeError('<error>Your requirements could not be resolved to an installable set of packages.</error>', true, IOInterface::QUIET);
-            $this->io->writeError($e->getMessage());
+            $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool));
             if (!$this->devMode) {
             if (!$this->devMode) {
                 $this->io->writeError('<warning>Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.</warning>', true, IOInterface::QUIET);
                 $this->io->writeError('<warning>Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.</warning>', true, IOInterface::QUIET);
             }
             }
@@ -529,14 +529,14 @@ class Installer
         $pool = $repositorySet->createPool($request);
         $pool = $repositorySet->createPool($request);
 
 
         //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request);
         //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request);
-        $solver = new Solver($policy, $pool, $this->io);
+        $solver = new Solver($policy, $pool, $this->io, $repositorySet);
         try {
         try {
             $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs);
             $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs);
             //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops);
             //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops);
             $solver = null;
             $solver = null;
         } catch (SolverProblemsException $e) {
         } catch (SolverProblemsException $e) {
             $this->io->writeError('<error>Unable to find a compatible set of packages based on your non-dev requirements alone.</error>', true, IOInterface::QUIET);
             $this->io->writeError('<error>Unable to find a compatible set of packages based on your non-dev requirements alone.</error>', true, IOInterface::QUIET);
-            $this->io->writeError($e->getMessage());
+            $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool));
 
 
             return max(1, $e->getCode());
             return max(1, $e->getCode());
         }
         }
@@ -589,7 +589,7 @@ class Installer
             $pool = $repositorySet->createPool($request);
             $pool = $repositorySet->createPool($request);
 
 
             // solve dependencies
             // solve dependencies
-            $solver = new Solver($policy, $pool, $this->io);
+            $solver = new Solver($policy, $pool, $this->io, $repositorySet);
             try {
             try {
                 $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs);
                 $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs);
                 $solver = null;
                 $solver = null;
@@ -602,7 +602,7 @@ class Installer
                 }
                 }
             } catch (SolverProblemsException $e) {
             } catch (SolverProblemsException $e) {
                 $this->io->writeError('<error>Your lock file does not contain a compatible set of packages. Please run composer update.</error>', true, IOInterface::QUIET);
                 $this->io->writeError('<error>Your lock file does not contain a compatible set of packages. Please run composer update.</error>', true, IOInterface::QUIET);
-                $this->io->writeError($e->getMessage());
+                $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool));
 
 
                 return max(1, $e->getCode());
                 return max(1, $e->getCode());
             }
             }
@@ -884,7 +884,7 @@ class Installer
             $packageQueue = new \SplQueue;
             $packageQueue = new \SplQueue;
             $nameMatchesRequiredPackage = false;
             $nameMatchesRequiredPackage = false;
 
 
-            $depPackages = $repositorySet->findPackages($packageName, null, false);
+            $depPackages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_PROVIDERS_REPLACERS);
             $matchesByPattern = array();
             $matchesByPattern = array();
 
 
             // check if the name is a glob pattern that did not match directly
             // check if the name is a glob pattern that did not match directly
@@ -892,7 +892,7 @@ class Installer
                 // add any installed package matching the whitelisted name/pattern
                 // add any installed package matching the whitelisted name/pattern
                 $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$');
                 $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$');
                 foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) {
                 foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) {
-                    $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, false);
+                    $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, RepositorySet::ALLOW_PROVIDERS_REPLACERS);
                 }
                 }
 
 
                 // add root requirements which match the whitelisted name/pattern
                 // add root requirements which match the whitelisted name/pattern
@@ -933,7 +933,7 @@ class Installer
                 $requires = $package->getRequires();
                 $requires = $package->getRequires();
 
 
                 foreach ($requires as $require) {
                 foreach ($requires as $require) {
-                    $requirePackages = $repositorySet->findPackages($require->getTarget(), null, false);
+                    $requirePackages = $repositorySet->findPackages($require->getTarget(), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS);
 
 
                     foreach ($requirePackages as $requirePackage) {
                     foreach ($requirePackages as $requirePackage) {
                         if (isset($this->updateWhitelist[$requirePackage->getName()])) {
                         if (isset($this->updateWhitelist[$requirePackage->getName()])) {

+ 1 - 1
src/Composer/Plugin/PluginManager.php

@@ -412,7 +412,7 @@ class PluginManager
      */
      */
     private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link)
     private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link)
     {
     {
-        $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), false);
+        $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), RepositorySet::ALLOW_PROVIDERS_REPLACERS | RepositorySet::ALLOW_SHADOWED_REPOSITORIES);
 
 
         return !empty($packages) ? $packages[0] : null;
         return !empty($packages) ? $packages[0] : null;
     }
     }

+ 8 - 0
src/Composer/Repository/ArrayRepository.php

@@ -42,6 +42,11 @@ class ArrayRepository extends BaseRepository
         }
         }
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'array repo (defining '.count($this->packages).' package'.(count($this->packages) > 1 ? 's' : '').')';
+    }
+
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
@@ -57,7 +62,9 @@ class ArrayRepository extends BaseRepository
                     (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion())))
                     (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion())))
                     && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability())
                     && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability())
                 ) {
                 ) {
+                    // add selected packages which match stability requirements
                     $result[spl_object_hash($package)] = $package;
                     $result[spl_object_hash($package)] = $package;
+                    // add the aliased package for packages where the alias matches
                     if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) {
                     if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) {
                         $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf();
                         $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf();
                     }
                     }
@@ -67,6 +74,7 @@ class ArrayRepository extends BaseRepository
             }
             }
         }
         }
 
 
+        // add aliases of packages that were selected, even if the aliases did not match
         foreach ($packages as $package) {
         foreach ($packages as $package) {
             if ($package instanceof AliasPackage) {
             if ($package instanceof AliasPackage) {
                 if (isset($result[spl_object_hash($package->getAliasOf())])) {
                 if (isset($result[spl_object_hash($package->getAliasOf())])) {

+ 5 - 0
src/Composer/Repository/ArtifactRepository.php

@@ -43,6 +43,11 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
         $this->repoConfig = $repoConfig;
         $this->repoConfig = $repoConfig;
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'artifact repo ('.$this->lookup.')';
+    }
+
     public function getRepoConfig()
     public function getRepoConfig()
     {
     {
         return $this->repoConfig;
         return $this->repoConfig;

+ 23 - 0
src/Composer/Repository/ComposerRepository.php

@@ -34,6 +34,7 @@ use Composer\Semver\Constraint\Constraint;
 use Composer\Semver\Constraint\EmptyConstraint;
 use Composer\Semver\Constraint\EmptyConstraint;
 use Composer\Util\Http\Response;
 use Composer\Util\Http\Response;
 use Composer\Util\MetadataMinifier;
 use Composer\Util\MetadataMinifier;
+use Composer\Util\Url;
 use React\Promise\Util as PromiseUtil;
 use React\Promise\Util as PromiseUtil;
 
 
 /**
 /**
@@ -52,6 +53,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
     protected $cache;
     protected $cache;
     protected $notifyUrl;
     protected $notifyUrl;
     protected $searchUrl;
     protected $searchUrl;
+    /** @var string|null a URL containing %package% which can be queried to get providers of a given name */
+    protected $providersApiUrl;
     protected $hasProviders = false;
     protected $hasProviders = false;
     protected $providersUrl;
     protected $providersUrl;
     protected $availablePackages;
     protected $availablePackages;
@@ -125,6 +128,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
         $this->loop = new Loop($this->httpDownloader);
         $this->loop = new Loop($this->httpDownloader);
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'composer repo ('.Url::sanitize($this->url).')';
+    }
+
     public function getRepoConfig()
     public function getRepoConfig()
     {
     {
         return $this->repoConfig;
         return $this->repoConfig;
@@ -411,6 +419,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
         return parent::search($query, $mode);
         return parent::search($query, $mode);
     }
     }
 
 
+    public function getProviders($packageName)
+    {
+        if (!$this->providersApiUrl) {
+            return array();
+        }
+
+        $result = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson();
+
+        return $result['providers'];
+    }
+
     private function getProviderNames()
     private function getProviderNames()
     {
     {
         $this->loadRootServerFile();
         $this->loadRootServerFile();
@@ -805,6 +824,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
             $this->hasProviders = true;
             $this->hasProviders = true;
         }
         }
 
 
+        if (!empty($data['providers-api'])) {
+            $this->providersApiUrl = $data['providers-api'];
+        }
+
         return $this->rootData = $data;
         return $this->rootData = $data;
     }
     }
 
 

+ 5 - 0
src/Composer/Repository/CompositeRepository.php

@@ -39,6 +39,11 @@ class CompositeRepository extends BaseRepository
         }
         }
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'composite repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')';
+    }
+
     /**
     /**
      * Returns all the wrapped repositories
      * Returns all the wrapped repositories
      *
      *

+ 4 - 0
src/Composer/Repository/InstalledArrayRepository.php

@@ -21,4 +21,8 @@ namespace Composer\Repository;
  */
  */
 class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface
 class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface
 {
 {
+    public function getRepoName()
+    {
+        return 'installed '.parent::getRepoName();
+    }
 }
 }

+ 4 - 0
src/Composer/Repository/InstalledFilesystemRepository.php

@@ -19,4 +19,8 @@ namespace Composer\Repository;
  */
  */
 class InstalledFilesystemRepository extends FilesystemRepository implements InstalledRepositoryInterface
 class InstalledFilesystemRepository extends FilesystemRepository implements InstalledRepositoryInterface
 {
 {
+    public function getRepoName()
+    {
+        return 'installed '.parent::getRepoName();
+    }
 }
 }

+ 4 - 0
src/Composer/Repository/LockArrayRepository.php

@@ -21,5 +21,9 @@ namespace Composer\Repository;
  */
  */
 class LockArrayRepository extends ArrayRepository implements RepositoryInterface
 class LockArrayRepository extends ArrayRepository implements RepositoryInterface
 {
 {
+    public function getRepoName()
+    {
+        return 'lock '.parent::getRepoName();
+    }
 }
 }
 
 

+ 5 - 0
src/Composer/Repository/PackageRepository.php

@@ -58,4 +58,9 @@ class PackageRepository extends ArrayRepository
             $this->addPackage($package);
             $this->addPackage($package);
         }
         }
     }
     }
+
+    public function getRepoName()
+    {
+        return preg_replace('{^array }', 'package ', parent::getRepoName());
+    }
 }
 }

+ 6 - 0
src/Composer/Repository/PathRepository.php

@@ -20,6 +20,7 @@ use Composer\Package\Version\VersionGuesser;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
 use Composer\Util\Platform;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
+use Composer\Util\Url;
 
 
 /**
 /**
  * This repository allows installing local packages that are not necessarily under their own VCS.
  * This repository allows installing local packages that are not necessarily under their own VCS.
@@ -111,6 +112,11 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
         parent::__construct();
         parent::__construct();
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'path repo ('.Url::sanitize($this->repoConfig['url']).')';
+    }
+
     public function getRepoConfig()
     public function getRepoConfig()
     {
     {
         return $this->repoConfig;
         return $this->repoConfig;

+ 5 - 0
src/Composer/Repository/PearRepository.php

@@ -67,6 +67,11 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
         $this->repoConfig = $repoConfig;
         $this->repoConfig = $repoConfig;
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'pear repo ('.$this->url.')';
+    }
+
     public function getRepoConfig()
     public function getRepoConfig()
     {
     {
         return $this->repoConfig;
         return $this->repoConfig;

+ 7 - 2
src/Composer/Repository/PlatformRepository.php

@@ -51,6 +51,11 @@ class PlatformRepository extends ArrayRepository
         parent::__construct($packages);
         parent::__construct($packages);
     }
     }
 
 
+    public function getRepoName()
+    {
+        return 'platform repo';
+    }
+
     protected function initialize()
     protected function initialize()
     {
     {
         parent::initialize();
         parent::initialize();
@@ -275,7 +280,7 @@ class PlatformRepository extends ArrayRepository
             } else {
             } else {
                 $actualText = 'actual: '.$package->getPrettyVersion();
                 $actualText = 'actual: '.$package->getPrettyVersion();
             }
             }
-            $overrider->setDescription($overrider->getDescription().' ('.$actualText.')');
+            $overrider->setDescription($overrider->getDescription().', '.$actualText);
 
 
             return;
             return;
         }
         }
@@ -288,7 +293,7 @@ class PlatformRepository extends ArrayRepository
             } else {
             } else {
                 $actualText = 'actual: '.$package->getPrettyVersion();
                 $actualText = 'actual: '.$package->getPrettyVersion();
             }
             }
-            $overrider->setDescription($overrider->getDescription().' ('.$actualText.')');
+            $overrider->setDescription($overrider->getDescription().', '.$actualText);
 
 
             return;
             return;
         }
         }

+ 9 - 0
src/Composer/Repository/RepositoryInterface.php

@@ -83,4 +83,13 @@ interface RepositoryInterface extends \Countable
      * @return array[] an array of array('name' => '...', 'description' => '...')
      * @return array[] an array of array('name' => '...', 'description' => '...')
      */
      */
     public function search($query, $mode = 0, $type = null);
     public function search($query, $mode = 0, $type = null);
+
+    /**
+     * Returns a name representing this repository to the user
+     *
+     * This is best effort and definitely can not always be very precise
+     *
+     * @return string
+     */
+    public function getRepoName();
 }
 }

+ 61 - 20
src/Composer/Repository/RepositorySet.php

@@ -29,6 +29,19 @@ use Composer\Package\Version\StabilityFilter;
  */
  */
 class RepositorySet
 class RepositorySet
 {
 {
+    /**
+     * Packages which replace/provide the given name might be returned as well even if they do not match the name exactly
+     */
+    const ALLOW_PROVIDERS_REPLACERS = 1;
+    /**
+     * Packages are returned even though their stability does not match the required stability
+     */
+    const ALLOW_UNACCEPTABLE_STABILITIES = 2;
+    /**
+     * Packages will be looked up in all repositories, even after they have been found in a higher prio one
+     */
+    const ALLOW_SHADOWED_REPOSITORIES = 4;
+
     /** @var array */
     /** @var array */
     private $rootAliases;
     private $rootAliases;
     /** @var array */
     /** @var array */
@@ -39,10 +52,10 @@ class RepositorySet
 
 
     private $acceptableStabilities;
     private $acceptableStabilities;
     private $stabilityFlags;
     private $stabilityFlags;
-    protected $rootRequires;
+    private $rootRequires;
 
 
-    /** @var Pool */
-    private $pool;
+    /** @var bool */
+    private $locked = false;
 
 
     public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $rootRequires = array())
     public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $rootRequires = array())
     {
     {
@@ -64,6 +77,11 @@ class RepositorySet
         }
         }
     }
     }
 
 
+    public function getRootRequires()
+    {
+        return $this->rootRequires;
+    }
+
     /**
     /**
      * Adds a repository to this repository set
      * Adds a repository to this repository set
      *
      *
@@ -74,7 +92,7 @@ class RepositorySet
      */
      */
     public function addRepository(RepositoryInterface $repo)
     public function addRepository(RepositoryInterface $repo)
     {
     {
-        if ($this->pool) {
+        if ($this->locked) {
             throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore.");
             throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore.");
         }
         }
 
 
@@ -96,15 +114,32 @@ class RepositorySet
      *
      *
      * @param string $name
      * @param string $name
      * @param ConstraintInterface|null $constraint
      * @param ConstraintInterface|null $constraint
-     * @param bool $exactMatch if set to false, packages which replace/provide the given name might be returned as well even if they do not match the name exactly
-     * @param bool $ignoreStability if set to true, packages are returned even though their stability does not match the required stability
+     * @param int $flags any of the ALLOW_* constants from this class to tweak what is returned
      * @return array
      * @return array
      */
      */
-    public function findPackages($name, ConstraintInterface $constraint = null, $exactMatch = true, $ignoreStability = false)
+    public function findPackages($name, ConstraintInterface $constraint = null, $flags = 0)
     {
     {
+        $exactMatch = ($flags & self::ALLOW_PROVIDERS_REPLACERS) === 0;
+        $ignoreStability = ($flags & self::ALLOW_UNACCEPTABLE_STABILITIES) !== 0;
+        $loadFromAllRepos = ($flags & self::ALLOW_SHADOWED_REPOSITORIES) !== 0;
+
         $packages = array();
         $packages = array();
-        foreach ($this->repositories as $repository) {
-            $packages[] = $repository->findPackages($name, $constraint) ?: array();
+        if ($loadFromAllRepos) {
+            foreach ($this->repositories as $repository) {
+                $packages[] = $repository->findPackages($name, $constraint) ?: array();
+            }
+        } else {
+            foreach ($this->repositories as $repository) {
+                $result = $repository->loadPackages(array($name => $constraint), $ignoreStability ? BasePackage::$stabilities : $this->acceptableStabilities, $ignoreStability ? array() : $this->stabilityFlags);
+
+                $packages[] = $result['packages'];
+                foreach ($result['namesFound'] as $nameFound) {
+                    // avoid loading the same package again from other repositories once it has been found
+                    if ($name === $nameFound) {
+                        break 2;
+                    }
+                }
+            }
         }
         }
 
 
         $candidates = $packages ? call_user_func_array('array_merge', $packages) : array();
         $candidates = $packages ? call_user_func_array('array_merge', $packages) : array();
@@ -123,6 +158,19 @@ class RepositorySet
         return $candidates;
         return $candidates;
     }
     }
 
 
+    public function getProviders($packageName)
+    {
+        foreach ($this->repositories as $repository) {
+            if ($repository instanceof ComposerRepository) {
+                if ($providers = $repository->getProviders($packageName)) {
+                    return $providers;
+                }
+            }
+        }
+
+        return array();
+    }
+
     public function isPackageAcceptable($names, $stability)
     public function isPackageAcceptable($names, $stability)
     {
     {
         return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability);
         return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability);
@@ -135,7 +183,7 @@ class RepositorySet
      */
      */
     public function createPool(Request $request)
     public function createPool(Request $request)
     {
     {
-        $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $this->rootRequires);
+        $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences);
 
 
         foreach ($this->repositories as $repo) {
         foreach ($this->repositories as $repo) {
             if ($repo instanceof InstalledRepositoryInterface) {
             if ($repo instanceof InstalledRepositoryInterface) {
@@ -143,7 +191,9 @@ class RepositorySet
             }
             }
         }
         }
 
 
-        return $this->pool = $poolBuilder->buildPool($this->repositories, $request);
+        $this->locked = true;
+
+        return $poolBuilder->buildPool($this->repositories, $request);
     }
     }
 
 
     // TODO unify this with above in some simpler version without "request"?
     // TODO unify this with above in some simpler version without "request"?
@@ -162,13 +212,4 @@ class RepositorySet
 
 
         return $this->createPool($request);
         return $this->createPool($request);
     }
     }
-
-    /**
-     * Access the pool object after it has been created, relevant for plugins which need to read info from the pool
-     * @return Pool
-     */
-    public function getPool()
-    {
-        return $this->pool;
-    }
 }
 }

+ 4 - 0
src/Composer/Repository/RootPackageRepository.php

@@ -21,4 +21,8 @@ namespace Composer\Repository;
  */
  */
 class RootPackageRepository extends ArrayRepository
 class RootPackageRepository extends ArrayRepository
 {
 {
+    public function getRepoName()
+    {
+        return 'root package repo';
+    }
 }
 }

+ 12 - 0
src/Composer/Repository/VcsRepository.php

@@ -22,6 +22,7 @@ use Composer\Package\Loader\LoaderInterface;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\HttpDownloader;
 use Composer\Util\HttpDownloader;
+use Composer\Util\Url;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Config;
 use Composer\Config;
 
 
@@ -79,6 +80,17 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
         $this->processExecutor = new ProcessExecutor($io);
         $this->processExecutor = new ProcessExecutor($io);
     }
     }
 
 
+    public function getRepoName()
+    {
+        $driverClass = get_class($this->getDriver());
+        $driverType = array_search($driverClass, $this->drivers);
+        if (!$driverType) {
+            $driverType = $driverClass;
+        }
+
+        return 'vcs repo ('.$driverType.' '.Url::sanitize($this->url).')';
+    }
+
     public function getRepoConfig()
     public function getRepoConfig()
     {
     {
         return $this->repoConfig;
         return $this->repoConfig;

+ 0 - 11
src/Composer/Util/AuthHelper.php

@@ -255,15 +255,4 @@ class AuthHelper
 
 
         return count($pathParts) >= 4 && $pathParts[3] == 'downloads';
         return count($pathParts) >= 4 && $pathParts[3] == 'downloads';
     }
     }
-
-    /**
-     * @param string $url
-     * @return string
-     */
-    public function stripCredentialsFromUrl($url)
-    {
-        // GitHub repository rename result in redirect locations containing the access_token as GET parameter
-        // e.g. https://api.github.com/repositories/9999999999?access_token=github_token
-        return preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url);
-    }
 }
 }

+ 2 - 13
src/Composer/Util/Git.php

@@ -362,27 +362,16 @@ class Git
         return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')';
         return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')';
     }
     }
 
 
-    public static function sanitizeUrl($message)
-    {
-        return preg_replace_callback('{://(?P<user>[^@]+?):(?P<password>.+?)@}', function ($m) {
-            if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) {
-                return '://***:***@';
-            }
-
-            return '://' . $m[1] . ':***@';
-        }, $message);
-    }
-
     private function throwException($message, $url)
     private function throwException($message, $url)
     {
     {
         // git might delete a directory when it fails and php will not know
         // git might delete a directory when it fails and php will not know
         clearstatcache();
         clearstatcache();
 
 
         if (0 !== $this->process->execute('git --version', $ignoredOutput)) {
         if (0 !== $this->process->execute('git --version', $ignoredOutput)) {
-            throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()));
+            throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()));
         }
         }
 
 
-        throw new \RuntimeException(self::sanitizeUrl($message));
+        throw new \RuntimeException(Url::sanitize($message));
     }
     }
 
 
     /**
     /**

+ 2 - 13
src/Composer/Util/Hg.php

@@ -72,23 +72,12 @@ class Hg
         $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url);
         $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url);
     }
     }
 
 
-    public static function sanitizeUrl($message)
-    {
-        return preg_replace_callback('{://(?P<user>[^@]+?):(?P<password>.+?)@}', function ($m) {
-            if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) {
-                return '://***:***@';
-            }
-
-            return '://' . $m[1] . ':***@';
-        }, $message);
-    }
-
     private function throwException($message, $url)
     private function throwException($message, $url)
     {
     {
         if (0 !== $this->process->execute('hg --version', $ignoredOutput)) {
         if (0 !== $this->process->execute('hg --version', $ignoredOutput)) {
-            throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()));
+            throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()));
         }
         }
 
 
-        throw new \RuntimeException(self::sanitizeUrl($message));
+        throw new \RuntimeException(Url::sanitize($message));
     }
     }
 }
 }

+ 4 - 4
src/Composer/Util/Http/CurlDownloader.php

@@ -195,7 +195,7 @@ class CurlDownloader
         $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : '';
         $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : '';
         $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : '';
         $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : '';
         if ($attributes['redirects'] === 0) {
         if ($attributes['redirects'] === 0) {
-            $this->io->writeError('Downloading ' . $this->authHelper->stripCredentialsFromUrl($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG);
+            $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG);
         }
         }
 
 
         $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle));
         $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle));
@@ -254,12 +254,12 @@ class CurlDownloader
                         $contents = stream_get_contents($job['bodyHandle']);
                         $contents = stream_get_contents($job['bodyHandle']);
                     }
                     }
                     $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents);
                     $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents);
-                    $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG);
+                    $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG);
                 } else {
                 } else {
                     rewind($job['bodyHandle']);
                     rewind($job['bodyHandle']);
                     $contents = stream_get_contents($job['bodyHandle']);
                     $contents = stream_get_contents($job['bodyHandle']);
                     $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents);
                     $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents);
-                    $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG);
+                    $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG);
                 }
                 }
                 fclose($job['bodyHandle']);
                 fclose($job['bodyHandle']);
 
 
@@ -362,7 +362,7 @@ class CurlDownloader
         }
         }
 
 
         if (!empty($targetUrl)) {
         if (!empty($targetUrl)) {
-            $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG);
+            $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, Url::sanitize($targetUrl)), true, IOInterface::DEBUG);
 
 
             return $targetUrl;
             return $targetUrl;
         }
         }

+ 2 - 2
src/Composer/Util/RemoteFilesystem.php

@@ -246,7 +246,7 @@ class RemoteFilesystem
 
 
         $actualContextOptions = stream_context_get_options($ctx);
         $actualContextOptions = stream_context_get_options($ctx);
         $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : '';
         $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : '';
-        $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $this->authHelper->stripCredentialsFromUrl($origFileUrl) . $usingProxy, true, IOInterface::DEBUG);
+        $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG);
         unset($origFileUrl, $actualContextOptions);
         unset($origFileUrl, $actualContextOptions);
 
 
         // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
         // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
@@ -704,7 +704,7 @@ class RemoteFilesystem
             $this->redirects++;
             $this->redirects++;
 
 
             $this->io->writeError('', true, IOInterface::DEBUG);
             $this->io->writeError('', true, IOInterface::DEBUG);
-            $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG);
+            $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, Url::sanitize($targetUrl)), true, IOInterface::DEBUG);
 
 
             $additionalOptions['redirects'] = $this->redirects;
             $additionalOptions['redirects'] = $this->redirects;
 
 

+ 17 - 0
src/Composer/Util/Url.php

@@ -102,4 +102,21 @@ class Url
 
 
         return $origin;
         return $origin;
     }
     }
+
+    public static function sanitize($url)
+    {
+        // GitHub repository rename result in redirect locations containing the access_token as GET parameter
+        // e.g. https://api.github.com/repositories/9999999999?access_token=github_token
+        $url = preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url);
+
+        $url = preg_replace_callback('{://(?P<user>[^:/\s@]+):(?P<password>[^@\s/]+)@}i', function ($m) {
+            if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) {
+                return '://***:***@';
+            }
+
+            return '://'.$m['user'].':***@';
+        }, $url);
+
+        return $url;
+    }
 }
 }

+ 4 - 1
tests/Composer/Test/DependencyResolver/RuleSetTest.php

@@ -143,12 +143,15 @@ class RuleSetTest extends TestCase
             $p = $this->getPackage('foo', '2.1'),
             $p = $this->getPackage('foo', '2.1'),
         ));
         ));
 
 
+        $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock();
+        $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock();
+
         $ruleSet = new RuleSet;
         $ruleSet = new RuleSet;
         $literal = $p->getId();
         $literal = $p->getId();
         $rule = new GenericRule(array($literal), Rule::RULE_ROOT_REQUIRE, array('packageName' => 'foo/bar', 'constraint' => null));
         $rule = new GenericRule(array($literal), Rule::RULE_ROOT_REQUIRE, array('packageName' => 'foo/bar', 'constraint' => null));
 
 
         $ruleSet->add($rule, RuleSet::TYPE_REQUEST);
         $ruleSet->add($rule, RuleSet::TYPE_REQUEST);
 
 
-        $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($pool));
+        $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock, $pool));
     }
     }
 }
 }

+ 4 - 1
tests/Composer/Test/DependencyResolver/RuleTest.php

@@ -99,8 +99,11 @@ class RuleTest extends TestCase
             $p2 = $this->getPackage('baz', '1.1'),
             $p2 = $this->getPackage('baz', '1.1'),
         ));
         ));
 
 
+        $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock();
+        $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock();
+
         $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo'));
         $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo'));
 
 
-        $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($pool));
+        $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool));
     }
     }
 }
 }

+ 10 - 14
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -34,6 +34,7 @@ class SolverTest extends TestCase
     protected $request;
     protected $request;
     protected $policy;
     protected $policy;
     protected $solver;
     protected $solver;
+    protected $pool;
 
 
     public function setUp()
     public function setUp()
     {
     {
@@ -82,7 +83,7 @@ class SolverTest extends TestCase
             $problems = $e->getProblems();
             $problems = $e->getProblems();
             $this->assertCount(1, $problems);
             $this->assertCount(1, $problems);
             $this->assertEquals(2, $e->getCode());
             $this->assertEquals(2, $e->getCode());
-            $this->assertEquals("\n    - The requested package b could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString());
+            $this->assertEquals("\n    - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool));
         }
         }
     }
     }
 
 
@@ -651,9 +652,9 @@ class SolverTest extends TestCase
             $msg = "\n";
             $msg = "\n";
             $msg .= "  Problem 1\n";
             $msg .= "  Problem 1\n";
             $msg .= "    - Root composer.json requires a -> satisfiable by A[1.0].\n";
             $msg .= "    - Root composer.json requires a -> satisfiable by A[1.0].\n";
-            $msg .= "    - B 1.0 conflicts with A[1.0].\n";
+            $msg .= "    - A 1.0 conflicts with B 1.0.\n";
             $msg .= "    - Root composer.json requires b -> satisfiable by B[1.0].\n";
             $msg .= "    - Root composer.json requires b -> satisfiable by B[1.0].\n";
-            $this->assertEquals($msg, $e->getMessage());
+            $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool));
         }
         }
     }
     }
 
 
@@ -682,14 +683,8 @@ class SolverTest extends TestCase
             $msg = "\n";
             $msg = "\n";
             $msg .= "  Problem 1\n";
             $msg .= "  Problem 1\n";
             $msg .= "    - Root composer.json requires a -> satisfiable by A[1.0].\n";
             $msg .= "    - Root composer.json requires a -> satisfiable by A[1.0].\n";
-            $msg .= "    - A 1.0 requires b >= 2.0 -> no matching package found.\n\n";
-            $msg .= "Potential causes:\n";
-            $msg .= " - A typo in the package name\n";
-            $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n";
-            $msg .= "   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n";
-            $msg .= " - It's a private package and you forgot to add a custom repository to find it\n\n";
-            $msg .= "Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
-            $this->assertEquals($msg, $e->getMessage());
+            $msg .= "    - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match your constraint.\n";
+            $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool));
         }
         }
     }
     }
 
 
@@ -731,10 +726,10 @@ class SolverTest extends TestCase
             $msg .= "    - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n";
             $msg .= "    - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n";
             $msg .= "    - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n";
             $msg .= "    - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n";
             $msg .= "    - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n";
             $msg .= "    - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n";
-            $msg .= "    - Same name, can only install one of: B[0.9, 1.0].\n";
+            $msg .= "    - You can only install one version of a package, so only one of these can be installed: B[0.9, 1.0].\n";
             $msg .= "    - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n";
             $msg .= "    - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n";
             $msg .= "    - Root composer.json requires a -> satisfiable by A[1.0].\n";
             $msg .= "    - Root composer.json requires a -> satisfiable by A[1.0].\n";
-            $this->assertEquals($msg, $e->getMessage());
+            $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool));
         }
         }
     }
     }
 
 
@@ -895,7 +890,8 @@ class SolverTest extends TestCase
 
 
     protected function createSolver()
     protected function createSolver()
     {
     {
-        $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO());
+        $this->pool = $this->repoSet->createPool($this->request);
+        $this->solver = new Solver($this->policy, $this->pool, new NullIO());
     }
     }
 
 
     protected function checkSolverResult(array $expected)
     protected function checkSolverResult(array $expected)

+ 1 - 1
tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test

@@ -26,7 +26,7 @@ Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 Your requirements could not be resolved to an installable set of packages.
 
 
   Problem 1
   Problem 1
-    - c/c 1.0.0 requires x/x 1.0 -> no matching package found.
+    - c/c 1.0.0 requires x/x 1.0 -> could not be found in any version, there may be a typo in the package name.
     - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0].
     - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0].
     - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0].
     - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0].
 
 

+ 38 - 0
tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test

@@ -0,0 +1,38 @@
+--TEST--
+Test the error output of solver problems for conflicts between two dependents
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": "1.0.0"} },
+                { "name": "victim/pkg", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "conflicter/pkg": "1.0.0",
+        "victim/pkg": "1.0.0"
+    }
+}
+
+
+--RUN--
+update
+
+--EXPECT-EXIT-CODE--
+2
+
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Updating dependencies
+Your requirements could not be resolved to an installable set of packages.
+
+  Problem 1
+    - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0].
+    - conflicter/pkg 1.0.0 conflicts with victim/pkg 1.0.0.
+    - Root composer.json requires victim/pkg 1.0.0 -> satisfiable by victim/pkg[1.0.0].
+
+--EXPECT--
+

+ 40 - 0
tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test

@@ -0,0 +1,40 @@
+--TEST--
+Test conflicts between a dependency's requirements and the root requirements
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "requirer/pkg", "version": "1.0.0", "require": {
+                    "dependency/pkg": "1.0.0",
+                    "dependency/unstable-pkg": "1.0.0-dev"
+                } },
+                { "name": "dependency/pkg", "version": "2.0.0" },
+                { "name": "dependency/pkg", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "requirer/pkg": "1.*",
+        "dependency/pkg": "2.*"
+    }
+}
+
+--RUN--
+update
+
+--EXPECT-EXIT-CODE--
+2
+
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Updating dependencies
+Your requirements could not be resolved to an installable set of packages.
+
+  Problem 1
+    - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0].
+    - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*).
+
+--EXPECT--
+

+ 1 - 1
tests/Composer/Test/Fixtures/installer/github-issues-4319.test

@@ -37,7 +37,7 @@ Your requirements could not be resolved to an installable set of packages.
 
 
   Problem 1
   Problem 1
     - Root composer.json requires a/a ~1.0 -> satisfiable by a/a[1.0.0].
     - Root composer.json requires a/a ~1.0 -> satisfiable by a/a[1.0.0].
-    - a/a 1.0.0 requires php 5.5 -> your PHP version (%s) overridden by "config.platform.php" version (5.3) does not satisfy that requirement.
+    - a/a 1.0.0 requires php 5.5 -> your php version (5.3; overridden via config.platform, actual: %s) does not satisfy that requirement.
 
 
 --EXPECT--
 --EXPECT--
 
 

+ 2 - 10
tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test

@@ -1,5 +1,5 @@
 --TEST--
 --TEST--
-Partial update from lock file should apply lock file and downgrade unstable packages even if not whitelisted
+Partial update from lock file should apply lock file and if an unstable package is not allowed anymore by latest composer.json it should fail
 --COMPOSER--
 --COMPOSER--
 {
 {
     "repositories": [
     "repositories": [
@@ -59,12 +59,4 @@ Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 Your requirements could not be resolved to an installable set of packages.
 
 
   Problem 1
   Problem 1
-    - The requested package b/unstable could not be found in any version, there may be a typo in the package name.
-
-Potential causes:
- - A typo in the package name
- - The package is not available in a stable-enough version according to your minimum-stability setting
-   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.
- - It's a private package and you forgot to add a custom repository to find it
-
-Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
+    - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update.

+ 49 - 0
tests/Composer/Test/Fixtures/installer/provider-conflicts.test

@@ -0,0 +1,49 @@
+--TEST--
+Test that names provided by a dependent and root package cause a conflict only for replace
+--COMPOSER--
+{
+    "version": "1.2.3",
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                {
+                    "name": "provider/pkg",
+                    "version": "1.0.0",
+                    "provide": { "root-provided/transitive-provided": "2.*", "root-replaced/transitive-provided": "2.*" },
+                    "replace": { "root-provided/transitive-replaced": "2.*", "root-replaced/transitive-replaced": "2.*" }
+                }
+            ]
+        }
+    ],
+    "require": {
+        "provider/pkg": "*"
+    },
+    "provide": {
+        "root-provided/transitive-replaced": "2.*",
+        "root-provided/transitive-provided": "2.*"
+    },
+    "replace": {
+        "root-replaced/transitive-replaced": "2.*",
+        "root-replaced/transitive-provided": "2.*"
+    }
+}
+
+--RUN--
+update
+
+--EXPECT-EXIT-CODE--
+2
+
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Updating dependencies
+Your requirements could not be resolved to an installable set of packages.
+
+  Problem 1
+    - __root__ is present at version 1.2.3 and cannot be modified by Composer
+    - provider/pkg 1.0.0 can not be installed as that would require removing __root__ 1.2.3. They both replace root-replaced/transitive-replaced and can thus not coexist.
+    - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0].
+
+--EXPECT--
+

+ 45 - 0
tests/Composer/Test/Fixtures/installer/provider-conflicts2.test

@@ -0,0 +1,45 @@
+--TEST--
+Test that names provided by two dependents cause a conflict
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                {
+                    "name": "provider/pkg",
+                    "version": "1.0.0",
+                    "provide": { "third/pkg": "2.*" }
+                },
+                {
+                    "name": "replacer/pkg",
+                    "version": "1.0.0",
+                    "replace": { "third/pkg": "2.*" }
+                }
+            ]
+        }
+    ],
+    "require": {
+        "provider/pkg": "*",
+        "replacer/pkg": "*"
+    }
+}
+
+--RUN--
+update
+
+--EXPECT-EXIT-CODE--
+2
+
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Updating dependencies
+Your requirements could not be resolved to an installable set of packages.
+
+  Problem 1
+    - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0].
+    - Only one of these can be installed: replacer/pkg 1.0.0, provider/pkg 1.0.0.
+    - Root composer.json requires replacer/pkg * -> satisfiable by replacer/pkg[1.0.0].
+
+--EXPECT--
+

+ 54 - 0
tests/Composer/Test/Fixtures/installer/provider-conflicts3.test

@@ -0,0 +1,54 @@
+--TEST--
+Test that a replacer can not be installed together with another version of the package it replaces
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                {"name": "replacer/pkg", "version": "2.0.0", "replace": { "regular/pkg": "self.version" }},
+                {"name": "replacer/pkg", "version": "2.0.1", "replace": { "regular/pkg": "self.version" }},
+                {"name": "replacer/pkg", "version": "2.0.2", "replace": { "regular/pkg": "self.version" }},
+                {"name": "replacer/pkg", "version": "2.0.3", "replace": { "regular/pkg": "self.version" }},
+                {"name": "regular/pkg", "version": "1.0.0"},
+                {"name": "regular/pkg", "version": "1.0.1"},
+                {"name": "regular/pkg", "version": "1.0.2"},
+                {"name": "regular/pkg", "version": "1.0.3"},
+                {"name": "regular/pkg", "version": "2.0.0"},
+                {"name": "regular/pkg", "version": "2.0.1"}
+            ]
+        }
+    ],
+    "require": {
+        "regular/pkg": "1.*",
+        "replacer/pkg": "2.*"
+    }
+}
+
+--RUN--
+update
+
+--EXPECT-EXIT-CODE--
+2
+
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Updating dependencies
+Your requirements could not be resolved to an installable set of packages.
+
+  Problem 1
+    - Conclusion: don't install regular/pkg 1.0.3, learned rules:
+        - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
+        - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it.
+    - Conclusion: don't install regular/pkg 1.0.2, learned rules:
+        - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
+        - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it.
+    - Conclusion: don't install regular/pkg 1.0.1, learned rules:
+        - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
+        - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it.
+    - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it.
+    - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3].
+    - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
+
+--EXPECT--
+

+ 1 - 1
tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test

@@ -41,7 +41,7 @@ Your requirements could not be resolved to an installable set of packages.
 
 
   Problem 1
   Problem 1
     - Root composer.json requires foo/standard 1.0.0 -> satisfiable by foo/standard[1.0.0].
     - Root composer.json requires foo/standard 1.0.0 -> satisfiable by foo/standard[1.0.0].
-    - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> no matching package found.
+    - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> could not be found in any version, there may be a typo in the package name.
 
 
 Potential causes:
 Potential causes:
  - A typo in the package name
  - A typo in the package name

+ 1 - 8
tests/Composer/Test/Fixtures/installer/repositories-priorities.test

@@ -28,15 +28,8 @@ Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 Your requirements could not be resolved to an installable set of packages.
 
 
   Problem 1
   Problem 1
-    - The requested package foo/a could not be found in any version, there may be a typo in the package name.
+    - Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[1.0.0] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.
 
 
-Potential causes:
- - A typo in the package name
- - The package is not available in a stable-enough version according to your minimum-stability setting
-   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.
- - It's a private package and you forgot to add a custom repository to find it
-
-Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
 --EXPECT--
 --EXPECT--
 --EXPECT-EXIT-CODE--
 --EXPECT-EXIT-CODE--
 2
 2

+ 93 - 10
tests/Composer/Test/Fixtures/installer/solver-problems.test

@@ -6,34 +6,84 @@ Test the error output of solver problems.
         {
         {
             "type": "package",
             "type": "package",
             "package": [
             "package": [
+                { "name": "package/found", "version": "2.0.0", "require": {
+                    "unstable/package2": "2.*"
+                } },
+                { "name": "package/found2", "version": "2.0.0", "require": {
+                    "invalid/💩package": "*"
+                } },
+                { "name": "package/found3", "version": "2.0.0", "require": {
+                    "unstable/package2": "2.*"
+                } },
+                { "name": "package/found4", "version": "2.0.0", "require": {
+                    "non-existent/pkg2": "1.*"
+                } },
+                { "name": "package/found5", "version": "2.0.0", "require": {
+                    "requirer/pkg": "1.*"
+                } },
+                { "name": "package/found6", "version": "2.0.0", "require": {
+                    "stable-requiree-excluded/pkg2": "1.0.1"
+                } },
+                { "name": "package/found7", "version": "2.0.0", "require": {
+                    "php-64bit": "1.0.1"
+                } },
+                { "name": "conflict/requirer", "version": "2.0.0", "require": {
+                    "conflict/dep": "1.0.0"
+                } },
+                { "name": "conflict/requirer2", "version": "2.0.0", "require": {
+                    "conflict/dep": "2.0.0"
+                } },
+                { "name": "conflict/dep", "version": "1.0.0" },
+                { "name": "conflict/dep", "version": "2.0.0" },
                 { "name": "unstable/package", "version": "2.0.0-alpha" },
                 { "name": "unstable/package", "version": "2.0.0-alpha" },
                 { "name": "unstable/package", "version": "1.0.0" },
                 { "name": "unstable/package", "version": "1.0.0" },
-                { "name": "requirer/pkg", "version": "1.0.0", "require": {"dependency/pkg": "1.0.0" } },
+                { "name": "unstable/package2", "version": "2.0.0-alpha" },
+                { "name": "unstable/package2", "version": "1.0.0" },
+                { "name": "requirer/pkg", "version": "1.0.0", "require": {
+                    "dependency/pkg": "1.0.0",
+                    "dependency/unstable-pkg": "1.0.0-dev"
+                } },
                 { "name": "dependency/pkg", "version": "2.0.0" },
                 { "name": "dependency/pkg", "version": "2.0.0" },
                 { "name": "dependency/pkg", "version": "1.0.0" },
                 { "name": "dependency/pkg", "version": "1.0.0" },
+                { "name": "dependency/unstable-pkg", "version": "1.0.0-dev" },
                 { "name": "stable-requiree-excluded/pkg", "version": "1.0.1" },
                 { "name": "stable-requiree-excluded/pkg", "version": "1.0.1" },
                 { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }
                 { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }
             ]
             ]
         }
         }
     ],
     ],
     "require": {
     "require": {
+        "package/found": "2.*",
+        "package/found2": "2.*",
+        "package/found3": "2.*",
+        "package/found4": "2.*",
+        "package/found5": "2.*",
+        "package/found6": "2.*",
+        "package/found7": "2.*",
+        "conflict/requirer": "2.*",
+        "conflict/requirer2": "2.*",
         "unstable/package": "2.*",
         "unstable/package": "2.*",
-        "bogus/pkg": "1.*",
+        "non-existent/pkg": "1.*",
         "requirer/pkg": "1.*",
         "requirer/pkg": "1.*",
         "dependency/pkg": "2.*",
         "dependency/pkg": "2.*",
-        "stable-requiree-excluded/pkg": "1.0.1"
+        "stable-requiree-excluded/pkg": "1.0.1",
+        "lib-xml": "1002.*",
+        "lib-icu": "1001.*",
+        "ext-xml": "1002.*",
+        "php": "1"
     }
     }
 }
 }
 
 
 --INSTALLED--
 --INSTALLED--
 [
 [
-    { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }
+    { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" },
+    { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" }
 ]
 ]
 
 
 --LOCK--
 --LOCK--
 {
 {
     "packages": [
     "packages": [
-        { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }
+        { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" },
+        { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" }
     ],
     ],
     "packages-dev": [],
     "packages-dev": [],
     "aliases": [],
     "aliases": [],
@@ -46,7 +96,7 @@ Test the error output of solver problems.
 }
 }
 
 
 --RUN--
 --RUN--
-update unstable/package requirer/pkg dependency/pkg
+update unstable/package requirer/pkg dependency/pkg conflict/requirer
 
 
 --EXPECT-EXIT-CODE--
 --EXPECT-EXIT-CODE--
 2
 2
@@ -57,14 +107,44 @@ Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 Your requirements could not be resolved to an installable set of packages.
 
 
   Problem 1
   Problem 1
-    - The requested package unstable/package could not be found in any version, there may be a typo in the package name.
+    - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability.
   Problem 2
   Problem 2
-    - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name.
+    - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name.
   Problem 3
   Problem 3
-    - The requested package stable-requiree-excluded/pkg could not be found in any version, there may be a typo in the package name.
+    - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update.
   Problem 4
   Problem 4
+    - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it.
+  Problem 5
+    - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension.
+  Problem 6
+    - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension.
+  Problem 7
+    - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement.
+  Problem 8
+    - Root composer.json requires package/found 2.* -> satisfiable by package/found[2.0.0].
+    - package/found 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability.
+  Problem 9
+    - Root composer.json requires package/found2 2.* -> satisfiable by package/found2[2.0.0].
+    - package/found2 2.0.0 requires invalid/💩package * -> could not be found, it looks like its name is invalid, "💩" is not allowed in package names.
+  Problem 10
+    - Root composer.json requires package/found3 2.* -> satisfiable by package/found3[2.0.0].
+    - package/found3 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability.
+  Problem 11
+    - Root composer.json requires package/found4 2.* -> satisfiable by package/found4[2.0.0].
+    - package/found4 2.0.0 requires non-existent/pkg2 1.* -> could not be found in any version, there may be a typo in the package name.
+  Problem 12
+    - Root composer.json requires package/found6 2.* -> satisfiable by package/found6[2.0.0].
+    - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match your constraint.
+  Problem 13
+    - Root composer.json requires package/found7 2.* -> satisfiable by package/found7[2.0.0].
+    - package/found7 2.0.0 requires php-64bit 1.0.1 -> your php-64bit version (%s) does not satisfy that requirement.
+  Problem 14
     - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0].
     - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0].
-    - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found.
+    - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*).
+  Problem 15
+    - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*).
+    - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0].
+    - Root composer.json requires package/found5 2.* -> satisfiable by package/found5[2.0.0].
 
 
 Potential causes:
 Potential causes:
  - A typo in the package name
  - A typo in the package name
@@ -73,6 +153,9 @@ Potential causes:
  - It's a private package and you forgot to add a custom repository to find it
  - It's a private package and you forgot to add a custom repository to find it
 
 
 Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
 Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
+  To enable extensions, verify that they are enabled in your .ini files:
+__inilist__
+  You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode.
 
 
 --EXPECT--
 --EXPECT--
 
 

+ 3 - 0
tests/Composer/Test/InstallerTest.php

@@ -323,6 +323,9 @@ class InstallerTest extends TestCase
         $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace()));
         $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace()));
 
 
         if ($expectOutput) {
         if ($expectOutput) {
+            $output = preg_replace('{^    - .*?\.ini$}m', '__inilist__', $output);
+            $output = preg_replace('{(__inilist__\r?\n)+}', "__inilist__\n", $output);
+
             $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output));
             $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output));
         }
         }
     }
     }

+ 21 - 0
tests/Composer/Test/Util/UrlTest.php

@@ -58,4 +58,25 @@ class UrlTest extends TestCase
             array('https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=abcd', 'https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=65', array('gitlab-domains' => array('mygitlab.com')), '65'),
             array('https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=abcd', 'https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=65', array('gitlab-domains' => array('mygitlab.com')), '65'),
         );
         );
     }
     }
+
+    /**
+     * @dataProvider sanitizeProvider
+     */
+    public function testSanitize($expected, $url)
+    {
+        $this->assertSame($expected, Url::sanitize($url));
+    }
+
+    public static function sanitizeProvider()
+    {
+        return array(
+            array('https://foo:***@example.org/', 'https://foo:bar@example.org/'),
+            array('https://foo@example.org/', 'https://foo@example.org/'),
+            array('https://example.org/', 'https://example.org/'),
+            array('http://***:***@example.org', 'http://10a8f08e8d7b7b9:foo@example.org'),
+            array('https://foo:***@example.org:123/', 'https://foo:bar@example.org:123/'),
+            array('https://example.org/foo/bar?access_token=***', 'https://example.org/foo/bar?access_token=abcdef'),
+            array('https://example.org/foo/bar?foo=bar&access_token=***', 'https://example.org/foo/bar?foo=bar&access_token=abcdef'),
+        );
+    }
 }
 }