1
0
Fork 0

Merge branch 'master' into 2.0

pull/8669/head
Jordi Boggiano 2020-02-14 15:42:17 +01:00
commit bc002ae1fb
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC
31 changed files with 485 additions and 56 deletions

View File

@ -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. * 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 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 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 `@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 `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 support for IPv6 addresses in NO_PROXY
* Added package homepage display in the show command * Added package homepage display in the show command
* Added debug info about HTTP authentications * Added debug info about HTTP authentications
* Added Symfony 5 compatibility * 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 * 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 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 init/require command to avoid suggesting packages which are already selected in the search results
* Fixed create-project UX issues * 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 ### [1.9.3] 2020-02-04
@ -804,7 +811,7 @@
* Initial release * 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.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.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 [1.9.1]: https://github.com/composer/composer/compare/1.9.0...1.9.1

34
composer.lock generated
View File

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

View File

@ -19,13 +19,14 @@ This idea is not new and Composer is strongly inspired by node's
Suppose: Suppose:
1. You have a project that depends on a number of libraries. 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: Composer:
1. Enables you to declare the libraries you depend on. 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). 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 See the [Basic usage](01-basic-usage.md) chapter for more details on declaring
dependencies. dependencies.

View File

@ -248,7 +248,8 @@ php composer.phar dump-autoload
``` ```
This command will re-generate the `vendor/autoload.php` file. 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 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. the return value of the include call in a variable and add more namespaces.

View File

@ -419,6 +419,11 @@ If you only want a list of suggested package names, use `--list`.
* **--list:** Show only list of suggested package names. * **--list:** Show only list of suggested package names.
* **--no-dev:** Excludes suggestions from `require-dev` packages. * **--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) ## depends (why)
The `depends` command tells you which other packages depend on a certain 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 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) JSON string which similar to what the [repositories](04-schema.md#repositories)
key accepts. key accepts.
* **--add-repository:** Add the repository option to the composer.json.
* **--dev:** Install packages listed in `require-dev`. * **--dev:** Install packages listed in `require-dev`.
* **--no-dev:** Disables installation of require-dev packages. * **--no-dev:** Disables installation of require-dev packages.
* **--no-scripts:** Disables the execution of the scripts defined in the root * **--no-scripts:** Disables the execution of the scripts defined in the root

View File

@ -258,6 +258,39 @@ An example:
Optional. 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 ### Package links
All of the following take an object which maps package names to All of the following take an object which maps package names to

View File

@ -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": { "non-feature-branches": {
"type": ["array"], "type": ["array"],
"description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.", "description": "A set of string or regex patterns for non-numeric branch names that will not be handled as feature branches.",

View File

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

View File

@ -181,16 +181,16 @@ class ClassMapGenerator
if (empty($validClasses)) { if (empty($validClasses)) {
foreach ($rejectedClasses as $class) { foreach ($rejectedClasses as $class) {
trigger_error( 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 E_USER_DEPRECATED
); );
} }
// TODO enable in Composer v1.11 or 2.0 whichever comes first // TODO enable in Composer 2.0
//return array(); //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 $validClasses;
return $classes; return $classes;
} }

View File

@ -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('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', 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('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('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-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.'), 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-progress'),
$input->getOption('no-install'), $input->getOption('no-install'),
$input->getOption('ignore-platform-reqs'), $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(); $oldCwd = getcwd();
@ -164,6 +166,25 @@ EOT
$composer = Factory::create($io, null, $disablePlugins); $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(); $fs = new Filesystem();
if ($noScripts === false) { if ($noScripts === false) {

View File

@ -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;
}
}

View File

@ -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>`.'; $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 // $errors include publish and lock errors when exists
$exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); $exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0);
@ -108,8 +108,10 @@ EOT
$file = $path . '/composer.json'; $file = $path . '/composer.json';
if (is_dir($path) && file_exists($file)) { if (is_dir($path) && file_exists($file)) {
list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll); list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll);
$this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors); $this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors);
// $errors include publish errors when exists
$depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); $depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0);
$exitCode = max($depCode, $exitCode); $exitCode = max($depCode, $exitCode);
} }
@ -123,44 +125,48 @@ EOT
return $exitCode; 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) { $doPrintSchemaUrl = false;
$io->write('<info>' . $name . ' is valid</info>');
} elseif (!$errors && !$publishErrors) { if ($errors) {
$io->writeError('<info>' . $name . ' is valid, but with a few warnings</info>'); $io->writeError('<error>' . $name . ' is invalid, the following errors/warnings were found:</error>');
if ($printSchemaUrl) { } elseif ($publishErrors) {
$io->writeError('<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>');
}
} elseif (!$errors) {
$io->writeError('<info>' . $name . ' is valid for simple usage with composer but has</info>'); $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>'); $io->writeError('<info>strict errors that make it unable to be published as a package:</info>');
if ($printSchemaUrl) { $doPrintSchemaUrl = $printSchemaUrl;
$io->writeError('<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>'); } elseif ($warnings) {
} $io->writeError('<info>' . $name . ' is valid, but with a few warnings</info>');
$doPrintSchemaUrl = $printSchemaUrl;
} else { } 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 // 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) { if ($checkPublish) {
$errors = array_merge($errors, $publishErrors); $errors = array_merge($errors, $publishErrors);
} elseif (!$isStrict) { } else {
$warnings = array_merge($warnings, $publishErrors); $extraWarnings = array_merge($extraWarnings, $publishErrors);
} }
// If checking lock errors, display them as errors, otherwise just show them as warnings // 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) { if ($checkLock) {
$errors = array_merge($errors, $lockErrors); $errors = array_merge($errors, $lockErrors);
} elseif (!$isStrict) { } else {
$warnings = array_merge($warnings, $lockErrors); $extraWarnings = array_merge($extraWarnings, $lockErrors);
} }
$messages = array( $messages = array(
'error' => $errors, 'error' => $errors,
'warning' => $warnings, 'warning' => array_merge($warnings, $extraWarnings),
); );
foreach ($messages as $style => $msgs) { foreach ($messages as $style => $msgs) {

View File

@ -246,6 +246,12 @@ class JsonConfigSource implements ConfigSourceInterface
$config = $this->file->read(); $config = $this->file->read();
$this->arrayUnshiftRef($args, $config); $this->arrayUnshiftRef($args, $config);
call_user_func_array($fallback, $args); 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); $this->file->write($config);
} }

View File

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

View File

@ -38,6 +38,7 @@ use Composer\Package\AliasPackage;
use Composer\Package\RootAliasPackage; use Composer\Package\RootAliasPackage;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
use Composer\Package\CompletePackage; use Composer\Package\CompletePackage;
use Composer\Package\CompletePackageInterface;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\Loader\ArrayLoader; 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) { if ($this->runScripts) {
// dispatch post event // dispatch post event
$eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD; $eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD;

View File

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

View File

@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface
protected $homepage; protected $homepage;
protected $scripts = array(); protected $scripts = array();
protected $support = array(); protected $support = array();
protected $funding = array();
protected $abandoned = false; protected $abandoned = false;
/** /**
@ -171,6 +172,24 @@ class CompletePackage extends Package implements CompletePackageInterface
return $this->support; 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 * @return bool
*/ */

View File

@ -79,6 +79,15 @@ interface CompletePackageInterface extends PackageInterface
*/ */
public function getSupport(); 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 * Returns if the package is abandoned or not
* *

View File

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

View File

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

View File

@ -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')); $unboundConstraint = new Constraint('=', $this->versionParser->normalize('dev-master'));
$stableConstraint = new Constraint('=', '1.0.0'); $stableConstraint = new Constraint('=', '1.0.0');

View File

@ -153,10 +153,8 @@ class RepositoryFactory
if (!isset($repo['type'])) { if (!isset($repo['type'])) {
throw new \UnexpectedValueException('Repository "'.$index.'" ('.json_encode($repo).') must have a type defined'); 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 = self::generateRepositoryName($index, $repo, $repos);
$name .= '2';
}
if ($repo['type'] === 'filesystem') { if ($repo['type'] === 'filesystem') {
$repos[$name] = new FilesystemRepository($repo['json']); $repos[$name] = new FilesystemRepository($repo['json']);
} else { } else {
@ -166,4 +164,14 @@ class RepositoryFactory
return $repos; 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;
}
} }

View File

@ -36,6 +36,7 @@ class GitHubDriver extends VcsDriver
protected $infoCache = array(); protected $infoCache = array();
protected $isPrivate = false; protected $isPrivate = false;
private $isArchived = false; private $isArchived = false;
private $fundingInfo;
/** /**
* Git Driver * Git Driver
@ -167,6 +168,10 @@ class GitHubDriver extends VcsDriver
if (!isset($composer['abandoned']) && $this->isArchived) { if (!isset($composer['abandoned']) && $this->isArchived) {
$composer['abandoned'] = true; $composer['abandoned'] = true;
} }
if (!isset($composer['funding']) && $funding = $this->getFundingInfo()) {
$composer['funding'] = $funding;
}
} }
if ($this->shouldCache($identifier)) { if ($this->shouldCache($identifier)) {
@ -179,6 +184,40 @@ class GitHubDriver extends VcsDriver
return $this->infoCache[$identifier]; 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} * {@inheritdoc}
*/ */

View File

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

View File

@ -1525,6 +1525,8 @@ EOF;
'/composersrc/ClassToExclude.php', '/composersrc/ClassToExclude.php',
'/composersrc/*/excluded/excsubpath', '/composersrc/*/excluded/excsubpath',
'**/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'); $this->fs->ensureDirectoryExists($this->workingDir.'/composersrc/tests');
file_put_contents($this->workingDir.'/composersrc/foo.php', '<?php class ClassMapFoo {}'); 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'); $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/excludedTests/bar.php', '<?php class ClassExcludeMapFoo {}');
file_put_contents($this->workingDir.'/composersrc/ClassToExclude.php', '<?php class ClassClassToExclude {}'); file_put_contents($this->workingDir.'/composersrc/ClassToExclude.php', '<?php class ClassClassToExclude {}');

View File

@ -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)

View File

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

View File

@ -191,6 +191,10 @@ class ArrayDumperTest extends TestCase
'support', 'support',
array('foo' => 'bar'), array('foo' => 'bar'),
), ),
array(
'funding',
array('type' => 'foo', 'url' => 'https://example.com'),
),
array( array(
'require', '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')), 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')),

View File

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

View File

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

View File

@ -48,4 +48,24 @@ class RepositoryFactoryTest extends TestCase
'path', 'path',
), array_keys($repositoryClasses)); ), 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'),
);
}
} }