diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 6861b814c..683c06e78 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -66,7 +66,7 @@ class Problem * @param array $installedMap A map of all present packages * @return string */ - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { // 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)); @@ -90,13 +90,13 @@ class Problem } if (empty($packages)) { - return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint)); + return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $packageName, $constraint)); } } $messages = array(); foreach ($reasons as $rule) { - $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); + $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); } return "\n - ".implode("\n - ", array_unique($messages)); @@ -138,7 +138,7 @@ class Problem /** * @internal */ - public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null) + public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $packageName, $constraint = null) { // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { @@ -210,7 +210,7 @@ class Problem 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().').'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); } } @@ -220,7 +220,7 @@ class Problem 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 list it as an argument for the update command.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.'); } } @@ -229,15 +229,15 @@ class Problem }); if (!$nonLockedPackages) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.'); } - 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.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' 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.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); } // check if the package is found when bypassing the constraint check @@ -257,10 +257,10 @@ class Problem } } - 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. See https://getcomposer.org/repoprio for details and assistance.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.'); } - 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.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.'); } if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { @@ -287,7 +287,7 @@ class Problem /** * @internal */ - public static function getPackageList(array $packages) + public static function getPackageList(array $packages, $isVerbose) { $prepared = array(); foreach ($packages as $package) { @@ -299,6 +299,25 @@ class Problem if (isset($package['versions'][VersionParser::DEV_MASTER_ALIAS]) && isset($package['versions']['dev-master'])) { unset($package['versions'][VersionParser::DEV_MASTER_ALIAS]); } + if (!$isVerbose && count($package['versions']) > 4) { + $filtered = array(); + $byMajor = array(); + foreach ($package['versions'] as $version => $pretty) { + $byMajor[preg_replace('{^(\d+)\..*}', '$1', $version)][] = $pretty; + } + foreach ($byMajor as $versions) { + if (count($versions) > 4) { + $filtered[] = $versions[0]; + $filtered[] = '...'; + $filtered[] = $versions[count($versions) - 1]; + } else { + $filtered = array_merge($filtered, $versions); + } + } + + $package['versions'] = $filtered; + } + $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 2e82dab82..07550d8d0 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -127,7 +127,7 @@ abstract class Rule return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']; } - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); @@ -152,7 +152,7 @@ abstract class Rule return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : ''); } - return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages).'.'; + return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages, $isVerbose).'.'; case self::RULE_FIXED: $package = $this->deduplicateMasterAlias($this->reasonData['package']); @@ -179,11 +179,11 @@ abstract class Rule $text = $this->reasonData->getPrettyString($sourcePackage); if ($requires) { - $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires) . '.'; + $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires, $isVerbose) . '.'; } else { $targetName = $this->reasonData->getTarget(); - $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint()); + $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $targetName, $this->reasonData->getConstraint()); return $text . ' -> ' . $reason[1]; } @@ -227,13 +227,13 @@ abstract class Rule } if ($installedPackages && $removablePackages) { - return $this->formatPackagesUnique($pool, $removablePackages).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages).'. '.$reason; + return $this->formatPackagesUnique($pool, $removablePackages, $isVerbose).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages, $isVerbose).'. '.$reason; } - return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals).'. '.$reason; + return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals, $isVerbose).'. '.$reason; } - return 'You can only install one version of a package, so only one of these can be installed: ' . $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, $isVerbose) . '.'; case self::RULE_LEARNED: if (isset($learnedPool[$this->reasonData])) { $learnedString = ', learned rules:'."\n - "; @@ -260,7 +260,7 @@ abstract class Rule * * @return string */ - protected function formatPackagesUnique($pool, array $packages) + protected function formatPackagesUnique($pool, array $packages, $isVerbose) { $prepared = array(); foreach ($packages as $index => $package) { @@ -269,7 +269,7 @@ abstract class Rule } } - return Problem::getPackageList($packages); + return Problem::getPackageList($packages, $isVerbose); } private function getReplacedNames(PackageInterface $package) diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index d37ca1e9f..8058f9f68 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -157,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable return array_keys($types); } - public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null) + public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null, $isVerbose = false) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n"; + $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose) : $rule)."\n"; } $string .= "\n\n"; } diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 1821bde91..082b4c4f7 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -31,7 +31,7 @@ class SolverProblemsException extends \RuntimeException parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2); } - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isDevExtraction = false) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $isDevExtraction = false) { $installedMap = $request->getPresentMap(true); $hasExtensionProblems = false; @@ -39,7 +39,7 @@ class SolverProblemsException extends \RuntimeException $problems = array(); foreach ($this->problems as $problem) { - $problems[] = $problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; + $problems[] = $problem->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 7d1da54a4..c1e8099c4 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -402,7 +402,7 @@ class Installer $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose())); if (!$this->devMode) { $this->io->writeError('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.', true, IOInterface::QUIET); } @@ -563,7 +563,7 @@ class Installer $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); $this->io->writeError('Your requirements can be resolved successfully when require-dev packages are present.'); $this->io->writeError('You may need to move packages from require-dev or some of their dependencies to require.'); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, true)); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose(), true)); return max(1, $e->getCode()); } @@ -627,7 +627,7 @@ class Installer } } catch (SolverProblemsException $e) { $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose())); return max(1, $e->getCode()); } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index f819397fb..2e1a9921d 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -104,6 +104,6 @@ class RuleTest extends TestCase $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($repositorySetMock, $requestMock, $pool)); + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool, false)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 3090d9d83..5fca6bfc2 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -83,7 +83,7 @@ class SolverTest extends TestCase $problems = $e->getProblems(); $this->assertCount(1, $problems); $this->assertEquals(2, $e->getCode()); - $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)); + $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, false)); } } @@ -654,7 +654,7 @@ class SolverTest extends TestCase $msg .= " - Root composer.json requires a -> satisfiable by 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"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -684,7 +684,7 @@ class SolverTest extends TestCase $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $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)); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -729,7 +729,7 @@ class SolverTest extends TestCase $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 .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } diff --git a/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test b/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test new file mode 100644 index 000000000..1235e9467 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test @@ -0,0 +1,116 @@ +--TEST-- +Test the error output minifies version lists +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "a/a", "version": "1.0.0", "require": {"b/b": "1.0.0"}}, + {"name": "b/b", "version": "1.0.0"}, + {"name": "b/b", "version": "1.0.1"}, + {"name": "b/b", "version": "1.0.2"}, + {"name": "b/b", "version": "1.0.3"}, + {"name": "b/b", "version": "1.0.4"}, + {"name": "b/b", "version": "1.0.5"}, + {"name": "b/b", "version": "1.0.6"}, + {"name": "b/b", "version": "1.0.7"}, + {"name": "b/b", "version": "1.0.8"}, + {"name": "b/b", "version": "1.0.9"}, + {"name": "b/b", "version": "1.1.0"}, + {"name": "b/b", "version": "1.1.1"}, + {"name": "b/b", "version": "1.1.2"}, + {"name": "b/b", "version": "1.1.3"}, + {"name": "b/b", "version": "v1.1.4"}, + {"name": "b/b", "version": "1.1.5"}, + {"name": "b/b", "version": "v1.1.6"}, + {"name": "b/b", "version": "1.1.7-alpha"}, + {"name": "b/b", "version": "1.1.8"}, + {"name": "b/b", "version": "1.1.9"}, + {"name": "b/b", "version": "1.2.0"}, + {"name": "b/b", "version": "1.2.1"}, + {"name": "b/b", "version": "1.2.2"}, + {"name": "b/b", "version": "1.2.3"}, + {"name": "b/b", "version": "1.2.4"}, + {"name": "b/b", "version": "1.2.5"}, + {"name": "b/b", "version": "1.2.6"}, + {"name": "b/b", "version": "1.2.7"}, + {"name": "b/b", "version": "1.2.8"}, + {"name": "b/b", "version": "1.2.9"}, + {"name": "b/b", "version": "2.0.0"}, + {"name": "b/b", "version": "2.0.1"}, + {"name": "b/b", "version": "2.0.2"}, + {"name": "b/b", "version": "2.0.3"}, + {"name": "b/b", "version": "2.0.4"}, + {"name": "b/b", "version": "2.0.5"}, + {"name": "b/b", "version": "2.0.6"}, + {"name": "b/b", "version": "2.0.7"}, + {"name": "b/b", "version": "2.0.8"}, + {"name": "b/b", "version": "2.0.9"}, + {"name": "b/b", "version": "2.1.0"}, + {"name": "b/b", "version": "2.1.1"}, + {"name": "b/b", "version": "2.1.2"}, + {"name": "b/b", "version": "2.1.3"}, + {"name": "b/b", "version": "2.1.4"}, + {"name": "b/b", "version": "2.1.5"}, + {"name": "b/b", "version": "2.1.6"}, + {"name": "b/b", "version": "2.1.7"}, + {"name": "b/b", "version": "2.1.8"}, + {"name": "b/b", "version": "2.1.9"}, + {"name": "b/b", "version": "2.2.0"}, + {"name": "b/b", "version": "2.2.1"}, + {"name": "b/b", "version": "2.2.2"}, + {"name": "b/b", "version": "2.2.3"}, + {"name": "b/b", "version": "2.2.4"}, + {"name": "b/b", "version": "2.2.5"}, + {"name": "b/b", "version": "2.2.6"}, + {"name": "b/b", "version": "2.2.7"}, + {"name": "b/b", "version": "2.2.8"}, + {"name": "b/b", "version": "2.2.9"}, + {"name": "b/b", "version": "2.3.0-RC"}, + {"name": "b/b", "version": "3.0.0"}, + {"name": "b/b", "version": "3.0.1"}, + {"name": "b/b", "version": "3.0.2"}, + {"name": "b/b", "version": "3.0.3"}, + {"name": "b/b", "version": "4.0.0"} + ] + } + ], + "require": { + "a/a": "*", + "b/b": "^1.1 || ^2.0 || ^3.0" + }, + "minimum-stability": "dev" +} + +--LOCK-- +{ + "packages": [ + {"name": "b/b", "version": "1.0.0"} + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} + +--RUN-- +update a/a + +--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 b/b ^1.1 || ^2.0 || ^3.0, found b/b[1.1.0, ..., 1.2.9, 2.0.0, ..., 2.3.0-RC, 3.0.0, 3.0.1, 3.0.2, 3.0.3] 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 list it as an argument for the update command. + +--EXPECT-- +