From f95471f2217cce7effa8f24e0209f78163f94ea7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Aug 2022 13:08:59 +0300 Subject: [PATCH] Show/outdated command fixes (#11000) * Fix show command showing the split of direct/transitive deps from outdated, fixes #10999 * Fix a few minor edge cases in show command, add tests for show and outdated commands --- src/Composer/Command/ShowCommand.php | 24 +- src/Composer/Repository/RepositoryUtils.php | 26 ++ .../Composer/Test/Command/ShowCommandTest.php | 236 ++++++++++++++++++ 3 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 tests/Composer/Test/Command/ShowCommandTest.php diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index ef0c34475..3471156ff 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -291,7 +291,7 @@ EOT $hint .= ', try using --platform (-p) to show platform packages'; } if (!$input->getOption('all')) { - $hint .= ', try using --all (-a) to show all available packages'; + $hint .= ', try using --available (-a) to show all available packages'; } throw new \InvalidArgumentException('Package "' . $packageFilter . '" not found'.$hint.'.'); @@ -374,7 +374,7 @@ EOT $packageFilterRegex = '{^'.str_replace('\\*', '.*?', preg_quote($packageFilter)).'$}i'; } - $packageListFilter = array(); + $packageListFilter = null; if ($input->getOption('direct')) { $packageListFilter = $this->getRootRequires(); } @@ -384,7 +384,7 @@ EOT $input->setOption('path', false); } - foreach ($repos->getRepositories() as $repo) { + foreach (RepositoryUtils::flattenRepositories($repos) as $repo) { if ($repo === $platformRepo) { $type = 'platform'; } elseif ($lockedRepo !== null && $repo === $lockedRepo) { @@ -408,7 +408,7 @@ EOT $package = $package->getAliasOf(); } if (!$packageFilterRegex || Preg::isMatch($packageFilterRegex, $package->getName())) { - if (!$packageListFilter || in_array($package->getName(), $packageListFilter, true)) { + if (null === $packageListFilter || in_array($package->getName(), $packageListFilter, true)) { $packages[$type][$package->getName()] = $package; } } @@ -535,6 +535,7 @@ EOT 'nameLength' => $nameLength, 'versionLength' => $versionLength, 'latestLength' => $latestLength, + 'writeLatest' => $writeLatest, ); if ($input->getOption('strict') && $hasOutdatedPackages) { $exitCode = 1; @@ -570,12 +571,13 @@ EOT $nameLength = $viewMetaData[$type]['nameLength']; $versionLength = $viewMetaData[$type]['versionLength']; $latestLength = $viewMetaData[$type]['latestLength']; + $writeLatest = $viewMetaData[$type]['writeLatest']; - $writeVersion = $nameLength + $versionLength + 3 <= $width; - $writeLatest = $nameLength + $versionLength + $latestLength + 3 <= $width; - $writeDescription = $nameLength + $versionLength + $latestLength + 24 <= $width; + $versionFits = $nameLength + $versionLength + 3 <= $width; + $latestFits = $nameLength + $versionLength + $latestLength + 3 <= $width; + $descriptionFits = $nameLength + $versionLength + $latestLength + 24 <= $width; - if ($writeLatest && !$io->isDecorated()) { + if ($latestFits && !$io->isDecorated()) { $latestLength += 2; } @@ -601,19 +603,19 @@ EOT $io->write(''); $io->write('Direct dependencies:'); if (\count($directDeps) > 0) { - $this->printPackages($io, $directDeps, $indent, $writeVersion, $writeLatest, $writeDescription, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $directDeps, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength); } else { $io->write('Everything up to date'); } $io->write(''); $io->write('Transitive dependencies:'); if (\count($transitiveDeps) > 0) { - $this->printPackages($io, $transitiveDeps, $indent, $writeVersion, $writeLatest, $writeDescription, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $transitiveDeps, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength); } else { $io->write('Everything up to date'); } } else { - $this->printPackages($io, $packages, $indent, $writeVersion, $writeLatest, $writeDescription, $width, $versionLength, $nameLength, $latestLength); + $this->printPackages($io, $packages, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength); } if ($showAllTypes) { diff --git a/src/Composer/Repository/RepositoryUtils.php b/src/Composer/Repository/RepositoryUtils.php index e62c9dfdf..f963b34b5 100644 --- a/src/Composer/Repository/RepositoryUtils.php +++ b/src/Composer/Repository/RepositoryUtils.php @@ -49,4 +49,30 @@ class RepositoryUtils return $bucket; } + + /** + * Unwraps CompositeRepository, InstalledRepository and optionally FilterRepository to get a flat array of pure repository instances + * + * @return RepositoryInterface[] + */ + public static function flattenRepositories(RepositoryInterface $repo, bool $unwrapFilterRepos = true): array + { + // unwrap filter repos + if ($unwrapFilterRepos && $repo instanceof FilterRepository) { + $repo = $repo->getRepository(); + } + + if (!$repo instanceof CompositeRepository) { + return [$repo]; + } + + $repos = []; + foreach ($repo->getRepositories() as $r) { + foreach (self::flattenRepositories($r, $unwrapFilterRepos) as $r2) { + $repos[] = $r2; + } + } + + return $repos; + } } diff --git a/tests/Composer/Test/Command/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php new file mode 100644 index 000000000..9f44b3ce7 --- /dev/null +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -0,0 +1,236 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Command; + +use Composer\Pcre\Preg; +use Composer\Pcre\Regex; +use Composer\Repository\PlatformRepository; +use Composer\Test\TestCase; + +class ShowCommandTest extends TestCase +{ + /** + * @dataProvider provideShow + * @param array $command + * @param array $requires + */ + public function testShow(array $command, string $expected, array $requires = []): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor/package', 'description' => 'generic description', 'version' => '1.0.0'], + + ['name' => 'outdated/major', 'description' => 'outdated/major v1.0.0 description', 'version' => '1.0.0'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.0.1 description', 'version' => '1.0.1'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.1.0 description', 'version' => '1.1.0'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.1.1 description', 'version' => '1.1.1'], + ['name' => 'outdated/major', 'description' => 'outdated/major v2.0.0 description', 'version' => '2.0.0'], + + ['name' => 'outdated/minor', 'description' => 'outdated/minor v1.0.0 description', 'version' => '1.0.0'], + ['name' => 'outdated/minor', 'description' => 'outdated/minor v1.0.1 description', 'version' => '1.0.1'], + ['name' => 'outdated/minor', 'description' => 'outdated/minor v1.1.0 description', 'version' => '1.1.0'], + ['name' => 'outdated/minor', 'description' => 'outdated/minor v1.1.1 description', 'version' => '1.1.1'], + + ['name' => 'outdated/patch', 'description' => 'outdated/patch v1.0.0 description', 'version' => '1.0.0'], + ['name' => 'outdated/patch', 'description' => 'outdated/patch v1.0.1 description', 'version' => '1.0.1'], + ], + ], + ], + 'require' => $requires === [] ? new \stdClass : $requires, + ]); + + $pkg = $this->getPackage('vendor/package', '1.0.0'); + $pkg->setDescription('description of installed package'); + + $this->createInstalledJson([ + $pkg, + $this->getPackage('outdated/major', '1.0.0'), + $this->getPackage('outdated/minor', '1.0.0'), + $this->getPackage('outdated/patch', '1.0.0'), + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(array_merge(['command' => 'show'], $command)); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); + } + + public function provideShow(): \Generator + { + yield 'default shows installed with version and description' => [ + [], +'outdated/major 1.0.0 +outdated/minor 1.0.0 +outdated/patch 1.0.0 +vendor/package 1.0.0 description of installed package', + ]; + + yield 'with -a show available packages with description but no version' => [ + ['-a' => true], +'outdated/major outdated/major v2.0.0 description +outdated/minor outdated/minor v1.1.1 description +outdated/patch outdated/patch v1.0.1 description +vendor/package generic description', + ]; + + yield 'show with --direct shows nothing if no deps' => [ + ['--direct' => true], + '', + ]; + + yield 'show with --direct shows only root deps' => [ + ['--direct' => true], + 'outdated/major 1.0.0', + ['outdated/major' => '*'], + ]; + + yield 'outdated deps' => [ + ['command' => 'outdated'], +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies: +Everything up to date + +Transitive dependencies: +outdated/major 1.0.0 ~ 2.0.0 +outdated/minor 1.0.0 ! 1.1.1 +outdated/patch 1.0.0 ! 1.0.1', + ]; + + yield 'outdated deps with --direct only show direct deps with updated' => [ + ['command' => 'outdated', '--direct' => true], +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible +outdated/major 1.0.0 ~ 2.0.0', + [ + 'vendor/package' => '*', + 'outdated/major' => '*', + ], + ]; + + yield 'outdated deps with --major-only only shows major updates' => [ + ['command' => 'outdated', '--major-only' => true], +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies: +Everything up to date + +Transitive dependencies: +outdated/major 1.0.0 ~ 2.0.0', + ]; + + yield 'outdated deps with --minor-only only shows minor updates' => [ + ['command' => 'outdated', '--minor-only' => true], +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies: +outdated/minor 1.0.0 ! 1.1.1 + +Transitive dependencies: +outdated/major 1.0.0 ! 1.1.1 +outdated/patch 1.0.0 ! 1.0.1', + ['outdated/minor' => '*'], + ]; + + yield 'outdated deps with --patch-only only shows patch updates' => [ + ['command' => 'outdated', '--patch-only' => true], +'Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies: +Everything up to date + +Transitive dependencies: +outdated/major 1.0.0 ! 1.0.1 +outdated/minor 1.0.0 ! 1.0.1 +outdated/patch 1.0.0 ! 1.0.1', + ]; + } + + public function testShowPlatformOnlyShowsPlatformPackages(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor/package', 'description' => 'generic description', 'version' => '1.0.0'], + ], + ], + ], + ]); + + $this->createInstalledJson([ + $this->getPackage('vendor/package', '1.0.0'), + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '-p' => true]); + $output = trim($appTester->getDisplay(true)); + foreach (Regex::matchAll('{^(\w+)}m', $output)->matches as $m) { + self::assertTrue(PlatformRepository::isPlatformPackage((string) $m[1])); + } + } + + public function testShowAllShowsAllSections(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor/available', 'description' => 'generic description', 'version' => '1.0.0'], + ], + ], + ], + ]); + + $pkg = $this->getPackage('vendor/installed', '2.0.0'); + $pkg->setDescription('description of installed package'); + $this->createInstalledJson([ + $pkg, + ]); + + $pkg = $this->getPackage('vendor/locked', '3.0.0'); + $pkg->setDescription('description of locked package'); + $this->createComposerLock([ + $pkg, + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '--all' => true]); + $output = trim($appTester->getDisplay(true)); + $output = Preg::replace('{platform:(\n .*)+}', 'platform: wiped', $output); + + self::assertSame('platform: wiped + +locked: + vendor/locked 3.0.0 description of locked package + +available: + vendor/available generic description + +installed: + vendor/installed 2.0.0 description of installed package', $output); + } +}