From 96c44b1c3ad6c372749c662798339ccabd1446ee Mon Sep 17 00:00:00 2001 From: Christopher CHEN Date: Wed, 26 Jun 2019 11:45:15 +0800 Subject: [PATCH] Allow merging "scripts" in a deep way Introduce a new "merge-scripts-deep" configuration setting to control merging the "scripts" section of included/required files in a way similar to to array_merge_recursive() - however duplicate string array keys are replaced instead of merged, while numeric array keys are merged as usual. --- README.md | 8 ++- src/Merge/ExtraPackage.php | 4 +- src/Merge/PluginState.php | 33 +++++++++++ tests/phpunit/MergePluginTest.php | 55 ++++++++++++++++++- .../testMergeScriptsDeep/composer.json | 14 +++++ .../testMergeScriptsDeep/composer.local.json | 8 +++ .../testMergeScriptsDeepReplace/composer.json | 15 +++++ .../composer.local.json | 8 +++ 8 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 tests/phpunit/fixtures/testMergeScriptsDeep/composer.json create mode 100644 tests/phpunit/fixtures/testMergeScriptsDeep/composer.local.json create mode 100644 tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.json create mode 100644 tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.local.json diff --git a/README.md b/README.md index 15c2ae1..025c80e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ Usage "merge-dev": true, "merge-extra": false, "merge-extra-deep": false, - "merge-scripts": false + "merge-scripts": false, + "merge-scripts-deep": false } } } @@ -177,7 +178,10 @@ scripts section is to accept the first version of any key found (e.g. a key in the master config wins over the version found in any imported config). If `replace` mode is active ([see above](#replace)) then this behavior changes and the last key found will win (e.g. the key in the master config is replaced -by the key in the imported config). +by the key in the imported config). If `"merge-scripts-deep": true` is +specified then, the sections are merged similar to array_merge_recursive() - +however duplicate string array keys are replaced instead of merged, while +numeric array keys are merged as usual. Note: [custom commands][] added by merged configuration will work when invoked as `composer run-script my-cool-command` but will not be available using the diff --git a/src/Merge/ExtraPackage.php b/src/Merge/ExtraPackage.php index 85997ae..ee2f10a 100644 --- a/src/Merge/ExtraPackage.php +++ b/src/Merge/ExtraPackage.php @@ -469,11 +469,11 @@ public function mergeScripts(RootPackageInterface $root, PluginState $state) if ($state->replaceDuplicateLinks()) { $unwrapped->setScripts( - array_merge($rootScripts, $scripts) + self::mergeExtraArray($state->shouldMergeScriptsDeep(), $rootScripts, $scripts) ); } else { $unwrapped->setScripts( - array_merge($scripts, $rootScripts) + self::mergeExtraArray($state->shouldMergeScriptsDeep(), $scripts, $rootScripts) ); } } diff --git a/src/Merge/PluginState.php b/src/Merge/PluginState.php index 7aa90ac..94f4390 100644 --- a/src/Merge/PluginState.php +++ b/src/Merge/PluginState.php @@ -104,6 +104,20 @@ class PluginState */ protected $mergeScripts = false; + /** + * Whether to merge the scripts section in a deep / recursive way. + * + * By default the scripts section is merged with array_merge() and duplicate + * keys are ignored. When enabled this allows to merge the arrays recursively + * using the following rule: Integer keys are merged, while array values are + * replaced where the later values overwrite the former. + * + * When 'replace' mode is activated the order of array merges is exchanged. + * + * @var bool $mergeScriptsDeep + */ + protected $mergeScriptsDeep; + /** * @var bool $firstInstall */ @@ -149,6 +163,7 @@ public function loadSettings() 'merge-extra' => false, 'merge-extra-deep' => false, 'merge-scripts' => false, + 'merge-scripts-deep' => false, ), isset($extra['merge-plugin']) ? $extra['merge-plugin'] : array() ); @@ -164,6 +179,7 @@ public function loadSettings() $this->mergeExtra = (bool)$config['merge-extra']; $this->mergeExtraDeep = (bool)$config['merge-extra-deep']; $this->mergeScripts = (bool)$config['merge-scripts']; + $this->mergeScriptsDeep = (bool)$config['merge-scripts-deep']; } /** @@ -413,5 +429,22 @@ public function shouldMergeScripts() { return $this->mergeScripts; } + + /** + * Should the scripts section be merged deep / recursively? + * + * By default the scripts section is merged with array_merge() and duplicate + * keys are ignored. When enabled this allows to merge the arrays recursively + * using the following rule: Integer keys are merged, while array values are + * replaced where the later values overwrite the former. + * + * When 'replace' mode is activated the order of array merges is exchanged. + * + * @return bool + */ + public function shouldMergeScriptsDeep() + { + return $this->mergeScriptsDeep; + } } // vim:sw=4:ts=4:sts=4:et: diff --git a/tests/phpunit/MergePluginTest.php b/tests/phpunit/MergePluginTest.php index 7071a36..b5bbba5 100644 --- a/tests/phpunit/MergePluginTest.php +++ b/tests/phpunit/MergePluginTest.php @@ -10,6 +10,7 @@ namespace Wikimedia\Composer; +use Prophecy\Prophecy\ObjectProphecy; use Wikimedia\Composer\Merge\ExtraPackage; use Wikimedia\Composer\Merge\PluginState; @@ -859,7 +860,7 @@ public function provideDeepMerge() } /** - * Given a root package with an scripts section + * Given a root package with a scripts section * and a composer.local.json with an extra section with no conflicting keys * When the plugin is run * Then the root package scripts section should be extended with content from the local config. @@ -897,7 +898,7 @@ function ($args) use ($that) { } /** - * Given a root package with an scripts section + * Given a root package with a scripts section * and a composer.local.json with an extra section with a conflicting key * When the plugin is run * Then the version in the root package should win. @@ -970,6 +971,56 @@ function ($args) use ($that) { $this->assertEquals(0, count($extraInstalls)); } + /** + * Given a root package with a scripts section + * and a composer.local.json with a scripts section with conflicting keys that are arrays + * and the 'merge-scripts-deep' option being activated + * When the plugin is run + * Then the root package scripts section should be extended with content from the base config + * and deep keys should be merged together, but root config wins on key conflicts. + * + * @dataProvider provideDeepMerge + */ + public function testMergeScriptsDeep($suffix, $replace) + { + $that = $this; + $dir = $this->fixtureDir(__FUNCTION__ . $suffix); + + /* @var ObjectProphecy|RootPackage $root */ + $root = $this->rootFromJson("{$dir}/composer.json"); + + $root->setScripts(Argument::type('array'))->will( + function ($args) use ($that, $replace) { + $scripts = $args[0]; + $that->assertEquals(2, count($scripts)); + $that->assertArrayHasKey('example-script', $scripts); + if ($replace) { + $that->assertEquals(array( + "echo 'hello world'", + "echo 'hello again world'", + ), $scripts['example-script']); + } else { + $that->assertEquals(array( + "echo 'hello again world'", + "echo 'hello world'", + ), $scripts['example-script']); + } + $that->assertArrayHasKey('example-script2', $scripts); + $that->assertEquals(array("echo 'hello world'"), $scripts['example-script2']); + } + )->shouldBeCalled(); + + $root->getRepositories()->shouldNotBeCalled(); + $root->getConflicts()->shouldNotBeCalled(); + $root->getReplaces()->shouldNotBeCalled(); + $root->getProvides()->shouldNotBeCalled(); + $root->getSuggests()->shouldNotBeCalled(); + + $extraInstalls = $this->triggerPlugin($root->reveal(), $dir); + + $this->assertEquals(0, count($extraInstalls)); + } + /** * @dataProvider provideOnPostPackageInstall * @param string $package Package installed diff --git a/tests/phpunit/fixtures/testMergeScriptsDeep/composer.json b/tests/phpunit/fixtures/testMergeScriptsDeep/composer.json new file mode 100644 index 0000000..7a0b19d --- /dev/null +++ b/tests/phpunit/fixtures/testMergeScriptsDeep/composer.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "example-script": [ + "echo 'hello world'" + ] + }, + "extra": { + "merge-plugin": { + "merge-scripts": true, + "merge-scripts-deep": true, + "include": "composer.local.json" + } + } +} diff --git a/tests/phpunit/fixtures/testMergeScriptsDeep/composer.local.json b/tests/phpunit/fixtures/testMergeScriptsDeep/composer.local.json new file mode 100644 index 0000000..87bee38 --- /dev/null +++ b/tests/phpunit/fixtures/testMergeScriptsDeep/composer.local.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "example-script": [ + "echo 'hello again world'" + ], + "example-script2": "echo 'hello world'" + } +} diff --git a/tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.json b/tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.json new file mode 100644 index 0000000..74b5e3b --- /dev/null +++ b/tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "example-script": [ + "echo 'hello world'" + ] + }, + "extra": { + "merge-plugin": { + "merge-scripts": true, + "merge-scripts-deep": true, + "replace": true, + "include": "composer.local.json" + } + } +} diff --git a/tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.local.json b/tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.local.json new file mode 100644 index 0000000..87bee38 --- /dev/null +++ b/tests/phpunit/fixtures/testMergeScriptsDeepReplace/composer.local.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "example-script": [ + "echo 'hello again world'" + ], + "example-script2": "echo 'hello world'" + } +}