Merge branch 'master' into 2.0

main
Jordi Boggiano 4 years ago
commit bc002ae1fb
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC

@ -1,19 +1,26 @@
### [1.10.0] 2020-01-XX
### [1.10.0-RC] 2020-02-14
* Breaking: `composer global exec ...` now executes the process in the current working directory instead of executing it in the global directory.
* Warning: Added a warning when class names are being loaded by a PSR-4 or PSR-0 rule only due to classmap optimization, but would not otherwise be autoloadable. The next minor version will stop autoloading these classes so make sure you fix your autoload configs.
* Warning: Added a warning when class names are being loaded by a PSR-4 or PSR-0 rule only due to classmap optimization, but would not otherwise be autoloadable. Composer 2.0 will stop autoloading these classes so make sure you fix your autoload configs.
* Added new funding key to composer.json to describe ways your package's maintenance can be funded. This reads info from GitHub's FUNDING.yml by default so better configure it there so it shows on GitHub and Composer/Packagist
* Added `composer fund` command to show funding info of your dependencies
* Added support for --format=json output for show command when showing a single package
* Added support for configuring suggestions using config command, e.g. `composer config suggest.foo/bar some text`
* Added support for configuring fine-grained preferred-install using config command, e.g. `composer config preferred-install.foo/* dist`
* Added `@putenv` script handler to set environment variables from composer.json for following scripts
* Added `lock` option that can be set to false, in which case no composer.lock file will be generated
* Added --add-repository flag to create-project command which will persist the repo given in --repository into the composer.json of the package being installed
* Added support for IPv6 addresses in NO_PROXY
* Added package homepage display in the show command
* Added debug info about HTTP authentications
* Added Symfony 5 compatibility
* Added --fixed flag to require command to make it use a fixed constraint instead of a ^x.y constraint when adding the requirement
* Fixed exclude-from-classmap matching subsets of directories e.g. foo/ was excluding foobar/
* Fixed archive command to persist file permissions inside the zip files
* Fixed init/require command to avoid suggesting packages which are already selected in the search results
* Fixed create-project UX issues
* Fixed filemtime for vendor/composer/* files is now only changing when the files actually change
* Fixed issues detecting docker environment with an active open_basedir
### [1.9.3] 2020-02-04
@ -804,7 +811,7 @@
* Initial release
[1.10.0]: https://github.com/composer/composer/compare/1.9.3...1.10.0
[1.10.0-RC]: https://github.com/composer/composer/compare/1.9.3...1.10.0-RC
[1.9.3]: https://github.com/composer/composer/compare/1.9.2...1.9.3
[1.9.2]: https://github.com/composer/composer/compare/1.9.1...1.9.2
[1.9.1]: https://github.com/composer/composer/compare/1.9.0...1.9.1

34
composer.lock generated

@ -125,16 +125,16 @@
},
{
"name": "composer/spdx-licenses",
"version": "1.5.2",
"version": "1.5.3",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
"reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5"
"reference": "0c3e51e1880ca149682332770e25977c70cf9dae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7ac1e6aec371357df067f8a688c3d6974df68fa5",
"reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5",
"url": "https://api.github.com/repos/composer/spdx-licenses/zipball/0c3e51e1880ca149682332770e25977c70cf9dae",
"reference": "0c3e51e1880ca149682332770e25977c70cf9dae",
"shasum": ""
},
"require": {
@ -181,7 +181,7 @@
"spdx",
"validator"
],
"time": "2019-07-29T10:31:59+00:00"
"time": "2020-02-14T07:44:31+00:00"
},
{
"name": "composer/xdebug-handler",
@ -724,16 +724,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.13.1",
"version": "v1.14.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
"reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/fbdeaec0df06cf3d51c93de80c7eb76e271f5a38",
"reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38",
"shasum": ""
},
"require": {
@ -745,7 +745,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.13-dev"
"dev-master": "1.14-dev"
}
},
"autoload": {
@ -768,20 +768,20 @@
"polyfill",
"portable"
],
"time": "2019-11-27T13:56:44+00:00"
"time": "2020-01-13T11:15:53+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.13.1",
"version": "v1.14.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
"reference": "34094cfa9abe1f0f14f48f490772db7a775559f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/34094cfa9abe1f0f14f48f490772db7a775559f2",
"reference": "34094cfa9abe1f0f14f48f490772db7a775559f2",
"shasum": ""
},
"require": {
@ -793,7 +793,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.13-dev"
"dev-master": "1.14-dev"
}
},
"autoload": {
@ -827,7 +827,7 @@
"portable",
"shim"
],
"time": "2019-11-27T14:18:11+00:00"
"time": "2020-01-13T11:15:53+00:00"
},
{
"name": "symfony/process",

@ -19,13 +19,14 @@ This idea is not new and Composer is strongly inspired by node's
Suppose:
1. You have a project that depends on a number of libraries.
1. Some of those libraries depend on other libraries.
2. Some of those libraries depend on other libraries.
Composer:
1. Enables you to declare the libraries you depend on.
1. Finds out which versions of which packages can and need to be installed, and
2. Finds out which versions of which packages can and need to be installed, and
installs them (meaning it downloads them into your project).
3. You can update all your dependencies in one command.
See the [Basic usage](01-basic-usage.md) chapter for more details on declaring
dependencies.

@ -248,7 +248,8 @@ php composer.phar dump-autoload
```
This command will re-generate the `vendor/autoload.php` file.
See the [`dump-autoload`](03-cli.md#dump-autoload) section for more information.
See the [`dump-autoload`](03-cli.md#dump-autoload-dumpautoload-) section for
more information.
Including that file will also return the autoloader instance, so you can store
the return value of the include call in a variable and add more namespaces.

@ -419,6 +419,11 @@ If you only want a list of suggested package names, use `--list`.
* **--list:** Show only list of suggested package names.
* **--no-dev:** Excludes suggestions from `require-dev` packages.
## fund
Discover how to help fund the maintenance of your dependencies. This lists
all funding links from the installed dependencies.
## depends (why)
The `depends` command tells you which other packages depend on a certain
@ -664,6 +669,7 @@ By default the command checks for the packages on packagist.org.
to a `composer` repository, a path to a local `packages.json` file, or a
JSON string which similar to what the [repositories](04-schema.md#repositories)
key accepts.
* **--add-repository:** Add the repository option to the composer.json.
* **--dev:** Install packages listed in `require-dev`.
* **--no-dev:** Disables installation of require-dev packages.
* **--no-scripts:** Disables the execution of the scripts defined in the root

@ -258,6 +258,39 @@ An example:
Optional.
### funding
A list of URLs to provide funding to the package authors for maintenance and
development of new functionality.
Each entry consists of the following
* **type:** The type of funding or the platform through which funding can be provided, e.g. patreon, opencollective, tidelift or github.
* **url:** URL to a website with details and a way to fund the package.
An example:
```json
{
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/phpdoctrine"
},
{
"type": "tidelift",
"url": "https://tidelift.com/subscription/pkg/packagist-doctrine_doctrine-bundle"
},
{
"type": "other",
"url": "https://www.doctrine-project.org/sponsorship.html"
}
]
}
```
Optional.
### Package links
All of the following take an object which maps package names to

@ -522,6 +522,24 @@
}
}
},
"funding": {
"type": "array",
"description": "A list of options to fund the development and maintenance of the package.",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Type of funding or platform through which funding is possible."
},
"url": {
"type": "string",
"description": "URL to a website with details on funding and a way to fund the package.",
"format": "uri"
}
}
}
},
"non-feature-branches": {
"type": ["array"],
"description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.",

@ -901,7 +901,7 @@ INITIALIZER;
}
$resolvedPath = realpath($installPath . '/' . $updir);
$autoloads[] = preg_quote(strtr($resolvedPath, '\\', '/')) . '/' . $path;
$autoloads[] = preg_quote(strtr($resolvedPath, '\\', '/')) . '/' . $path . '($|/)';
continue;
}

@ -181,16 +181,16 @@ class ClassMapGenerator
if (empty($validClasses)) {
foreach ($rejectedClasses as $class) {
trigger_error(
"Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. It will not autoload anymore in Composer v1.11+.",
"Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. It will not autoload anymore in Composer v2.0.",
E_USER_DEPRECATED
);
}
// TODO enable in Composer v1.11 or 2.0 whichever comes first
// TODO enable in Composer 2.0
//return array();
}
// TODO enable in Composer v1.11 or 2.0 whichever comes first & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses
// TODO enable in Composer 2.0 & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses
//return $validClasses;
return $classes;
}

@ -70,6 +70,7 @@ class CreateProjectCommand extends BaseCommand
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
new InputOption('repository', null, InputOption::VALUE_REQUIRED, 'Pick a different repository (as url or json config) to look for the package.'),
new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'DEPRECATED: Use --repository instead.'),
new InputOption('add-repository', null, InputOption::VALUE_NONE, 'Add the repository option to the composer.json.'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'),
@ -143,11 +144,12 @@ EOT
$input->getOption('no-progress'),
$input->getOption('no-install'),
$input->getOption('ignore-platform-reqs'),
!$input->getOption('no-secure-http')
!$input->getOption('no-secure-http'),
$input->getOption('add-repository')
);
}
public function installProject(IOInterface $io, Config $config, InputInterface $input, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repository = null, $disablePlugins = false, $noScripts = false, $noProgress = false, $noInstall = false, $ignorePlatformReqs = false, $secureHttp = true)
public function installProject(IOInterface $io, Config $config, InputInterface $input, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repository = null, $disablePlugins = false, $noScripts = false, $noProgress = false, $noInstall = false, $ignorePlatformReqs = false, $secureHttp = true, $addRepository = false)
{
$oldCwd = getcwd();
@ -164,6 +166,25 @@ EOT
$composer = Factory::create($io, null, $disablePlugins);
// add the repository to the composer.json and use it for the install run later
if ($repository !== null && $addRepository) {
if ($composer->getLocker()->isLocked()) {
$io->writeError('<error>Adding a repository when creating a project that provides a composer.lock file is not supported</error>');
return false;
}
$repoConfig = RepositoryFactory::configFromString($io, $composer->getConfig(), $repository, true);
$composerJsonRepositoriesConfig = $composer->getConfig()->getRepositories();
$name = RepositoryFactory::generateRepositoryName(0, $repoConfig, $composerJsonRepositoriesConfig);
$configSource = new JsonConfigSource(new JsonFile('composer.json'));
$configSource->addRepository($name, $repoConfig);
$composer = Factory::create($io, null, $disablePlugins);
}
$composer->getDownloadManager()->setOutputProgress(!$noProgress);
$fs = new Filesystem();
if ($noScripts === false) {

@ -0,0 +1,89 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Command;
use Composer\Package\CompletePackageInterface;
use Composer\Package\AliasPackage;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class FundCommand extends BaseCommand
{
protected function configure()
{
$this->setName('fund')
->setDescription('Discover how to help fund the maintenance of your dependencies.')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$composer = $this->getComposer();
$repo = $composer->getRepositoryManager()->getLocalRepository();
$fundings = array();
foreach ($repo->getPackages() as $package) {
if ($package instanceof AliasPackage) {
continue;
}
if ($package instanceof CompletePackageInterface && $funding = $package->getFunding()) {
foreach ($funding as $fundingOption) {
list($vendor, $packageName) = explode('/', $package->getPrettyName());
$url = $fundingOption['url'];
if (!empty($fundingOption['type']) && $fundingOption['type'] === 'github' && preg_match('{^https://github.com/([^/]+)$}', $url, $match)) {
$url = 'https://github.com/sponsors/'.$match[1];
}
$fundings[$vendor][$url][] = $packageName;
}
}
}
ksort($fundings);
$io = $this->getIO();
if ($fundings) {
$prev = null;
$io->write('The following packages were found in your dependencies which publish funding information:');
foreach ($fundings as $vendor => $links) {
$io->write('');
$io->write(sprintf("<comment>%s</comment>", $vendor));
foreach ($links as $url => $packages) {
$line = sprintf(' <info>%s</info>', implode(', ', $packages));
if ($prev !== $line) {
$io->write($line);
$prev = $line;
}
$io->write(sprintf(' %s', $url));
}
}
$io->write("");
$io->write("Please consider following these links and sponsoring the work of package authors!");
$io->write("Thank you!");
} else {
$io->write("No funding links were found in your package dependencies. This doesn't mean they don't need your support!");
}
return 0;
}
}

@ -96,7 +96,7 @@ EOT
$lockErrors[] = 'The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update` or `composer update <package name>`.';
}
$this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true, $isStrict);
$this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true);
// $errors include publish and lock errors when exists
$exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0);
@ -108,8 +108,10 @@ EOT
$file = $path . '/composer.json';
if (is_dir($path) && file_exists($file)) {
list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll);
$this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors);
// $errors include publish errors when exists
$depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0);
$exitCode = max($depCode, $exitCode);
}
@ -123,44 +125,48 @@ EOT
return $exitCode;
}
private function outputResult($io, $name, &$errors, &$warnings, $checkPublish = false, $publishErrors = array(), $checkLock = false, $lockErrors = array(), $printSchemaUrl = false, $isStrict = false)
private function outputResult($io, $name, &$errors, &$warnings, $checkPublish = false, $publishErrors = array(), $checkLock = false, $lockErrors = array(), $printSchemaUrl = false)
{
if (!$errors && !$publishErrors && !$warnings) {
$io->write('<info>' . $name . ' is valid</info>');
} elseif (!$errors && !$publishErrors) {
$io->writeError('<info>' . $name . ' is valid, but with a few warnings</info>');
if ($printSchemaUrl) {
$io->writeError('<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>');
}
} elseif (!$errors) {
$doPrintSchemaUrl = false;
if ($errors) {
$io->writeError('<error>' . $name . ' is invalid, the following errors/warnings were found:</error>');
} elseif ($publishErrors) {
$io->writeError('<info>' . $name . ' is valid for simple usage with composer but has</info>');
$io->writeError('<info>strict errors that make it unable to be published as a package:</info>');
if ($printSchemaUrl) {
$io->writeError('<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>');
}
$doPrintSchemaUrl = $printSchemaUrl;
} elseif ($warnings) {
$io->writeError('<info>' . $name . ' is valid, but with a few warnings</info>');
$doPrintSchemaUrl = $printSchemaUrl;
} else {
$io->writeError('<error>' . $name . ' is invalid, the following errors/warnings were found:</error>');
$io->write('<info>' . $name . ' is valid</info>');
// if ($lockErrors) then they will be displayed below
}
if ($doPrintSchemaUrl) {
$io->writeError('<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>');
}
// Avoid setting the exit code to 1 in case --strict and --no-check-publish/--no-check-lock are combined
$extraWarnings = array();
// If checking publish errors, display them as errors, otherwise just show them as warnings
// Skip when it is a strict check and we don't want to check publish errors
if ($checkPublish) {
$errors = array_merge($errors, $publishErrors);
} elseif (!$isStrict) {
$warnings = array_merge($warnings, $publishErrors);
} else {
$extraWarnings = array_merge($extraWarnings, $publishErrors);
}
// If checking lock errors, display them as errors, otherwise just show them as warnings
// Skip when it is a strict check and we don't want to check lock errors
if ($checkLock) {
$errors = array_merge($errors, $lockErrors);
} elseif (!$isStrict) {
$warnings = array_merge($warnings, $lockErrors);
} else {
$extraWarnings = array_merge($extraWarnings, $lockErrors);
}
$messages = array(
'error' => $errors,
'warning' => $warnings,
'warning' => array_merge($warnings, $extraWarnings),
);
foreach ($messages as $style => $msgs) {

@ -246,6 +246,12 @@ class JsonConfigSource implements ConfigSourceInterface
$config = $this->file->read();
$this->arrayUnshiftRef($args, $config);
call_user_func_array($fallback, $args);
// avoid ending up with arrays for keys that should be objects
foreach (array('require', 'require-dev', 'conflict', 'provide', 'replace', 'suggest', 'config', 'autoload', 'autoload-dev') as $linkType) {
if (isset($config[$linkType]) && $config[$linkType] === array()) {
$config[$linkType] = new \stdClass;
}
}
$this->file->write($config);
}

@ -442,6 +442,7 @@ class Application extends BaseApplication
new Command\ExecCommand(),
new Command\OutdatedCommand(),
new Command\CheckPlatformReqsCommand(),
new Command\FundCommand(),
));
if ('phar:' === substr(__FILE__, 0, 5)) {

@ -38,6 +38,7 @@ use Composer\Package\AliasPackage;
use Composer\Package\RootAliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Package\CompletePackageInterface;
use Composer\Package\Link;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\Loader\ArrayLoader;
@ -308,6 +309,24 @@ class Installer
}
}
$fundingCount = 0;
foreach ($localRepo->getPackages() as $package) {
if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) {
$fundingCount++;
}
}
if ($fundingCount) {
$this->io->writeError(array(
sprintf(
"<info>%d package%s you are using %s looking for funding.</info>",
$fundingCount,
1 === $fundingCount ? '' : 's',
1 === $fundingCount ? 'is' : 'are'
),
'<info>Use the composer fund command to find out more!</info>',
));
}
if ($this->runScripts) {
// dispatch post event
$eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD;

@ -377,6 +377,11 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
return $this->aliasOf->getSupport();
}
public function getFunding()
{
return $this->aliasOf->getFunding();
}
public function getNotificationUrl()
{
return $this->aliasOf->getNotificationUrl();

@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface
protected $homepage;
protected $scripts = array();
protected $support = array();
protected $funding = array();
protected $abandoned = false;
/**
@ -171,6 +172,24 @@ class CompletePackage extends Package implements CompletePackageInterface
return $this->support;
}
/**
* Set the funding
*
* @param array $funding
*/
public function setFunding(array $funding)
{
$this->funding = $funding;
}
/**
* {@inheritDoc}
*/
public function getFunding()
{
return $this->funding;
}
/**
* @return bool
*/

@ -79,6 +79,15 @@ interface CompletePackageInterface extends PackageInterface
*/
public function getSupport();
/**
* Returns an array of funding options for the package
*
* Each item will contain type and url keys
*
* @return array
*/
public function getFunding();
/**
* Returns if the package is abandoned or not
*

@ -104,6 +104,7 @@ class ArrayDumper
'keywords',
'repositories',
'support',
'funding',
);
$data = $this->dumpValues($package, $keys, $data);

@ -228,6 +228,10 @@ class ArrayLoader implements LoaderInterface
$package->setSupport($config['support']);
}
if (!empty($config['funding']) && is_array($config['funding'])) {
$package->setFunding($config['funding']);
}
if (isset($config['abandoned'])) {
$package->setAbandoned($config['abandoned']);
}

@ -193,6 +193,32 @@ class ValidatingArrayLoader implements LoaderInterface
}
}
if ($this->validateArray('funding') && !empty($this->config['funding'])) {
foreach ($this->config['funding'] as $key => $fundingOption) {
if (!is_array($fundingOption)) {
$this->errors[] = 'funding.'.$key.' : should be an array, '.gettype($fundingOption).' given';
unset($this->config['funding'][$key]);
continue;
}
foreach (array('type', 'url') as $fundingData) {
if (isset($fundingOption[$fundingData]) && !is_string($fundingOption[$fundingData])) {
$this->errors[] = 'funding.'.$key.'.'.$fundingData.' : invalid value, must be a string';
unset($this->config['funding'][$key][$fundingData]);
}
}
if (isset($fundingOption['url']) && !$this->filterUrl($fundingOption['url'])) {
$this->warnings[] = 'funding.'.$key.'.url : invalid value ('.$fundingOption['url'].'), must be an http/https URL';
unset($this->config['funding'][$key]['url']);
}
if (empty($this->config['funding'][$key])) {
unset($this->config['funding'][$key]);
}
}
if (empty($this->config['funding'])) {
unset($this->config['funding']);
}
}
$unboundConstraint = new Constraint('=', $this->versionParser->normalize('dev-master'));
$stableConstraint = new Constraint('=', '1.0.0');

@ -153,10 +153,8 @@ class RepositoryFactory
if (!isset($repo['type'])) {
throw new \UnexpectedValueException('Repository "'.$index.'" ('.json_encode($repo).') must have a type defined');
}
$name = is_int($index) && isset($repo['url']) ? preg_replace('{^https?://}i', '', $repo['url']) : $index;
while (isset($repos[$name])) {
$name .= '2';
}
$name = self::generateRepositoryName($index, $repo, $repos);
if ($repo['type'] === 'filesystem') {
$repos[$name] = new FilesystemRepository($repo['json']);
} else {
@ -166,4 +164,14 @@ class RepositoryFactory
return $repos;
}
public static function generateRepositoryName($index, array $repo, array $existingRepos)
{
$name = is_int($index) && isset($repo['url']) ? preg_replace('{^https?://}i', '', $repo['url']) : $index;
while (isset($existingRepos[$name])) {
$name .= '2';
}
return $name;
}
}

@ -36,6 +36,7 @@ class GitHubDriver extends VcsDriver
protected $infoCache = array();
protected $isPrivate = false;
private $isArchived = false;
private $fundingInfo;
/**
* Git Driver
@ -167,6 +168,10 @@ class GitHubDriver extends VcsDriver
if (!isset($composer['abandoned']) && $this->isArchived) {
$composer['abandoned'] = true;
}
if (!isset($composer['funding']) && $funding = $this->getFundingInfo()) {
$composer['funding'] = $funding;
}
}
if ($this->shouldCache($identifier)) {
@ -179,6 +184,40 @@ class GitHubDriver extends VcsDriver
return $this->infoCache[$identifier];
}
private function getFundingInfo()
{
if (null !== $this->fundingInfo) {
return $this->fundingInfo;
}
if ($this->originUrl !== 'github.com') {
return $this->fundingInfo = false;
}
$graphql = 'query{repository(owner:"'.$this->owner.'",name:"'.$this->repository.'"){fundingLinks{platform,url}}}';
try {
$result = $this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/graphql', false, array(
'http' => array(
'method' => 'POST',
'content' => json_encode(array('query' => $graphql)),
'header' => array('Content-Type: application/json'),
),
'retry-auth-failure' => false,
));
} catch (\TransportException $e) {
return $this->fundingInfo = false;
}
$result = json_decode($result, true);
if (empty($result['data']['repository']['fundingLinks'])) {
return $this->fundingInfo = false;
}
return $this->fundingInfo = array_map(function ($link) {
return array('type' => strtolower($link['platform']), 'url' => $link['url']);
}, $result['data']['repository']['fundingLinks']);
}
/**
* {@inheritdoc}
*/

@ -97,6 +97,7 @@ class Git
$auth = null;
if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $ignoredOutput, $cwd)) {
$errorMsg = $this->process->getErrorOutput();
// private github repository without ssh key access, try https with auth
if (preg_match('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match)
|| preg_match('{^https?://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match)
@ -117,6 +118,8 @@ class Git
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return;
}
$errorMsg = $this->process->getErrorOutput();
}
} elseif (preg_match('{^https://(bitbucket\.org)/(.*)(\.git)?$}U', $url, $match)) { //bitbucket oauth
$bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process);
@ -149,6 +152,8 @@ class Git
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return;
}
$errorMsg = $this->process->getErrorOutput();
} else { // Falling back to ssh
$sshUrl = 'git@bitbucket.org:' . $match[2] . '.git';
$this->io->writeError(' No bitbucket authentication configured. Falling back to ssh.');
@ -156,6 +161,8 @@ class Git
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return;
}
$errorMsg = $this->process->getErrorOutput();
}
} elseif (
preg_match('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match)
@ -186,6 +193,8 @@ class Git
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return;
}
$errorMsg = $this->process->getErrorOutput();
}
} elseif ($this->isAuthenticationFailure($url, $match)) { // private non-github/gitlab/bitbucket repo that failed to authenticate
if (strpos($match[2], '@')) {
@ -224,10 +233,11 @@ class Git
return;
}
$errorMsg = $this->process->getErrorOutput();
}
}
$errorMsg = $this->process->getErrorOutput();
if ($initialClone) {
$this->filesystem->removeDirectory($origCwd);
}
@ -252,6 +262,8 @@ class Git
};
$this->runCommand($commandCallable, $url, $dir);
} catch (\Exception $e) {
$this->io->writeError('<error>Sync mirror failed: ' . $e->getMessage() . '</error>', true, IOInterface::DEBUG);
return false;
}

@ -1525,6 +1525,8 @@ EOF;
'/composersrc/ClassToExclude.php',
'/composersrc/*/excluded/excsubpath',
'**/excsubpath',
'composers', // should _not_ cause exclusion of /composersrc/**, as it is equivalent to /composers/**
'/src-ca/', // should _not_ cause exclusion of /src-cake/**, as it is equivalent to /src-ca/**
),
));
@ -1547,7 +1549,7 @@ EOF;
$this->fs->ensureDirectoryExists($this->workingDir.'/composersrc/tests');
file_put_contents($this->workingDir.'/composersrc/foo.php', '<?php class ClassMapFoo {}');
// this classes should not be found in the classmap
// these classes should not be found in the classmap
$this->fs->ensureDirectoryExists($this->workingDir.'/composersrc/excludedTests');
file_put_contents($this->workingDir.'/composersrc/excludedTests/bar.php', '<?php class ClassExcludeMapFoo {}');
file_put_contents($this->workingDir.'/composersrc/ClassToExclude.php', '<?php class ClassClassToExclude {}');

@ -0,0 +1,54 @@
--TEST--
Installs a simple package with exact match requirement
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{
"name": "a/a",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }],
"require": {
"d/d": "^1.0"
}
},
{
"name": "b/b",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
},
{
"name": "c/c",
"version": "1.0.0",
"funding": [{ "type": "example", "url": "http://example.org/fund" }]
},
{
"name": "d/d",
"version": "1.0.0",
"require": {
"b/b": "^1.0"
}
}
]
}
],
"require": {
"a/a": "1.0.0"
}
}
--RUN--
install
--EXPECT-OUTPUT--
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
Writing lock file
Generating autoload files
2 packages you are using are looking for funding.
Use the composer fund command to find out more!
--EXPECT--
Installing b/b (1.0.0)
Installing d/d (1.0.0)
Installing a/a (1.0.0)

@ -21,6 +21,12 @@
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/composer/issues"
},
"funding": [
{
"type": "service-subscription",
"url": "https://packagist.com"
}
],
"require": {
"php": ">=5.3.2",
"justinrainbow/json-schema": "~1.4",

@ -191,6 +191,10 @@ class ArrayDumperTest extends TestCase
'support',
array('foo' => 'bar'),
),
array(
'funding',
array('type' => 'foo', 'url' => 'https://example.com'),
),
array(
'require',
array(new Link('foo', 'foo/bar', new Constraint('=', '1.0.0.0'), 'requires', '1.0.0'), new Link('bar', 'bar/baz', new Constraint('=', '1.0.0.0'), 'requires', '1.0.0')),

@ -97,6 +97,9 @@ class ArrayLoaderTest extends TestCase
'authors' => array(
array('name' => 'Bob', 'email' => 'bob@example.org', 'homepage' => 'example.org', 'role' => 'Developer'),
),
'funding' => array(
array('type' => 'example', 'url' => 'https://example.org/fund'),
),
'require' => array(
'foo/bar' => '1.0',
),

@ -73,6 +73,15 @@ class ValidatingArrayLoaderTest extends TestCase
'rss' => 'http://example.org/rss',
'chat' => 'http://example.org/chat',
),
'funding' => array(
array(
'type' => 'example',
'url' => 'https://example.org/fund'
),
array(
'url' => 'https://example.org/fund'
),
),
'require' => array(
'a/b' => '1.*',
'b/c' => '~2',

@ -48,4 +48,24 @@ class RepositoryFactoryTest extends TestCase
'path',
), array_keys($repositoryClasses));
}
/**
* @dataProvider generateRepositoryNameProvider
*/
public function testGenerateRepositoryName($index, array $config, array $existingRepos, $expected)
{
$this->assertSame($expected, RepositoryFactory::generateRepositoryName($index, $config, $existingRepos));
}
public function generateRepositoryNameProvider()
{
return array(
array(0, array(), array(), 0),
array(0, array(), array(array()), '02'),
array(0, array('url' => 'https://example.org'), array(), 'example.org'),
array(0, array('url' => 'https://example.org'), array('example.org' => array()), 'example.org2'),
array('example.org', array('url' => 'https://example.org/repository'), array(), 'example.org'),
array('example.org', array('url' => 'https://example.org/repository'), array('example.org' => array()), 'example.org2'),
);
}
}

Loading…
Cancel
Save