Update to drupal 8.0.0-rc1. For more information, see https://www.drupal.org/node/2582663
This commit is contained in:
parent
eb34d130a8
commit
f32e58e4b1
8476 changed files with 211648 additions and 170042 deletions
19
vendor/wikimedia/composer-merge-plugin/LICENSE
vendored
Normal file
19
vendor/wikimedia/composer-merge-plugin/LICENSE
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2015 Bryan Davis, Wikimedia Foundation, and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
131
vendor/wikimedia/composer-merge-plugin/README.md
vendored
Normal file
131
vendor/wikimedia/composer-merge-plugin/README.md
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
[](https://packagist.org/packages/wikimedia/composer-merge-plugin) [](https://github.com/wikimedia/composer-merge-plugin/blob/master/LICENSE)
|
||||
[](https://travis-ci.org/wikimedia/composer-merge-plugin)
|
||||
[](https://scrutinizer-ci.com/g/wikimedia/composer-merge-plugin/?branch=master)
|
||||
|
||||
Composer Merge Plugin
|
||||
=====================
|
||||
|
||||
Merge one or more additional composer.json files at [Composer] runtime.
|
||||
|
||||
Composer Merge Plugin is intended to allow easier dependency management for
|
||||
applications which ship a composer.json file and expect some deployments to
|
||||
install additional Composer managed libraries. It does this by allowing the
|
||||
application's top level `composer.json` file to provide a list of optional
|
||||
additional configuration files. When Composer is run it will parse these files
|
||||
and merge their configuration into the base configuration. This combined
|
||||
configuration will allow downloading additional libraries and generating the
|
||||
autoloader. It was specifically created to help with installation of
|
||||
[MediaWiki] which has core Composer managed library requirements and optional
|
||||
libraries and extensions which may also be managed via Composer.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
```
|
||||
$ composer require wikimedia/composer-merge-plugin
|
||||
```
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
```
|
||||
{
|
||||
"require": {
|
||||
"wikimedia/composer-merge-plugin": "dev-master"
|
||||
},
|
||||
"extra": {
|
||||
"merge-plugin": {
|
||||
"include": [
|
||||
"composer.local.json",
|
||||
"extensions/*/composer.json"
|
||||
],
|
||||
"recurse": false,
|
||||
"replace": false,
|
||||
"merge-extra": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `include` key can specify either a single value or an array of values.
|
||||
Each value is treated as a `glob()` pattern identifying additional
|
||||
composer.json style configuration files to merge into the configuration for
|
||||
the current Composer execution. By default the merge plugin is recursive, if
|
||||
an included file also has a "merge-plugin" section it will also be processed.
|
||||
This functionality can be disabled by setting `"recurse": false` inside the
|
||||
"merge-plugin" section.
|
||||
|
||||
These sections of the found configuration files will be merged into the root
|
||||
package configuration as though they were directly included in the top-level
|
||||
composer.json file:
|
||||
|
||||
* [autoload](https://getcomposer.org/doc/04-schema.md#autoload)
|
||||
* [autoload-dev](https://getcomposer.org/doc/04-schema.md#autoload-dev)
|
||||
* [conflict](https://getcomposer.org/doc/04-schema.md#conflict)
|
||||
* [provide](https://getcomposer.org/doc/04-schema.md#provide)
|
||||
* [replace](https://getcomposer.org/doc/04-schema.md#replace)
|
||||
* [repositories](https://getcomposer.org/doc/04-schema.md#repositories)
|
||||
* [require](https://getcomposer.org/doc/04-schema.md#require)
|
||||
* [require-dev](https://getcomposer.org/doc/04-schema.md#require-dev)
|
||||
* [suggest](https://getcomposer.org/doc/04-schema.md#suggest)
|
||||
|
||||
A `"merge-extra": true` setting enables the merging of the "extra" section of
|
||||
included files as well. The normal merge mode for the extra 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 an imported config). If `replace` mode is
|
||||
active (see below) then this behavior changes and the last found key will win
|
||||
(the key in the master config is replaced by the key in the imported config).
|
||||
Note that the `merge-plugin` key itself is excluded from this merge process.
|
||||
Your mileage with merging the extra section will vary depending on the plugins
|
||||
being used and the order in which they are processed by Composer.
|
||||
|
||||
By default, Composer's normal conflict resolution engine is used to determine
|
||||
which version of a package should be installed if multiple files specify the
|
||||
same package. A `"replace": true` setting can be provided inside the
|
||||
"merge-plugin" section to change to a "last version specified wins" conflict
|
||||
resolution strategy. In this mode, duplicate package declarations in merged
|
||||
files will overwrite the declarations made in earlier files. Files are loaded
|
||||
in the order specified in the `include` section with globbed files being
|
||||
loaded in alphabetical order.
|
||||
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
```
|
||||
$ composer install
|
||||
$ composer test
|
||||
```
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
Bug, feature requests and other issues should be reported to the [GitHub
|
||||
project]. We accept code and documentation contributions via Pull Requests on
|
||||
GitHub as well.
|
||||
|
||||
- [PSR-2 Coding Standard][] is used by the project. The included test
|
||||
configuration uses [PHP Code Sniffer][] to validate the conventions.
|
||||
- Tests are encouraged. Our test coverage isn't perfect but we'd like it to
|
||||
get better rather than worse, so please try to include tests with your
|
||||
changes.
|
||||
- Keep the documentation up to date. Make sure `README.md` and other
|
||||
relevant documentation is kept up to date with your changes.
|
||||
- One pull request per feature. Try to keep your changes focused on solving
|
||||
a single problem. This will make it easier for us to review the change and
|
||||
easier for you to make sure you have updated the necessary tests and
|
||||
documentation.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
Composer Merge plugin is licensed under the MIT license. See the `LICENSE`
|
||||
file for more details.
|
||||
|
||||
|
||||
---
|
||||
[Composer]: https://getcomposer.org/
|
||||
[MediaWiki]: https://www.mediawiki.org/wiki/MediaWiki
|
||||
[GitHub project]: https://github.com/wikimedia/composer-merge-plugin
|
||||
[PSR-2 Coding Standard]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
|
||||
[PHP Code Sniffer]: http://pear.php.net/package/PHP_CodeSniffer
|
47
vendor/wikimedia/composer-merge-plugin/composer.json
vendored
Normal file
47
vendor/wikimedia/composer-merge-plugin/composer.json
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "wikimedia/composer-merge-plugin",
|
||||
"description": "Composer plugin to merge multiple composer.json files",
|
||||
"type": "composer-plugin",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bryan Davis",
|
||||
"email": "bd808@wikimedia.org"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0",
|
||||
"php": ">=5.3.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "1.0.*@dev",
|
||||
"jakub-onderka/php-parallel-lint": "~0.8",
|
||||
"phpspec/prophecy-phpunit": "~1.0",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"squizlabs/php_codesniffer": "~2.1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Wikimedia\\Composer\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.3.x-dev"
|
||||
},
|
||||
"class": "Wikimedia\\Composer\\MergePlugin"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"composer validate --no-interaction",
|
||||
"parallel-lint src tests",
|
||||
"phpunit --log-junit=reports/unitreport.xml --coverage-text --coverage-html=reports/coverage --coverage-clover=reports/coverage.xml",
|
||||
"phpcs --encoding=utf-8 --standard=PSR2 --report-checkstyle=reports/checkstyle-phpcs.xml --report-full --extensions=php src/* tests/phpunit/*"
|
||||
]
|
||||
}
|
||||
}
|
66
vendor/wikimedia/composer-merge-plugin/src/Logger.php
vendored
Normal file
66
vendor/wikimedia/composer-merge-plugin/src/Logger.php
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of the Composer Merge plugin.
|
||||
*
|
||||
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
|
||||
*
|
||||
* This software may be modified and distributed under the terms of the MIT
|
||||
* license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Wikimedia\Composer;
|
||||
|
||||
use Composer\IO\IOInterface;
|
||||
|
||||
/**
|
||||
* Simple logging wrapper for Composer\IO\IOInterface
|
||||
*
|
||||
* @author Bryan Davis <bd808@bd808.com>
|
||||
*/
|
||||
class Logger
|
||||
{
|
||||
/**
|
||||
* @var string $name
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var IOInterface $inputOutput
|
||||
*/
|
||||
protected $inputOutput;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param IOInterface $io
|
||||
*/
|
||||
public function __construct($name, IOInterface $io)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->inputOutput = $io;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message
|
||||
*
|
||||
* Messages will be output at the "verbose" logging level (eg `-v` needed
|
||||
* on the Composer command).
|
||||
*
|
||||
* @param string $message
|
||||
*/
|
||||
public function debug($message)
|
||||
{
|
||||
if ($this->inputOutput->isVerbose()) {
|
||||
$message = " <info>[{$this->name}]</info> {$message}";
|
||||
|
||||
if (method_exists($this->inputOutput, 'writeError')) {
|
||||
$this->inputOutput->writeError($message);
|
||||
} else {
|
||||
// @codeCoverageIgnoreStart
|
||||
// Backwards compatiblity for Composer before cb336a5
|
||||
$this->inputOutput->write($message);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// vim:sw=4:ts=4:sts=4:et:
|
437
vendor/wikimedia/composer-merge-plugin/src/Merge/ExtraPackage.php
vendored
Normal file
437
vendor/wikimedia/composer-merge-plugin/src/Merge/ExtraPackage.php
vendored
Normal file
|
@ -0,0 +1,437 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of the Composer Merge plugin.
|
||||
*
|
||||
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
|
||||
*
|
||||
* This software may be modified and distributed under the terms of the MIT
|
||||
* license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Wikimedia\Composer\Merge;
|
||||
|
||||
use Wikimedia\Composer\Logger;
|
||||
|
||||
use Composer\Composer;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Package\CompletePackage;
|
||||
use Composer\Package\Loader\ArrayLoader;
|
||||
use Composer\Package\RootPackage;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Processing for a composer.json file that will be merged into a RootPackage
|
||||
*
|
||||
* @author Bryan Davis <bd808@bd808.com>
|
||||
*/
|
||||
class ExtraPackage
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Composer $composer
|
||||
*/
|
||||
protected $composer;
|
||||
|
||||
/**
|
||||
* @var Logger $logger
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* @var string $path
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* @var array $json
|
||||
*/
|
||||
protected $json;
|
||||
|
||||
/**
|
||||
* @var CompletePackage $package
|
||||
*/
|
||||
protected $package;
|
||||
|
||||
/**
|
||||
* @param string $path Path to composer.json file
|
||||
* @param Composer $composer
|
||||
* @param Logger $logger
|
||||
*/
|
||||
public function __construct($path, Composer $composer, Logger $logger)
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->composer = $composer;
|
||||
$this->logger = $logger;
|
||||
$this->json = $this->readPackageJson($path);
|
||||
$this->package = $this->loadPackage($this->json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of additional packages to include if precessing recursively.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getIncludes()
|
||||
{
|
||||
return isset($this->json['extra']['merge-plugin']['include']) ?
|
||||
$this->json['extra']['merge-plugin']['include'] : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a composer.json style file into an array.
|
||||
*
|
||||
* The package contents are fixed up to be usable to create a Package
|
||||
* object by providing dummy "name" and "version" values if they have not
|
||||
* been provided in the file. This is consistent with the default root
|
||||
* package loading behavior of Composer.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
protected function readPackageJson($path)
|
||||
{
|
||||
$file = new JsonFile($path);
|
||||
$json = $file->read();
|
||||
if (!isset($json['name'])) {
|
||||
$json['name'] = 'merge-plugin/' .
|
||||
strtr($path, DIRECTORY_SEPARATOR, '-');
|
||||
}
|
||||
if (!isset($json['version'])) {
|
||||
$json['version'] = '1.0.0';
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CompletePackage
|
||||
*/
|
||||
protected function loadPackage($json)
|
||||
{
|
||||
$loader = new ArrayLoader();
|
||||
$package = $loader->load($json);
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!$package instanceof CompletePackage) {
|
||||
throw new UnexpectedValueException(
|
||||
'Expected instance of CompletePackage, got ' .
|
||||
get_class($package)
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
return $package;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this package into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
* @param PluginState $state
|
||||
*/
|
||||
public function mergeInto(RootPackage $root, PluginState $state)
|
||||
{
|
||||
$this->addRepositories($root);
|
||||
|
||||
$this->mergeRequires($root, $state);
|
||||
$this->mergeDevRequires($root, $state);
|
||||
|
||||
$this->mergeConflicts($root);
|
||||
$this->mergeReplaces($root);
|
||||
$this->mergeProvides($root);
|
||||
$this->mergeSuggests($root);
|
||||
|
||||
$this->mergeAutoload($root);
|
||||
$this->mergeDevAutoload($root);
|
||||
|
||||
$this->mergeExtra($root, $state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a collection of repositories described by the given configuration
|
||||
* to the given package and the global repository manager.
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function addRepositories(RootPackage $root)
|
||||
{
|
||||
if (!isset($this->json['repositories'])) {
|
||||
return;
|
||||
}
|
||||
$repoManager = $this->composer->getRepositoryManager();
|
||||
$newRepos = array();
|
||||
|
||||
foreach ($this->json['repositories'] as $repoJson) {
|
||||
if (!isset($repoJson['type'])) {
|
||||
continue;
|
||||
}
|
||||
$this->logger->debug("Adding {$repoJson['type']} repository");
|
||||
$repo = $repoManager->createRepository(
|
||||
$repoJson['type'],
|
||||
$repoJson
|
||||
);
|
||||
$repoManager->addRepository($repo);
|
||||
$newRepos[] = $repo;
|
||||
}
|
||||
|
||||
$root->setRepositories(array_merge(
|
||||
$newRepos,
|
||||
$root->getRepositories()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge require into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
* @param PluginState $state
|
||||
*/
|
||||
protected function mergeRequires(RootPackage $root, PluginState $state)
|
||||
{
|
||||
$requires = $this->package->getRequires();
|
||||
if (empty($requires)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->mergeStabilityFlags($root, $requires);
|
||||
|
||||
$dups = array();
|
||||
$root->setRequires($this->mergeLinks(
|
||||
$root->getRequires(),
|
||||
$requires,
|
||||
$state->replaceDuplicateLinks(),
|
||||
$dups
|
||||
));
|
||||
$state->addDuplicateLinks('require', $dups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge require-dev into RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
* @param PluginState $state
|
||||
*/
|
||||
protected function mergeDevRequires(RootPackage $root, PluginState $state)
|
||||
{
|
||||
$requires = $this->package->getDevRequires();
|
||||
if (empty($requires)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->mergeStabilityFlags($root, $requires);
|
||||
|
||||
$dups = array();
|
||||
$root->setDevRequires($this->mergeLinks(
|
||||
$root->getDevRequires(),
|
||||
$requires,
|
||||
$state->replaceDuplicateLinks(),
|
||||
$dups
|
||||
));
|
||||
$state->addDuplicateLinks('require-dev', $dups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two collections of package links and collect duplicates for
|
||||
* subsequent processing.
|
||||
*
|
||||
* @param array $origin Primary collection
|
||||
* @param array $merge Additional collection
|
||||
* @param bool $replace Replace exising links?
|
||||
* @param array &dups Duplicate storage
|
||||
* @return array Merged collection
|
||||
*/
|
||||
protected function mergeLinks(
|
||||
array $origin,
|
||||
array $merge,
|
||||
$replace,
|
||||
array &$dups
|
||||
) {
|
||||
foreach ($merge as $name => $link) {
|
||||
if (!isset($origin[$name]) || $replace) {
|
||||
$this->logger->debug("Merging <comment>{$name}</comment>");
|
||||
$origin[$name] = $link;
|
||||
} else {
|
||||
// Defer to solver.
|
||||
$this->logger->debug(
|
||||
"Deferring duplicate <comment>{$name}</comment>"
|
||||
);
|
||||
$dups[] = $link;
|
||||
}
|
||||
}
|
||||
return $origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge autoload into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function mergeAutoload(RootPackage $root)
|
||||
{
|
||||
$autoload = $this->package->getAutoload();
|
||||
if (empty($autoload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$root->setAutoload(array_merge_recursive(
|
||||
$root->getAutoload(),
|
||||
$this->fixRelativePaths($autoload)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge autoload-dev into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function mergeDevAutoload(RootPackage $root)
|
||||
{
|
||||
$autoload = $this->package->getDevAutoload();
|
||||
if (empty($autoload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$root->setDevAutoload(array_merge_recursive(
|
||||
$root->getDevAutoload(),
|
||||
$this->fixRelativePaths($autoload)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix a collection of paths that are relative to this package to be
|
||||
* relative to the base package.
|
||||
*
|
||||
* @param array $paths
|
||||
* @return array
|
||||
*/
|
||||
protected function fixRelativePaths(array $paths)
|
||||
{
|
||||
$base = dirname($this->path);
|
||||
$base = ($base === '.') ? '' : "{$base}/";
|
||||
|
||||
array_walk_recursive(
|
||||
$paths,
|
||||
function (&$path) use ($base) {
|
||||
$path = "{$base}{$path}";
|
||||
}
|
||||
);
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and merge stability flags from the given collection of
|
||||
* requires and merge them into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
* @param array $requires
|
||||
*/
|
||||
protected function mergeStabilityFlags(
|
||||
RootPackage $root,
|
||||
array $requires
|
||||
) {
|
||||
$flags = $root->getStabilityFlags();
|
||||
foreach ($requires as $name => $link) {
|
||||
$name = strtolower($name);
|
||||
$version = $link->getPrettyConstraint();
|
||||
$stability = VersionParser::parseStability($version);
|
||||
$flags[$name] = BasePackage::$stabilities[$stability];
|
||||
}
|
||||
$root->setStabilityFlags($flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge conflicting packages into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function mergeConflicts(RootPackage $root)
|
||||
{
|
||||
$conflicts = $this->package->getConflicts();
|
||||
if (!empty($conflicts)) {
|
||||
$root->setconflicts(array_merge(
|
||||
$root->getConflicts(),
|
||||
$conflicts
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge replaced packages into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function mergeReplaces(RootPackage $root)
|
||||
{
|
||||
$replaces = $this->package->getReplaces();
|
||||
if (!empty($replaces)) {
|
||||
$root->setReplaces(array_merge(
|
||||
$root->getReplaces(),
|
||||
$replaces
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge provided virtual packages into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function mergeProvides(RootPackage $root)
|
||||
{
|
||||
$provides = $this->package->getProvides();
|
||||
if (!empty($provides)) {
|
||||
$root->setProvides(array_merge(
|
||||
$root->getProvides(),
|
||||
$provides
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge suggested packages into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
*/
|
||||
protected function mergeSuggests(RootPackage $root)
|
||||
{
|
||||
$suggests = $this->package->getSuggests();
|
||||
if (!empty($suggests)) {
|
||||
$root->setSuggests(array_merge(
|
||||
$root->getSuggests(),
|
||||
$suggests
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge extra config into a RootPackage
|
||||
*
|
||||
* @param RootPackage $root
|
||||
* @param PluginState $state
|
||||
*/
|
||||
public function mergeExtra(RootPackage $root, PluginState $state)
|
||||
{
|
||||
$extra = $this->package->getExtra();
|
||||
unset($extra['merge-plugin']);
|
||||
if (!$state->shouldMergeExtra() || empty($extra)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rootExtra = $root->getExtra();
|
||||
|
||||
if ($state->replaceDuplicateLinks()) {
|
||||
$root->setExtra(array_merge($rootExtra, $extra));
|
||||
|
||||
} else {
|
||||
foreach ($extra as $key => $value) {
|
||||
if (isset($rootExtra[$key])) {
|
||||
$this->logger->debug(
|
||||
"Ignoring duplicate <comment>{$key}</comment> in ".
|
||||
"<comment>{$this->path}</comment> extra config."
|
||||
);
|
||||
}
|
||||
}
|
||||
$root->setExtra(array_merge($extra, $rootExtra));
|
||||
}
|
||||
}
|
||||
}
|
||||
// vim:sw=4:ts=4:sts=4:et:
|
326
vendor/wikimedia/composer-merge-plugin/src/Merge/PluginState.php
vendored
Normal file
326
vendor/wikimedia/composer-merge-plugin/src/Merge/PluginState.php
vendored
Normal file
|
@ -0,0 +1,326 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of the Composer Merge plugin.
|
||||
*
|
||||
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
|
||||
*
|
||||
* This software may be modified and distributed under the terms of the MIT
|
||||
* license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Wikimedia\Composer\Merge;
|
||||
|
||||
use Composer\Composer;
|
||||
use Composer\Package\AliasPackage;
|
||||
use Composer\Package\RootPackage;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Mutable plugin state
|
||||
*
|
||||
* @author Bryan Davis <bd808@bd808.com>
|
||||
*/
|
||||
class PluginState
|
||||
{
|
||||
/**
|
||||
* @var Composer $composer
|
||||
*/
|
||||
protected $composer;
|
||||
|
||||
/**
|
||||
* @var array $includes
|
||||
*/
|
||||
protected $includes = array();
|
||||
|
||||
/**
|
||||
* @var array $duplicateLinks
|
||||
*/
|
||||
protected $duplicateLinks = array();
|
||||
|
||||
/**
|
||||
* @var bool $devMode
|
||||
*/
|
||||
protected $devMode = false;
|
||||
|
||||
/**
|
||||
* @var bool $recurse
|
||||
*/
|
||||
protected $recurse = true;
|
||||
|
||||
/**
|
||||
* @var bool $replace
|
||||
*/
|
||||
protected $replace = false;
|
||||
|
||||
/**
|
||||
* Whether to merge the extra section.
|
||||
*
|
||||
* By default, the extra section is not merged and there will be many
|
||||
* cases where the merge of the extra section is performed too late
|
||||
* to be of use to other plugins. When enabled, merging uses one of
|
||||
* two strategies - either 'first wins' or 'last wins'. When enabled,
|
||||
* 'first wins' is the default behaviour. If Replace mode is activated
|
||||
* then 'last wins' is used.
|
||||
*
|
||||
* @var bool $mergeExtra
|
||||
*/
|
||||
protected $mergeExtra = false;
|
||||
|
||||
/**
|
||||
* @var bool $firstInstall
|
||||
*/
|
||||
protected $firstInstall = false;
|
||||
|
||||
/**
|
||||
* @var bool $locked
|
||||
*/
|
||||
protected $locked = false;
|
||||
|
||||
/**
|
||||
* @var bool $dumpAutoloader
|
||||
*/
|
||||
protected $dumpAutoloader = false;
|
||||
|
||||
/**
|
||||
* @var bool $optimizeAutoloader
|
||||
*/
|
||||
protected $optimizeAutoloader = false;
|
||||
|
||||
/**
|
||||
* @param Composer $composer
|
||||
*/
|
||||
public function __construct(Composer $composer)
|
||||
{
|
||||
$this->composer = $composer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin settings
|
||||
*/
|
||||
public function loadSettings()
|
||||
{
|
||||
$extra = $this->getRootPackage()->getExtra();
|
||||
$config = array_merge(
|
||||
array(
|
||||
'include' => array(),
|
||||
'recurse' => true,
|
||||
'replace' => false,
|
||||
'merge-extra' => false,
|
||||
),
|
||||
isset($extra['merge-plugin']) ? $extra['merge-plugin'] : array()
|
||||
);
|
||||
|
||||
$this->includes = (is_array($config['include'])) ?
|
||||
$config['include'] : array($config['include']);
|
||||
$this->recurse = (bool)$config['recurse'];
|
||||
$this->replace = (bool)$config['replace'];
|
||||
$this->mergeExtra = (bool)$config['merge-extra'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root package
|
||||
*
|
||||
* @return RootPackage
|
||||
*/
|
||||
public function getRootPackage()
|
||||
{
|
||||
$root = $this->composer->getPackage();
|
||||
if ($root instanceof AliasPackage) {
|
||||
$root = $root->getAliasOf();
|
||||
}
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!$root instanceof RootPackage) {
|
||||
throw new UnexpectedValueException(
|
||||
'Expected instance of RootPackage, got ' .
|
||||
get_class($root)
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of filenames and/or glob patterns to include
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getIncludes()
|
||||
{
|
||||
return $this->includes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the first install flag
|
||||
*
|
||||
* @param bool $flag
|
||||
*/
|
||||
public function setFirstInstall($flag)
|
||||
{
|
||||
$this->firstInstall = (bool)$flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this the first time that the plugin has been installed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFirstInstall()
|
||||
{
|
||||
return $this->firstInstall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locked flag
|
||||
*
|
||||
* @param bool $flag
|
||||
*/
|
||||
public function setLocked($flag)
|
||||
{
|
||||
$this->locked = (bool)$flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Was a lockfile present when the plugin was installed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocked()
|
||||
{
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should an update be forced?
|
||||
*
|
||||
* @return true If packages are not locked
|
||||
*/
|
||||
public function forceUpdate()
|
||||
{
|
||||
return !$this->locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the devMode flag
|
||||
*
|
||||
* @param bool $flag
|
||||
*/
|
||||
public function setDevMode($flag)
|
||||
{
|
||||
$this->devMode = (bool)$flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should devMode settings be processed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDevMode()
|
||||
{
|
||||
return $this->devMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dumpAutoloader flag
|
||||
*
|
||||
* @param bool $flag
|
||||
*/
|
||||
public function setDumpAutoloader($flag)
|
||||
{
|
||||
$this->dumpAutoloader = (bool)$flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the autoloader file supposed to be written out?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldDumpAutoloader()
|
||||
{
|
||||
return $this->dumpAutoloader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the optimizeAutoloader flag
|
||||
*
|
||||
* @param bool $flag
|
||||
*/
|
||||
public function setOptimizeAutoloader($flag)
|
||||
{
|
||||
$this->optimizeAutoloader = (bool)$flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the autoloader be optimized?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldOptimizeAutoloader()
|
||||
{
|
||||
return $this->optimizeAutoloader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add duplicate packages
|
||||
*
|
||||
* @param string $type Package type
|
||||
* @param array $packages
|
||||
*/
|
||||
public function addDuplicateLinks($type, array $packages)
|
||||
{
|
||||
if (!isset($this->duplicateLinks[$type])) {
|
||||
$this->duplicateLinks[$type] = array();
|
||||
}
|
||||
$this->duplicateLinks[$type] =
|
||||
array_merge($this->duplicateLinks[$type], $packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get duplicate packages
|
||||
*
|
||||
* @param string $type Package type
|
||||
* @return array
|
||||
*/
|
||||
public function getDuplicateLinks($type)
|
||||
{
|
||||
return isset($this->duplicateLinks[$type]) ?
|
||||
$this->duplicateLinks[$type] : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should includes be recursively processed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function recurseIncludes()
|
||||
{
|
||||
return $this->recurse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should duplicate links be replaced in a 'last definition wins' order?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function replaceDuplicateLinks()
|
||||
{
|
||||
return $this->replace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the extra section be merged?
|
||||
*
|
||||
* By default, the extra section is not merged and there will be many
|
||||
* cases where the merge of the extra section is performed too late
|
||||
* to be of use to other plugins. When enabled, merging uses one of
|
||||
* two strategies - either 'first wins' or 'last wins'. When enabled,
|
||||
* 'first wins' is the default behaviour. If Replace mode is activated
|
||||
* then 'last wins' is used.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldMergeExtra()
|
||||
{
|
||||
return $this->mergeExtra;
|
||||
}
|
||||
}
|
||||
// vim:sw=4:ts=4:sts=4:et:
|
296
vendor/wikimedia/composer-merge-plugin/src/MergePlugin.php
vendored
Normal file
296
vendor/wikimedia/composer-merge-plugin/src/MergePlugin.php
vendored
Normal file
|
@ -0,0 +1,296 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of the Composer Merge plugin.
|
||||
*
|
||||
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
|
||||
*
|
||||
* This software may be modified and distributed under the terms of the MIT
|
||||
* license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Wikimedia\Composer;
|
||||
|
||||
use Wikimedia\Composer\Merge\ExtraPackage;
|
||||
use Wikimedia\Composer\Merge\PluginState;
|
||||
|
||||
use Composer\Composer;
|
||||
use Composer\DependencyResolver\Operation\InstallOperation;
|
||||
use Composer\EventDispatcher\EventSubscriberInterface;
|
||||
use Composer\Factory;
|
||||
use Composer\Installer;
|
||||
use Composer\Installer\InstallerEvent;
|
||||
use Composer\Installer\InstallerEvents;
|
||||
use Composer\Installer\PackageEvent;
|
||||
use Composer\Installer\PackageEvents;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Package\RootPackage;
|
||||
use Composer\Plugin\PluginInterface;
|
||||
use Composer\Script\Event;
|
||||
use Composer\Script\ScriptEvents;
|
||||
|
||||
/**
|
||||
* Composer plugin that allows merging multiple composer.json files.
|
||||
*
|
||||
* When installed, this plugin will look for a "merge-plugin" key in the
|
||||
* composer configuration's "extra" section. The value of this setting can be
|
||||
* either a single value or an array of values. Each value is treated as
|
||||
* a glob() pattern identifying additional composer.json style configuration
|
||||
* files to merge into the configuration for the current compser execution.
|
||||
*
|
||||
* The "require", "require-dev", "repositories", "extra" and "suggest" sections
|
||||
* of the found configuration files will be merged into the root package
|
||||
* configuration as though they were directly included in the top-level
|
||||
* composer.json file.
|
||||
*
|
||||
* If included files specify conflicting package versions for "require" or
|
||||
* "require-dev", the normal Composer dependency solver process will be used
|
||||
* to attempt to resolve the conflict. Specifying the 'replace' key as true will
|
||||
* change this default behaviour so that the last-defined version of a package
|
||||
* will win, allowing for force-overrides of package defines.
|
||||
*
|
||||
* By default the "extra" section is not merged. This can be enabled with the
|
||||
* 'merge-extra' key by setting it to true. In normal mode, when the same key
|
||||
* is found in both the original and the imported extra section, the version
|
||||
* in the original config is used and the imported version is skipped. If
|
||||
* 'replace' mode is active, this behaviour changes so the imported version of
|
||||
* the key is used, replacing the version in the original config.
|
||||
*
|
||||
*
|
||||
* @code
|
||||
* {
|
||||
* "require": {
|
||||
* "wikimedia/composer-merge-plugin": "dev-master"
|
||||
* },
|
||||
* "extra": {
|
||||
* "merge-plugin": {
|
||||
* "include": [
|
||||
* "composer.local.json"
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @author Bryan Davis <bd808@bd808.com>
|
||||
*/
|
||||
class MergePlugin implements PluginInterface, EventSubscriberInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Offical package name
|
||||
*/
|
||||
const PACKAGE_NAME = 'wikimedia/composer-merge-plugin';
|
||||
|
||||
/**
|
||||
* @var Composer $composer
|
||||
*/
|
||||
protected $composer;
|
||||
|
||||
/**
|
||||
* @var PluginState $state
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* @var Logger $logger
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Files that have already been processed
|
||||
*
|
||||
* @var string[] $loadedFiles
|
||||
*/
|
||||
protected $loadedFiles = array();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function activate(Composer $composer, IOInterface $io)
|
||||
{
|
||||
$this->composer = $composer;
|
||||
$this->state = new PluginState($this->composer);
|
||||
$this->logger = new Logger('merge-plugin', $io);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
InstallerEvents::PRE_DEPENDENCIES_SOLVING => 'onDependencySolve',
|
||||
PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
|
||||
ScriptEvents::POST_INSTALL_CMD => 'onPostInstallOrUpdate',
|
||||
ScriptEvents::POST_UPDATE_CMD => 'onPostInstallOrUpdate',
|
||||
ScriptEvents::PRE_AUTOLOAD_DUMP => 'onInstallUpdateOrDump',
|
||||
ScriptEvents::PRE_INSTALL_CMD => 'onInstallUpdateOrDump',
|
||||
ScriptEvents::PRE_UPDATE_CMD => 'onInstallUpdateOrDump',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an event callback for an install, update or dump command by
|
||||
* checking for "merge-plugin" in the "extra" data and merging package
|
||||
* contents if found.
|
||||
*
|
||||
* @param Event $event
|
||||
*/
|
||||
public function onInstallUpdateOrDump(Event $event)
|
||||
{
|
||||
$this->state->loadSettings();
|
||||
$this->state->setDevMode($event->isDevMode());
|
||||
$this->mergeIncludes($this->state->getIncludes());
|
||||
|
||||
if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
|
||||
$this->state->setDumpAutoloader(true);
|
||||
$flags = $event->getFlags();
|
||||
if (isset($flags['optimize'])) {
|
||||
$this->state->setOptimizeAutoloader($flags['optimize']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find configuration files matching the configured glob patterns and
|
||||
* merge their contents with the master package.
|
||||
*
|
||||
* @param array $includes List of files/glob patterns
|
||||
*/
|
||||
protected function mergeIncludes(array $includes)
|
||||
{
|
||||
$root = $this->state->getRootPackage();
|
||||
foreach (array_reduce(
|
||||
array_map('glob', $includes),
|
||||
'array_merge',
|
||||
array()
|
||||
) as $path) {
|
||||
$this->mergeFile($root, $path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a JSON file and merge its contents
|
||||
*
|
||||
* @param RootPackage $root
|
||||
* @param string $path
|
||||
*/
|
||||
protected function mergeFile(RootPackage $root, $path)
|
||||
{
|
||||
if (isset($this->loadedFiles[$path])) {
|
||||
$this->logger->debug(
|
||||
"Skipping duplicate <comment>$path</comment>..."
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
$this->loadedFiles[$path] = true;
|
||||
}
|
||||
$this->logger->debug("Loading <comment>{$path}</comment>...");
|
||||
|
||||
$package = new ExtraPackage($path, $this->composer, $this->logger);
|
||||
$package->mergeInto($root, $this->state);
|
||||
|
||||
if ($this->state->recurseIncludes()) {
|
||||
$this->mergeIncludes($package->getIncludes());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an event callback for pre-dependency solving phase of an install
|
||||
* or update by adding any duplicate package dependencies found during
|
||||
* initial merge processing to the request that will be processed by the
|
||||
* dependency solver.
|
||||
*
|
||||
* @param InstallerEvent $event
|
||||
*/
|
||||
public function onDependencySolve(InstallerEvent $event)
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
foreach ($this->state->getDuplicateLinks('require') as $link) {
|
||||
$this->logger->debug(
|
||||
"Adding dependency <comment>{$link}</comment>"
|
||||
);
|
||||
$request->install($link->getTarget(), $link->getConstraint());
|
||||
}
|
||||
if ($this->state->isDevMode()) {
|
||||
foreach ($this->state->getDuplicateLinks('require-dev') as $link) {
|
||||
$this->logger->debug(
|
||||
"Adding dev dependency <comment>{$link}</comment>"
|
||||
);
|
||||
$request->install($link->getTarget(), $link->getConstraint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an event callback following installation of a new package by
|
||||
* checking to see if the package that was installed was our plugin.
|
||||
*
|
||||
* @param PackageEvent $event
|
||||
*/
|
||||
public function onPostPackageInstall(PackageEvent $event)
|
||||
{
|
||||
$op = $event->getOperation();
|
||||
if ($op instanceof InstallOperation) {
|
||||
$package = $op->getPackage()->getName();
|
||||
if ($package === self::PACKAGE_NAME) {
|
||||
$this->logger->debug('composer-merge-plugin installed');
|
||||
$this->state->setFirstInstall(true);
|
||||
$this->state->setLocked(
|
||||
$event->getComposer()->getLocker()->isLocked()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an event callback following an install or update command. If our
|
||||
* plugin was installed during the run then trigger an update command to
|
||||
* process any merge-patterns in the current config.
|
||||
*
|
||||
* @param Event $event
|
||||
*/
|
||||
public function onPostInstallOrUpdate(Event $event)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($this->state->isFirstInstall()) {
|
||||
$this->state->setFirstInstall(false);
|
||||
$this->logger->debug(
|
||||
'<comment>' .
|
||||
'Running additional update to apply merge settings' .
|
||||
'</comment>'
|
||||
);
|
||||
|
||||
$config = $this->composer->getConfig();
|
||||
|
||||
$preferSource = $config->get('preferred-install') == 'source';
|
||||
$preferDist = $config->get('preferred-install') == 'dist';
|
||||
|
||||
$installer = Installer::create(
|
||||
$event->getIO(),
|
||||
// Create a new Composer instance to ensure full processing of
|
||||
// the merged files.
|
||||
Factory::create($event->getIO(), null, false)
|
||||
);
|
||||
|
||||
$installer->setPreferSource($preferSource);
|
||||
$installer->setPreferDist($preferDist);
|
||||
$installer->setDevMode($event->isDevMode());
|
||||
$installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
|
||||
$installer->setOptimizeAutoloader(
|
||||
$this->state->shouldOptimizeAutoloader()
|
||||
);
|
||||
|
||||
if ($this->state->forceUpdate()) {
|
||||
// Force update mode so that new packages are processed rather
|
||||
// than just telling the user that composer.json and
|
||||
// composer.lock don't match.
|
||||
$installer->setUpdate(true);
|
||||
}
|
||||
|
||||
$installer->run();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
// vim:sw=4:ts=4:sts=4:et:
|
Reference in a new issue