Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for global config #93

Merged
merged 3 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
88 changes: 52 additions & 36 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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} needs to provide the 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;
}
}
209 changes: 178 additions & 31 deletions tests/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Composer\Composer;
use Composer\IO\NullIO;
use Composer\Package\RootPackageInterface;
use Composer\Plugin\PluginManager;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -36,7 +37,9 @@ class ConfigurationTest extends TestCase
private $configIgnoreByComposer;
private $configOptionUrl;

private $singleConfig;
private $localConfig;
private $globalConfig;
private $splitConfig;
private $repository;

private $configType;
Expand All @@ -45,6 +48,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;
Expand All @@ -53,7 +60,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;
Expand Down Expand Up @@ -145,7 +153,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();
Expand Down Expand Up @@ -256,6 +264,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);
Expand Down Expand Up @@ -311,36 +398,96 @@ 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);
// PartialComposer is returned for 2.3.0+ composer
$globalComposer = class_exists('Composer\PartialComposer')
? $this->createMock('Composer\PartialComposer')
: $this->createMock('Composer\Composer');
$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() {
Expand Down
Loading