diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e6075148..ade8d99cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +### [1.9.1] 2019-11-01 + + * Fixed various credential handling issues with gitlab and github + * Fixed credentials being present in git remotes in Composer cache and vendor directory when not using SSH keys + * Fixed `composer why` not listing replacers as a reason something is present + * Fixed various PHP 7.4 compatibility issues + * Fixed root warnings always present in Docker containers, setting COMPOSER_ALLOW_SUPERUSER is not necessary anymore + * Fixed GitHub access tokens leaking into debug-verbosity output + * Fixed several edge case issues detecting GitHub, Bitbucket and GitLab repository types + * Fixed Composer asking if you want to use a composer.json in a parent directory when ran in non-interactive mode + * Fixed classmap autoloading issue finding classes located within a few non-PHP context blocks (?>... `mkdir -p /usr/local/bin`. > **Note:** For information on changing your PATH, please read the -> [Wikipedia article](https://en.wikipedia.org/wiki/PATH_(variable)) and/or use Google. +> [Wikipedia article](https://en.wikipedia.org/wiki/PATH_(variable)) and/or use +> your search engine of choice. Now run `composer` in order to run Composer instead of `php composer.phar`. @@ -139,7 +140,7 @@ C:\bin>echo @php "%~dp0composer.phar" %*>composer.bat Add the directory to your PATH environment variable if it isn't already. For information on changing your PATH variable, please see [this article](https://www.computerhope.com/issues/ch000549.htm) and/or -use Google. +use your search engine of choice. Close your current terminal. Test usage with a new terminal: diff --git a/doc/06-config.md b/doc/06-config.md index f3afc4eb1..6ffbea411 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -296,4 +296,9 @@ Example: Defaults to `true`. If set to `false`, Composer will not create `.htaccess` files in the composer home, cache, and data directories. +## lock + +Defaults to `true`. If set to `false`, Composer will not create a `composer.lock` +file. + ← [Repositories](05-repositories.md) | [Community](07-community.md) → diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index fbc5d0417..1185bd689 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -339,6 +339,9 @@ One limitation of this is that you can not call multiple commands in a row like `@php install && @php foo`. You must split them up in a JSON array of commands. +You can also call a shell/bash script, which will have the path to +the PHP executable available in it as a `PHP_BINARY` env var. + ## Custom descriptions. You can set custom script descriptions with the following in your `composer.json`: diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index d0a64506f..7a5725ec4 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -211,6 +211,19 @@ To enable the swap you can use for example: ``` You can make a permanent swap file following this [tutorial](https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04). +## proc_open(): failed to open stream errors (Windows) + +If composer shows proc_open(NUL) errors on Windows: + +`proc_open(NUL): failed to open stream: No such file or directory` + +This could be happening because you are working in a _OneDrive_ directory and +using a version of PHP that does not support the file system semantics of this +service. The issue was fixed in PHP 7.2.23 and 7.3.10. + +Alternatively it could be because the Windows Null Service is not enabled. For +more information, see this [issue](https://github.com/composer/composer/issues/7186#issuecomment-373134916). + ## Degraded Mode Due to some intermittent issues on Travis and other systems, we introduced a diff --git a/res/composer-schema.json b/res/composer-schema.json index cb3594f7b..bd04c5d5f 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -11,7 +11,8 @@ }, "type": { "description": "Package type, either 'library' for common packages, 'composer-plugin' for plugins, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.", - "type": "string" + "type": "string", + "pattern": "^[a-z0-9-]+$" }, "target-dir": { "description": "DEPRECATED: Forces the package to be installed into the given subdirectory path. This is used for autoloading PSR-0 packages that do not contain their full path. Use forward slashes for cross-platform compatibility.", @@ -39,7 +40,8 @@ }, "version": { "type": "string", - "description": "Package version, see https://getcomposer.org/doc/04-schema.md#version for more info on valid schemes." + "description": "Package version, see https://getcomposer.org/doc/04-schema.md#version for more info on valid schemes.", + "pattern": "^v?\\d+(((\\.\\d+)?\\.\\d+)?\\.\\d+)?" }, "time": { "type": "string", @@ -290,6 +292,10 @@ "sort-packages": { "type": "boolean", "description": "Defaults to false. If set to true, Composer will sort packages when adding/updating a new dependency." + }, + "lock": { + "type": "boolean", + "description": "Defaults to true. If set to false, Composer will not create a composer.lock file." } } }, diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index d970ca5b1..5e068ccff 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -256,15 +256,14 @@ EOF; continue; } - $namespaceFilter = $namespace === '' ? null : $namespace; - $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespaceFilter, $classMap); + $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespace, $group['type'], $classMap); } } } } foreach ($autoloads['classmap'] as $dir) { - $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, null, $classMap); + $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, null, null, $classMap); } ksort($classMap); @@ -317,9 +316,9 @@ EOF; return count($classMap); } - private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist = null, $namespaceFilter = null, array $classMap = array()) + private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist = null, $namespaceFilter = null, $autoloadType = null, array $classMap = array()) { - foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter) as $class => $path) { + foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType) as $class => $path) { $pathCode = $this->getPathCode($filesystem, $basePath, $vendorPath, $path).",\n"; if (!isset($classMap[$class])) { $classMap[$class] = $pathCode; @@ -334,9 +333,9 @@ EOF; return $classMap; } - private function generateClassMap($dir, $blacklist = null, $namespaceFilter = null, $showAmbiguousWarning = true) + private function generateClassMap($dir, $blacklist = null, $namespaceFilter = null, $autoloadType = null, $showAmbiguousWarning = true) { - return ClassMapGenerator::createMap($dir, $blacklist, $showAmbiguousWarning ? $this->io : null, $namespaceFilter); + return ClassMapGenerator::createMap($dir, $blacklist, $showAmbiguousWarning ? $this->io : null, $namespaceFilter, $autoloadType); } public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) @@ -447,7 +446,7 @@ EOF; foreach ($autoloads['classmap'] as $dir) { try { - $loader->addClassMap($this->generateClassMap($dir, $blacklist, null, false)); + $loader->addClassMap($this->generateClassMap($dir, $blacklist, null, null, false)); } catch (\RuntimeException $e) { $this->io->writeError(''.$e->getMessage().''); } @@ -592,6 +591,9 @@ class ComposerAutoloaderInit$suffix } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::\$loader) { diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index 1ecf96bfe..ef12809d5 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -50,17 +50,19 @@ class ClassMapGenerator /** * Iterate over all files in the given directory searching for classes * - * @param \Iterator|string $path The path to search in or an iterator - * @param string $blacklist Regex that matches against the file path that exclude from the classmap. - * @param IOInterface $io IO object - * @param string $namespace Optional namespace prefix to filter by + * @param \Iterator|string $path The path to search in or an iterator + * @param string $blacklist Regex that matches against the file path that exclude from the classmap. + * @param IOInterface $io IO object + * @param string $namespace Optional namespace prefix to filter by + * @param string $autoloadType psr-0|psr-4 Optional autoload standard to use mapping rules * * @throws \RuntimeException When the path is neither an existing file nor directory * @return array A class map array */ - public static function createMap($path, $blacklist = null, IOInterface $io = null, $namespace = null) + public static function createMap($path, $blacklist = null, IOInterface $io = null, $namespace = null, $autoloadType = null) { if (is_string($path)) { + $basePath = $path; if (is_file($path)) { $path = array(new \SplFileInfo($path)); } elseif (is_dir($path)) { @@ -71,6 +73,8 @@ class ClassMapGenerator '" which does not appear to be a file nor a folder' ); } + } elseif (null !== $autoloadType) { + throw new \RuntimeException('Path must be a string when specifying an autoload type'); } $map = array(); @@ -100,10 +104,14 @@ class ClassMapGenerator } $classes = self::findClasses($filePath); + if (null !== $autoloadType) { + $classes = self::filterByNamespace($classes, $filePath, $namespace, $autoloadType, $basePath, $io); + } foreach ($classes as $class) { // skip classes not within the given namespace prefix - if (null !== $namespace && 0 !== strpos($class, $namespace)) { + // TODO enable in Composer v1.11 or 2.0 whichever comes first + if (/* null === $autoloadType && */ null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) { continue; } @@ -121,6 +129,72 @@ class ClassMapGenerator return $map; } + /** + * Remove classes which could not have been loaded by namespace autoloaders + * + * @param array $classes found classes in given file + * @param string $filePath current file + * @param string $baseNamespace prefix of given autoload mapping + * @param string $namespaceType psr-0|psr-4 + * @param string $basePath root directory of given autoload mapping + * @param IOInterface $io IO object + * @return array valid classes + */ + private static function filterByNamespace($classes, $filePath, $baseNamespace, $namespaceType, $basePath, $io) + { + $validClasses = array(); + $rejectedClasses = array(); + + $realSubPath = substr($filePath, strlen($basePath) + 1); + $realSubPath = substr($realSubPath, 0, strrpos($realSubPath, '.')); + + foreach ($classes as $class) { + // silently skip if ns doesn't have common root + if ('' !== $baseNamespace && 0 !== strpos($class, $baseNamespace)) { + continue; + } + // transform class name to file path and validate + if ('psr-0' === $namespaceType) { + $namespaceLength = strrpos($class, '\\'); + if (false !== $namespaceLength) { + $namespace = substr($class, 0, $namespaceLength + 1); + $className = substr($class, $namespaceLength + 1); + $subPath = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) + . str_replace('_', DIRECTORY_SEPARATOR, $className); + } + else { + $subPath = str_replace('_', DIRECTORY_SEPARATOR, $class); + } + } elseif ('psr-4' === $namespaceType) { + $subNamespace = ('' !== $baseNamespace) ? substr($class, strlen($baseNamespace)) : $class; + $subPath = str_replace('\\', DIRECTORY_SEPARATOR, $subNamespace); + } else { + throw new \RuntimeException("namespaceType must be psr-0 or psr-4, $namespaceType given"); + } + if ($subPath === $realSubPath) { + $validClasses[] = $class; + } else { + $rejectedClasses[] = $class; + } + } + // warn only if no valid classes, else silently skip invalid + 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+.", + E_USER_DEPRECATED + ); + } + + // TODO enable in Composer v1.11 or 2.0 whichever comes first + //return array(); + } + + // TODO enable in Composer v1.11 or 2.0 whichever comes first & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses + //return $validClasses; + return $classes; + } + /** * Extract the classes in the given file * @@ -173,7 +247,7 @@ class ClassMapGenerator } } // strip non-php blocks in the file - $contents = preg_replace('{\?>.+<\?}s', '?>(?:[^<]++|<(?!\?))*+<\?}s', '?>'); if (false !== $pos && false === strpos(substr($contents, $pos), ' EOT ); + + return 0; } } diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index b2b617c42..00fed9240 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -62,7 +62,7 @@ class BaseDependencyCommand extends BaseCommand * @param InputInterface $input * @param OutputInterface $output * @param bool $inverted Whether to invert matching process (why-not vs why behaviour) - * @return int|null Exit code of the operation. + * @return int Exit code of the operation. */ protected function doExecute(InputInterface $input, OutputInterface $output, $inverted = false) { diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php index ec51c56d3..08ec04701 100644 --- a/src/Composer/Command/ClearCacheCommand.php +++ b/src/Composer/Command/ClearCacheCommand.php @@ -70,5 +70,7 @@ EOT } $io->writeError('All caches cleared.'); + + return 0; } } diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 38b103bb6..329230e2e 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -412,6 +412,7 @@ EOT ), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), 'htaccess-protect' => array($booleanValidator, $booleanNormalizer), + 'lock' => array($booleanValidator, $booleanNormalizer), ); $multiConfigValues = array( 'github-protocols' => array( @@ -463,13 +464,19 @@ EOT $this->getIO()->writeError('You are now running Composer with SSL/TLS protection enabled.'); } - return $this->configSource->removeConfigSetting($settingKey); + $this->configSource->removeConfigSetting($settingKey); + + return 0; } if (isset($uniqueConfigValues[$settingKey])) { - return $this->handleSingleValue($settingKey, $uniqueConfigValues[$settingKey], $values, 'addConfigSetting'); + $this->handleSingleValue($settingKey, $uniqueConfigValues[$settingKey], $values, 'addConfigSetting'); + + return 0; } if (isset($multiConfigValues[$settingKey])) { - return $this->handleMultiValue($settingKey, $multiConfigValues[$settingKey], $values, 'addConfigSetting'); + $this->handleMultiValue($settingKey, $multiConfigValues[$settingKey], $values, 'addConfigSetting'); + + return 0; } // handle properties @@ -530,38 +537,51 @@ EOT throw new \InvalidArgumentException('The '.$settingKey.' property can not be set in the global config.json file. Use `composer global config` to apply changes to the global composer.json'); } if ($input->getOption('unset') && (isset($uniqueProps[$settingKey]) || isset($multiProps[$settingKey]))) { - return $this->configSource->removeProperty($settingKey); + $this->configSource->removeProperty($settingKey); + + return 0; } if (isset($uniqueProps[$settingKey])) { - return $this->handleSingleValue($settingKey, $uniqueProps[$settingKey], $values, 'addProperty'); + $this->handleSingleValue($settingKey, $uniqueProps[$settingKey], $values, 'addProperty'); + + return 0; } if (isset($multiProps[$settingKey])) { - return $this->handleMultiValue($settingKey, $multiProps[$settingKey], $values, 'addProperty'); + $this->handleMultiValue($settingKey, $multiProps[$settingKey], $values, 'addProperty'); + + return 0; } // handle repositories if (preg_match('/^repos?(?:itories)?\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { - return $this->configSource->removeRepository($matches[1]); + $this->configSource->removeRepository($matches[1]); + + return 0; } if (2 === count($values)) { - return $this->configSource->addRepository($matches[1], array( + $this->configSource->addRepository($matches[1], array( 'type' => $values[0], 'url' => $values[1], )); + + return 0; } if (1 === count($values)) { $value = strtolower($values[0]); if (true === $booleanValidator($value)) { if (false === $booleanNormalizer($value)) { - return $this->configSource->addRepository($matches[1], false); + $this->configSource->addRepository($matches[1], false); + + return 0; } } else { $value = JsonFile::parseJson($values[0]); + $this->configSource->addRepository($matches[1], $value); - return $this->configSource->addRepository($matches[1], $value); + return 0; } } @@ -571,22 +591,32 @@ EOT // handle extra if (preg_match('/^extra\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { - return $this->configSource->removeProperty($settingKey); + $this->configSource->removeProperty($settingKey); + + return 0; } - return $this->configSource->addProperty($settingKey, $values[0]); + $this->configSource->addProperty($settingKey, $values[0]); + + return 0; } // handle platform if (preg_match('/^platform\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { - return $this->configSource->removeConfigSetting($settingKey); + $this->configSource->removeConfigSetting($settingKey); + + return 0; } - return $this->configSource->addConfigSetting($settingKey, $values[0]); + $this->configSource->addConfigSetting($settingKey, $values[0]); + + return 0; } if ($settingKey === 'platform' && $input->getOption('unset')) { - return $this->configSource->removeConfigSetting($settingKey); + $this->configSource->removeConfigSetting($settingKey); + + return 0; } // handle auth @@ -595,7 +625,7 @@ EOT $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); - return; + return 0; } if ($matches[1] === 'bitbucket-oauth') { @@ -618,16 +648,20 @@ EOT $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('username' => $values[0], 'password' => $values[1])); } - return; + return 0; } // handle script if (preg_match('/^scripts\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { - return $this->configSource->removeProperty($settingKey); + $this->configSource->removeProperty($settingKey); + + return 0; } - return $this->configSource->addProperty($settingKey, count($values) > 1 ? $values : $values[0]); + $this->configSource->addProperty($settingKey, count($values) > 1 ? $values : $values[0]); + + return 0; } throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command'); diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index d6adec083..c350fde9b 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -48,7 +48,7 @@ EOT * * @param InputInterface $input * @param OutputInterface $output - * @return int|null + * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index 3add15166..f1e91f15c 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -63,11 +63,11 @@ EOT $apcu = $input->getOption('apcu') || $config->get('apcu-autoloader'); if ($authoritative) { - $this->getIO()->writeError('Generating optimized autoload files (authoritative)', false); + $this->getIO()->write('Generating optimized autoload files (authoritative)'); } elseif ($optimize) { - $this->getIO()->writeError('Generating optimized autoload files', false); + $this->getIO()->write('Generating optimized autoload files'); } else { - $this->getIO()->writeError('Generating autoload files', false); + $this->getIO()->write('Generating autoload files'); } $generator = $composer->getAutoloadGenerator(); @@ -78,11 +78,13 @@ EOT $numberOfClasses = $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); if ($authoritative) { - $this->getIO()->overwriteError('Generated optimized autoload files (authoritative) containing '. $numberOfClasses .' classes'); + $this->getIO()->write('Generated optimized autoload files (authoritative) containing '. $numberOfClasses .' classes'); } elseif ($optimize) { - $this->getIO()->overwriteError('Generated optimized autoload files containing '. $numberOfClasses .' classes'); + $this->getIO()->write('Generated optimized autoload files containing '. $numberOfClasses .' classes'); } else { - $this->getIO()->overwriteError('Generated autoload files containing '. $numberOfClasses .' classes'); + $this->getIO()->write('Generated autoload files containing '. $numberOfClasses .' classes'); } + + return 0; } } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 19ff99e9d..1c7704e83 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -152,6 +152,8 @@ EOT if ($input->isInteractive() && $this->hasDependencies($options) && $io->askConfirmation($question, true)) { $this->installDependencies($output); } + + return 0; } /** @@ -400,7 +402,7 @@ EOT return $this->repos; } - protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), $phpVersion = null, $preferredStability = 'stable', $checkProvidedVersions = true) + final protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), $phpVersion = null, $preferredStability = 'stable', $checkProvidedVersions = true, $fixed = false) { if ($requires) { $requires = $this->normalizeRequirements($requires); @@ -410,7 +412,7 @@ EOT foreach ($requires as $requirement) { if (!isset($requirement['version'])) { // determine the best version automatically - list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability); + list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, null, null, $fixed); $requirement['version'] = $version; // replace package name from packagist.org @@ -423,7 +425,7 @@ EOT )); } else { // check that the specified version/constraint exists before we proceed - list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, $checkProvidedVersions ? $requirement['version'] : null, 'dev'); + list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, $checkProvidedVersions ? $requirement['version'] : null, 'dev', $fixed); // replace package name from packagist.org $requirement['name'] = $name; @@ -700,10 +702,11 @@ EOT * @param string $preferredStability * @param string|null $requiredVersion * @param string $minimumStability + * @param bool $fixed * @throws \InvalidArgumentException * @return array name version */ - private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null) + private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null) { // find the latest version allowed in this repo set $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability)); @@ -777,7 +780,7 @@ EOT return array( $package->getPrettyName(), - $versionSelector->findRecommendedRequireVersion($package), + $fixed ? $package->getPrettyVersion() : $versionSelector->findRecommendedRequireVersion($package), ); } diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index b3c30d63b..7537945e9 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -110,6 +110,8 @@ EOT default: throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format)); } + + return 0; } /** diff --git a/src/Composer/Command/OutdatedCommand.php b/src/Composer/Command/OutdatedCommand.php index ae26a7487..599087246 100644 --- a/src/Composer/Command/OutdatedCommand.php +++ b/src/Composer/Command/OutdatedCommand.php @@ -59,7 +59,7 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $args = array( - 'show', + 'command' => 'show', '--latest' => true, ); if (!$input->getOption('all')) { diff --git a/src/Composer/Command/ProhibitsCommand.php b/src/Composer/Command/ProhibitsCommand.php index 9e5575c74..1e18e5e23 100644 --- a/src/Composer/Command/ProhibitsCommand.php +++ b/src/Composer/Command/ProhibitsCommand.php @@ -48,7 +48,7 @@ EOT * * @param InputInterface $input * @param OutputInterface $output - * @return int|null + * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 876371635..af16a3898 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -49,6 +49,7 @@ class RequireCommand extends InitCommand new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), + new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), @@ -99,7 +100,9 @@ EOT return 1; } - if (!is_readable($this->file)) { + // check for readability by reading the file as is_readable can not be trusted on network-mounts + // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 + if (!is_readable($this->file) && false === Silencer::call('file_get_contents', $this->file)) { $io->writeError(''.$this->file.' is not readable.'); return 1; @@ -120,6 +123,25 @@ EOT return 1; } + if ($input->getOption('fixed') === true) { + $config = $this->json->read(); + + $packageType = empty($config['type']) ? 'library' : $config['type']; + + /** + * @see https://github.com/composer/composer/pull/8313#issuecomment-532637955 + */ + if ($packageType !== 'project') { + $io->writeError('"--fixed" option is allowed for "project" package types only to prevent possible misuses.'); + + if (empty($config['type'])) { + $io->writeError('If your package is not library, you should explicitly specify "type" parameter in composer.json.'); + } + + return 1; + } + } + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $repos = $composer->getRepositoryManager()->getRepositories(); @@ -137,7 +159,15 @@ EOT } $phpVersion = $this->repos->findPackage('php', '*')->getPrettyVersion(); - $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'), $phpVersion, $preferredStability, !$input->getOption('no-update')); + try { + $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'), $phpVersion, $preferredStability, !$input->getOption('no-update'), $input->getOption('fixed')); + } catch (\Exception $e) { + if ($this->newlyCreated) { + throw new \RuntimeException('No composer.json present in the current directory, this may be the cause of the following exception.', 0, $e); + } + + throw $e; + } $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index 54aa4dcea..0e8aa60e4 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -79,5 +79,7 @@ EOT foreach ($results as $result) { $io->write($result['name'] . (isset($result['description']) ? ' '. $result['description'] : '')); } + + return 0; } } diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 0e85f0ce3..9beb0b8c4 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -254,6 +254,8 @@ TAGSPUBKEY } else { $io->writeError('A backup of the current version could not be written to '.$backupFile.', no rollback possible'); } + + return 0; } protected function fetchKeys(IOInterface $io, Config $config) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index dde0f0062..ffc0776dd 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -129,6 +129,12 @@ EOT return 1; } + if ($input->getOption('tree') && $input->getOption('path')) { + $io->writeError('The --tree (-t) option is not usable in combination with --path (-P)'); + + return 1; + } + $format = $input->getOption('format'); if (!in_array($format, array('text', 'json'))) { $io->writeError(sprintf('Unsupported format "%s". See help for supported formats.', $format)); @@ -586,6 +592,7 @@ EOT } $io->write('type : ' . $package->getType()); $this->printLicenses($package); + $io->write('homepage : ' . $package->getHomepage()); $io->write('source : ' . sprintf('[%s] %s %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference())); $io->write('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); if ($installedRepo->hasPackage($package)) { diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index 06fb2ab05..b9708c3db 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -61,7 +61,7 @@ EOT /** * @param InputInterface $input * @param OutputInterface $output - * @return int|null + * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 411feb202..6c2619751 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -93,7 +93,7 @@ EOT continue; } foreach ($package['suggest'] as $suggestion => $reason) { - if (false === strpos('/', $suggestion) && null !== $platform->findPackage($suggestion, '*')) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $suggestion) && null !== $platform->findPackage($suggestion, '*')) { continue; } if (!isset($installed[$suggestion])) { @@ -121,7 +121,7 @@ EOT $io->write(sprintf('%s', $suggestion)); } - return null; + return 0; } // Grouped by package @@ -151,5 +151,7 @@ EOT $io->write(''); } } + + return 0; } } diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 7abca7dfa..1050096b1 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -63,6 +63,7 @@ class Config 'archive-dir' => '.', 'htaccess-protect' => true, 'use-github-api' => true, + 'lock' => true, // valid keys without defaults (auth config stuff): // bitbucket-oauth // github-oauth @@ -329,6 +330,8 @@ class Config return $this->config[$key] !== 'false' && (bool) $this->config[$key]; case 'use-github-api': return $this->config[$key] !== 'false' && (bool) $this->config[$key]; + case 'lock': + return $this->config[$key] !== 'false' && (bool) $this->config[$key]; default: if (!isset($this->config[$key])) { return null; diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index a9fb2e117..c7ec2dad6 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -113,6 +113,10 @@ class Application extends BaseApplication { $this->disablePluginsByDefault = $input->hasParameterOption('--no-plugins'); + if (getenv('COMPOSER_NO_INTERACTION')) { + $input->setInteractive(false); + } + $io = $this->io = new ConsoleIO($input, $output, new HelperSet(array( new QuestionHelper(), ))); @@ -208,11 +212,7 @@ class Application extends BaseApplication $io->writeError(sprintf('Warning: This development build of composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF'])); } - if (getenv('COMPOSER_NO_INTERACTION')) { - $input->setInteractive(false); - } - - if (!Platform::isWindows() && function_exists('exec') && !getenv('COMPOSER_ALLOW_SUPERUSER')) { + if (!Platform::isWindows() && function_exists('exec') && !getenv('COMPOSER_ALLOW_SUPERUSER') && !file_exists('/.dockerenv')) { if (function_exists('posix_getuid') && posix_getuid() === 0) { if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { $io->writeError('Do not run Composer as root/super user! See https://getcomposer.org/root for details'); diff --git a/src/Composer/DependencyResolver/Decisions.php b/src/Composer/DependencyResolver/Decisions.php index 86b62c3d3..e2773501f 100644 --- a/src/Composer/DependencyResolver/Decisions.php +++ b/src/Composer/DependencyResolver/Decisions.php @@ -183,7 +183,7 @@ class Decisions implements \Iterator, \Countable $previousDecision = isset($this->decisionMap[$packageId]) ? $this->decisionMap[$packageId] : null; if ($previousDecision != 0) { - $literalString = $this->pool->literalToString($literal); + $literalString = $this->pool->literalToPrettyString($literal, array()); $package = $this->pool->literalToPackage($literal); throw new SolverBugException( "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."." diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 3d3d947f4..ba026d2e2 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -74,10 +74,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $command = 'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% ' . '&& cd '.$flag.'%path% ' - . '&& git remote set-url origin %url% && git remote add composer %url%'; + . '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%'; } else { $msg = "Cloning ".$this->getShortHash($ref); - $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer'; + $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer && git remote set-url origin %sanitizedUrl% && git remote set-url composer %sanitizedUrl%'; if (getenv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); } @@ -87,11 +87,12 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $commandCallable = function ($url) use ($path, $command, $cachePath) { return str_replace( - array('%url%', '%path%', '%cachePath%'), + array('%url%', '%path%', '%cachePath%', '%sanitizedUrl%'), array( ProcessExecutor::escape($url), ProcessExecutor::escape($path), ProcessExecutor::escape($cachePath), + ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url)), ), $command ); @@ -129,10 +130,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface if (!empty($this->cachedPackages[$target->getId()][$ref])) { $msg = "Checking out ".$this->getShortHash($ref).' from cache'; - $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %url%'; + $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%'; } else { $msg = "Checking out ".$this->getShortHash($ref); - $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer)'; + $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%'; if (getenv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); } @@ -142,11 +143,12 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $commandCallable = function ($url) use ($ref, $command, $cachePath) { return str_replace( - array('%url%', '%ref%', '%cachePath%'), + array('%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'), array( ProcessExecutor::escape($url), ProcessExecutor::escape($ref.'^{commit}'), ProcessExecutor::escape($cachePath), + ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url)), ), $command ); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9fa0b1573..8b5c85057 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -130,7 +130,7 @@ class Installer protected $preferStable = false; protected $preferLowest = false; protected $skipSuggest = false; - protected $writeLock = true; + protected $writeLock; protected $executeOperations = true; /** @@ -177,6 +177,8 @@ class Installer $this->installationManager = $installationManager; $this->eventDispatcher = $eventDispatcher; $this->autoloadGenerator = $autoloadGenerator; + + $this->writeLock = $config->get('lock'); } /** diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index fb10fb678..ddf8b6e73 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -98,6 +98,27 @@ abstract class BaseRepository implements RepositoryInterface // Replacements are considered valid reasons for a package to be installed during forward resolution if (!$invert) { $links += $package->getReplaces(); + + // On forward search, check if any replaced package was required and add the replaced + // packages to the list of needles. Contrary to the cross-reference link check below, + // replaced packages are the target of links. + foreach ($package->getReplaces() as $link) { + foreach ($needles as $needle) { + if ($link->getSource() === $needle) { + if ($constraint === null || ($link->getConstraint()->matches($constraint) === !$invert)) { + // already displayed this node's dependencies, cutting short + if (in_array($link->getTarget(), $packagesInTree)) { + $results[] = array($package, $link, false); + continue; + } + $packagesInTree[] = $link->getTarget(); + $dependents = $recurse ? $this->getDependents($link->getTarget(), null, false, true, $packagesInTree) : array(); + $results[] = array($package, $link, $dependents); + $needles[] = $link->getTarget(); + } + } + } + } } // Require-dev is only relevant for the root package @@ -112,12 +133,12 @@ abstract class BaseRepository implements RepositoryInterface if ($constraint === null || ($link->getConstraint()->matches($constraint) === !$invert)) { // already displayed this node's dependencies, cutting short if (in_array($link->getSource(), $packagesInTree)) { - $results[$link->getSource()] = array($package, $link, false); + $results[] = array($package, $link, false); continue; } $packagesInTree[] = $link->getSource(); $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true, $packagesInTree) : array(); - $results[$link->getSource()] = array($package, $link, $dependents); + $results[] = array($package, $link, $dependents); } } } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 61ebc8d8c..2f26af2f7 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -125,7 +125,13 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn { parent::initialize(); - foreach ($this->getUrlMatches() as $url) { + $urlMatches = $this->getUrlMatches(); + + if (empty($urlMatches)) { + throw new \RuntimeException('The `url` supplied for the path (' . $this->url . ') repository does not exist'); + } + + foreach ($urlMatches as $url) { $path = realpath($url) . DIRECTORY_SEPARATOR; $composerFilePath = $path.'composer.json'; @@ -155,7 +161,11 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn if (!isset($package['version'])) { $versionData = $this->versionGuesser->guessVersion($package, $path); - $package['version'] = $versionData['pretty_version'] ?: 'dev-master'; + if (is_array($versionData) && $versionData['pretty_version']) { + $package['version'] = $versionData['pretty_version']; + } else { + $package['version'] = 'dev-master'; + } } $output = ''; diff --git a/src/Composer/Repository/Vcs/BitbucketDriver.php b/src/Composer/Repository/Vcs/BitbucketDriver.php index 5b0f26639..22180b55d 100644 --- a/src/Composer/Repository/Vcs/BitbucketDriver.php +++ b/src/Composer/Repository/Vcs/BitbucketDriver.php @@ -47,7 +47,7 @@ abstract class BitbucketDriver extends VcsDriver */ public function initialize() { - preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+?)(\.git|/?)$#', $this->url, $match); + preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+?)(\.git|/?)$#i', $this->url, $match); $this->owner = $match[1]; $this->repository = $match[2]; $this->originUrl = 'bitbucket.org'; diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 5770a8326..08dbe9233 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -53,7 +53,7 @@ class GitBitbucketDriver extends BitbucketDriver */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^https?://bitbucket\.org/([^/]+)/(.+?)\.git$#', $url)) { + if (!preg_match('#^https?://bitbucket\.org/([^/]+)/(.+?)\.git$#i', $url)) { return false; } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 55940e212..c9ed229b8 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -48,10 +48,10 @@ class GitHubDriver extends VcsDriver */ public function initialize() { - preg_match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $this->url, $match); + preg_match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/(.+?)(?:\.git|/)?$#', $this->url, $match); $this->owner = $match[3]; $this->repository = $match[4]; - $this->originUrl = !empty($match[1]) ? $match[1] : $match[2]; + $this->originUrl = strtolower(!empty($match[1]) ? $match[1] : $match[2]); if ($this->originUrl === 'www.github.com') { $this->originUrl = 'github.com'; } @@ -270,12 +270,12 @@ class GitHubDriver extends VcsDriver */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $url, $matches)) { + if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/(.+?)(?:\.git|/)?$#', $url, $matches)) { return false; } $originUrl = !empty($matches[2]) ? $matches[2] : $matches[3]; - if (!in_array(preg_replace('{^www\.}i', '', $originUrl), $config->get('github-domains'))) { + if (!in_array(strtolower(preg_replace('{^www\.}i', '', $originUrl)), $config->get('github-domains'))) { return false; } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 2037a8c63..a1f810bfa 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -497,6 +497,8 @@ class GitLabDriver extends VcsDriver */ private static function determineOrigin(array $configuredDomains, $guessedDomain, array &$urlParts, $portNumber) { + $guessedDomain = strtolower($guessedDomain); + if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array($guessedDomain.':'.$portNumber, $configuredDomains))) { if ($portNumber) { return $guessedDomain.':'.$portNumber; diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index a919e7860..2d0596467 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -53,7 +53,7 @@ class HgBitbucketDriver extends BitbucketDriver */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url)) { + if (!preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+)/?$#i', $url)) { return false; } diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index 3812e7a66..04a363442 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -71,7 +71,7 @@ class HgDriver extends VcsDriver return sprintf('hg clone --noupdate %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($repoDir)); }; - $hgUtils->runCommand($command, $this->url, $this->repoDir); + $hgUtils->runCommand($command, $this->url, null); } } diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 7085f2561..7b8d33d68 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -23,6 +23,7 @@ class AuthHelper { protected $io; protected $config; + private $displayedOriginAuthentications = array(); public function __construct(IOInterface $io, Config $config) { @@ -116,7 +117,7 @@ class AuthHelper $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit'); $gitLabUtil = new GitLab($this->io, $this->config, null); - if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && $auth['password'] === 'private-token') { + if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && in_array($auth['password'], array('gitlab-ci-token', 'private-token'), true)) { throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } @@ -172,7 +173,7 @@ class AuthHelper throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } - $this->io->writeError(' Authentication required ('.parse_url($url, PHP_URL_HOST).'):'); + $this->io->writeError(' Authentication required ('.$origin.'):'); $username = $this->io->ask(' Username: '); $password = $this->io->askAndHideAnswer(' Password: '); $this->io->setAuthentication($origin, $username, $password); @@ -193,14 +194,18 @@ class AuthHelper public function addAuthenticationHeader(array $headers, $origin, $url) { if ($this->io->hasAuthentication($origin)) { + $authenticationDisplayMessage = null; $auth = $this->io->getAuthentication($origin); if ('github.com' === $origin && 'x-oauth-basic' === $auth['password']) { $headers[] = 'Authorization: token '.$auth['username']; + $authenticationDisplayMessage = 'Using GitHub token authentication'; } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { if ($auth['password'] === 'oauth2') { $headers[] = 'Authorization: Bearer '.$auth['username']; - } elseif ($auth['password'] === 'private-token') { + $authenticationDisplayMessage = 'Using GitLab OAuth token authentication'; + } elseif ($auth['password'] === 'private-token' || $auth['password'] === 'gitlab-ci-token') { $headers[] = 'PRIVATE-TOKEN: '.$auth['username']; + $authenticationDisplayMessage = 'Using GitLab private token authentication'; } } elseif ( 'bitbucket.org' === $origin @@ -209,10 +214,17 @@ class AuthHelper ) { if (!$this->isPublicBitBucketDownload($url)) { $headers[] = 'Authorization: Bearer ' . $auth['password']; + $authenticationDisplayMessage = 'Using Bitbucket OAuth token authentication'; } } else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; + $authenticationDisplayMessage = 'Using HTTP basic authentication with username "' . $auth['username'] . '"'; + } + + if ($authenticationDisplayMessage && !in_array($origin, $this->displayedOriginAuthentications, true)) { + $this->io->writeError($authenticationDisplayMessage, true, IOInterface::DEBUG); + $this->displayedOriginAuthentications[] = $origin; } } @@ -243,4 +255,15 @@ class AuthHelper return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; } + + /** + * @param string $url + * @return string + */ + public function stripCredentialsFromUrl($url) + { + // GitHub repository rename result in redirect locations containing the access_token as GET parameter + // e.g. https://api.github.com/repositories/9999999999?access_token=github_token + return preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); + } } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 6118df938..b1a106709 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -687,12 +687,14 @@ class Filesystem if (!Platform::isWindows()) { return false; } + + // Important to clear all caches first + clearstatcache(true, $junction); + if (!is_dir($junction) || is_link($junction)) { return false; } - // Important to clear all caches first - clearstatcache(true, $junction); $stat = lstat($junction); // S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask) diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 48e91ba84..39a66f898 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -54,9 +54,9 @@ class Git } if (!$initialClone) { - // capture username/password from URL if there is one + // capture username/password from URL if there is one and we have no auth configured yet $this->process->execute('git remote -v', $output, $cwd); - if (preg_match('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match)) { + if (preg_match('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match) && !$this->io->hasAuthentication($match[3])) { $this->io->setAuthentication($match[3], rawurldecode($match[1]), rawurldecode($match[2])); } } @@ -95,8 +95,10 @@ class Git $auth = null; if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { - // private github repository without git access, try https with auth - if (preg_match('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match)) { + // 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) + ) { if (!$this->io->hasAuthentication($match[1])) { $gitHubUtil = new GitHub($this->io, $this->config, $this->process); $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos'; @@ -153,7 +155,14 @@ class Git return; } } - } elseif (preg_match('{^(https?)://' . self::getGitLabDomainsRegex($this->config) . '/(.*)}', $url, $match)) { + } elseif ( + preg_match('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match) + || preg_match('{^(https?)://' . self::getGitLabDomainsRegex($this->config) . '/(.*)}', $url, $match) + ) { + if ($match[1] === 'git') { + $match[1] = 'https'; + } + if (!$this->io->hasAuthentication($match[2])) { $gitLabUtil = new GitLab($this->io, $this->config, $this->process); $message = 'Cloning failed, enter your GitLab credentials to access private repos'; @@ -165,17 +174,18 @@ class Git if ($this->io->hasAuthentication($match[2])) { $auth = $this->io->getAuthentication($match[2]); - if($auth['password'] === 'private-token' || $auth['password'] === 'oauth2') { + if ($auth['password'] === 'private-token' || $auth['password'] === 'oauth2' || $auth['password'] === 'gitlab-ci-token') { $authUrl = $match[1] . '://' . rawurlencode($auth['password']) . ':' . rawurlencode($auth['username']) . '@' . $match[2] . '/' . $match[3]; // swap username and password } else { $authUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[2] . '/' . $match[3]; } + $command = call_user_func($commandCallable, $authUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } } - } elseif ($this->isAuthenticationFailure($url, $match)) { // private non-github repo that failed to authenticate + } elseif ($this->isAuthenticationFailure($url, $match)) { // private non-github/gitlab/bitbucket repo that failed to authenticate if (strpos($match[2], '@')) { list($authParts, $match[2]) = explode('@', $match[2], 2); } @@ -193,7 +203,7 @@ class Git } } - $this->io->writeError(' Authentication required (' . parse_url($url, PHP_URL_HOST) . '):'); + $this->io->writeError(' Authentication required (' . $match[2] . '):'); $auth = array( 'username' => $this->io->ask(' Username: ', $defaultUsername), 'password' => $this->io->askAndHideAnswer(' Password: '), @@ -215,10 +225,12 @@ class Git } } + $errorMsg = $this->process->getErrorOutput(); if ($initialClone) { $this->filesystem->removeDirectory($origCwd); } - $this->throwException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(), $url); + + $this->throwException('Failed to execute ' . $command . "\n\n" . $errorMsg, $url); } } @@ -232,7 +244,9 @@ class Git if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { try { $commandCallable = function ($url) { - return sprintf('git remote set-url origin %s && git remote update --prune origin', ProcessExecutor::escape($url)); + $sanitizedUrl = preg_replace('{://([^@]+?):(.+?)@}', '://', $url); + + return sprintf('git remote set-url origin %s && git remote update --prune origin && git remote set-url origin %s', ProcessExecutor::escape($url), ProcessExecutor::escape($sanitizedUrl)); }; $this->runCommand($commandCallable, $url, $dir); } catch (\Exception $e) { @@ -255,16 +269,29 @@ class Git } public function fetchRefOrSyncMirror($url, $dir, $ref) + { + if ($this->checkRefIsInMirror($url, $dir, $ref)) { + return true; + } + + if ($this->syncMirror($url, $dir)) { + return $this->checkRefIsInMirror($url, $dir, $ref); + } + + return false; + } + + private function checkRefIsInMirror($url, $dir, $ref) { if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { $escapedRef = ProcessExecutor::escape($ref.'^{commit}'); - $exitCode = $this->process->execute(sprintf('git rev-parse --quiet --verify %s', $escapedRef), $output, $dir); + $exitCode = $this->process->execute(sprintf('git rev-parse --quiet --verify %s', $escapedRef), $ignoredOutput, $dir); if ($exitCode === 0) { return true; } } - return $this->syncMirror($url, $dir); + return false; } private function isAuthenticationFailure($url, &$match) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index a163290fe..99ee1ca3c 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -195,7 +195,7 @@ class CurlDownloader $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0) { - $this->io->writeError('Downloading ' . $url . $usingProxy . $ifModified, true, IOInterface::DEBUG); + $this->io->writeError('Downloading ' . $this->authHelper->stripCredentialsFromUrl($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); } $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); @@ -254,12 +254,12 @@ class CurlDownloader $contents = stream_get_contents($job['bodyHandle']); } $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); + $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG); } else { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); + $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG); } fclose($job['bodyHandle']); @@ -362,7 +362,7 @@ class CurlDownloader } if (!empty($targetUrl)) { - $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $targetUrl), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); return $targetUrl; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 76a2176a8..cf39aaf9d 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -246,7 +246,7 @@ class RemoteFilesystem $actualContextOptions = stream_context_get_options($ctx); $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : ''; - $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $origFileUrl . $usingProxy, true, IOInterface::DEBUG); + $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $this->authHelper->stripCredentialsFromUrl($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $actualContextOptions); // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 @@ -704,7 +704,7 @@ class RemoteFilesystem $this->redirects++; $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); $additionalOptions['redirects'] = $this->redirects; diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php index 8c79d106c..ab10d5bbf 100644 --- a/src/Composer/Util/Zip.php +++ b/src/Composer/Util/Zip.php @@ -21,7 +21,6 @@ class Zip * Gets content of the root composer.json inside a ZIP archive. * * @param string $pathToZip - * @param string $filename * * @return string|null */ diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php index 39d462544..94128e737 100644 --- a/tests/Composer/Test/ApplicationTest.php +++ b/tests/Composer/Test/ApplicationTest.php @@ -25,12 +25,18 @@ class ApplicationTest extends TestCase $inputMock = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); $outputMock = $this->getMockBuilder('Symfony\Component\Console\Output\OutputInterface')->getMock(); + putenv('COMPOSER_NO_INTERACTION=1'); + $index = 0; $inputMock->expects($this->at($index++)) ->method('hasParameterOption') ->with($this->equalTo('--no-plugins')) ->will($this->returnValue(true)); + $inputMock->expects($this->at($index++)) + ->method('setInteractive') + ->with($this->equalTo(false)); + $inputMock->expects($this->at($index++)) ->method('hasParameterOption') ->with($this->equalTo('--no-cache')) @@ -83,12 +89,18 @@ class ApplicationTest extends TestCase $inputMock = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); $outputMock = $this->getMockBuilder('Symfony\Component\Console\Output\OutputInterface')->getMock(); + putenv('COMPOSER_NO_INTERACTION=1'); + $index = 0; $inputMock->expects($this->at($index++)) ->method('hasParameterOption') ->with($this->equalTo('--no-plugins')) ->will($this->returnValue(true)); + $inputMock->expects($this->at($index++)) + ->method('setInteractive') + ->with($this->equalTo(false)); + $inputMock->expects($this->at($index++)) ->method('hasParameterOption') ->with($this->equalTo('--no-cache')) diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 84ac16df7..ba89ccea9 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -548,6 +548,48 @@ class AutoloadGeneratorTest extends TestCase ); } + public function testPSRToClassMapIgnoresNonPSRClasses() + { + $package = new Package('a', '1.0', '1.0'); + + $this->markTestSkipped('Skipped until ClassMapGenerator ignoring of invalid PSR-x classes is enabled'); + + $package->setAutoload(array( + 'psr-0' => array('psr0_' => 'psr0/'), + 'psr-4' => array('psr4\\' => 'psr4/'), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->workingDir.'/psr0/psr0'); + $this->fs->ensureDirectoryExists($this->workingDir.'/psr4'); + file_put_contents($this->workingDir.'/psr0/psr0/match.php', 'workingDir.'/psr0/psr0/badfile.php', 'workingDir.'/psr4/match.php', 'workingDir.'/psr4/badfile.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); + $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + + $expectedClassmap = << \$baseDir . '/psr0/psr0/match.php', + 'psr4\\\\match' => \$baseDir . '/psr4/match.php', +); + +EOF; + $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); + } + public function testVendorsClassMapAutoloading() { $package = new Package('a', '1.0', '1.0'); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php index e830e21f8..4e5e0a5cb 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php @@ -13,6 +13,9 @@ class ComposerAutoloaderInitFilesAutoloadOrder } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php index ede883fd7..0f1201b8e 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php @@ -13,6 +13,9 @@ class ComposerAutoloaderInitFilesAutoload } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php index 6459cac05..fc457c406 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php @@ -13,6 +13,9 @@ class ComposerAutoloaderInitFilesAutoload } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php index 75306684a..b07825176 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php @@ -13,6 +13,9 @@ class ComposerAutoloaderInitFilesAutoload } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php index 32f822812..12ac24108 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php @@ -13,6 +13,9 @@ class ComposerAutoloaderInitIncludePath } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php index 1d6676c83..084d04f30 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php @@ -13,6 +13,9 @@ class ComposerAutoloaderInitTargetDir } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { diff --git a/tests/Composer/Test/Autoload/Fixtures/classmap/LargeGap.php b/tests/Composer/Test/Autoload/Fixtures/classmap/LargeGap.php index 1ad22ecf8..7fe3bee86 100644 --- a/tests/Composer/Test/Autoload/Fixtures/classmap/LargeGap.php +++ b/tests/Composer/Test/Autoload/Fixtures/classmap/LargeGap.php @@ -1385,6 +1385,10 @@ namespace Foo; + public function a1381() { var_dump(var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null), var_dump(null)); } + winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer && git remote set-url origin 'https://example.com/composer/composer' && git remote set-url composer 'https://example.com/composer/composer'"); $processExecutor->expects($this->at(1)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -170,6 +170,9 @@ class GitDownloaderTest extends TestCase $this->setupConfig($config); $cachePath = $config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', 'https://example.com/composer/composer').'/'; + $filesystem = new \Composer\Util\Filesystem; + $filesystem->removeDirectory($cachePath); + $expectedGitCommand = $this->winCompat(sprintf("git clone --mirror 'https://example.com/composer/composer' '%s'", $cachePath)); $processExecutor->expects($this->at(1)) ->method('execute') @@ -179,24 +182,36 @@ class GitDownloaderTest extends TestCase return 0; })); + $processExecutor->expects($this->at(2)) + ->method('execute') + ->with($this->equalTo('git rev-parse --git-dir'), $this->anything(), $this->equalTo($this->winCompat($cachePath))) + ->will($this->returnCallback(function ($command, &$output = null) { + $output = '.'; + + return 0; + })); + $processExecutor->expects($this->at(3)) + ->method('execute') + ->with($this->equalTo($this->winCompat('git rev-parse --quiet --verify \'1234567890123456789012345678901234567890^{commit}\'')), $this->equalTo(null), $this->equalTo($this->winCompat($cachePath))) + ->will($this->returnValue(0)); $expectedGitCommand = $this->winCompat(sprintf("git clone --no-checkout '%1\$s' 'composerPath' --dissociate --reference '%1\$s' && cd 'composerPath' && git remote set-url origin 'https://example.com/composer/composer' && git remote add composer 'https://example.com/composer/composer'", $cachePath)); - $processExecutor->expects($this->at(2)) + $processExecutor->expects($this->at(4)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) + $processExecutor->expects($this->at(5)) ->method('execute') ->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) + $processExecutor->expects($this->at(6)) ->method('execute') ->with($this->equalTo($this->winCompat("git checkout 'master' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(5)) + $processExecutor->expects($this->at(7)) ->method('execute') ->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890' --")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); @@ -235,7 +250,7 @@ class GitDownloaderTest extends TestCase return 0; })); - $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer && git remote set-url origin 'https://github.com/mirrors/composer' && git remote set-url composer 'https://github.com/mirrors/composer'"); $processExecutor->expects($this->at(1)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -246,7 +261,7 @@ class GitDownloaderTest extends TestCase ->with() ->will($this->returnValue('Error1')); - $expectedGitCommand = $this->winCompat("git clone --no-checkout 'git@github.com:mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git@github.com:mirrors/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'git@github.com:mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git@github.com:mirrors/composer' && git fetch composer && git remote set-url origin 'git@github.com:mirrors/composer' && git remote set-url composer 'git@github.com:mirrors/composer'"); $processExecutor->expects($this->at(3)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -322,7 +337,7 @@ class GitDownloaderTest extends TestCase return 0; })); - $expectedGitCommand = $this->winCompat("git clone --no-checkout '{$url}' 'composerPath' && cd 'composerPath' && git remote add composer '{$url}' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout '{$url}' 'composerPath' && cd 'composerPath' && git remote add composer '{$url}' && git fetch composer && git remote set-url origin '{$url}' && git remote set-url composer '{$url}'"); $processExecutor->expects($this->at(1)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -350,7 +365,7 @@ class GitDownloaderTest extends TestCase public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer && git remote set-url origin 'https://example.com/composer/composer' && git remote set-url composer 'https://example.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -408,7 +423,7 @@ class GitDownloaderTest extends TestCase public function testUpdate() { - $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -440,7 +455,7 @@ class GitDownloaderTest extends TestCase public function testUpdateWithNewRepoUrl() { - $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -519,8 +534,8 @@ composer https://github.com/old/url (push) */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); - $expectedGitUpdateCommand2 = $this->winCompat("git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); + $expectedGitUpdateCommand2 = $this->winCompat("git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'git@github.com:composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -563,8 +578,8 @@ composer https://github.com/old/url (push) public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover() { - $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); - $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); + $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '/'"); + $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) diff --git a/tests/Composer/Test/Fixtures/installer/install-without-lock.test b/tests/Composer/Test/Fixtures/installer/install-without-lock.test new file mode 100644 index 000000000..c5d73dcbb --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-without-lock.test @@ -0,0 +1,25 @@ +--TEST-- +Installs from composer.json without writing a lock file +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" } + ] + } + ], + "require": { + "a/a": "1.0.0" + }, + "config": { + "lock": "false" + } +} +--RUN-- +install +--EXPECT-- +Installing a/a (1.0.0) +--EXPECT-LOCK-- +false diff --git a/tests/Composer/Test/Fixtures/installer/update-without-lock.test b/tests/Composer/Test/Fixtures/installer/update-without-lock.test new file mode 100644 index 000000000..0fd1562c3 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-without-lock.test @@ -0,0 +1,25 @@ +--TEST-- +Updates when no lock file is present without writing a lock file +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" } + ] + } + ], + "require": { + "a/a": "1.0.0" + }, + "config": { + "lock": false + } +} +--RUN-- +update +--EXPECT-- +Installing a/a (1.0.0) +--EXPECT-LOCK-- +false diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 758121979..bb0643cf1 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -243,6 +243,9 @@ class InstallerTest extends TestCase // so store value temporarily in reference for later assetion $actualLock = $hash; })); + } elseif ($expectLock === false) { + $lockJsonMock->expects($this->never()) + ->method('write'); } $contents = json_encode($composerConfig); @@ -334,15 +337,15 @@ class InstallerTest extends TestCase continue; } - $testData = $this->readTestFile($file, $fixturesDir); - - $installed = array(); - $installedDev = array(); - $lock = array(); - $expectLock = array(); - $expectResult = 0; - try { + $testData = $this->readTestFile($file, $fixturesDir); + + $installed = array(); + $installedDev = array(); + $lock = array(); + $expectLock = array(); + $expectResult = 0; + $message = $testData['TEST']; $condition = !empty($testData['CONDITION']) ? $testData['CONDITION'] : null; $composer = JsonFile::parseJson($testData['COMPOSER']); @@ -373,7 +376,11 @@ class InstallerTest extends TestCase } $run = $testData['RUN']; if (!empty($testData['EXPECT-LOCK'])) { - $expectLock = JsonFile::parseJson($testData['EXPECT-LOCK']); + if ($testData['EXPECT-LOCK'] === 'false') { + $expectLock = false; + } else { + $expectLock = JsonFile::parseJson($testData['EXPECT-LOCK']); + } } $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; $expect = $testData['EXPECT']; diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index abe6063f4..4f7b41809 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -19,6 +19,22 @@ use Composer\Package\Version\VersionParser; class PathRepositoryTest extends TestCase { + + /** + * @expectedException RuntimeException + */ + public function testLoadPackageFromFileSystemWithIncorrectPath() + { + $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') + ->getMock(); + + $config = new \Composer\Config(); + + $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', 'missing')); + $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); + $repository->getPackages(); + } + public function testLoadPackageFromFileSystemWithVersion() { $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index 9f684dbfb..f3fb30278 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -300,16 +300,16 @@ class FilesystemTest extends TestCase // Create and detect junction $fs->junction($target, $junction); - $this->assertTrue($fs->isJunction($junction)); - $this->assertFalse($fs->isJunction($target)); - $this->assertTrue($fs->isJunction($target . '/../../junction')); - $this->assertFalse($fs->isJunction($junction . '/../real')); - $this->assertTrue($fs->isJunction($junction . '/../junction')); + $this->assertTrue($fs->isJunction($junction), $junction . ': is a junction'); + $this->assertFalse($fs->isJunction($target), $target . ': is not a junction'); + $this->assertTrue($fs->isJunction($target . '/../../junction'), $target . '/../../junction: is a junction'); + $this->assertFalse($fs->isJunction($junction . '/../real'), $junction . '/../real: is not a junction'); + $this->assertTrue($fs->isJunction($junction . '/../junction'), $junction . '/../junction: is a junction'); // Remove junction - $this->assertTrue(is_dir($junction)); - $this->assertTrue($fs->removeJunction($junction)); - $this->assertFalse(is_dir($junction)); + $this->assertTrue(is_dir($junction), $junction . ' is a directory'); + $this->assertTrue($fs->removeJunction($junction), $junction . ' has been removed'); + $this->assertFalse(is_dir($junction), $junction . ' is not a directory'); } public function testCopy()