From 44c5b6cde6b478f36bc42f7246ae1ccd44866a2b Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 11 Nov 2021 14:17:58 +0000 Subject: [PATCH] Config: add source option for command to show where a config value is loaded from (#10129) --- doc/03-cli.md | 2 + src/Composer/Command/ConfigCommand.php | 27 +++++--- src/Composer/Command/CreateProjectCommand.php | 2 +- src/Composer/Command/DiagnoseCommand.php | 2 +- src/Composer/Config.php | 61 ++++++++++++++++++- src/Composer/Factory.php | 14 +++-- tests/Composer/Test/ConfigTest.php | 22 +++++++ 7 files changed, 113 insertions(+), 17 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index 6722fa8b3..e9b78e593 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -736,6 +736,8 @@ See the [Config](06-config.md) chapter for valid configuration options. instead of relative. * **--json:** JSON decode the setting value, to be used with `extra.*` keys. * **--merge:** Merge the setting value with the current value, to be used with `extra.*` keys in combination with `--json`. +* **--append:** When adding a repository, append it (lowest priority) to the existing ones instead of prepending it (highest priority). +* **--source:** Display where the config value is loaded from. ### Modifying Repositories diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 03ebd9682..e9a22ab46 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -77,6 +77,7 @@ class ConfigCommand extends BaseCommand new InputOption('json', 'j', InputOption::VALUE_NONE, 'JSON decode the setting value, to be used with extra.* keys'), new InputOption('merge', 'm', InputOption::VALUE_NONE, 'Merge the setting value with the current value, to be used with extra.* keys in combination with --json'), new InputOption('append', null, InputOption::VALUE_NONE, 'When adding a repository, append it (lowest priority) to the existing ones instead of prepending it (highest priority)'), + new InputOption('source', null, InputOption::VALUE_NONE, 'Display where the config value is loaded from'), new InputArgument('setting-key', null, 'Setting key'), new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'), )) @@ -234,13 +235,13 @@ EOT } if (!$input->getOption('global')) { - $this->config->merge($this->configFile->read()); - $this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array())); + $this->config->merge($this->configFile->read(), $this->configFile->getPath()); + $this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array()), $this->authConfigFile->getPath()); } // List the configuration of the file settings if ($input->getOption('list')) { - $this->listConfiguration($this->config->all(), $this->config->raw(), $output); + $this->listConfiguration($this->config->all(), $this->config->raw(), $output, null, (bool) $input->getOption('source')); return 0; } @@ -305,7 +306,12 @@ EOT $value = json_encode($value); } - $this->getIO()->write($value, true, IOInterface::QUIET); + $sourceOfConfigValue = ''; + if ($input->getOption('source')) { + $sourceOfConfigValue = ' (' . $this->config->getSourceOfValue($settingKey) . ')'; + } + + $this->getIO()->write($value . $sourceOfConfigValue, true, IOInterface::QUIET); return 0; } @@ -823,10 +829,11 @@ EOT * @param array $contents * @param array $rawContents * @param string|null $k + * @param bool $showSource * * @return void */ - protected function listConfiguration(array $contents, array $rawContents, OutputInterface $output, $k = null) + protected function listConfiguration(array $contents, array $rawContents, OutputInterface $output, $k = null, $showSource = false) { $origK = $k; $io = $this->getIO(); @@ -839,7 +846,7 @@ EOT if (is_array($value) && (!is_numeric(key($value)) || ($key === 'repositories' && null === $k))) { $k .= preg_replace('{^config\.}', '', $key . '.'); - $this->listConfiguration($value, $rawVal, $output, $k); + $this->listConfiguration($value, $rawVal, $output, $k, $showSource); $k = $origK; continue; @@ -857,10 +864,14 @@ EOT $value = var_export($value, true); } + $source = ''; + if ($showSource) { + $source = ' (' . $this->config->getSourceOfValue($k . $key) . ')'; + } if (is_string($rawVal) && $rawVal != $value) { - $io->write('[' . $k . $key . '] ' . $rawVal . ' (' . $value . ')', true, IOInterface::QUIET); + $io->write('[' . $k . $key . '] ' . $rawVal . ' (' . $value . ')' . $source, true, IOInterface::QUIET); } else { - $io->write('[' . $k . $key . '] ' . $value . '', true, IOInterface::QUIET); + $io->write('[' . $k . $key . '] ' . $value . '' . $source, true, IOInterface::QUIET); } } } diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 0cf206a53..233ed1a2e 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -354,7 +354,7 @@ EOT protected function installRootPackage(IOInterface $io, Config $config, $packageName, PlatformRequirementFilterInterface $platformRequirementFilter, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, array $repositories = null, $disablePlugins = false, $noScripts = false, $noProgress = false, $secureHttp = true) { if (!$secureHttp) { - $config->merge(array('config' => array('secure-http' => false))); + $config->merge(array('config' => array('secure-http' => false)), Config::SOURCE_COMMAND); } $parser = new VersionParser(); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 8e4e617cc..80c951833 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -90,7 +90,7 @@ EOT $config = Factory::createConfig(); } - $config->merge(array('config' => array('secure-http' => false))); + $config->merge(array('config' => array('secure-http' => false)), Config::SOURCE_COMMAND); $config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO); $this->httpDownloader = Factory::createHttpDownloader($io, $config); diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 0cbbf27dd..9d879013a 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -23,6 +23,10 @@ use Composer\Util\ProcessExecutor; */ class Config { + const SOURCE_DEFAULT = 'default'; + const SOURCE_COMMAND = 'command'; + const SOURCE_UNKNOWN = 'unknown'; + const RELATIVE_PATHS = 1; /** @var array */ @@ -100,6 +104,8 @@ class Config private $useEnvironment; /** @var array */ private $warnedHosts = array(); + /** @var array */ + private $sourceOfConfigValue = array(); /** * @param bool $useEnvironment Use COMPOSER_ environment variables to replace config settings @@ -112,6 +118,14 @@ class Config $this->repositories = static::$defaultRepositories; $this->useEnvironment = (bool) $useEnvironment; $this->baseDir = $baseDir; + + foreach ($this->config as $configKey => $configValue) { + $this->setSourceOfConfigValue($configValue, $configKey, self::SOURCE_DEFAULT); + } + + foreach ($this->repositories as $configKey => $configValue) { + $this->setSourceOfConfigValue($configValue, 'repositories.' . $configKey, self::SOURCE_DEFAULT); + } } /** @@ -150,18 +164,21 @@ class Config * Merges new config values with the existing ones (overriding) * * @param array $config + * @param string $source * * @return void */ - public function merge($config) + public function merge($config, $source = self::SOURCE_UNKNOWN) { // override defaults with given config if (!empty($config['config']) && is_array($config['config'])) { foreach ($config['config'] as $key => $val) { if (in_array($key, array('bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer')) && isset($this->config[$key])) { $this->config[$key] = array_merge($this->config[$key], $val); + $this->setSourceOfConfigValue($val, $key, $source); } elseif (in_array($key, array('gitlab-domains', 'github-domains')) && isset($this->config[$key])) { $this->config[$key] = array_unique(array_merge($this->config[$key], $val)); + $this->setSourceOfConfigValue($val, $key, $source); } elseif ('preferred-install' === $key && isset($this->config[$key])) { if (is_array($val) || is_array($this->config[$key])) { if (is_string($val)) { @@ -169,8 +186,10 @@ class Config } if (is_string($this->config[$key])) { $this->config[$key] = array('*' => $this->config[$key]); + $this->sourceOfConfigValue[$key . '*'] = $source; } $this->config[$key] = array_merge($this->config[$key], $val); + $this->setSourceOfConfigValue($val, $key, $source); // the full match pattern needs to be last if (isset($this->config[$key]['*'])) { $wildcard = $this->config[$key]['*']; @@ -179,9 +198,11 @@ class Config } } else { $this->config[$key] = $val; + $this->setSourceOfConfigValue($val, $key, $source); } } else { $this->config[$key] = $val; + $this->setSourceOfConfigValue($val, $key, $source); } } } @@ -210,11 +231,14 @@ class Config // store repo if (is_int($name)) { $this->repositories[] = $repository; + $this->setSourceOfConfigValue($repository, 'repositories.' . array_search($repository, $this->repositories, true), $source); } else { if ($name === 'packagist') { // BC support for default "packagist" named repo $this->repositories[$name . '.org'] = $repository; + $this->setSourceOfConfigValue($repository, 'repositories.' . $name . '.org', $source); } else { $this->repositories[$name] = $repository; + $this->setSourceOfConfigValue($repository, 'repositories.' . $name, $source); } } } @@ -257,6 +281,10 @@ class Config $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); $val = $this->getComposerEnv($env); + if ($val !== false) { + $this->setSourceOfConfigValue($val, $key, $env); + } + $val = rtrim((string) $this->process(false !== $val ? $val : $this->config[$key], $flags), '/\\'); $val = Platform::expandPath($val); @@ -275,6 +303,8 @@ class Config $val = $this->getComposerEnv($env); if (false === $val) { $val = $this->config[$key]; + } else { + $this->setSourceOfConfigValue($val, $key, $env); } return $val !== 'false' && (bool) $val; @@ -405,6 +435,35 @@ class Config return $all; } + /** + * @param string $key + * @return string + */ + public function getSourceOfValue($key) + { + $this->get($key); + + return isset($this->sourceOfConfigValue[$key]) ? $this->sourceOfConfigValue[$key] : self::SOURCE_UNKNOWN; + } + + /** + * @param mixed $configValue + * @param string $path + * @param string $source + * + * @return void + */ + private function setSourceOfConfigValue($configValue, $path, $source) + { + $this->sourceOfConfigValue[$path] = $source; + + if (is_array($configValue)) { + foreach ($configValue as $key => $value) { + $this->setSourceOfConfigValue($value, $path . '.' . $key, $source); + } + } + } + /** * @return array */ diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index fdcee9034..6b5e38c03 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -186,7 +186,7 @@ class Factory 'home' => $home, 'cache-dir' => self::getCacheDir($home), 'data-dir' => self::getDataDir($home), - ))); + )), Config::SOURCE_DEFAULT); // load global config $file = new JsonFile($config->get('home').'/config.json'); @@ -194,7 +194,7 @@ class Factory if ($io && $io->isDebug()) { $io->writeError('Loading config file ' . $file->getPath()); } - $config->merge($file->read()); + $config->merge($file->read(), $file->getPath()); } $config->setConfigSource(new JsonConfigSource($file)); @@ -220,7 +220,7 @@ class Factory if ($io && $io->isDebug()) { $io->writeError('Loading config file ' . $file->getPath()); } - $config->merge(array('config' => $file->read())); + $config->merge(array('config' => $file->read()), $file->getPath()); } $config->setAuthConfigSource(new JsonConfigSource($file, true)); @@ -236,7 +236,7 @@ class Factory if ($io && $io->isDebug()) { $io->writeError('Loading auth config from COMPOSER_AUTH'); } - $config->merge(array('config' => $authData)); + $config->merge(array('config' => $authData), 'COMPOSER_AUTH'); } } @@ -309,6 +309,7 @@ class Factory $localConfig = static::getComposerFile(); } + $localConfigSource = Config::SOURCE_UNKNOWN; if (is_string($localConfig)) { $composerFile = $localConfig; @@ -340,11 +341,12 @@ class Factory } $localConfig = $file->read(); + $localConfigSource = $file->getPath(); } // Load config and override with local config/auth config $config = static::createConfig($io, $cwd); - $config->merge($localConfig); + $config->merge($localConfig, $localConfigSource); if (isset($composerFile)) { $io->writeError('Loading config file ' . $composerFile .' ('.realpath($composerFile).')', true, IOInterface::DEBUG); $config->setConfigSource(new JsonConfigSource(new JsonFile(realpath($composerFile), null, $io))); @@ -352,7 +354,7 @@ class Factory $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json', null, $io); if ($localAuthFile->exists()) { $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG); - $config->merge(array('config' => $localAuthFile->read())); + $config->merge(array('config' => $localAuthFile->read()), $localAuthFile->getPath()); $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true)); } } diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php index ebc75ce04..06f655721 100644 --- a/tests/Composer/Test/ConfigTest.php +++ b/tests/Composer/Test/ConfigTest.php @@ -340,4 +340,26 @@ class ConfigTest extends TestCase $this->assertEquals(0, $config->get('htaccess-protect')); putenv('COMPOSER_HTACCESS_PROTECT'); } + + public function testGetSourceOfValue() + { + $config = new Config; + + $this->assertSame(Config::SOURCE_DEFAULT, $config->getSourceOfValue('process-timeout')); + + $config->merge( + array('config' => array('process-timeout' => 1)), + 'phpunit-test' + ); + + $this->assertSame('phpunit-test', $config->getSourceOfValue('process-timeout')); + } + + public function testGetSourceOfValueEnvVariables() + { + putenv('COMPOSER_HTACCESS_PROTECT=0'); + $config = new Config; + $this->assertEquals('COMPOSER_HTACCESS_PROTECT', $config->getSourceOfValue('htaccess-protect')); + putenv('COMPOSER_HTACCESS_PROTECT'); + } }