diff --git a/README.md b/README.md index 58f5cb4..a0f6d7f 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,21 @@ The new version continues to support parsing the unique repository configuration The `username` and `password` can be specified in the `auth.json` file on a per-user basis with the [authentication mechanism provided by Composer](https://getcomposer.org/doc/articles/http-basic-authentication.md). +### Global configuration +It's also possible to add some configuration inside global `composer.json` located at composer home (`composer config -g home`). + +Following precedence order will be used for each key: +- command-line parameter +- local `composer.json` +- global `composer.json` +- default + +Array values will not be merged. + +The command-line parameter -- repository is required if local configuration is multi repository. Global unique repository configuration will be ignored in that case. + +Multi repository configuration will be merged by the `name` key. + ## Providers Specificity for some of the providers. diff --git a/src/Configuration.php b/src/Configuration.php index f2571e7..34751ed 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -279,58 +279,74 @@ private function getComposerJsonArchiveExcludeIgnores(InputInterface $input) /** * @param InputInterface $input + * @param Composer $composer + * + * @return array + * @throws \InvalidArgumentException|InvalidConfigException */ private function parseNexusExtra(InputInterface $input, Composer $composer) { - $this->checkNexusPushValid($input, $composer); - - $repository = $input->getOption(PushCommand::REPOSITORY); - $extras = $composer->getPackage()->getExtra(); - - $extrasConfigurationKey = 'push'; - - if (empty($extras['push'])) { - if (!empty($extras['nexus-push'])) { - $extrasConfigurationKey = 'nexus-push'; + $globalComposer = $composer->getPluginManager()->getGlobalComposer(); + $globalExtras = !empty($globalComposer) ? $globalComposer->getPackage()->getExtra() : null; + $localExtras = $composer->getPackage()->getExtra(); + + $localExtrasConfigurationKey = 'push'; + if (empty($localExtras['push'])) { + if (!empty($localExtras['nexus-push'])) { + $localExtrasConfigurationKey = 'nexus-push'; $this->io->warning('Configuration under extra - nexus-push in composer.json is deprecated, please replace it by extra - push'); } } - if (empty($repository)) { - // configurations in composer.json support Only upload to unique repository - if (!empty($extras[$extrasConfigurationKey])) { - return $extras[$extrasConfigurationKey]; - } - } else { - // configurations in composer.json support upload to multi repository - foreach ($extras[$extrasConfigurationKey] as $key => $nexusPushConfigItem) { - if (empty($nexusPushConfigItem[self::PUSH_CFG_NAME])) { - $fmt = 'The push configuration array in composer.json with index {%s} need provide value for key "%s"'; - $exceptionMsg = sprintf($fmt, $key, self::PUSH_CFG_NAME); - throw new InvalidConfigException($exceptionMsg); - } - if ($nexusPushConfigItem[self::PUSH_CFG_NAME] == $repository) { - return $nexusPushConfigItem; - } - } + $globalConfig = !empty($globalExtras['push']) ? $globalExtras['push'] : null; + $localConfig = !empty($localExtras[$localExtrasConfigurationKey]) ? $localExtras[$localExtrasConfigurationKey] : null; + + $repository = $input->getOption(PushCommand::REPOSITORY); + if (empty($repository) && !empty($localConfig[0])) { + throw new \InvalidArgumentException('As configurations in composer.json support upload to multi repository, the option --repository is required'); + } + if (!empty($repository) && empty($globalConfig[0]) && empty($localConfig[0])) { + throw new InvalidConfigException('the option --repository is offered, but configurations in composer.json doesn\'t support upload to multi repository, please check'); + } - if (empty($this->nexusPushConfig)) { + if (!empty($repository)) { + $globalRepository = $this->getRepositoryConfig($globalConfig, $repository); + $localRepository = $this->getRepositoryConfig($localConfig, $repository); + + if (empty($globalRepository) && empty($localRepository)) { throw new \InvalidArgumentException('The value of option --repository match no push configuration, please check'); } + + return array_replace($globalRepository ?? [], $localRepository ?? []); } - return []; + return array_replace($globalConfig ?? [], $localConfig ?? []); } - private function checkNexusPushValid(InputInterface $input, Composer $composer) + /** + * @param mixed $extras + * @param string $name + * + * @return mixed|null + * @throws InvalidConfigException + */ + private function getRepositoryConfig($extras, $name) { - $repository = $input->getOption(PushCommand::REPOSITORY); - $extras = $composer->getPackage()->getExtra(); - if (empty($repository) && (!empty($extras['push'][0]) || !empty($extras['nexus-push'][0]))) { - throw new \InvalidArgumentException('As configurations in composer.json support upload to multi repository, the option --repository is required'); + if (empty($extras[0])) { + return null; } - if (!empty($repository) && empty($extras['push'][0]) && empty($extras['nexus-push'][0])) { - throw new InvalidConfigException('the option --repository is offered, but configurations in composer.json doesn\'t support upload to multi repository, please check'); + + foreach ($extras as $key => $repository) { + if (empty($repository[self::PUSH_CFG_NAME])) { + $fmt = 'The push configuration array in composer.json with index {%s} need provide value for key "%s"'; + $exceptionMsg = sprintf($fmt, $key, self::PUSH_CFG_NAME); + throw new InvalidConfigException($exceptionMsg); + } + if ($repository[self::PUSH_CFG_NAME] === $name) { + return $repository; + } } + + return null; } } diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index 839acd5..1c057a5 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -5,6 +5,8 @@ use Composer\Composer; use Composer\IO\NullIO; use Composer\Package\RootPackageInterface; +use Composer\PartialComposer; +use Composer\Plugin\PluginManager; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; @@ -36,7 +38,9 @@ class ConfigurationTest extends TestCase private $configIgnoreByComposer; private $configOptionUrl; - private $singleConfig; + private $localConfig; + private $globalConfig; + private $splitConfig; private $repository; private $configType; @@ -45,6 +49,10 @@ class ConfigurationTest extends TestCase private $configVerifySsl; private $extraVerifySsl; + private const ComposerConfigEmpty = 0; + private const ComposerConfigSingle = 1; + private const ComposerConfigMulti = 2; + public function setUp(): void { $this->keepVendor = null; @@ -53,7 +61,8 @@ public function setUp(): void $this->configIgnoreByComposer = null; $this->configOptionUrl = "https://option-url.com"; - $this->singleConfig = true; + $this->localConfig = self::ComposerConfigSingle; + $this->globalConfig = self::ComposerConfigEmpty; $this->configName = null; $this->configType = null; @@ -145,7 +154,7 @@ public function testGet() $this->assertEquals('push-username', $this->configuration->get('username')); $this->assertEquals('push-password', $this->configuration->get('password')); - $this->singleConfig = false; + $this->localConfig = self::ComposerConfigMulti; $this->repository = 'A'; $this->initGlobalConfiguration(); @@ -256,6 +265,85 @@ public function testGetOptionUsername() $this->assertEquals("my-username", $this->configuration->getOptionUsername()); } + public function testGetGlobalConfig() + { + $this->configIgnore = ['dir1', 'dir2']; + + $this->splitConfig = true; + $this->localConfig = self::ComposerConfigSingle; + $this->globalConfig = self::ComposerConfigSingle; + $this->repository = null; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://global.example.com', $this->configuration->get('url')); + $this->assertArrayEquals($this->configIgnore, $this->configuration->get('ignore')); + + $this->splitConfig = false; + $this->localConfig = self::ComposerConfigSingle; + $this->globalConfig = self::ComposerConfigMulti; + $this->repository = null; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://example.com', $this->configuration->get('url')); + + $this->repository = 'A'; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://global.a.com', $this->configuration->get('url')); + + $this->repository = 'B'; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://global.b.com', $this->configuration->get('url')); + + $this->localConfig = self::ComposerConfigMulti; + $this->globalConfig = self::ComposerConfigSingle; + $this->repository = null; + + $this->initGlobalConfiguration(); + $this->expectException(\InvalidArgumentException::class); + $this->configuration->get('url'); + + $this->localConfig = self::ComposerConfigMulti; + $this->globalConfig = self::ComposerConfigMulti; + $this->repository = 'A'; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://a.com', $this->configuration->get('url')); + $this->assertEquals('global-push-username-a', $this->configuration->get('username')); + + $this->repository = 'B'; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://b.com', $this->configuration->get('url')); + $this->assertEquals('global-push-username-b', $this->configuration->get('username')); + + + $this->splitConfig = false; + + $this->localConfig = self::ComposerConfigEmpty; + $this->globalConfig = self::ComposerConfigSingle; + $this->repository = null; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://global.example.com', $this->configuration->get('url')); + $this->assertEquals(null, $this->configuration->get('ignore')); + + $this->localConfig = self::ComposerConfigEmpty; + $this->globalConfig = self::ComposerConfigMulti; + $this->repository = 'A'; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://global.a.com', $this->configuration->get('url')); + $this->assertEquals('global-push-username-a', $this->configuration->get('username')); + + $this->repository = 'B'; + + $this->initGlobalConfiguration(); + $this->assertEquals('https://global.b.com', $this->configuration->get('url')); + $this->assertEquals('global-push-username-b', $this->configuration->get('username')); + } + private function createInputMock() { $input = $this->createMock(InputInterface::class); @@ -311,36 +399,93 @@ private function createComposerMock() $packageInterface->method('getVersion')->willReturn('1.2.3'); $packageInterface->method('getExtra')->willReturnCallback(function() { - if ($this->singleConfig) { - return [ - 'push' => [ - 'url' => 'https://example.com', - "username" => "push-username", - "password" => "push-password", - "ignore" => $this->configIgnore, - "type" => $this->extraConfigType, - "ssl-verify" => $this->extraVerifySsl, - ] - ]; - } else { - return [ - 'push' => [ - [ - 'name' => 'A', - 'url' => 'https://a.com', - "username" => "push-username-a", - "password" => "push-password-a", - ], - [ - 'name' => 'B', - 'url' => 'https://b.com', - "username" => "push-username-b", - "password" => "push-password-b", - ] - ] - ]; + switch ($this->localConfig) { + case self::ComposerConfigSingle: + return [ + 'push' => array_replace([ + "ignore" => $this->configIgnore, + ], (!$this->splitConfig) ? [ + 'url' => 'https://example.com', + "username" => "push-username", + "password" => "push-password", + "type" => $this->extraConfigType, + "ssl-verify" => $this->extraVerifySsl, + ] : []) + ]; + case self::ComposerConfigMulti: + return [ + 'push' => array_replace_recursive([ + [ + 'name' => 'A', + 'url' => 'https://a.com', + ], + [ + 'name' => 'B', + 'url' => 'https://b.com', + ] + ], (!$this->splitConfig) ? [ + [ + "username" => "push-username-a", + "password" => "push-password-a", + ], + [ + "username" => "push-username-b", + "password" => "push-password-b", + ] + ] : []) + ]; + default: + return []; } + }); + $pluginManager = $this->createMock(PluginManager::class); + $globalComposer = $this->createMock(PartialComposer::class); + $globalPackageInterface = $this->createMock(RootPackageInterface::class); + + $composer->method('getPluginManager')->willReturn($pluginManager); + $pluginManager->method('getGlobalComposer')->willReturn($globalComposer); + $globalComposer->method('getPackage')->willReturn($globalPackageInterface); + + $globalPackageInterface->method('getExtra')->willReturnCallback(function () { + switch ($this->globalConfig) { + case self::ComposerConfigSingle: + return [ + 'push' => array_replace([ + 'url' => 'https://global.example.com', + "username" => "global-push-username", + "password" => "global-push-password", + "type" => $this->extraConfigType, + "ssl-verify" => $this->extraVerifySsl, + ], (!$this->splitConfig) ? [ + "ignore" => $this->configIgnore, + ] : []) + ]; + case self::ComposerConfigMulti: + return [ + 'push' => array_replace_recursive([ + [ + 'name' => 'B', + "username" => "global-push-username-b", + "password" => "global-push-password-b", + ], + [ + 'name' => 'A', + "username" => "global-push-username-a", + "password" => "global-push-password-a", + ] + ], (!$this->splitConfig) ? [ + [ + 'url' => 'https://global.b.com', + ], + [ + 'url' => 'https://global.a.com', + ] + ] : []) + ]; + default: + return []; + } }); $packageInterface->method('getArchiveExcludes')->willReturnCallback(function() {