1
0
Fork 0

Add completion on commands

pull/10320/head
Jérôme TAMARELLE 2021-11-30 01:03:23 +01:00 committed by Jordi Boggiano
parent 766943c767
commit fe6be142b1
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC
22 changed files with 423 additions and 28 deletions

View File

@ -27,6 +27,8 @@ use Composer\Util\Filesystem;
use Composer\Util\Loop; use Composer\Util\Loop;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -39,6 +41,17 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class ArchiveCommand extends BaseCommand class ArchiveCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($this->completeAvailablePackage($input, $suggestions)) {
return;
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['tar', 'tar.gz', 'tar.bz2', 'zip']);
}
}
/** /**
* @return void * @return void
*/ */

View File

@ -20,10 +20,19 @@ use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\IO\NullIO; use Composer\IO\NullIO;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Plugin\PreCommandRunEvent; use Composer\Plugin\PreCommandRunEvent;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Repository\CompositeRepository;
use Composer\Repository\InstalledRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\RootPackageRepository;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -317,7 +326,96 @@ abstract class BaseCommand extends Command
} }
/** /**
* @param array<string> $requirements * Suggestion values for "prefer-install" option
*/
protected function completePreferInstall(CompletionInput $input, CompletionSuggestions $suggestions): bool
{
if ($input->mustSuggestOptionValuesFor('prefer-install')) {
$suggestions->suggestValues(['dist', 'source', 'auto']);
return true;
}
return false;
}
/**
* Suggest package names from installed ones.
*/
protected function completeInstalledPackage(CompletionInput $input, CompletionSuggestions $suggestions): bool
{
if (!$input->mustSuggestArgumentValuesFor('packages') &&
!$input->mustSuggestArgumentValuesFor('package') &&
!$input->mustSuggestOptionValuesFor('ignore')
) {
return false;
}
$composer = $this->getComposer();
$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);
$suggestions->suggestValues(array_map(function (PackageInterface $package) {
return $package->getName();
}, $installedRepo->getPackages()));
return true;
}
/**
* Suggest package names available on all configured repositories.
*/
protected function completeAvailablePackage(CompletionInput $input, CompletionSuggestions $suggestions): bool
{
if (!$input->mustSuggestArgumentValuesFor('packages') &&
!$input->mustSuggestArgumentValuesFor('package') &&
!$input->mustSuggestOptionValuesFor('require') &&
!$input->mustSuggestOptionValuesFor('require-dev')
) {
return false;
}
$composer = $this->getComposer();
$repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories());
$packages = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME);
foreach (array_slice($packages, 0, 150) as $package) {
$suggestions->suggestValue($package['name']);
}
return true;
}
/**
* Suggests ext- packages from the ones available on the currently-running PHP
*/
protected function completePlatformPackage(CompletionInput $input, CompletionSuggestions $suggestions): bool
{
if (!$input->mustSuggestOptionValuesFor('require') &&
!$input->mustSuggestOptionValuesFor('require-dev') &&
!str_starts_with($input->getCompletionValue(), 'ext-')
) {
return false;
}
$repos = new PlatformRepository([], $this->getComposer()->getConfig()->get('platform') ?? []);
$suggestions->suggestValues(array_map(function (PackageInterface $package) {
return $package->getName();
}, $repos->getPackages()));
return true;
}
/**
* @param array<string, string> $requirements
* *
* @return array<string, string> * @return array<string, string>
*/ */

View File

@ -33,6 +33,8 @@ use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\RepositorySet; use Composer\Repository\RepositorySet;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -55,6 +57,11 @@ use Composer\Package\Version\VersionParser;
*/ */
class CreateProjectCommand extends BaseCommand class CreateProjectCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeAvailablePackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions);
}
/** /**
* @var SuggestedPackagesReporter * @var SuggestedPackagesReporter
*/ */

View File

@ -12,6 +12,8 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -22,6 +24,11 @@ use Symfony\Component\Console\Input\InputOption;
*/ */
class DependsCommand extends BaseDependencyCommand class DependsCommand extends BaseDependencyCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions);
}
/** /**
* Configure command metadata. * Configure command metadata.
* *

View File

@ -12,6 +12,8 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -22,6 +24,13 @@ use Symfony\Component\Console\Input\InputArgument;
*/ */
class ExecCommand extends BaseCommand class ExecCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('binary')) {
$suggestions->suggestValues($this->getBinaries(false));
}
}
/** /**
* @return void * @return void
*/ */
@ -52,14 +61,11 @@ EOT
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$composer = $this->requireComposer(); $composer = $this->requireComposer();
if ($input->getOption('list') || !$input->getArgument('binary')) {
$bins = $this->getBinaries(true);
if (count($bins) > 0) {
$binDir = $composer->getConfig()->get('bin-dir'); $binDir = $composer->getConfig()->get('bin-dir');
if ($input->getOption('list') || null === $input->getArgument('binary')) {
$bins = glob($binDir . '/*');
$bins = array_merge($bins, array_map(function ($e) {
return "$e (local)";
}, $composer->getPackage()->getBinaries()));
if (!$bins) {
throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)"); throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)");
} }
@ -70,13 +76,6 @@ EOT
); );
foreach ($bins as $bin) { foreach ($bins as $bin) {
// skip .bat copies
if (isset($previousBin) && $bin === $previousBin.'.bat') {
continue;
}
$previousBin = $bin;
$bin = basename($bin);
$this->getIO()->write( $this->getIO()->write(
<<<EOT <<<EOT
<info>- $bin</info> <info>- $bin</info>
@ -105,4 +104,30 @@ EOT
return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args')); return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args'));
} }
private function getBinaries(bool $forDisplay): array
{
$composer = $this->getComposer();
$binDir = $composer->getConfig()->get('bin-dir');
$bins = glob($binDir . '/*');
$localBins = $composer->getPackage()->getBinaries();
if ($forDisplay) {
$localBins = array_map(function ($e) {
return "$e (local)";
}, $localBins);
}
$binaries = [];
foreach (array_merge($bins, $localBins) as $bin) {
// skip .bat copies
if (isset($previousBin) && $bin === $previousBin.'.bat') {
continue;
}
$previousBin = $bin;
$binaries[] = basename($bin);
}
return $binaries;
}
} }

View File

@ -16,6 +16,9 @@ use Composer\Factory;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Input\StringInput;
@ -26,6 +29,16 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class GlobalCommand extends BaseCommand class GlobalCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$application = $this->getApplication();
if ($input->mustSuggestArgumentValuesFor('command-name')) {
$suggestions->suggestValues(array_filter(array_map(function (Command $command) {
return $command->isHidden() ? null : $command->getName();
}, $application->all())));
}
}
/** /**
* @return void * @return void
*/ */

View File

@ -18,6 +18,8 @@ use Composer\Repository\RootPackageRepository;
use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryFactory;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -28,6 +30,11 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class HomeCommand extends BaseCommand class HomeCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions);
}
/** /**
* @inheritDoc * @inheritDoc
* *

View File

@ -23,6 +23,8 @@ use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryFactory;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -42,6 +44,14 @@ class InitCommand extends BaseCommand
/** @var array<string, string> */ /** @var array<string, string> */
private $gitConfig; private $gitConfig;
/** @var RepositorySet[] */
private $repositorySets;
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeAvailablePackage($input, $suggestions);
}
/** /**
* @inheritDoc * @inheritDoc
* *

View File

@ -16,6 +16,8 @@ use Composer\Installer;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -29,6 +31,11 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class InstallCommand extends BaseCommand class InstallCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completePreferInstall($input, $suggestions) || $this->completeInstalledPackage($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -12,6 +12,8 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
@ -23,6 +25,11 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class OutdatedCommand extends BaseCommand class OutdatedCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -12,6 +12,8 @@
namespace Composer\Command; namespace Composer\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -22,6 +24,11 @@ use Symfony\Component\Console\Input\InputOption;
*/ */
class ProhibitsCommand extends BaseDependencyCommand class ProhibitsCommand extends BaseDependencyCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeAvailablePackage($input, $suggestions);
}
/** /**
* Configure command metadata. * Configure command metadata.
* *

View File

@ -22,6 +22,8 @@ use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -32,6 +34,11 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class ReinstallCommand extends BaseCommand class ReinstallCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -20,6 +20,8 @@ use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Factory; use Composer\Factory;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -32,6 +34,11 @@ use Composer\Package\BasePackage;
*/ */
class RemoveCommand extends BaseCommand class RemoveCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -14,6 +14,8 @@ namespace Composer\Command;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -58,6 +60,11 @@ class RequireCommand extends BaseCommand
/** @var bool */ /** @var bool */
private $dependencyResolutionCompleted = false; private $dependencyResolutionCompleted = false;
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completePlatformPackage($input, $suggestions) || $this->completeAvailablePackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -16,6 +16,8 @@ use Composer\Script\Event as ScriptEvent;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Platform; use Composer\Util\Platform;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -44,6 +46,13 @@ class RunScriptCommand extends BaseCommand
ScriptEvents::POST_AUTOLOAD_DUMP, ScriptEvents::POST_AUTOLOAD_DUMP,
); );
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('script')) {
$suggestions->suggestValues(array_keys($this->getComposer()->getPackage()->getScripts()));
}
}
/** /**
* @return void * @return void
*/ */

View File

@ -14,6 +14,8 @@ namespace Composer\Command;
use Composer\Factory; use Composer\Factory;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -30,6 +32,13 @@ use Composer\Plugin\PluginEvents;
*/ */
class SearchCommand extends BaseCommand class SearchCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['json', 'text']);
}
}
/** /**
* @return void * @return void
*/ */

View File

@ -20,7 +20,6 @@ use Composer\Package\BasePackage;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\Package;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Package\Version\VersionSelector; use Composer\Package\Version\VersionSelector;
@ -41,6 +40,8 @@ use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Semver; use Composer\Semver\Semver;
use Composer\Spdx\SpdxLicenses; use Composer\Spdx\SpdxLicenses;
use Composer\Util\PackageInfo; use Composer\Util\PackageInfo;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -64,6 +65,17 @@ class ShowCommand extends BaseCommand
/** @var ?RepositorySet */ /** @var ?RepositorySet */
private $repositorySet; private $repositorySet;
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($this->completeInstalledPackage($input, $suggestions)) {
return;
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['json', 'text']);
}
}
/** /**
* @return void * @return void
*/ */

View File

@ -16,6 +16,8 @@ use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository; use Composer\Repository\RootPackageRepository;
use Composer\Repository\InstalledRepository; use Composer\Repository\InstalledRepository;
use Composer\Installer\SuggestedPackagesReporter; use Composer\Installer\SuggestedPackagesReporter;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -23,6 +25,11 @@ use Symfony\Component\Console\Output\OutputInterface;
class SuggestsCommand extends BaseCommand class SuggestsCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -24,6 +24,8 @@ use Composer\Package\Version\VersionParser;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Constraint\MultiConstraint;
use Composer\Package\Link; use Composer\Package\Link;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -37,6 +39,11 @@ use Symfony\Component\Console\Question\Question;
*/ */
class UpdateCommand extends BaseCommand class UpdateCommand extends BaseCommand
{ {
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->completeInstalledPackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions);
}
/** /**
* @return void * @return void
*/ */

View File

@ -74,6 +74,12 @@ class VersionGuesser
return null; return null;
} }
// bypass version guessing in bash completions as it takes time to create
// new processes and the root version is usually not that important
if (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] === '_complete') {
return null;
}
$versionData = $this->guessGitVersion($packageConfig, $path); $versionData = $this->guessGitVersion($packageConfig, $path);
if (null !== $versionData && null !== $versionData['version']) { if (null !== $versionData && null !== $versionData['version']) {
return $this->postprocess($versionData); return $this->postprocess($versionData);

View File

@ -44,8 +44,12 @@ class HttpDownloader
private $config; private $config;
/** @var array<Job> */ /** @var array<Job> */
private $jobs = array(); private $jobs = array();
/** @var bool */
private $disableTls;
/** @var mixed[] */ /** @var mixed[] */
private $options = array(); private $options = array();
/** @var mixed[]|null */
private $tlsDefaultOptions = null;
/** @var int */ /** @var int */
private $runningJobs = 0; private $runningJobs = 0;
/** @var int */ /** @var int */
@ -73,22 +77,19 @@ class HttpDownloader
$this->disabled = (bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK'); $this->disabled = (bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK');
// Setup TLS options if ($disableTls === true) {
// The cafile option can be set via config.json // make sure the tlsDefaultOptions are not loaded later
if ($disableTls === false) { $this->tlsDefaultOptions = [];
$this->options = StreamContextFactory::getTlsDefaults($options, $io);
} }
// handle the other externally set options normally. $this->disableTls = $disableTls;
$this->options = array_replace_recursive($this->options, $options); $this->options = $options;
$this->config = $config; $this->config = $config;
if (self::isCurlEnabled()) { if (self::isCurlEnabled()) {
$this->curl = new CurlDownloader($io, $config, $options, $disableTls); $this->curl = new CurlDownloader($io, $config, $options, $disableTls);
} }
$this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
if (is_numeric($maxJobs = Platform::getEnv('COMPOSER_MAX_PARALLEL_HTTP'))) { if (is_numeric($maxJobs = Platform::getEnv('COMPOSER_MAX_PARALLEL_HTTP'))) {
$this->maxJobs = max(1, min(50, (int) $maxJobs)); $this->maxJobs = max(1, min(50, (int) $maxJobs));
} }
@ -171,7 +172,13 @@ class HttpDownloader
*/ */
public function getOptions() public function getOptions()
{ {
return $this->options; if ($this->tlsDefaultOptions === null) {
// Setup TLS options
// The cafile option can be set via config.json
$this->tlsDefaultOptions = StreamContextFactory::getTlsDefaults($this->options, $this->io);
}
return array_replace_recursive($this->tlsDefaultOptions, $this->options);
} }
/** /**
@ -191,7 +198,7 @@ class HttpDownloader
*/ */
private function addJob(array $request, bool $sync = false): array private function addJob(array $request, bool $sync = false): array
{ {
$request['options'] = array_replace_recursive($this->options, $request['options']); $request['options'] = array_replace_recursive($this->getOptions(), $request['options']);
/** @var Job */ /** @var Job */
$job = array( $job = array(
@ -211,8 +218,6 @@ class HttpDownloader
$this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
} }
$rfs = $this->rfs;
if ($this->canUseCurl($job)) { if ($this->canUseCurl($job)) {
$resolver = function ($resolve, $reject) use (&$job): void { $resolver = function ($resolve, $reject) use (&$job): void {
$job['status'] = HttpDownloader::STATUS_QUEUED; $job['status'] = HttpDownloader::STATUS_QUEUED;
@ -285,6 +290,15 @@ class HttpDownloader
return array($job, $promise); return array($job, $promise);
} }
private function getRFS(): RemoteFilesystem
{
if (null === $this->rfs) {
$this->rfs = new RemoteFilesystem($this->io, $this->config, $this->options, $this->disableTls);
}
return $this->rfs;
}
/** /**
* @param int $id * @param int $id
* @return void * @return void

View File

@ -0,0 +1,109 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test;
use Composer\Console\Application;
use Symfony\Component\Console\Tester\CommandCompletionTester;
/**
* Validate autocompletion for all commands.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class CompletionFunctionalTest extends TestCase
{
public function getCommandSuggestions(): iterable
{
$randomProject = '104corp/cache';
$installedPackages = ['composer/semver', 'psr/log'];
$preferInstall = ['dist', 'source', 'auto'];
yield ['archive ', [$randomProject]];
yield ['archive symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']];
yield ['archive --format ', ['tar', 'zip']];
yield ['create-project ', [$randomProject]];
yield ['create-project symfony/skeleton --prefer-install ', $preferInstall];
yield ['depends ', $installedPackages];
yield ['why ', $installedPackages];
yield ['exec ', ['composer', 'compile']];
yield ['browse ', $installedPackages];
yield ['home -H ', $installedPackages];
yield ['init --require ', [$randomProject]];
yield ['init --require-dev foo/bar --require-dev ', [$randomProject]];
yield ['install --prefer-install ', $preferInstall];
yield ['install ', $installedPackages];
yield ['outdated ', $installedPackages];
yield ['prohibits ', [$randomProject]];
yield ['why-not symfony/http-ker', ['symfony/http-kernel']];
yield ['reinstall --prefer-install ', $preferInstall];
yield ['reinstall ', $installedPackages];
yield ['remove ', $installedPackages];
yield ['require --prefer-install ', $preferInstall];
yield ['require ', [$randomProject]];
yield ['require --dev symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']];
yield ['run-script ', ['compile', 'test', 'phpstan']];
yield ['run-script test ', null];
yield ['search --format ', ['text', 'json']];
yield ['show --format ', ['text', 'json']];
yield ['info ', $installedPackages];
yield ['suggests ', $installedPackages];
yield ['update --prefer-install ', $preferInstall];
yield ['update ', $installedPackages];
}
/**
* @dataProvider getCommandSuggestions
*
* @param string $input The command that is typed
* @param string[]|null $expectedSuggestions Sample expected suggestions. Null if nothing is expected.
*/
public function testComplete(string $input, ?array $expectedSuggestions): void
{
$input = explode(' ', $input);
$commandName = array_shift($input);
$command = $this->getApplication()->get($commandName);
$tester = new CommandCompletionTester($command);
$suggestions = $tester->complete($input);
if (null === $expectedSuggestions) {
$this->assertEmpty($suggestions);
return;
}
$diff = array_diff($expectedSuggestions, $suggestions);
$this->assertEmpty($diff, sprintf('Suggestions must contain "%s". Got "%s".', implode('", "', $diff), implode('", "', $suggestions)));
}
private function getApplication(): Application
{
return new Application();
}
}