Skip to content

Commit 2aa371d

Browse files
authored
Merge pull request #93 from pnkov/global-config
Add support for global config
2 parents 01dddda + fd09367 commit 2aa371d

File tree

3 files changed

+245
-67
lines changed

3 files changed

+245
-67
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ The new version continues to support parsing the unique repository configuration
106106
107107
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).
108108
109+
### Global configuration
110+
It's also possible to add some configuration inside global `composer.json` located at composer home (`composer config -g home`).
111+
112+
Following precedence order will be used for each key:
113+
- command-line parameter
114+
- local `composer.json`
115+
- global `composer.json`
116+
- default
117+
118+
Array values will not be merged.
119+
120+
The command-line parameter -- repository is required if local configuration is multi repository. Global unique repository configuration will be ignored in that case.
121+
122+
Multi repository configuration will be merged by the `name` key.
123+
109124
## Providers
110125
Specificity for some of the providers.
111126

src/Configuration.php

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -279,58 +279,74 @@ private function getComposerJsonArchiveExcludeIgnores(InputInterface $input)
279279

280280
/**
281281
* @param InputInterface $input
282+
* @param Composer $composer
283+
*
284+
* @return array
285+
* @throws \InvalidArgumentException|InvalidConfigException
282286
*/
283287
private function parseNexusExtra(InputInterface $input, Composer $composer)
284288
{
285-
$this->checkNexusPushValid($input, $composer);
286-
287-
$repository = $input->getOption(PushCommand::REPOSITORY);
288-
$extras = $composer->getPackage()->getExtra();
289-
290-
$extrasConfigurationKey = 'push';
291-
292-
if (empty($extras['push'])) {
293-
if (!empty($extras['nexus-push'])) {
294-
$extrasConfigurationKey = 'nexus-push';
289+
$globalComposer = $composer->getPluginManager()->getGlobalComposer();
290+
$globalExtras = !empty($globalComposer) ? $globalComposer->getPackage()->getExtra() : null;
291+
$localExtras = $composer->getPackage()->getExtra();
292+
293+
$localExtrasConfigurationKey = 'push';
294+
if (empty($localExtras['push'])) {
295+
if (!empty($localExtras['nexus-push'])) {
296+
$localExtrasConfigurationKey = 'nexus-push';
295297
$this->io->warning('Configuration under extra - nexus-push in composer.json is deprecated, please replace it by extra - push');
296298
}
297299
}
298300

299-
if (empty($repository)) {
300-
// configurations in composer.json support Only upload to unique repository
301-
if (!empty($extras[$extrasConfigurationKey])) {
302-
return $extras[$extrasConfigurationKey];
303-
}
304-
} else {
305-
// configurations in composer.json support upload to multi repository
306-
foreach ($extras[$extrasConfigurationKey] as $key => $nexusPushConfigItem) {
307-
if (empty($nexusPushConfigItem[self::PUSH_CFG_NAME])) {
308-
$fmt = 'The push configuration array in composer.json with index {%s} need provide value for key "%s"';
309-
$exceptionMsg = sprintf($fmt, $key, self::PUSH_CFG_NAME);
310-
throw new InvalidConfigException($exceptionMsg);
311-
}
312-
if ($nexusPushConfigItem[self::PUSH_CFG_NAME] == $repository) {
313-
return $nexusPushConfigItem;
314-
}
315-
}
301+
$globalConfig = !empty($globalExtras['push']) ? $globalExtras['push'] : null;
302+
$localConfig = !empty($localExtras[$localExtrasConfigurationKey]) ? $localExtras[$localExtrasConfigurationKey] : null;
303+
304+
$repository = $input->getOption(PushCommand::REPOSITORY);
305+
if (empty($repository) && !empty($localConfig[0])) {
306+
throw new \InvalidArgumentException('As configurations in composer.json support upload to multi repository, the option --repository is required');
307+
}
308+
if (!empty($repository) && empty($globalConfig[0]) && empty($localConfig[0])) {
309+
throw new InvalidConfigException('the option --repository is offered, but configurations in composer.json doesn\'t support upload to multi repository, please check');
310+
}
316311

317-
if (empty($this->nexusPushConfig)) {
312+
if (!empty($repository)) {
313+
$globalRepository = $this->getRepositoryConfig($globalConfig, $repository);
314+
$localRepository = $this->getRepositoryConfig($localConfig, $repository);
315+
316+
if (empty($globalRepository) && empty($localRepository)) {
318317
throw new \InvalidArgumentException('The value of option --repository match no push configuration, please check');
319318
}
319+
320+
return array_replace($globalRepository ?? [], $localRepository ?? []);
320321
}
321322

322-
return [];
323+
return array_replace($globalConfig ?? [], $localConfig ?? []);
323324
}
324325

325-
private function checkNexusPushValid(InputInterface $input, Composer $composer)
326+
/**
327+
* @param mixed $extras
328+
* @param string $name
329+
*
330+
* @return mixed|null
331+
* @throws InvalidConfigException
332+
*/
333+
private function getRepositoryConfig($extras, $name)
326334
{
327-
$repository = $input->getOption(PushCommand::REPOSITORY);
328-
$extras = $composer->getPackage()->getExtra();
329-
if (empty($repository) && (!empty($extras['push'][0]) || !empty($extras['nexus-push'][0]))) {
330-
throw new \InvalidArgumentException('As configurations in composer.json support upload to multi repository, the option --repository is required');
335+
if (empty($extras[0])) {
336+
return null;
331337
}
332-
if (!empty($repository) && empty($extras['push'][0]) && empty($extras['nexus-push'][0])) {
333-
throw new InvalidConfigException('the option --repository is offered, but configurations in composer.json doesn\'t support upload to multi repository, please check');
338+
339+
foreach ($extras as $key => $repository) {
340+
if (empty($repository[self::PUSH_CFG_NAME])) {
341+
$fmt = 'The push configuration array in composer.json with index {%s} needs to provide the value for key "%s"';
342+
$exceptionMsg = sprintf($fmt, $key, self::PUSH_CFG_NAME);
343+
throw new InvalidConfigException($exceptionMsg);
344+
}
345+
if ($repository[self::PUSH_CFG_NAME] === $name) {
346+
return $repository;
347+
}
334348
}
349+
350+
return null;
335351
}
336352
}

tests/ConfigurationTest.php

Lines changed: 178 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Composer\Composer;
66
use Composer\IO\NullIO;
77
use Composer\Package\RootPackageInterface;
8+
use Composer\Plugin\PluginManager;
89
use PHPUnit\Framework\ExpectationFailedException;
910
use PHPUnit\Framework\TestCase;
1011
use Symfony\Component\Console\Input\InputInterface;
@@ -36,7 +37,9 @@ class ConfigurationTest extends TestCase
3637
private $configIgnoreByComposer;
3738
private $configOptionUrl;
3839

39-
private $singleConfig;
40+
private $localConfig;
41+
private $globalConfig;
42+
private $splitConfig;
4043
private $repository;
4144

4245
private $configType;
@@ -45,6 +48,10 @@ class ConfigurationTest extends TestCase
4548
private $configVerifySsl;
4649
private $extraVerifySsl;
4750

51+
private const ComposerConfigEmpty = 0;
52+
private const ComposerConfigSingle = 1;
53+
private const ComposerConfigMulti = 2;
54+
4855
public function setUp(): void
4956
{
5057
$this->keepVendor = null;
@@ -53,7 +60,8 @@ public function setUp(): void
5360
$this->configIgnoreByComposer = null;
5461
$this->configOptionUrl = "https://option-url.com";
5562

56-
$this->singleConfig = true;
63+
$this->localConfig = self::ComposerConfigSingle;
64+
$this->globalConfig = self::ComposerConfigEmpty;
5765
$this->configName = null;
5866

5967
$this->configType = null;
@@ -145,7 +153,7 @@ public function testGet()
145153
$this->assertEquals('push-username', $this->configuration->get('username'));
146154
$this->assertEquals('push-password', $this->configuration->get('password'));
147155

148-
$this->singleConfig = false;
156+
$this->localConfig = self::ComposerConfigMulti;
149157
$this->repository = 'A';
150158

151159
$this->initGlobalConfiguration();
@@ -256,6 +264,85 @@ public function testGetOptionUsername()
256264
$this->assertEquals("my-username", $this->configuration->getOptionUsername());
257265
}
258266

267+
public function testGetGlobalConfig()
268+
{
269+
$this->configIgnore = ['dir1', 'dir2'];
270+
271+
$this->splitConfig = true;
272+
$this->localConfig = self::ComposerConfigSingle;
273+
$this->globalConfig = self::ComposerConfigSingle;
274+
$this->repository = null;
275+
276+
$this->initGlobalConfiguration();
277+
$this->assertEquals('https://global.example.com', $this->configuration->get('url'));
278+
$this->assertArrayEquals($this->configIgnore, $this->configuration->get('ignore'));
279+
280+
$this->splitConfig = false;
281+
$this->localConfig = self::ComposerConfigSingle;
282+
$this->globalConfig = self::ComposerConfigMulti;
283+
$this->repository = null;
284+
285+
$this->initGlobalConfiguration();
286+
$this->assertEquals('https://example.com', $this->configuration->get('url'));
287+
288+
$this->repository = 'A';
289+
290+
$this->initGlobalConfiguration();
291+
$this->assertEquals('https://global.a.com', $this->configuration->get('url'));
292+
293+
$this->repository = 'B';
294+
295+
$this->initGlobalConfiguration();
296+
$this->assertEquals('https://global.b.com', $this->configuration->get('url'));
297+
298+
$this->localConfig = self::ComposerConfigMulti;
299+
$this->globalConfig = self::ComposerConfigSingle;
300+
$this->repository = null;
301+
302+
$this->initGlobalConfiguration();
303+
$this->expectException(\InvalidArgumentException::class);
304+
$this->configuration->get('url');
305+
306+
$this->localConfig = self::ComposerConfigMulti;
307+
$this->globalConfig = self::ComposerConfigMulti;
308+
$this->repository = 'A';
309+
310+
$this->initGlobalConfiguration();
311+
$this->assertEquals('https://a.com', $this->configuration->get('url'));
312+
$this->assertEquals('global-push-username-a', $this->configuration->get('username'));
313+
314+
$this->repository = 'B';
315+
316+
$this->initGlobalConfiguration();
317+
$this->assertEquals('https://b.com', $this->configuration->get('url'));
318+
$this->assertEquals('global-push-username-b', $this->configuration->get('username'));
319+
320+
321+
$this->splitConfig = false;
322+
323+
$this->localConfig = self::ComposerConfigEmpty;
324+
$this->globalConfig = self::ComposerConfigSingle;
325+
$this->repository = null;
326+
327+
$this->initGlobalConfiguration();
328+
$this->assertEquals('https://global.example.com', $this->configuration->get('url'));
329+
$this->assertEquals(null, $this->configuration->get('ignore'));
330+
331+
$this->localConfig = self::ComposerConfigEmpty;
332+
$this->globalConfig = self::ComposerConfigMulti;
333+
$this->repository = 'A';
334+
335+
$this->initGlobalConfiguration();
336+
$this->assertEquals('https://global.a.com', $this->configuration->get('url'));
337+
$this->assertEquals('global-push-username-a', $this->configuration->get('username'));
338+
339+
$this->repository = 'B';
340+
341+
$this->initGlobalConfiguration();
342+
$this->assertEquals('https://global.b.com', $this->configuration->get('url'));
343+
$this->assertEquals('global-push-username-b', $this->configuration->get('username'));
344+
}
345+
259346
private function createInputMock()
260347
{
261348
$input = $this->createMock(InputInterface::class);
@@ -311,36 +398,96 @@ private function createComposerMock()
311398

312399
$packageInterface->method('getVersion')->willReturn('1.2.3');
313400
$packageInterface->method('getExtra')->willReturnCallback(function() {
314-
if ($this->singleConfig) {
315-
return [
316-
'push' => [
317-
'url' => 'https://example.com',
318-
"username" => "push-username",
319-
"password" => "push-password",
320-
"ignore" => $this->configIgnore,
321-
"type" => $this->extraConfigType,
322-
"ssl-verify" => $this->extraVerifySsl,
323-
]
324-
];
325-
} else {
326-
return [
327-
'push' => [
328-
[
329-
'name' => 'A',
330-
'url' => 'https://a.com',
331-
"username" => "push-username-a",
332-
"password" => "push-password-a",
333-
],
334-
[
335-
'name' => 'B',
336-
'url' => 'https://b.com',
337-
"username" => "push-username-b",
338-
"password" => "push-password-b",
339-
]
340-
]
341-
];
401+
switch ($this->localConfig) {
402+
case self::ComposerConfigSingle:
403+
return [
404+
'push' => array_replace([
405+
"ignore" => $this->configIgnore,
406+
], (!$this->splitConfig) ? [
407+
'url' => 'https://example.com',
408+
"username" => "push-username",
409+
"password" => "push-password",
410+
"type" => $this->extraConfigType,
411+
"ssl-verify" => $this->extraVerifySsl,
412+
] : [])
413+
];
414+
case self::ComposerConfigMulti:
415+
return [
416+
'push' => array_replace_recursive([
417+
[
418+
'name' => 'A',
419+
'url' => 'https://a.com',
420+
],
421+
[
422+
'name' => 'B',
423+
'url' => 'https://b.com',
424+
]
425+
], (!$this->splitConfig) ? [
426+
[
427+
"username" => "push-username-a",
428+
"password" => "push-password-a",
429+
],
430+
[
431+
"username" => "push-username-b",
432+
"password" => "push-password-b",
433+
]
434+
] : [])
435+
];
436+
default:
437+
return [];
342438
}
439+
});
343440

441+
$pluginManager = $this->createMock(PluginManager::class);
442+
// PartialComposer is returned for 2.3.0+ composer
443+
$globalComposer = class_exists('Composer\PartialComposer')
444+
? $this->createMock('Composer\PartialComposer')
445+
: $this->createMock('Composer\Composer');
446+
$globalPackageInterface = $this->createMock(RootPackageInterface::class);
447+
448+
$composer->method('getPluginManager')->willReturn($pluginManager);
449+
$pluginManager->method('getGlobalComposer')->willReturn($globalComposer);
450+
$globalComposer->method('getPackage')->willReturn($globalPackageInterface);
451+
452+
$globalPackageInterface->method('getExtra')->willReturnCallback(function () {
453+
switch ($this->globalConfig) {
454+
case self::ComposerConfigSingle:
455+
return [
456+
'push' => array_replace([
457+
'url' => 'https://global.example.com',
458+
"username" => "global-push-username",
459+
"password" => "global-push-password",
460+
"type" => $this->extraConfigType,
461+
"ssl-verify" => $this->extraVerifySsl,
462+
], (!$this->splitConfig) ? [
463+
"ignore" => $this->configIgnore,
464+
] : [])
465+
];
466+
case self::ComposerConfigMulti:
467+
return [
468+
'push' => array_replace_recursive([
469+
[
470+
'name' => 'B',
471+
"username" => "global-push-username-b",
472+
"password" => "global-push-password-b",
473+
],
474+
[
475+
'name' => 'A',
476+
"username" => "global-push-username-a",
477+
"password" => "global-push-password-a",
478+
]
479+
], (!$this->splitConfig) ? [
480+
[
481+
'url' => 'https://global.b.com',
482+
],
483+
[
484+
'url' => 'https://global.a.com',
485+
]
486+
] : [])
487+
];
488+
default:
489+
return [];
490+
}
344491
});
345492

346493
$packageInterface->method('getArchiveExcludes')->willReturnCallback(function() {

0 commit comments

Comments
 (0)