diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index 89c6b68e5..444d69554 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -117,6 +117,36 @@ trait CompletionTrait }; } + /** + * Suggest package names from installed. + */ + private function suggestInstalledPackageTypes(bool $includeRootPackage = true): \Closure + { + return function (CompletionInput $input) use ($includeRootPackage): array { + $composer = $this->requireComposer(); + $installedRepos = []; + + if ($includeRootPackage) { + $installedRepos[] = new RootPackageRepository(clone $composer->getPackage()); + } + + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $installedRepos[] = $locker->getLockedRepository(true); + } else { + $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); + } + + $installedRepo = new InstalledRepository($installedRepos); + + return array_values(array_unique( + array_map(static function (PackageInterface $package) { + return $package->getType(); + }, $installedRepo->getPackages()) + )); + }; + } + /** * Suggest package names available on all configured repositories. */ diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php index 446d9eec1..cb7882a9c 100644 --- a/src/Composer/Command/ReinstallCommand.php +++ b/src/Composer/Command/ReinstallCommand.php @@ -51,7 +51,8 @@ class ReinstallCommand extends BaseCommand new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.', null, $this->suggestInstalledPackage(false)), + new InputOption('type', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Filter packages to reinstall by type(s)', null, $this->suggestInstalledPackageTypes(false)), + new InputArgument('packages', InputArgument::IS_ARRAY, 'List of package names to reinstall, can include a wildcard (*) to match any substring.', null, $this->suggestInstalledPackage(false)), ]) ->setHelp( <<getRepositoryManager()->getLocalRepository(); $packagesToReinstall = []; $packageNamesToReinstall = []; - foreach ($input->getArgument('packages') as $pattern) { - $patternRegexp = BasePackage::packageNameToRegexp($pattern); - $matched = false; + if (\count($input->getOption('type')) > 0) { + if (\count($input->getArgument('packages')) > 0) { + throw new \InvalidArgumentException('You cannot specify package names and filter by type at the same time.'); + } foreach ($localRepo->getCanonicalPackages() as $package) { - if (Preg::isMatch($patternRegexp, $package->getName())) { - $matched = true; + if (in_array($package->getType(), $input->getOption('type'), true)) { $packagesToReinstall[] = $package; $packageNamesToReinstall[] = $package->getName(); } } + } else { + if (\count($input->getArgument('packages')) === 0) { + throw new \InvalidArgumentException('You must pass one or more package names to be reinstalled.'); + } + foreach ($input->getArgument('packages') as $pattern) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + $matched = false; + foreach ($localRepo->getCanonicalPackages() as $package) { + if (Preg::isMatch($patternRegexp, $package->getName())) { + $matched = true; + $packagesToReinstall[] = $package; + $packageNamesToReinstall[] = $package->getName(); + } + } - if (!$matched) { - $io->writeError('Pattern "' . $pattern . '" does not match any currently installed packages.'); + if (!$matched) { + $io->writeError('Pattern "' . $pattern . '" does not match any currently installed packages.'); + } } } - if (!$packagesToReinstall) { + if (0 === \count($packagesToReinstall)) { $io->writeError('Found no packages to reinstall, aborting.'); return 1; diff --git a/tests/Composer/Test/Command/ReinstallCommandTest.php b/tests/Composer/Test/Command/ReinstallCommandTest.php index 1e81a7790..cc21ce545 100644 --- a/tests/Composer/Test/Command/ReinstallCommandTest.php +++ b/tests/Composer/Test/Command/ReinstallCommandTest.php @@ -19,49 +19,70 @@ class ReinstallCommandTest extends TestCase { /** * @dataProvider caseProvider - * @param array $packages + * @param array $options * @param string $expected */ - public function testReinstallCommand(array $packages, string $expected): void + public function testReinstallCommand(array $options, string $expected): void { $this->initTempComposer([ 'require' => [ 'root/req' => '1.*', - 'root/anotherreq' => '2.*' + ], + 'require-dev' => [ + 'root/anotherreq' => '2.*', + 'root/anotherreq2' => '2.*', + 'root/lala' => '2.*', ] ]); $rootReqPackage = self::getPackage('root/req'); $anotherReqPackage = self::getPackage('root/anotherreq'); + $anotherReqPackage2 = self::getPackage('root/anotherreq2'); + $anotherReqPackage3 = self::getPackage('root/lala'); $rootReqPackage->setType('metapackage'); $anotherReqPackage->setType('metapackage'); + $anotherReqPackage2->setType('metapackage'); + $anotherReqPackage3->setType('metapackage'); - $this->createComposerLock([$rootReqPackage], [$anotherReqPackage]); - $this->createInstalledJson([$rootReqPackage], [$anotherReqPackage]); + $this->createComposerLock([$rootReqPackage], [$anotherReqPackage, $anotherReqPackage2, $anotherReqPackage3]); + $this->createInstalledJson([$rootReqPackage], [$anotherReqPackage, $anotherReqPackage2, $anotherReqPackage3]); $appTester = $this->getApplicationTester(); - $appTester->run([ + $appTester->run(array_merge([ 'command' => 'reinstall', '--no-progress' => true, '--no-plugins' => true, - 'packages' => $packages - ]); + ], $options)); self::assertSame($expected, trim($appTester->getDisplay(true))); } public function caseProvider(): Generator { - yield 'reinstall a package' => [ - ['root/req', 'root/anotherreq'], + yield 'reinstall a package by name' => [ + ['packages' => ['root/req', 'root/anotherreq*']], '- Removing root/req (1.0.0) + - Removing root/anotherreq2 (1.0.0) - Removing root/anotherreq (1.0.0) - Installing root/anotherreq (1.0.0) + - Installing root/anotherreq2 (1.0.0) + - Installing root/req (1.0.0)' + ]; + + yield 'reinstall packages by type' => [ + ['--type' => ['metapackage']], +'- Removing root/req (1.0.0) + - Removing root/lala (1.0.0) + - Removing root/anotherreq2 (1.0.0) + - Removing root/anotherreq (1.0.0) + - Installing root/anotherreq (1.0.0) + - Installing root/anotherreq2 (1.0.0) + - Installing root/lala (1.0.0) - Installing root/req (1.0.0)' ]; yield 'reinstall a package that is not installed' => [ - ['root/unknownreq'], + ['packages' => ['root/unknownreq']], 'Pattern "root/unknownreq" does not match any currently installed packages. Found no packages to reinstall, aborting.' ];