1
0
Fork 0

Stop relying on OS to find executables on Windows, and migrate most Process calls to array syntax (#12180)

Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>
pull/12186/head
Nicolas Grekas 2024-11-06 13:49:06 +01:00 committed by GitHub
parent 5a75d32414
commit 3dc279cf66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 714 additions and 584 deletions

48
composer.lock generated
View File

@ -8,16 +8,16 @@
"packages": [ "packages": [
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
"version": "1.5.2", "version": "1.5.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/ca-bundle.git", "url": "https://github.com/composer/ca-bundle.git",
"reference": "48a792895a2b7a6ee65dd5442c299d7b835b6137" "reference": "3b1fc3f0be055baa7c6258b1467849c3e8204eb2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/48a792895a2b7a6ee65dd5442c299d7b835b6137", "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3b1fc3f0be055baa7c6258b1467849c3e8204eb2",
"reference": "48a792895a2b7a6ee65dd5442c299d7b835b6137", "reference": "3b1fc3f0be055baa7c6258b1467849c3e8204eb2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -64,7 +64,7 @@
"support": { "support": {
"irc": "irc://irc.freenode.org/composer", "irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues", "issues": "https://github.com/composer/ca-bundle/issues",
"source": "https://github.com/composer/ca-bundle/tree/1.5.2" "source": "https://github.com/composer/ca-bundle/tree/1.5.3"
}, },
"funding": [ "funding": [
{ {
@ -80,7 +80,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-25T07:49:53+00:00" "time": "2024-11-04T10:15:26+00:00"
}, },
{ {
"name": "composer/class-map-generator", "name": "composer/class-map-generator",
@ -941,16 +941,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v5.4.45", "version": "v5.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "108d436c2af470858bdaba3257baab3a74172017" "reference": "fb0d4760e7147d81ab4d9e2d57d56268261b4e4e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/108d436c2af470858bdaba3257baab3a74172017", "url": "https://api.github.com/repos/symfony/console/zipball/fb0d4760e7147d81ab4d9e2d57d56268261b4e4e",
"reference": "108d436c2af470858bdaba3257baab3a74172017", "reference": "fb0d4760e7147d81ab4d9e2d57d56268261b4e4e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1020,7 +1020,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v5.4.45" "source": "https://github.com/symfony/console/tree/v5.4.46"
}, },
"funding": [ "funding": [
{ {
@ -1036,7 +1036,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-10-08T07:27:17+00:00" "time": "2024-11-05T14:17:06+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@ -1787,16 +1787,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v5.4.45", "version": "v5.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "95f3f19d0f8f06e4253c66a0828ddb69f8b8ede4" "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/95f3f19d0f8f06e4253c66a0828ddb69f8b8ede4", "url": "https://api.github.com/repos/symfony/process/zipball/01906871cb9b5e3cf872863b91aba4ec9767daf4",
"reference": "95f3f19d0f8f06e4253c66a0828ddb69f8b8ede4", "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1829,7 +1829,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v5.4.45" "source": "https://github.com/symfony/process/tree/v5.4.46"
}, },
"funding": [ "funding": [
{ {
@ -1845,7 +1845,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-25T14:11:13+00:00" "time": "2024-11-06T09:18:28+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@ -2226,16 +2226,16 @@
}, },
{ {
"name": "phpstan/phpstan-symfony", "name": "phpstan/phpstan-symfony",
"version": "1.4.10", "version": "1.4.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git", "url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1" "reference": "270c2ee1478d1f8dc5121f539e890017bd64b04c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f7d5782044bedf93aeb3f38e09c91148ee90e5a1", "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/270c2ee1478d1f8dc5121f539e890017bd64b04c",
"reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1", "reference": "270c2ee1478d1f8dc5121f539e890017bd64b04c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2292,9 +2292,9 @@
"description": "Symfony Framework extensions and rules for PHPStan", "description": "Symfony Framework extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues", "issues": "https://github.com/phpstan/phpstan-symfony/issues",
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.10" "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.11"
}, },
"time": "2024-09-26T18:14:50+00:00" "time": "2024-10-30T12:07:21+00:00"
}, },
{ {
"name": "symfony/phpunit-bridge", "name": "symfony/phpunit-bridge",

View File

@ -300,11 +300,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Command/CreateProjectCommand.php path: ../src/Composer/Command/CreateProjectCommand.php
-
message: "#^Parameter \\#3 \\$existingRepos of static method Composer\\\\Repository\\\\RepositoryFactory\\:\\:generateRepositoryName\\(\\) expects array\\<string, mixed\\>, array\\<int\\|string, mixed\\> given\\.$#"
count: 1
path: ../src/Composer/Command/CreateProjectCommand.php
- -
message: "#^Variable method call on Composer\\\\Package\\\\RootPackageInterface\\.$#" message: "#^Variable method call on Composer\\\\Package\\\\RootPackageInterface\\.$#"
count: 1 count: 1
@ -495,11 +490,6 @@ parameters:
count: 2 count: 2
path: ../src/Composer/Command/LicensesCommand.php path: ../src/Composer/Command/LicensesCommand.php
-
message: "#^Only booleans are allowed in a negated boolean, array\\<int, Composer\\\\Package\\\\PackageInterface\\> given\\.$#"
count: 1
path: ../src/Composer/Command/ReinstallCommand.php
- -
message: "#^Foreach overwrites \\$type with its key variable\\.$#" message: "#^Foreach overwrites \\$type with its key variable\\.$#"
count: 1 count: 1
@ -955,11 +945,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Config.php path: ../src/Composer/Config.php
-
message: "#^Only booleans are allowed in a ternary operator condition, string\\|null given\\.$#"
count: 1
path: ../src/Composer/Config.php
- -
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1 count: 1
@ -1160,11 +1145,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Downloader/FileDownloader.php path: ../src/Composer/Downloader/FileDownloader.php
-
message: "#^Parameter \\#3 \\$cwd of method Composer\\\\Util\\\\ProcessExecutor\\:\\:execute\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 5
path: ../src/Composer/Downloader/FossilDownloader.php
- -
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 5 count: 5
@ -1367,7 +1347,7 @@ parameters:
- -
message: "#^Only booleans are allowed in a negated boolean, array\\<int, array\\<int, string\\>\\> given\\.$#" message: "#^Only booleans are allowed in a negated boolean, array\\<int, array\\<int, string\\>\\> given\\.$#"
count: 3 count: 2
path: ../src/Composer/Downloader/ZipDownloader.php path: ../src/Composer/Downloader/ZipDownloader.php
- -
@ -2330,11 +2310,6 @@ parameters:
count: 2 count: 2
path: ../src/Composer/Question/StrictConfirmationQuestion.php path: ../src/Composer/Question/StrictConfirmationQuestion.php
-
message: "#^Method Composer\\\\Repository\\\\ArrayRepository\\:\\:getProviders\\(\\) should return array\\<string, array\\{name\\: string, description\\: string, type\\: string\\}\\> but returns array\\<string, array\\{name\\: string, description\\: string\\|null, type\\: string\\}\\>\\.$#"
count: 1
path: ../src/Composer/Repository/ArrayRepository.php
- -
message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\|null given\\.$#" message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\|null given\\.$#"
count: 1 count: 1
@ -2381,7 +2356,7 @@ parameters:
path: ../src/Composer/Repository/ComposerRepository.php path: ../src/Composer/Repository/ComposerRepository.php
- -
message: "#^Method Composer\\\\Repository\\\\ComposerRepository\\:\\:getProviders\\(\\) should return array\\<string, array\\{name\\: string, description\\: string|null, type\\: string\\}\\> but returns array\\<int\\|string, array\\{name\\: mixed, description\\: mixed, type\\: mixed\\}\\>\\.$#" message: "#^Method Composer\\\\Repository\\\\ComposerRepository\\:\\:getProviders\\(\\) should return array\\<string, array\\{name\\: string, description\\: string\\|null, type\\: string\\}\\> but returns array\\<int\\|string, array\\{name\\: mixed, description\\: mixed, type\\: mixed\\}\\>\\.$#"
count: 1 count: 1
path: ../src/Composer/Repository/ComposerRepository.php path: ../src/Composer/Repository/ComposerRepository.php
@ -2496,7 +2471,7 @@ parameters:
path: ../src/Composer/Repository/CompositeRepository.php path: ../src/Composer/Repository/CompositeRepository.php
- -
message: "#^Only booleans are allowed in a ternary operator condition, array\\<int, array\\<string, array\\<string, string|null\\>\\>\\> given\\.$#" message: "#^Only booleans are allowed in a ternary operator condition, array\\<int, array\\<string, array\\<string, string\\|null\\>\\>\\> given\\.$#"
count: 1 count: 1
path: ../src/Composer/Repository/CompositeRepository.php path: ../src/Composer/Repository/CompositeRepository.php
@ -2714,7 +2689,7 @@ parameters:
path: ../src/Composer/Repository/RepositorySet.php path: ../src/Composer/Repository/RepositorySet.php
- -
message: "#^Only booleans are allowed in an if condition, array\\<string, array\\<string, string|null\\>\\> given\\.$#" message: "#^Only booleans are allowed in an if condition, array\\<string, array\\<string, string\\|null\\>\\> given\\.$#"
count: 1 count: 1
path: ../src/Composer/Repository/RepositorySet.php path: ../src/Composer/Repository/RepositorySet.php
@ -2723,21 +2698,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Repository/RepositorySet.php path: ../src/Composer/Repository/RepositorySet.php
-
message: "#^Only booleans are allowed in a negated boolean, string given\\.$#"
count: 1
path: ../src/Composer/Repository/Vcs/FossilDriver.php
-
message: "#^Parameter \\#1 \\$file of method Composer\\\\Util\\\\Filesystem\\:\\:remove\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: ../src/Composer/Repository/Vcs/FossilDriver.php
-
message: "#^Parameter \\#1 \\$filename of function is_file expects string, string\\|null given\\.$#"
count: 1
path: ../src/Composer/Repository/Vcs/FossilDriver.php
- -
message: "#^Call to function array_search\\(\\) requires parameter \\#3 to be set\\.$#" message: "#^Call to function array_search\\(\\) requires parameter \\#3 to be set\\.$#"
count: 2 count: 2
@ -3968,11 +3928,6 @@ parameters:
count: 1 count: 1
path: ../src/Composer/Util/Svn.php path: ../src/Composer/Util/Svn.php
-
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
count: 1
path: ../src/Composer/Util/Svn.php
- -
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1 count: 1
@ -4341,6 +4296,11 @@ parameters:
count: 2 count: 2
path: ../tests/Composer/Test/Mock/ProcessExecutorMock.php path: ../tests/Composer/Test/Mock/ProcessExecutorMock.php
-
message: "#^Property Composer\\\\Test\\\\Mock\\\\ProcessExecutorMock\\:\\:\\$expectations \\(array\\<array\\{cmd\\: list\\<string\\>\\|string, return\\: int, stdout\\: string, stderr\\: string, callback\\: \\(callable\\(\\)\\: mixed\\)\\|null\\}\\>\\|null\\) does not accept array\\<non\\-empty\\-array\\<'callback'\\|'cmd'\\|'return'\\|'stderr'\\|'stdout'\\|int\\<0, max\\>, non\\-empty\\-list\\<non\\-empty\\-list\\<string\\>\\|\\(callable\\(\\)\\: mixed\\)\\|int\\|string\\>\\|\\(callable\\(\\)\\: mixed\\)\\|int\\|string\\|null\\>\\>\\.$#"
count: 1
path: ../tests/Composer/Test/Mock/ProcessExecutorMock.php
- -
message: "#^Composer\\\\Test\\\\Mock\\\\VersionGuesserMock\\:\\:__construct\\(\\) does not call parent constructor from Composer\\\\Package\\\\Version\\\\VersionGuesser\\.$#" message: "#^Composer\\\\Test\\\\Mock\\\\VersionGuesserMock\\:\\:__construct\\(\\) does not call parent constructor from Composer\\\\Package\\\\Version\\\\VersionGuesser\\.$#"
count: 1 count: 1
@ -4486,9 +4446,14 @@ parameters:
count: 1 count: 1
path: ../tests/Composer/Test/TestCase.php path: ../tests/Composer/Test/TestCase.php
-
message: "#^Cannot access an offset on array\\<int, array\\<string, array\\<int, string\\>\\|int\\|string\\>\\>\\|false\\.$#"
count: 2
path: ../tests/Composer/Test/Util/GitTest.php
- -
message: "#^Cannot access an offset on array\\<int, array\\<string, int\\|string\\>\\>\\|false\\.$#" message: "#^Cannot access an offset on array\\<int, array\\<string, int\\|string\\>\\>\\|false\\.$#"
count: 3 count: 1
path: ../tests/Composer/Test/Util/GitTest.php path: ../tests/Composer/Test/Util/GitTest.php
- -

View File

@ -302,7 +302,7 @@ EOT
return '<comment>proc_open is not available, git cannot be used</comment>'; return '<comment>proc_open is not available, git cannot be used</comment>';
} }
$this->process->execute('git config color.ui', $output); $this->process->execute(['git', 'config', 'color.ui'], $output);
if (strtolower(trim($output)) === 'always') { if (strtolower(trim($output)) === 'always') {
return '<comment>Your git color.ui setting is set to always, this is known to create issues. Use "git config --global color.ui true" to set it correctly.</comment>'; return '<comment>Your git color.ui setting is set to always, this is known to create issues. Use "git config --global color.ui true" to set it correctly.</comment>';
} }

View File

@ -122,22 +122,20 @@ EOT
*/ */
private function openBrowser(string $url): void private function openBrowser(string $url): void
{ {
$url = ProcessExecutor::escape($url);
$process = new ProcessExecutor($this->getIO()); $process = new ProcessExecutor($this->getIO());
if (Platform::isWindows()) { if (Platform::isWindows()) {
$process->execute('start "web" explorer ' . $url, $output); $process->execute(['start', '"web"', 'explorer', $url], $output);
return; return;
} }
$linux = $process->execute('which xdg-open', $output); $linux = $process->execute(['which', 'xdg-open'], $output);
$osx = $process->execute('which open', $output); $osx = $process->execute(['which', 'open'], $output);
if (0 === $linux) { if (0 === $linux) {
$process->execute('xdg-open ' . $url, $output); $process->execute(['xdg-open', $url], $output);
} elseif (0 === $osx) { } elseif (0 === $osx) {
$process->execute('open ' . $url, $output); $process->execute(['open', $url], $output);
} else { } else {
$this->getIO()->writeError('No suitable browser opening command found, open yourself: ' . $url); $this->getIO()->writeError('No suitable browser opening command found, open yourself: ' . $url);
} }

View File

@ -27,6 +27,7 @@ use Composer\Util\Silencer;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Composer\Console\Input\InputOption; use Composer\Console\Input\InputOption;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
@ -535,15 +536,11 @@ EOT
return $this->gitConfig; return $this->gitConfig;
} }
$finder = new ExecutableFinder(); $process = new ProcessExecutor($this->getIO());
$gitBin = $finder->find('git');
$cmd = new Process([$gitBin, 'config', '-l']); if (0 === $process->execute(['git', 'config', '-l'], $output)) {
$cmd->run();
if ($cmd->isSuccessful()) {
$this->gitConfig = []; $this->gitConfig = [];
Preg::matchAllStrictGroups('{^([^=]+)=(.*)$}m', $cmd->getOutput(), $matches); Preg::matchAllStrictGroups('{^([^=]+)=(.*)$}m', $output, $matches);
foreach ($matches[1] as $key => $match) { foreach ($matches[1] as $key => $match) {
$this->gitConfig[$match] = $matches[2][$key]; $this->gitConfig[$match] = $matches[2][$key];
} }

View File

@ -15,6 +15,7 @@ namespace Composer;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\CaBundle\CaBundle; use Composer\CaBundle\CaBundle;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
use Seld\PharUtils\Timestamps; use Seld\PharUtils\Timestamps;
@ -48,23 +49,22 @@ class Compiler
unlink($pharFile); unlink($pharFile);
} }
$process = new Process(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], __DIR__); $process = new ProcessExecutor();
if ($process->run() !== 0) {
if (0 !== $process->execute(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], $output, __DIR__)) {
throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.');
} }
$this->version = trim($process->getOutput()); $this->version = trim($output);
$process = new Process(['git', 'log', '-n1', '--pretty=%ci', 'HEAD'], __DIR__); if (0 !== $process->execute(['git', 'log', '-n1', '--pretty=%ci', 'HEAD'], $output, __DIR__)) {
if ($process->run() !== 0) {
throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.');
} }
$this->versionDate = new \DateTime(trim($process->getOutput())); $this->versionDate = new \DateTime(trim($output));
$this->versionDate->setTimezone(new \DateTimeZone('UTC')); $this->versionDate->setTimezone(new \DateTimeZone('UTC'));
$process = new Process(['git', 'describe', '--tags', '--exact-match', 'HEAD'], __DIR__); if (0 === $process->execute(['git', 'describe', '--tags', '--exact-match', 'HEAD'], $output, __DIR__)) {
if ($process->run() === 0) { $this->version = trim($output);
$this->version = trim($process->getOutput());
} else { } else {
// get branch-alias defined in composer.json for dev-main (if any) // get branch-alias defined in composer.json for dev-main (if any)
$localConfig = __DIR__.'/../../composer.json'; $localConfig = __DIR__.'/../../composer.json';
@ -75,6 +75,10 @@ class Compiler
} }
} }
if ('' === $this->version) {
throw new \UnexpectedValueException('Version detection failed');
}
$phar = new \Phar($pharFile, 0, 'composer.phar'); $phar = new \Phar($pharFile, 0, 'composer.phar');
$phar->setSignatureAlgorithm(\Phar::SHA512); $phar->setSignatureAlgorithm(\Phar::SHA512);

View File

@ -12,10 +12,12 @@
namespace Composer\Downloader; namespace Composer\Downloader;
use Composer\Util\Platform;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use RuntimeException;
/** /**
* @author BohwaZ <http://bohwaz.net/> * @author BohwaZ <http://bohwaz.net/>
@ -38,22 +40,13 @@ class FossilDownloader extends VcsDownloader
// Ensure we are allowed to use this URL by config // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);
$url = ProcessExecutor::escape($url);
$ref = ProcessExecutor::escape($package->getSourceReference());
$repoFile = $path . '.fossil'; $repoFile = $path . '.fossil';
$realPath = Platform::realpath($path);
$this->io->writeError("Cloning ".$package->getSourceReference()); $this->io->writeError("Cloning ".$package->getSourceReference());
$command = sprintf('fossil clone -- %s %s', $url, ProcessExecutor::escape($repoFile)); $this->execute(['fossil', 'clone', '--', $url, $repoFile]);
if (0 !== $this->process->execute($command, $ignoredOutput)) { $this->execute(['fossil', 'open', '--nested', '--', $repoFile], $realPath);
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); $this->execute(['fossil', 'update', '--', (string) $package->getSourceReference()], $realPath);
}
$command = sprintf('fossil open --nested -- %s', ProcessExecutor::escape($repoFile));
if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
}
$command = sprintf('fossil update -- %s', $ref);
if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
}
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
@ -66,17 +59,15 @@ class FossilDownloader extends VcsDownloader
// Ensure we are allowed to use this URL by config // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);
$ref = ProcessExecutor::escape($target->getSourceReference());
$this->io->writeError(" Updating to ".$target->getSourceReference()); $this->io->writeError(" Updating to ".$target->getSourceReference());
if (!$this->hasMetadataRepository($path)) { if (!$this->hasMetadataRepository($path)) {
throw new \RuntimeException('The .fslckout file is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); throw new \RuntimeException('The .fslckout file is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
} }
$command = sprintf('fossil pull && fossil up %s', $ref); $realPath = Platform::realpath($path);
if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { $this->execute(['fossil', 'pull'], $realPath);
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); $this->execute(['fossil', 'up', (string) $target->getSourceReference()], $realPath);
}
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
@ -90,7 +81,7 @@ class FossilDownloader extends VcsDownloader
return null; return null;
} }
$this->process->execute('fossil changes', $output, realpath($path)); $this->process->execute(['fossil', 'changes'], $output, Platform::realpath($path));
$output = trim($output); $output = trim($output);
@ -102,11 +93,7 @@ class FossilDownloader extends VcsDownloader
*/ */
protected function getCommitLogs(string $fromReference, string $toReference, string $path): string protected function getCommitLogs(string $fromReference, string $toReference, string $path): string
{ {
$command = sprintf('fossil timeline -t ci -W 0 -n 0 before %s', ProcessExecutor::escape($toReference)); $this->execute(['fossil', 'timeline', '-t', 'ci', '-W', '0', '-n', '0', 'before', $toReference], Platform::realpath($path), $output);
if (0 !== $this->process->execute($command, $output, realpath($path))) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
}
$log = ''; $log = '';
$match = '/\d\d:\d\d:\d\d\s+\[' . $toReference . '\]/'; $match = '/\d\d:\d\d:\d\d\s+\[' . $toReference . '\]/';
@ -121,6 +108,17 @@ class FossilDownloader extends VcsDownloader
return $log; return $log;
} }
/**
* @param non-empty-list<string> $command
* @throws \RuntimeException
*/
private function execute(array $command, ?string $cwd = null, ?string &$output = null): void
{
if (0 !== $this->process->execute($command, $output, $cwd)) {
throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
}
}
/** /**
* @inheritDoc * @inheritDoc
*/ */

View File

@ -73,7 +73,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
// --dissociate option is only available since git 2.3.0-rc0 // --dissociate option is only available since git 2.3.0-rc0
if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) {
$this->io->writeError(" - Syncing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) into cache"); $this->io->writeError(" - Syncing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) into cache");
$this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); $this->io->writeError(sprintf(' Cloning to cache at %s', $cachePath), true, IOInterface::DEBUG);
$ref = $package->getSourceReference(); $ref = $package->getSourceReference();
if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref, $package->getPrettyVersion()) && is_dir($cachePath)) { if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref, $package->getPrettyVersion()) && is_dir($cachePath)) {
$this->cachedPackages[$package->getId()][$ref] = true; $this->cachedPackages[$package->getId()][$ref] = true;
@ -94,24 +94,30 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
$cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($url)).'/'; $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($url)).'/';
$ref = $package->getSourceReference(); $ref = $package->getSourceReference();
$flag = Platform::isWindows() ? '/D ' : '';
if (!empty($this->cachedPackages[$package->getId()][$ref])) { if (!empty($this->cachedPackages[$package->getId()][$ref])) {
$msg = "Cloning ".$this->getShortHash($ref).' from cache'; $msg = "Cloning ".$this->getShortHash($ref).' from cache';
$cloneFlags = '--dissociate --reference %cachePath% '; $cloneFlags = ['--dissociate', '--reference', $cachePath];
$transportOptions = $package->getTransportOptions(); $transportOptions = $package->getTransportOptions();
if (isset($transportOptions['git']['single_use_clone']) && $transportOptions['git']['single_use_clone']) { if (isset($transportOptions['git']['single_use_clone']) && $transportOptions['git']['single_use_clone']) {
$cloneFlags = ''; $cloneFlags = [];
} }
$command = $commands = [
'git clone --no-checkout %cachePath% %path% ' . $cloneFlags array_merge(['git', 'clone', '--no-checkout', $cachePath, $path], $cloneFlags),
. '&& cd '.$flag.'%path% ' ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'],
. '&& git remote set-url origin -- %sanitizedUrl% && git remote add composer -- %sanitizedUrl%'; ['git', 'remote', 'add', 'composer', '--', '%sanitizedUrl%'],
];
} else { } else {
$msg = "Cloning ".$this->getShortHash($ref); $msg = "Cloning ".$this->getShortHash($ref);
$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%'; $commands = [
array_merge(['git', 'clone', '--no-checkout', '--', '%url%', $path]),
['git', 'remote', 'add', 'composer', '--', '%url%'],
['git', 'fetch', 'composer'],
['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'],
['git', 'remote', 'set-url', 'composer', '--', '%sanitizedUrl%'],
];
if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) {
throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting');
} }
@ -119,20 +125,8 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$this->io->writeError($msg); $this->io->writeError($msg);
$commandCallable = static function (string $url) use ($path, $command, $cachePath): string { $this->gitUtil->runCommands($commands, $url, $path, true);
return str_replace(
['%url%', '%path%', '%cachePath%', '%sanitizedUrl%'],
[
ProcessExecutor::escape($url),
ProcessExecutor::escape($path),
ProcessExecutor::escape($cachePath),
ProcessExecutor::escape(Preg::replace('{://([^@]+?):(.+?)@}', '://', $url)),
],
$command
);
};
$this->gitUtil->runCommand($commandCallable, $url, $path, true);
$sourceUrl = $package->getSourceUrl(); $sourceUrl = $package->getSourceUrl();
if ($url !== $sourceUrl && $sourceUrl !== null) { if ($url !== $sourceUrl && $sourceUrl !== null) {
$this->updateOriginUrl($path, $sourceUrl); $this->updateOriginUrl($path, $sourceUrl);
@ -166,10 +160,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
if (!empty($this->cachedPackages[$target->getId()][$ref])) { if (!empty($this->cachedPackages[$target->getId()][$ref])) {
$msg = "Checking out ".$this->getShortHash($ref).' from cache'; $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 -- %sanitizedUrl%'; $remoteUrl = $cachePath;
} else { } else {
$msg = "Checking out ".$this->getShortHash($ref); $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)) && git remote set-url composer -- %sanitizedUrl%'; $remoteUrl = '%url%';
if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) {
throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting');
} }
@ -177,20 +171,19 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$this->io->writeError($msg); $this->io->writeError($msg);
$commandCallable = static function ($url) use ($ref, $command, $cachePath): string { if (0 !== $this->process->execute(['git', 'rev-parse', '--quiet', '--verify', $ref.'^{commit}'], $output, $path)) {
return str_replace( $commands = [
['%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'], ['git', 'remote', 'set-url', 'composer', '--', $remoteUrl],
[ ['git', 'fetch', 'composer'],
ProcessExecutor::escape($url), ['git', 'fetch', '--tags', 'composer'],
ProcessExecutor::escape($ref.'^{commit}'), ];
ProcessExecutor::escape($cachePath),
ProcessExecutor::escape(Preg::replace('{://([^@]+?):(.+?)@}', '://', $url)), $this->gitUtil->runCommands($commands, $url, $path);
], }
$command
); $command = ['git', 'remote', 'set-url', 'composer', '--', '%sanitizedUrl%'];
}; $this->gitUtil->runCommands([$command], $url, $path);
$this->gitUtil->runCommand($commandCallable, $url, $path);
if ($newRef = $this->updateToCommit($target, $path, (string) $ref, $target->getPrettyVersion())) { if ($newRef = $this->updateToCommit($target, $path, (string) $ref, $target->getPrettyVersion())) {
if ($target->getDistReference() === $target->getSourceReference()) { if ($target->getDistReference() === $target->getSourceReference()) {
$target->setDistReference($newRef); $target->setDistReference($newRef);
@ -200,7 +193,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$updateOriginUrl = false; $updateOriginUrl = false;
if ( if (
0 === $this->process->execute('git remote -v', $output, $path) 0 === $this->process->execute(['git', 'remote', '-v'], $output, $path)
&& Preg::isMatch('{^origin\s+(?P<url>\S+)}m', $output, $originMatch) && Preg::isMatch('{^origin\s+(?P<url>\S+)}m', $output, $originMatch)
&& Preg::isMatch('{^composer\s+(?P<url>\S+)}m', $output, $composerMatch) && Preg::isMatch('{^composer\s+(?P<url>\S+)}m', $output, $composerMatch)
) { ) {
@ -225,9 +218,9 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
return null; return null;
} }
$command = 'git status --porcelain --untracked-files=no'; $command = ['git', 'status', '--porcelain', '--untracked-files=no'];
if (0 !== $this->process->execute($command, $output, $path)) { if (0 !== $this->process->execute($command, $output, $path)) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
} }
$output = trim($output); $output = trim($output);
@ -243,9 +236,9 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
return null; return null;
} }
$command = 'git show-ref --head -d'; $command = ['git', 'show-ref', '--head', '-d'];
if (0 !== $this->process->execute($command, $output, $path)) { if (0 !== $this->process->execute($command, $output, $path)) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
} }
$refs = trim($output); $refs = trim($output);
@ -310,12 +303,12 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
// first pass and we found unpushed changes, fetch from all remotes to make sure we have up to date // first pass and we found unpushed changes, fetch from all remotes to make sure we have up to date
// remotes and then try again as outdated remotes can sometimes cause false-positives // remotes and then try again as outdated remotes can sometimes cause false-positives
if ($unpushedChanges && $i === 0) { if ($unpushedChanges && $i === 0) {
$this->process->execute('git fetch --all', $output, $path); $this->process->execute(['git', 'fetch', '--all'], $output, $path);
// update list of refs after fetching // update list of refs after fetching
$command = 'git show-ref --head -d'; $command = ['git', 'show-ref', '--head', '-d'];
if (0 !== $this->process->execute($command, $output, $path)) { if (0 !== $this->process->execute($command, $output, $path)) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
} }
$refs = trim($output); $refs = trim($output);
} }
@ -425,7 +418,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
if (!empty($this->hasStashedChanges[$path])) { if (!empty($this->hasStashedChanges[$path])) {
unset($this->hasStashedChanges[$path]); unset($this->hasStashedChanges[$path]);
$this->io->writeError(' <info>Re-applying stashed changes</info>'); $this->io->writeError(' <info>Re-applying stashed changes</info>');
if (0 !== $this->process->execute('git stash pop', $output, $path)) { if (0 !== $this->process->execute(['git', 'stash', 'pop'], $output, $path)) {
throw new \RuntimeException("Failed to apply stashed changes:\n\n".$this->process->getErrorOutput()); throw new \RuntimeException("Failed to apply stashed changes:\n\n".$this->process->getErrorOutput());
} }
} }
@ -441,18 +434,29 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
*/ */
protected function updateToCommit(PackageInterface $package, string $path, string $reference, string $prettyVersion): ?string protected function updateToCommit(PackageInterface $package, string $path, string $reference, string $prettyVersion): ?string
{ {
$force = !empty($this->hasDiscardedChanges[$path]) || !empty($this->hasStashedChanges[$path]) ? '-f ' : ''; $force = !empty($this->hasDiscardedChanges[$path]) || !empty($this->hasStashedChanges[$path]) ? ['-f'] : [];
// This uses the "--" sequence to separate branch from file parameters. // This uses the "--" sequence to separate branch from file parameters.
// //
// Otherwise git tries the branch name as well as file name. // Otherwise git tries the branch name as well as file name.
// If the non-existent branch is actually the name of a file, the file // If the non-existent branch is actually the name of a file, the file
// is checked out. // is checked out.
$template = 'git checkout '.$force.'%s -- && git reset --hard %1$s --';
$branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion); $branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion);
/**
* @var \Closure(non-empty-list<string>): bool $execute
* @phpstan-ignore varTag.nativeType
*/
$execute = function (array $command) use (&$output, $path) {
/** @var non-empty-list<string> $command */
$output = '';
return 0 === $this->process->execute($command, $output, $path);
};
$branches = null; $branches = null;
if (0 === $this->process->execute('git branch -r', $output, $path)) { if ($execute(['git', 'branch', '-r'])) {
$branches = $output; $branches = $output;
} }
@ -462,8 +466,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
&& null !== $branches && null !== $branches
&& Preg::isMatch('{^\s+composer/'.preg_quote($reference).'$}m', $branches) && Preg::isMatch('{^\s+composer/'.preg_quote($reference).'$}m', $branches)
) { ) {
$command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference)); $command1 = array_merge(['git', 'checkout'], $force, ['-B', $branch, 'composer/'.$reference, '--']);
if (0 === $this->process->execute($command, $output, $path)) { $command2 = ['git', 'reset', '--hard', 'composer/'.$reference, '--'];
if ($execute($command1) && $execute($command2)) {
return null; return null;
} }
} }
@ -475,17 +481,18 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$branch = 'v' . $branch; $branch = 'v' . $branch;
} }
$command = sprintf('git checkout %s --', ProcessExecutor::escape($branch)); $command = ['git', 'checkout', $branch, '--'];
$fallbackCommand = sprintf('git checkout '.$force.'-B %s %s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$branch)); $fallbackCommand = array_merge(['git', 'checkout'], $force, ['-B', $branch, 'composer/'.$branch, '--']);
$resetCommand = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference)); $resetCommand = ['git', 'reset', '--hard', $reference, '--'];
if (0 === $this->process->execute("($command || $fallbackCommand) && $resetCommand", $output, $path)) { if (($execute($command) || $execute($fallbackCommand)) && $execute($resetCommand)) {
return null; return null;
} }
} }
$command = sprintf($template, ProcessExecutor::escape($gitRef)); $command1 = array_merge(['git', 'checkout'], $force, [$gitRef, '--']);
if (0 === $this->process->execute($command, $output, $path)) { $command2 = ['git', 'reset', '--hard', $gitRef, '--'];
if ($execute($command1) && $execute($command2)) {
return null; return null;
} }
@ -497,12 +504,14 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$exceptionExtra = "\nIt looks like the commit hash is not available in the repository, maybe ".($package->isDev() ? 'the commit was removed from the branch' : 'the tag was recreated').'? Run "composer update '.$package->getPrettyName().'" to resolve this.'; $exceptionExtra = "\nIt looks like the commit hash is not available in the repository, maybe ".($package->isDev() ? 'the commit was removed from the branch' : 'the tag was recreated').'? Run "composer update '.$package->getPrettyName().'" to resolve this.';
} }
$command = implode(' ', $command1). ' && '.implode(' ', $command2);
throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() . $exceptionExtra)); throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() . $exceptionExtra));
} }
protected function updateOriginUrl(string $path, string $url): void protected function updateOriginUrl(string $path, string $url): void
{ {
$this->process->execute(sprintf('git remote set-url origin -- %s', ProcessExecutor::escape($url)), $output, $path); $this->process->execute(['git', 'remote', 'set-url', 'origin', '--', $url], $output, $path);
$this->setPushUrl($path, $url); $this->setPushUrl($path, $url);
} }
@ -515,7 +524,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
if (!in_array('ssh', $protocols, true)) { if (!in_array('ssh', $protocols, true)) {
$pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git'; $pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git';
} }
$cmd = sprintf('git remote set-url --push origin -- %s', ProcessExecutor::escape($pushUrl)); $cmd = ['git', 'remote', 'set-url', '--push', 'origin', '--', $pushUrl];
$this->process->execute($cmd, $ignoredOutput, $path); $this->process->execute($cmd, $ignoredOutput, $path);
} }
} }
@ -526,10 +535,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
protected function getCommitLogs(string $fromReference, string $toReference, string $path): string protected function getCommitLogs(string $fromReference, string $toReference, string $path): string
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
$command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"'.GitUtil::getNoShowSignatureFlag($this->process), ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); $command = array_merge(['git', 'log', $fromReference.'..'.$toReference, '--pretty=format:%h - %an: %s'], GitUtil::getNoShowSignatureFlags($this->process));
if (0 !== $this->process->execute($command, $output, $path)) { if (0 !== $this->process->execute($command, $output, $path)) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
} }
return $output; return $output;
@ -542,7 +551,10 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
protected function discardChanges(string $path): PromiseInterface protected function discardChanges(string $path): PromiseInterface
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
if (0 !== $this->process->execute('git clean -df && git reset --hard', $output, $path)) { if (0 !== $this->process->execute(['git', 'clean', '-df'], $output, $path)) {
throw new \RuntimeException("Could not reset changes\n\n:".$output);
}
if (0 !== $this->process->execute(['git', 'reset', '--hard'], $output, $path)) {
throw new \RuntimeException("Could not reset changes\n\n:".$output); throw new \RuntimeException("Could not reset changes\n\n:".$output);
} }
@ -558,7 +570,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
protected function stashChanges(string $path): PromiseInterface protected function stashChanges(string $path): PromiseInterface
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
if (0 !== $this->process->execute('git stash --include-untracked', $output, $path)) { if (0 !== $this->process->execute(['git', 'stash', '--include-untracked'], $output, $path)) {
throw new \RuntimeException("Could not stash changes\n\n:".$output); throw new \RuntimeException("Could not stash changes\n\n:".$output);
} }
@ -573,7 +585,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
protected function viewDiff(string $path): void protected function viewDiff(string $path): void
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
if (0 !== $this->process->execute('git diff HEAD', $output, $path)) { if (0 !== $this->process->execute(['git', 'diff', 'HEAD'], $output, $path)) {
throw new \RuntimeException("Could not view diff\n\n:".$output); throw new \RuntimeException("Could not view diff\n\n:".$output);
} }

View File

@ -31,7 +31,7 @@ class GzipDownloader extends ArchiveDownloader
// Try to use gunzip on *nix // Try to use gunzip on *nix
if (!Platform::isWindows()) { if (!Platform::isWindows()) {
$command = 'gzip -cd -- ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath); $command = ['sh', '-c', 'gzip -cd -- "$0" > "$1"', $file, $targetFilepath];
if (0 === $this->process->execute($command, $ignoredOutput)) { if (0 === $this->process->execute($command, $ignoredOutput)) {
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
@ -44,7 +44,7 @@ class GzipDownloader extends ArchiveDownloader
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
$processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); $processError = 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput();
throw new \RuntimeException($processError); throw new \RuntimeException($processError);
} }

View File

@ -41,16 +41,15 @@ class HgDownloader extends VcsDownloader
{ {
$hgUtils = new HgUtils($this->io, $this->config, $this->process); $hgUtils = new HgUtils($this->io, $this->config, $this->process);
$cloneCommand = static function (string $url) use ($path): string { $cloneCommand = static function (string $url) use ($path): array {
return sprintf('hg clone -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($path)); return ['hg', 'clone', '--', $url, $path];
}; };
$hgUtils->runCommand($cloneCommand, $url, $path); $hgUtils->runCommand($cloneCommand, $url, $path);
$ref = ProcessExecutor::escape($package->getSourceReference()); $command = ['hg', 'up', '--', (string) $package->getSourceReference()];
$command = sprintf('hg up -- %s', $ref);
if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
} }
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
@ -70,10 +69,14 @@ class HgDownloader extends VcsDownloader
throw new \RuntimeException('The .hg directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); throw new \RuntimeException('The .hg directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
} }
$command = static function ($url) use ($ref): string { $command = static function ($url): array {
return sprintf('hg pull -- %s && hg up -- %s', ProcessExecutor::escape($url), ProcessExecutor::escape($ref)); return ['hg', 'pull', '--', $url];
}; };
$hgUtils->runCommand($command, $url, $path);
$command = static function () use ($ref): array {
return ['hg', 'up', '--', $ref];
};
$hgUtils->runCommand($command, $url, $path); $hgUtils->runCommand($command, $url, $path);
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
@ -88,7 +91,7 @@ class HgDownloader extends VcsDownloader
return null; return null;
} }
$this->process->execute('hg st', $output, realpath($path)); $this->process->execute(['hg', 'st'], $output, realpath($path));
$output = trim($output); $output = trim($output);
@ -100,10 +103,10 @@ class HgDownloader extends VcsDownloader
*/ */
protected function getCommitLogs(string $fromReference, string $toReference, string $path): string protected function getCommitLogs(string $fromReference, string $toReference, string $path): string
{ {
$command = sprintf('hg log -r %s:%s --style compact', ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); $command = ['hg', 'log', '-r', $fromReference.':'.$toReference, '--style', 'compact'];
if (0 !== $this->process->execute($command, $output, realpath($path))) { if (0 !== $this->process->execute($command, $output, realpath($path))) {
throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput());
} }
return $output; return $output;

View File

@ -34,13 +34,13 @@ class RarDownloader extends ArchiveDownloader
// Try to use unrar on *nix // Try to use unrar on *nix
if (!Platform::isWindows()) { if (!Platform::isWindows()) {
$command = 'unrar x -- ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' >/dev/null && chmod -R u+w ' . ProcessExecutor::escape($path); $command = ['sh', '-c', 'unrar x -- "$0" "$1" >/dev/null && chmod -R u+w "$1"', $file, $path];
if (0 === $this->process->execute($command, $ignoredOutput)) { if (0 === $this->process->execute($command, $ignoredOutput)) {
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
$processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); $processError = 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput();
} }
if (!class_exists('RarArchive')) { if (!class_exists('RarArchive')) {

View File

@ -59,7 +59,7 @@ class SvnDownloader extends VcsDownloader
} }
$this->io->writeError(" Checking out ".$package->getSourceReference()); $this->io->writeError(" Checking out ".$package->getSourceReference());
$this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); $this->execute($package, $url, ['svn', 'co'], sprintf("%s/%s", $url, $ref), null, $path);
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
@ -77,13 +77,13 @@ class SvnDownloader extends VcsDownloader
} }
$util = new SvnUtil($url, $this->io, $this->config, $this->process); $util = new SvnUtil($url, $this->io, $this->config, $this->process);
$flags = ""; $flags = [];
if (version_compare($util->binaryVersion(), '1.7.0', '>=')) { if (version_compare($util->binaryVersion(), '1.7.0', '>=')) {
$flags .= ' --ignore-ancestry'; $flags[] = '--ignore-ancestry';
} }
$this->io->writeError(" Checking out " . $ref); $this->io->writeError(" Checking out " . $ref);
$this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); $this->execute($target, $url, array_merge(['svn', 'switch'], $flags), sprintf("%s/%s", $url, $ref), $path);
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
@ -97,7 +97,7 @@ class SvnDownloader extends VcsDownloader
return null; return null;
} }
$this->process->execute('svn status --ignore-externals', $output, $path); $this->process->execute(['svn', 'status', '--ignore-externals'], $output, $path);
return Preg::isMatch('{^ *[^X ] +}m', $output) ? $output : null; return Preg::isMatch('{^ *[^X ] +}m', $output) ? $output : null;
} }
@ -107,13 +107,13 @@ class SvnDownloader extends VcsDownloader
* if necessary. * if necessary.
* *
* @param string $baseUrl Base URL of the repository * @param string $baseUrl Base URL of the repository
* @param string $command SVN command to run * @param non-empty-list<string> $command SVN command to run
* @param string $url SVN url * @param string $url SVN url
* @param string $cwd Working directory * @param string $cwd Working directory
* @param string $path Target for a checkout * @param string $path Target for a checkout
* @throws \RuntimeException * @throws \RuntimeException
*/ */
protected function execute(PackageInterface $package, string $baseUrl, string $command, string $url, ?string $cwd = null, ?string $path = null): string protected function execute(PackageInterface $package, string $baseUrl, array $command, string $url, ?string $cwd = null, ?string $path = null): string
{ {
$util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process);
$util->setCacheCredentials($this->cacheCredentials); $util->setCacheCredentials($this->cacheCredentials);
@ -194,10 +194,10 @@ class SvnDownloader extends VcsDownloader
{ {
if (Preg::isMatch('{@(\d+)$}', $fromReference) && Preg::isMatch('{@(\d+)$}', $toReference)) { if (Preg::isMatch('{@(\d+)$}', $fromReference) && Preg::isMatch('{@(\d+)$}', $toReference)) {
// retrieve the svn base url from the checkout folder // retrieve the svn base url from the checkout folder
$command = sprintf('svn info --non-interactive --xml -- %s', ProcessExecutor::escape($path)); $command = ['svn', 'info', '--non-interactive', '--xml', '--', $path];
if (0 !== $this->process->execute($command, $output, $path)) { if (0 !== $this->process->execute($command, $output, $path)) {
throw new \RuntimeException( throw new \RuntimeException(
'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()
); );
} }
@ -214,7 +214,7 @@ class SvnDownloader extends VcsDownloader
$fromRevision = Preg::replace('{.*@(\d+)$}', '$1', $fromReference); $fromRevision = Preg::replace('{.*@(\d+)$}', '$1', $fromReference);
$toRevision = Preg::replace('{.*@(\d+)$}', '$1', $toReference); $toRevision = Preg::replace('{.*@(\d+)$}', '$1', $toReference);
$command = sprintf('svn log -r%s:%s --incremental', ProcessExecutor::escape($fromRevision), ProcessExecutor::escape($toRevision)); $command = ['svn', 'log', '-r', $fromRevision.':'.$toRevision, '--incremental'];
$util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process);
$util->setCacheCredentials($this->cacheCredentials); $util->setCacheCredentials($this->cacheCredentials);
@ -222,7 +222,7 @@ class SvnDownloader extends VcsDownloader
return $util->executeLocal($command, $path, null, $this->io->isVerbose()); return $util->executeLocal($command, $path, null, $this->io->isVerbose());
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
throw new \RuntimeException( throw new \RuntimeException(
'Failed to execute ' . $command . "\n\n".$e->getMessage() 'Failed to execute ' . implode(' ', $command) . "\n\n".$e->getMessage()
); );
} }
} }
@ -235,7 +235,7 @@ class SvnDownloader extends VcsDownloader
*/ */
protected function discardChanges(string $path): PromiseInterface protected function discardChanges(string $path): PromiseInterface
{ {
if (0 !== $this->process->execute('svn revert -R .', $output, $path)) { if (0 !== $this->process->execute(['svn', 'revert', '-R', '.'], $output, $path)) {
throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput());
} }

View File

@ -26,13 +26,13 @@ class XzDownloader extends ArchiveDownloader
{ {
protected function extract(PackageInterface $package, string $file, string $path): PromiseInterface protected function extract(PackageInterface $package, string $file, string $path): PromiseInterface
{ {
$command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); $command = ['tar', '-xJf', $file, '-C', $path];
if (0 === $this->process->execute($command, $ignoredOutput)) { if (0 === $this->process->execute($command, $ignoredOutput)) {
return \React\Promise\resolve(null); return \React\Promise\resolve(null);
} }
$processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); $processError = 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput();
throw new \RuntimeException($processError); throw new \RuntimeException($processError);
} }

View File

@ -27,7 +27,7 @@ use ZipArchive;
*/ */
class ZipDownloader extends ArchiveDownloader class ZipDownloader extends ArchiveDownloader
{ {
/** @var array<int, array{0: string, 1: string}> */ /** @var array<int, non-empty-list<string>> */
private static $unzipCommands; private static $unzipCommands;
/** @var bool */ /** @var bool */
private static $hasZipArchive; private static $hasZipArchive;
@ -46,16 +46,16 @@ class ZipDownloader extends ArchiveDownloader
self::$unzipCommands = []; self::$unzipCommands = [];
$finder = new ExecutableFinder; $finder = new ExecutableFinder;
if (Platform::isWindows() && ($cmd = $finder->find('7z', null, ['C:\Program Files\7-Zip']))) { if (Platform::isWindows() && ($cmd = $finder->find('7z', null, ['C:\Program Files\7-Zip']))) {
self::$unzipCommands[] = ['7z', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s']; self::$unzipCommands[] = ['7z', $cmd, 'x', '-bb0', '-y', '%file%', '-o%path%'];
} }
if ($cmd = $finder->find('unzip')) { if ($cmd = $finder->find('unzip')) {
self::$unzipCommands[] = ['unzip', ProcessExecutor::escape($cmd).' -qq %s -d %s']; self::$unzipCommands[] = ['unzip', $cmd, '-qq', '%file%', '-d', '%path%'];
} }
if (!Platform::isWindows() && ($cmd = $finder->find('7z'))) { // 7z linux/macOS support is only used if unzip is not present if (!Platform::isWindows() && ($cmd = $finder->find('7z'))) { // 7z linux/macOS support is only used if unzip is not present
self::$unzipCommands[] = ['7z', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s']; self::$unzipCommands[] = ['7z', $cmd, 'x', '-bb0', '-y', '%file%', '-o%path%'];
} }
if (!Platform::isWindows() && ($cmd = $finder->find('7zz'))) { // 7zz linux/macOS support is only used if unzip is not present if (!Platform::isWindows() && ($cmd = $finder->find('7zz'))) { // 7zz linux/macOS support is only used if unzip is not present
self::$unzipCommands[] = ['7zz', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s']; self::$unzipCommands[] = ['7zz', $cmd, 'x', '-bb0', '-y', '%file%', '-o%path%'];
} }
} }
@ -114,24 +114,28 @@ class ZipDownloader extends ArchiveDownloader
// Force Exception throwing if the other alternative extraction method is not available // Force Exception throwing if the other alternative extraction method is not available
$isLastChance = !self::$hasZipArchive; $isLastChance = !self::$hasZipArchive;
if (!self::$unzipCommands) { if (0 === \count(self::$unzipCommands)) {
// This was call as the favorite extract way, but is not available // This was call as the favorite extract way, but is not available
// We switch to the alternative // We switch to the alternative
return $this->extractWithZipArchive($package, $file, $path); return $this->extractWithZipArchive($package, $file, $path);
} }
$commandSpec = reset(self::$unzipCommands); $commandSpec = reset(self::$unzipCommands);
$command = sprintf($commandSpec[1], ProcessExecutor::escape($file), ProcessExecutor::escape($path));
// normalize separators to backslashes to avoid problems with 7-zip on windows
// see https://github.com/composer/composer/issues/10058
if (Platform::isWindows()) {
$command = sprintf($commandSpec[1], ProcessExecutor::escape(strtr($file, '/', '\\')), ProcessExecutor::escape(strtr($path, '/', '\\')));
}
$executable = $commandSpec[0]; $executable = $commandSpec[0];
$command = array_slice($commandSpec, 1);
$map = [
// normalize separators to backslashes to avoid problems with 7-zip on windows
// see https://github.com/composer/composer/issues/10058
'%file%' => strtr($file, '/', DIRECTORY_SEPARATOR),
'%path%' => strtr($path, '/', DIRECTORY_SEPARATOR),
];
$command = array_map(static function ($value) use ($map) {
return strtr($value, $map);
}, $command);
if (!$warned7ZipLinux && !Platform::isWindows() && in_array($executable, ['7z', '7zz'], true)) { if (!$warned7ZipLinux && !Platform::isWindows() && in_array($executable, ['7z', '7zz'], true)) {
$warned7ZipLinux = true; $warned7ZipLinux = true;
if (0 === $this->process->execute($executable, $output)) { if (0 === $this->process->execute([$commandSpec[1]], $output)) {
if (Preg::isMatchStrictGroups('{^\s*7-Zip(?: \[64\])? ([0-9.]+)}', $output, $match) && version_compare($match[1], '21.01', '<')) { if (Preg::isMatchStrictGroups('{^\s*7-Zip(?: \[64\])? ([0-9.]+)}', $output, $match) && version_compare($match[1], '21.01', '<')) {
$this->io->writeError(' <warning>Unzipping using '.$executable.' '.$match[1].' may result in incorrect file permissions. Install '.$executable.' 21.01+ or unzip to ensure you get correct permissions.</warning>'); $this->io->writeError(' <warning>Unzipping using '.$executable.' '.$match[1].' may result in incorrect file permissions. Install '.$executable.' 21.01+ or unzip to ensure you get correct permissions.</warning>');
} }
@ -186,7 +190,7 @@ class ZipDownloader extends ArchiveDownloader
$output = $process->getErrorOutput(); $output = $process->getErrorOutput();
$output = str_replace(', '.$file.'.zip or '.$file.'.ZIP', '', $output); $output = str_replace(', '.$file.'.zip or '.$file.'.ZIP', '', $output);
return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$output)); return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.implode(' ', $command)."\n\n".$output));
} }
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@ -554,13 +554,14 @@ class Locker
case 'git': case 'git':
GitUtil::cleanEnv(); GitUtil::cleanEnv();
if (0 === $this->process->execute('git log -n1 --pretty=%ct '.ProcessExecutor::escape($sourceRef).GitUtil::getNoShowSignatureFlag($this->process), $output, $path) && Preg::isMatch('{^\s*\d+\s*$}', $output)) { $command = array_merge(['git', 'log', '-n1', '--pretty=%ct', (string) $sourceRef], GitUtil::getNoShowSignatureFlags($this->process));
if (0 === $this->process->execute($command, $output, $path) && Preg::isMatch('{^\s*\d+\s*$}', $output)) {
$datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); $datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC'));
} }
break; break;
case 'hg': case 'hg':
if (0 === $this->process->execute('hg log --template "{date|hgdate}" -r '.ProcessExecutor::escape($sourceRef), $output, $path) && Preg::isMatch('{^\s*(\d+)\s*}', $output, $match)) { if (0 === $this->process->execute(['hg', 'log', '--template', '{date|hgdate}', '-r', (string) $sourceRef], $output, $path) && Preg::isMatch('{^\s*(\d+)\s*}', $output, $match)) {
$datetime = new \DateTime('@'.$match[1], new \DateTimeZone('UTC')); $datetime = new \DateTime('@'.$match[1], new \DateTimeZone('UTC'));
} }
break; break;

View File

@ -198,7 +198,7 @@ class VersionGuesser
} }
if (null === $commit) { if (null === $commit) {
$command = 'git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($this->process); $command = array_merge(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], GitUtil::getNoShowSignatureFlags($this->process));
if (0 === $this->process->execute($command, $output, $path)) { if (0 === $this->process->execute($command, $output, $path)) {
$commit = trim($output) ?: null; $commit = trim($output) ?: null;
} }
@ -217,7 +217,7 @@ class VersionGuesser
private function versionFromGitTags(string $path): ?array private function versionFromGitTags(string $path): ?array
{ {
// try to fetch current version from git tags // try to fetch current version from git tags
if (0 === $this->process->execute('git describe --exact-match --tags', $output, $path)) { if (0 === $this->process->execute(['git', 'describe', '--exact-match', '--tags'], $output, $path)) {
try { try {
$version = $this->versionParser->normalize(trim($output)); $version = $this->versionParser->normalize(trim($output));
@ -237,7 +237,7 @@ class VersionGuesser
private function guessHgVersion(array $packageConfig, string $path): ?array private function guessHgVersion(array $packageConfig, string $path): ?array
{ {
// try to fetch current version from hg branch // try to fetch current version from hg branch
if (0 === $this->process->execute('hg branch', $output, $path)) { if (0 === $this->process->execute(['hg', 'branch'], $output, $path)) {
$branch = trim($output); $branch = trim($output);
$version = $this->versionParser->normalizeBranch($branch); $version = $this->versionParser->normalizeBranch($branch);
$isFeatureBranch = 0 === strpos($version, 'dev-'); $isFeatureBranch = 0 === strpos($version, 'dev-');
@ -375,14 +375,14 @@ class VersionGuesser
$prettyVersion = null; $prettyVersion = null;
// try to fetch current version from fossil // try to fetch current version from fossil
if (0 === $this->process->execute('fossil branch list', $output, $path)) { if (0 === $this->process->execute(['fossil', 'branch', 'list'], $output, $path)) {
$branch = trim($output); $branch = trim($output);
$version = $this->versionParser->normalizeBranch($branch); $version = $this->versionParser->normalizeBranch($branch);
$prettyVersion = 'dev-' . $branch; $prettyVersion = 'dev-' . $branch;
} }
// try to fetch current version from fossil tags // try to fetch current version from fossil tags
if (0 === $this->process->execute('fossil tag list', $output, $path)) { if (0 === $this->process->execute(['fossil', 'tag', 'list'], $output, $path)) {
try { try {
$version = $this->versionParser->normalize(trim($output)); $version = $this->versionParser->normalize(trim($output));
$prettyVersion = trim($output); $prettyVersion = trim($output);
@ -403,7 +403,7 @@ class VersionGuesser
SvnUtil::cleanEnv(); SvnUtil::cleanEnv();
// try to fetch current version from svn // try to fetch current version from svn
if (0 === $this->process->execute('svn info --xml', $output, $path)) { if (0 === $this->process->execute(['svn', 'info', '--xml'], $output, $path)) {
$trunkPath = isset($packageConfig['trunk-path']) ? preg_quote($packageConfig['trunk-path'], '#') : 'trunk'; $trunkPath = isset($packageConfig['trunk-path']) ? preg_quote($packageConfig['trunk-path'], '#') : 'trunk';
$branchesPath = isset($packageConfig['branches-path']) ? preg_quote($packageConfig['branches-path'], '#') : 'branches'; $branchesPath = isset($packageConfig['branches-path']) ? preg_quote($packageConfig['branches-path'], '#') : 'branches';
$tagsPath = isset($packageConfig['tags-path']) ? preg_quote($packageConfig['tags-path'], '#') : 'tags'; $tagsPath = isset($packageConfig['tags-path']) ? preg_quote($packageConfig['tags-path'], '#') : 'tags';

View File

@ -49,11 +49,7 @@ class HhvmDetector
$hhvmPath = $this->executableFinder->find('hhvm'); $hhvmPath = $this->executableFinder->find('hhvm');
if ($hhvmPath !== null) { if ($hhvmPath !== null) {
$this->processExecutor = $this->processExecutor ?? new ProcessExecutor(); $this->processExecutor = $this->processExecutor ?? new ProcessExecutor();
$exitCode = $this->processExecutor->execute( $exitCode = $this->processExecutor->execute([$hhvmPath, '--php', '-d', 'hhvm.jit=0', '-r', 'echo HHVM_VERSION;'], self::$hhvmVersion);
ProcessExecutor::escape($hhvmPath).
' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null',
self::$hhvmVersion
);
if ($exitCode !== 0) { if ($exitCode !== 0) {
self::$hhvmVersion = false; self::$hhvmVersion = false;
} }

View File

@ -194,8 +194,8 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
// carry over the root package version if this path repo is in the same git repository as root package // carry over the root package version if this path repo is in the same git repository as root package
if (!isset($package['version']) && ($rootVersion = Platform::getEnv('COMPOSER_ROOT_VERSION'))) { if (!isset($package['version']) && ($rootVersion = Platform::getEnv('COMPOSER_ROOT_VERSION'))) {
if ( if (
0 === $this->process->execute('git rev-parse HEAD', $ref1, $path) 0 === $this->process->execute(['git', 'rev-parse', 'HEAD'], $ref1, $path)
&& 0 === $this->process->execute('git rev-parse HEAD', $ref2) && 0 === $this->process->execute(['git', 'rev-parse', 'HEAD'], $ref2)
&& $ref1 === $ref2 && $ref1 === $ref2
) { ) {
$package['version'] = $this->versionGuesser->getRootVersionFromEnv(); $package['version'] = $this->versionGuesser->getRootVersionFromEnv();
@ -203,7 +203,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
} }
$output = ''; $output = '';
if ('auto' === $reference && is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H'.GitUtil::getNoShowSignatureFlag($this->process), $output, $path)) { if ('auto' === $reference && is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute(array_merge(['git', 'log', '-n1', '--pretty=%H'], GitUtil::getNoShowSignatureFlags($this->process)), $output, $path)) {
$package['dist']['reference'] = trim($output); $package['dist']['reference'] = trim($output);
} }

View File

@ -71,7 +71,7 @@ class FossilDriver extends VcsDriver
*/ */
protected function checkFossil(): void protected function checkFossil(): void
{ {
if (0 !== $this->process->execute('fossil version', $ignoredOutput)) { if (0 !== $this->process->execute(['fossil', 'version'], $ignoredOutput)) {
throw new \RuntimeException("fossil was not found, check that it is installed and in your PATH env.\n\n" . $this->process->getErrorOutput()); throw new \RuntimeException("fossil was not found, check that it is installed and in your PATH env.\n\n" . $this->process->getErrorOutput());
} }
} }
@ -81,6 +81,8 @@ class FossilDriver extends VcsDriver
*/ */
protected function updateLocalRepo(): void protected function updateLocalRepo(): void
{ {
assert($this->repoFile !== null);
$fs = new Filesystem(); $fs = new Filesystem();
$fs->ensureDirectoryExists($this->checkoutDir); $fs->ensureDirectoryExists($this->checkoutDir);
@ -89,8 +91,8 @@ class FossilDriver extends VcsDriver
} }
// update the repo if it is a valid fossil repository // update the repo if it is a valid fossil repository
if (is_file($this->repoFile) && is_dir($this->checkoutDir) && 0 === $this->process->execute('fossil info', $output, $this->checkoutDir)) { if (is_file($this->repoFile) && is_dir($this->checkoutDir) && 0 === $this->process->execute(['fossil', 'info'], $output, $this->checkoutDir)) {
if (0 !== $this->process->execute('fossil pull', $output, $this->checkoutDir)) { if (0 !== $this->process->execute(['fossil', 'pull'], $output, $this->checkoutDir)) {
$this->io->writeError('<error>Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')</error>'); $this->io->writeError('<error>Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')</error>');
} }
} else { } else {
@ -100,13 +102,13 @@ class FossilDriver extends VcsDriver
$fs->ensureDirectoryExists($this->checkoutDir); $fs->ensureDirectoryExists($this->checkoutDir);
if (0 !== $this->process->execute(sprintf('fossil clone -- %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoFile)), $output)) { if (0 !== $this->process->execute(['fossil', 'clone', '--', $this->url, $this->repoFile], $output)) {
$output = $this->process->getErrorOutput(); $output = $this->process->getErrorOutput();
throw new \RuntimeException('Failed to clone '.$this->url.' to repository ' . $this->repoFile . "\n\n" .$output); throw new \RuntimeException('Failed to clone '.$this->url.' to repository ' . $this->repoFile . "\n\n" .$output);
} }
if (0 !== $this->process->execute(sprintf('fossil open --nested -- %s', ProcessExecutor::escape($this->repoFile)), $output, $this->checkoutDir)) { if (0 !== $this->process->execute(['fossil', 'open', '--nested', '--', $this->repoFile], $output, $this->checkoutDir)) {
$output = $this->process->getErrorOutput(); $output = $this->process->getErrorOutput();
throw new \RuntimeException('Failed to open repository '.$this->repoFile.' in ' . $this->checkoutDir . "\n\n" .$output); throw new \RuntimeException('Failed to open repository '.$this->repoFile.' in ' . $this->checkoutDir . "\n\n" .$output);
@ -155,10 +157,9 @@ class FossilDriver extends VcsDriver
*/ */
public function getFileContent(string $file, string $identifier): ?string public function getFileContent(string $file, string $identifier): ?string
{ {
$command = sprintf('fossil cat -r %s -- %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); $this->process->execute(['fossil', 'cat', '-r', $identifier, '--', $file], $content, $this->checkoutDir);
$this->process->execute($command, $content, $this->checkoutDir);
if (!trim($content)) { if ('' === trim($content)) {
return null; return null;
} }
@ -170,7 +171,7 @@ class FossilDriver extends VcsDriver
*/ */
public function getChangeDate(string $identifier): ?\DateTimeImmutable public function getChangeDate(string $identifier): ?\DateTimeImmutable
{ {
$this->process->execute('fossil finfo -b -n 1 composer.json', $output, $this->checkoutDir); $this->process->execute(['fossil', 'finfo', '-b', '-n', '1', 'composer.json'], $output, $this->checkoutDir);
[, $date] = explode(' ', trim($output), 3); [, $date] = explode(' ', trim($output), 3);
return new \DateTimeImmutable($date, new \DateTimeZone('UTC')); return new \DateTimeImmutable($date, new \DateTimeZone('UTC'));
@ -184,7 +185,7 @@ class FossilDriver extends VcsDriver
if (null === $this->tags) { if (null === $this->tags) {
$tags = []; $tags = [];
$this->process->execute('fossil tag list', $output, $this->checkoutDir); $this->process->execute(['fossil', 'tag', 'list'], $output, $this->checkoutDir);
foreach ($this->process->splitLines($output) as $tag) { foreach ($this->process->splitLines($output) as $tag) {
$tags[$tag] = $tag; $tags[$tag] = $tag;
} }
@ -203,7 +204,7 @@ class FossilDriver extends VcsDriver
if (null === $this->branches) { if (null === $this->branches) {
$branches = []; $branches = [];
$this->process->execute('fossil branch list', $output, $this->checkoutDir); $this->process->execute(['fossil', 'branch', 'list'], $output, $this->checkoutDir);
foreach ($this->process->splitLines($output) as $branch) { foreach ($this->process->splitLines($output) as $branch) {
$branch = trim(Preg::replace('/^\*/', '', trim($branch))); $branch = trim(Preg::replace('/^\*/', '', trim($branch)));
$branches[$branch] = $branch; $branches[$branch] = $branch;
@ -237,7 +238,7 @@ class FossilDriver extends VcsDriver
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
// check whether there is a fossil repo in that path // check whether there is a fossil repo in that path
if ($process->execute('fossil info', $output, $url) === 0) { if ($process->execute(['fossil', 'info'], $output, $url) === 0) {
return true; return true;
} }
} }

View File

@ -102,7 +102,7 @@ class GitDriver extends VcsDriver
} }
// select currently checked out branch as default branch // select currently checked out branch as default branch
$this->process->execute('git branch --no-color', $output, $this->repoDir); $this->process->execute(['git', 'branch', '--no-color'], $output, $this->repoDir);
$branches = $this->process->splitLines($output); $branches = $this->process->splitLines($output);
if (!in_array('* master', $branches)) { if (!in_array('* master', $branches)) {
foreach ($branches as $branch) { foreach ($branches as $branch) {
@ -150,8 +150,7 @@ class GitDriver extends VcsDriver
throw new \RuntimeException('Invalid git identifier detected. Identifier must not start with a -, given: ' . $identifier); throw new \RuntimeException('Invalid git identifier detected. Identifier must not start with a -, given: ' . $identifier);
} }
$resource = sprintf('%s:%s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); $this->process->execute(['git', 'show', $identifier.':'.$file], $content, $this->repoDir);
$this->process->execute(sprintf('git show %s', $resource), $content, $this->repoDir);
if (trim($content) === '') { if (trim($content) === '') {
return null; return null;
@ -165,10 +164,7 @@ class GitDriver extends VcsDriver
*/ */
public function getChangeDate(string $identifier): ?\DateTimeImmutable public function getChangeDate(string $identifier): ?\DateTimeImmutable
{ {
$this->process->execute(sprintf( $this->process->execute(['git', '-c', 'log.showSignature=false', 'log', '-1', '--format=%at', $identifier], $output, $this->repoDir);
'git -c log.showSignature=false log -1 --format=%%at %s',
ProcessExecutor::escape($identifier)
), $output, $this->repoDir);
return new \DateTimeImmutable('@'.trim($output), new \DateTimeZone('UTC')); return new \DateTimeImmutable('@'.trim($output), new \DateTimeZone('UTC'));
} }
@ -181,7 +177,7 @@ class GitDriver extends VcsDriver
if (null === $this->tags) { if (null === $this->tags) {
$this->tags = []; $this->tags = [];
$this->process->execute('git show-ref --tags --dereference', $output, $this->repoDir); $this->process->execute(['git', 'show-ref', '--tags', '--dereference'], $output, $this->repoDir);
foreach ($this->process->splitLines($output) as $tag) { foreach ($this->process->splitLines($output) as $tag) {
if ($tag !== '' && Preg::isMatch('{^([a-f0-9]{40}) refs/tags/(\S+?)(\^\{\})?$}', $tag, $match)) { if ($tag !== '' && Preg::isMatch('{^([a-f0-9]{40}) refs/tags/(\S+?)(\^\{\})?$}', $tag, $match)) {
$this->tags[$match[2]] = $match[1]; $this->tags[$match[2]] = $match[1];
@ -200,7 +196,7 @@ class GitDriver extends VcsDriver
if (null === $this->branches) { if (null === $this->branches) {
$branches = []; $branches = [];
$this->process->execute('git branch --no-color --no-abbrev -v', $output, $this->repoDir); $this->process->execute(['git', 'branch', '--no-color', '--no-abbrev', '-v'], $output, $this->repoDir);
foreach ($this->process->splitLines($output) as $branch) { foreach ($this->process->splitLines($output) as $branch) {
if ($branch !== '' && !Preg::isMatch('{^ *[^/]+/HEAD }', $branch)) { if ($branch !== '' && !Preg::isMatch('{^ *[^/]+/HEAD }', $branch)) {
if (Preg::isMatchStrictGroups('{^(?:\* )? *(\S+) *([a-f0-9]+)(?: .*)?$}', $branch, $match) && $match[1][0] !== '-') { if (Preg::isMatchStrictGroups('{^(?:\* )? *(\S+) *([a-f0-9]+)(?: .*)?$}', $branch, $match) && $match[1][0] !== '-') {
@ -233,7 +229,7 @@ class GitDriver extends VcsDriver
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
// check whether there is a git repo in that path // check whether there is a git repo in that path
if ($process->execute('git tag', $output, $url) === 0) { if ($process->execute(['git', 'tag'], $output, $url) === 0) {
return true; return true;
} }
GitUtil::checkForRepoOwnershipError($process->getErrorOutput(), $url); GitUtil::checkForRepoOwnershipError($process->getErrorOutput(), $url);
@ -247,9 +243,7 @@ class GitDriver extends VcsDriver
GitUtil::cleanEnv(); GitUtil::cleanEnv();
try { try {
$gitUtil->runCommand(static function ($url): string { $gitUtil->runCommands([['git', 'ls-remote', '--heads', '--', '%url%']], $url, sys_get_temp_dir());
return 'git ls-remote --heads -- ' . ProcessExecutor::escape($url);
}, $url, sys_get_temp_dir());
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
return false; return false;
} }

View File

@ -63,8 +63,8 @@ class HgDriver extends VcsDriver
$hgUtils = new HgUtils($this->io, $this->config, $this->process); $hgUtils = new HgUtils($this->io, $this->config, $this->process);
// update the repo if it is a valid hg repository // update the repo if it is a valid hg repository
if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { if (is_dir($this->repoDir) && 0 === $this->process->execute(['hg', 'summary'], $output, $this->repoDir)) {
if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { if (0 !== $this->process->execute(['hg', 'pull'], $output, $this->repoDir)) {
$this->io->writeError('<error>Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')</error>'); $this->io->writeError('<error>Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')</error>');
} }
} else { } else {
@ -72,8 +72,8 @@ class HgDriver extends VcsDriver
$fs->removeDirectory($this->repoDir); $fs->removeDirectory($this->repoDir);
$repoDir = $this->repoDir; $repoDir = $this->repoDir;
$command = static function ($url) use ($repoDir): string { $command = static function ($url) use ($repoDir): array {
return sprintf('hg clone --noupdate -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($repoDir)); return ['hg', 'clone', '--noupdate', '--', $url, $repoDir];
}; };
$hgUtils->runCommand($command, $this->url, null); $hgUtils->runCommand($command, $this->url, null);
@ -90,7 +90,7 @@ class HgDriver extends VcsDriver
public function getRootIdentifier(): string public function getRootIdentifier(): string
{ {
if (null === $this->rootIdentifier) { if (null === $this->rootIdentifier) {
$this->process->execute('hg tip --template "{node}"', $output, $this->repoDir); $this->process->execute(['hg', 'tip', '--template', '{node}'], $output, $this->repoDir);
$output = $this->process->splitLines($output); $output = $this->process->splitLines($output);
$this->rootIdentifier = $output[0]; $this->rootIdentifier = $output[0];
} }
@ -131,7 +131,7 @@ class HgDriver extends VcsDriver
throw new \RuntimeException('Invalid hg identifier detected. Identifier must not start with a -, given: ' . $identifier); throw new \RuntimeException('Invalid hg identifier detected. Identifier must not start with a -, given: ' . $identifier);
} }
$resource = sprintf('hg cat -r %s -- %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); $resource = ['hg', 'cat', '-r', $identifier, '--', $file];
$this->process->execute($resource, $content, $this->repoDir); $this->process->execute($resource, $content, $this->repoDir);
if (!trim($content)) { if (!trim($content)) {
@ -147,10 +147,7 @@ class HgDriver extends VcsDriver
public function getChangeDate(string $identifier): ?\DateTimeImmutable public function getChangeDate(string $identifier): ?\DateTimeImmutable
{ {
$this->process->execute( $this->process->execute(
sprintf( ['hg', 'log', '--template', '{date|rfc3339date}', '-r', $identifier],
'hg log --template "{date|rfc3339date}" -r %s',
ProcessExecutor::escape($identifier)
),
$output, $output,
$this->repoDir $this->repoDir
); );
@ -166,7 +163,7 @@ class HgDriver extends VcsDriver
if (null === $this->tags) { if (null === $this->tags) {
$tags = []; $tags = [];
$this->process->execute('hg tags', $output, $this->repoDir); $this->process->execute(['hg', 'tags'], $output, $this->repoDir);
foreach ($this->process->splitLines($output) as $tag) { foreach ($this->process->splitLines($output) as $tag) {
if ($tag && Preg::isMatchStrictGroups('(^([^\s]+)\s+\d+:(.*)$)', $tag, $match)) { if ($tag && Preg::isMatchStrictGroups('(^([^\s]+)\s+\d+:(.*)$)', $tag, $match)) {
$tags[$match[1]] = $match[2]; $tags[$match[1]] = $match[2];
@ -189,14 +186,14 @@ class HgDriver extends VcsDriver
$branches = []; $branches = [];
$bookmarks = []; $bookmarks = [];
$this->process->execute('hg branches', $output, $this->repoDir); $this->process->execute(['hg', 'branches'], $output, $this->repoDir);
foreach ($this->process->splitLines($output) as $branch) { foreach ($this->process->splitLines($output) as $branch) {
if ($branch && Preg::isMatchStrictGroups('(^([^\s]+)\s+\d+:([a-f0-9]+))', $branch, $match) && $match[1][0] !== '-') { if ($branch && Preg::isMatchStrictGroups('(^([^\s]+)\s+\d+:([a-f0-9]+))', $branch, $match) && $match[1][0] !== '-') {
$branches[$match[1]] = $match[2]; $branches[$match[1]] = $match[2];
} }
} }
$this->process->execute('hg bookmarks', $output, $this->repoDir); $this->process->execute(['hg', 'bookmarks'], $output, $this->repoDir);
foreach ($this->process->splitLines($output) as $branch) { foreach ($this->process->splitLines($output) as $branch) {
if ($branch && Preg::isMatchStrictGroups('(^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$)', $branch, $match) && $match[1][0] !== '-') { if ($branch && Preg::isMatchStrictGroups('(^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$)', $branch, $match) && $match[1][0] !== '-') {
$bookmarks[$match[1]] = $match[2]; $bookmarks[$match[1]] = $match[2];
@ -228,7 +225,7 @@ class HgDriver extends VcsDriver
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
// check whether there is a hg repo in that path // check whether there is a hg repo in that path
if ($process->execute('hg summary', $output, $url) === 0) { if ($process->execute(['hg', 'summary'], $output, $url) === 0) {
return true; return true;
} }
} }
@ -238,7 +235,7 @@ class HgDriver extends VcsDriver
} }
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
$exit = $process->execute(sprintf('hg identify -- %s', ProcessExecutor::escape($url)), $ignored); $exit = $process->execute(['hg', 'identify', '--', $url], $ignored);
return $exit === 0; return $exit === 0;
} }

View File

@ -187,7 +187,7 @@ class SvnDriver extends VcsDriver
try { try {
$resource = $path.$file; $resource = $path.$file;
$output = $this->execute('svn cat', $this->baseUrl . $resource . $rev); $output = $this->execute(['svn', 'cat'], $this->baseUrl . $resource . $rev);
if ('' === trim($output)) { if ('' === trim($output)) {
return null; return null;
} }
@ -213,7 +213,7 @@ class SvnDriver extends VcsDriver
$rev = ''; $rev = '';
} }
$output = $this->execute('svn info', $this->baseUrl . $path . $rev); $output = $this->execute(['svn', 'info'], $this->baseUrl . $path . $rev);
foreach ($this->process->splitLines($output) as $line) { foreach ($this->process->splitLines($output) as $line) {
if ($line !== '' && Preg::isMatchStrictGroups('{^Last Changed Date: ([^(]+)}', $line, $match)) { if ($line !== '' && Preg::isMatchStrictGroups('{^Last Changed Date: ([^(]+)}', $line, $match)) {
return new \DateTimeImmutable($match[1], new \DateTimeZone('UTC')); return new \DateTimeImmutable($match[1], new \DateTimeZone('UTC'));
@ -232,7 +232,7 @@ class SvnDriver extends VcsDriver
$tags = []; $tags = [];
if ($this->tagsPath !== false) { if ($this->tagsPath !== false) {
$output = $this->execute('svn ls --verbose', $this->baseUrl . '/' . $this->tagsPath); $output = $this->execute(['svn', 'ls', '--verbose'], $this->baseUrl . '/' . $this->tagsPath);
if ($output !== '') { if ($output !== '') {
$lastRev = 0; $lastRev = 0;
foreach ($this->process->splitLines($output) as $line) { foreach ($this->process->splitLines($output) as $line) {
@ -271,7 +271,7 @@ class SvnDriver extends VcsDriver
$trunkParent = $this->baseUrl . '/' . $this->trunkPath; $trunkParent = $this->baseUrl . '/' . $this->trunkPath;
} }
$output = $this->execute('svn ls --verbose', $trunkParent); $output = $this->execute(['svn', 'ls', '--verbose'], $trunkParent);
if ($output !== '') { if ($output !== '') {
foreach ($this->process->splitLines($output) as $line) { foreach ($this->process->splitLines($output) as $line) {
$line = trim($line); $line = trim($line);
@ -290,7 +290,7 @@ class SvnDriver extends VcsDriver
unset($output); unset($output);
if ($this->branchesPath !== false) { if ($this->branchesPath !== false) {
$output = $this->execute('svn ls --verbose', $this->baseUrl . '/' . $this->branchesPath); $output = $this->execute(['svn', 'ls', '--verbose'], $this->baseUrl . '/' . $this->branchesPath);
if ($output !== '') { if ($output !== '') {
$lastRev = 0; $lastRev = 0;
foreach ($this->process->splitLines(trim($output)) as $line) { foreach ($this->process->splitLines(trim($output)) as $line) {
@ -331,10 +331,7 @@ class SvnDriver extends VcsDriver
} }
$process = new ProcessExecutor($io); $process = new ProcessExecutor($io);
$exit = $process->execute( $exit = $process->execute(['svn', 'info', '--non-interactive', '--', $url], $ignoredOutput);
"svn info --non-interactive -- ".ProcessExecutor::escape($url),
$ignoredOutput
);
if ($exit === 0) { if ($exit === 0) {
// This is definitely a Subversion repository. // This is definitely a Subversion repository.
@ -375,11 +372,11 @@ class SvnDriver extends VcsDriver
* Execute an SVN command and try to fix up the process with credentials * Execute an SVN command and try to fix up the process with credentials
* if necessary. * if necessary.
* *
* @param string $command The svn command to run. * @param non-empty-list<string> $command The svn command to run.
* @param string $url The SVN URL. * @param string $url The SVN URL.
* @throws \RuntimeException * @throws \RuntimeException
*/ */
protected function execute(string $command, string $url): string protected function execute(array $command, string $url): string
{ {
if (null === $this->util) { if (null === $this->util) {
$this->util = new SvnUtil($this->baseUrl, $this->io, $this->config, $this->process); $this->util = new SvnUtil($this->baseUrl, $this->io, $this->config, $this->process);

View File

@ -77,7 +77,7 @@ class Bitbucket
} }
// if available use token from git config // if available use token from git config
if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) { if (0 === $this->process->execute(['git', 'config', 'bitbucket.accesstoken'], $output)) {
$this->io->setAuthentication($originUrl, 'x-token-auth', trim($output)); $this->io->setAuthentication($originUrl, 'x-token-auth', trim($output));
return true; return true;

View File

@ -109,9 +109,9 @@ class Filesystem
} }
if (Platform::isWindows()) { if (Platform::isWindows()) {
$cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); $cmd = ['rmdir', '/S', '/Q', Platform::realpath($directory)];
} else { } else {
$cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); $cmd = ['rm', '-rf', $directory];
} }
$result = $this->getProcess()->execute($cmd, $output) === 0; $result = $this->getProcess()->execute($cmd, $output) === 0;
@ -144,9 +144,9 @@ class Filesystem
} }
if (Platform::isWindows()) { if (Platform::isWindows()) {
$cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); $cmd = ['rmdir', '/S', '/Q', Platform::realpath($directory)];
} else { } else {
$cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); $cmd = ['rm', '-rf', $directory];
} }
$promise = $this->getProcess()->executeAsync($cmd); $promise = $this->getProcess()->executeAsync($cmd);
@ -427,8 +427,7 @@ class Filesystem
if (Platform::isWindows()) { if (Platform::isWindows()) {
// Try to copy & delete - this is a workaround for random "Access denied" errors. // Try to copy & delete - this is a workaround for random "Access denied" errors.
$command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->getProcess()->execute(['xcopy', $source, $target, '/E', '/I', '/Q', '/Y'], $output);
$result = $this->getProcess()->execute($command, $output);
// clear stat cache because external processes aren't tracked by the php stat cache // clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache(); clearstatcache();
@ -441,8 +440,7 @@ class Filesystem
} else { } else {
// We do not use PHP's "rename" function here since it does not support // We do not use PHP's "rename" function here since it does not support
// the case where $source, and $target are located on different partitions. // the case where $source, and $target are located on different partitions.
$command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->getProcess()->execute(['mv', $source, $target], $output);
$result = $this->getProcess()->execute($command, $output);
// clear stat cache because external processes aren't tracked by the php stat cache // clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache(); clearstatcache();
@ -841,11 +839,7 @@ class Filesystem
@rmdir($junction); @rmdir($junction);
} }
$cmd = sprintf( $cmd = ['mklink', '/J', str_replace('/', DIRECTORY_SEPARATOR, $junction), Platform::realpath($target)];
'mklink /J %s %s',
ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)),
ProcessExecutor::escape(realpath($target))
);
if ($this->getProcess()->execute($cmd, $output) !== 0) { if ($this->getProcess()->execute($cmd, $output) !== 0) {
throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 0, null, $target); throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 0, null, $target);
} }

View File

@ -63,26 +63,90 @@ class Git
} }
/** /**
* Runs a set of commands using the $url or a variation of it (with auth, ssh, ..)
*
* Commands should use %url% placeholders for the URL instead of inlining it to allow this function to do its job
* %sanitizedUrl% is also automatically replaced by the url without user/pass
*
* As soon as a single command fails it will halt, so assume the commands are run as && in bash
*
* @param non-empty-array<non-empty-list<string>> $commands
* @param mixed $commandOutput the output will be written into this var if passed by ref
* if a callable is passed it will be used as output handler
*/
public function runCommands(array $commands, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void
{
$callables = [];
foreach ($commands as $cmd) {
$callables[] = static function (string $url) use ($cmd): array {
$map = [
'%url%' => $url,
'%sanitizedUrl%' => Preg::replace('{://([^@]+?):(.+?)@}', '://', $url),
];
return array_map(static function ($value) use ($map): string {
return $map[$value] ?? $value;
}, $cmd);
};
}
// @phpstan-ignore method.deprecated
$this->runCommand($callables, $url, $cwd, $initialClone, $commandOutput);
}
/**
* @param callable|array<callable> $commandCallable
* @param mixed $commandOutput the output will be written into this var if passed by ref * @param mixed $commandOutput the output will be written into this var if passed by ref
* if a callable is passed it will be used as output handler * if a callable is passed it will be used as output handler
* @deprecated Use runCommands with placeholders instead of callbacks for simplicity
*/ */
public function runCommand(callable $commandCallable, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void public function runCommand($commandCallable, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void
{ {
$commandCallables = is_callable($commandCallable) ? [$commandCallable] : $commandCallable;
$lastCommand = '';
// Ensure we are allowed to use this URL by config // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);
if ($initialClone) { if ($initialClone) {
$origCwd = $cwd; $origCwd = $cwd;
$cwd = null;
} }
$runCommands = function ($url) use ($commandCallables, $cwd, &$commandOutput, &$lastCommand, $initialClone) {
$collectOutputs = !is_callable($commandOutput);
$outputs = [];
$status = 0;
$counter = 0;
foreach ($commandCallables as $callable) {
$lastCommand = $callable($url);
if ($collectOutputs) {
$outputs[] = '';
$output = &$outputs[count($outputs) - 1];
} else {
$output = &$commandOutput;
}
$status = $this->process->execute($lastCommand, $output, $initialClone && $counter === 0 ? null : $cwd);
if ($status !== 0) {
break;
}
$counter++;
}
if ($collectOutputs) {
$commandOutput = implode('', $outputs);
}
return $status;
};
if (Preg::isMatch('{^ssh://[^@]+@[^:]+:[^0-9]+}', $url)) { if (Preg::isMatch('{^ssh://[^@]+@[^:]+:[^0-9]+}', $url)) {
throw new \InvalidArgumentException('The source URL ' . $url . ' is invalid, ssh URLs should have a port number after ":".' . "\n" . 'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); throw new \InvalidArgumentException('The source URL ' . $url . ' is invalid, ssh URLs should have a port number after ":".' . "\n" . 'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.');
} }
if (!$initialClone) { if (!$initialClone) {
// capture username/password from URL if there is one and we have no auth configured yet // capture username/password from URL if there is one and we have no auth configured yet
$this->process->execute('git remote -v', $output, $cwd); $this->process->execute(['git', 'remote', '-v'], $output, $cwd);
if (Preg::isMatchStrictGroups('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match) && !$this->io->hasAuthentication($match[3])) { if (Preg::isMatchStrictGroups('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match) && !$this->io->hasAuthentication($match[3])) {
$this->io->setAuthentication($match[3], rawurldecode($match[1]), rawurldecode($match[2])); $this->io->setAuthentication($match[3], rawurldecode($match[1]), rawurldecode($match[2]));
} }
@ -100,7 +164,7 @@ class Git
$protoUrl = $protocol . "://" . $match[1] . "/" . $match[2]; $protoUrl = $protocol . "://" . $match[1] . "/" . $match[2];
} }
if (0 === $this->process->execute($commandCallable($protoUrl), $commandOutput, $cwd)) { if (0 === $runCommands($protoUrl)) {
return; return;
} }
$messages[] = '- ' . $protoUrl . "\n" . Preg::replace('#^#m', ' ', $this->process->getErrorOutput()); $messages[] = '- ' . $protoUrl . "\n" . Preg::replace('#^#m', ' ', $this->process->getErrorOutput());
@ -119,11 +183,9 @@ class Git
// if we have a private github url and the ssh protocol is disabled then we skip it and directly fallback to https // if we have a private github url and the ssh protocol is disabled then we skip it and directly fallback to https
$bypassSshForGitHub = Preg::isMatch('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true); $bypassSshForGitHub = Preg::isMatch('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true);
$command = $commandCallable($url);
$auth = null; $auth = null;
$credentials = []; $credentials = [];
if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $commandOutput, $cwd)) { if ($bypassSshForGitHub || 0 !== $runCommands($url)) {
$errorMsg = $this->process->getErrorOutput(); $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
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
@ -143,8 +205,7 @@ class Git
if ($this->io->hasAuthentication($match[1])) { if ($this->io->hasAuthentication($match[1])) {
$auth = $this->io->getAuthentication($match[1]); $auth = $this->io->getAuthentication($match[1]);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git';
$command = $commandCallable($authUrl); if (0 === $runCommands($authUrl)) {
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
return; return;
} }
@ -180,8 +241,7 @@ class Git
$auth = $this->io->getAuthentication($domain); $auth = $this->io->getAuthentication($domain);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part; $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part;
$command = $commandCallable($authUrl); if (0 === $runCommands($authUrl)) {
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
// Well if that succeeded on our first try, let's just // Well if that succeeded on our first try, let's just
// take the win. // take the win.
return; return;
@ -199,8 +259,7 @@ class Git
if ($this->io->hasAuthentication($domain)) { if ($this->io->hasAuthentication($domain)) {
$auth = $this->io->getAuthentication($domain); $auth = $this->io->getAuthentication($domain);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part; $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part;
$command = $commandCallable($authUrl); if (0 === $runCommands($authUrl)) {
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
return; return;
} }
@ -209,8 +268,7 @@ class Git
//Falling back to ssh //Falling back to ssh
$sshUrl = 'git@bitbucket.org:' . $repo_with_git_part; $sshUrl = 'git@bitbucket.org:' . $repo_with_git_part;
$this->io->writeError(' No bitbucket authentication configured. Falling back to ssh.'); $this->io->writeError(' No bitbucket authentication configured. Falling back to ssh.');
$command = $commandCallable($sshUrl); if (0 === $runCommands($sshUrl)) {
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
return; return;
} }
@ -242,8 +300,7 @@ class Git
$authUrl = $match[1] . '://' . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . '/' . $match[3]; $authUrl = $match[1] . '://' . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . '/' . $match[3];
} }
$command = $commandCallable($authUrl); if (0 === $runCommands($authUrl)) {
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
return; return;
} }
@ -280,8 +337,7 @@ class Git
if (null !== $auth) { if (null !== $auth) {
$authUrl = $match[1] . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . $match[3]; $authUrl = $match[1] . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . $match[3];
$command = $commandCallable($authUrl); if (0 === $runCommands($authUrl)) {
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
$this->io->setAuthentication($match[2], $auth['username'], $auth['password']); $this->io->setAuthentication($match[2], $auth['username'], $auth['password']);
$authHelper = new AuthHelper($this->io, $this->config); $authHelper = new AuthHelper($this->io, $this->config);
$authHelper->storeAuth($match[2], $storeAuth); $authHelper->storeAuth($match[2], $storeAuth);
@ -298,11 +354,12 @@ class Git
$this->filesystem->removeDirectory($origCwd); $this->filesystem->removeDirectory($origCwd);
} }
$lastCommand = implode(' ', $lastCommand);
if (count($credentials) > 0) { if (count($credentials) > 0) {
$command = $this->maskCredentials($command, $credentials); $lastCommand = $this->maskCredentials($lastCommand, $credentials);
$errorMsg = $this->maskCredentials($errorMsg, $credentials); $errorMsg = $this->maskCredentials($errorMsg, $credentials);
} }
$this->throwException('Failed to execute ' . $command . "\n\n" . $errorMsg, $url); $this->throwException('Failed to execute ' . $lastCommand . "\n\n" . $errorMsg, $url);
} }
} }
@ -315,14 +372,16 @@ class Git
} }
// update the repo if it is a valid git repository // update the repo if it is a valid git repository
if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { if (is_dir($dir) && 0 === $this->process->execute(['git', 'rev-parse', '--git-dir'], $output, $dir) && trim($output) === '.') {
try { try {
$commandCallable = static function ($url): string { $commands = [
$sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url); ['git', 'remote', 'set-url', 'origin', '--', '%url%'],
['git', 'remote', 'update', '--prune', 'origin'],
['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'],
['git', 'gc', '--auto'],
];
return sprintf('git remote set-url origin -- %s && git remote update --prune origin && git remote set-url origin -- %s && git gc --auto', ProcessExecutor::escape($url), ProcessExecutor::escape($sanitizedUrl)); $this->runCommands($commands, $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); $this->io->writeError('<error>Sync mirror failed: ' . $e->getMessage() . '</error>', true, IOInterface::DEBUG);
@ -336,11 +395,7 @@ class Git
// clean up directory and do a fresh clone into it // clean up directory and do a fresh clone into it
$this->filesystem->removeDirectory($dir); $this->filesystem->removeDirectory($dir);
$commandCallable = static function ($url) use ($dir): string { $this->runCommands([['git', 'clone', '--mirror', '--', '%url%', $dir]], $url, $dir, true);
return sprintf('git clone --mirror -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($dir));
};
$this->runCommand($commandCallable, $url, $dir, true);
return true; return true;
} }
@ -352,10 +407,10 @@ class Git
$branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion); $branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion);
$branches = null; $branches = null;
$tags = null; $tags = null;
if (0 === $this->process->execute('git branch', $output, $dir)) { if (0 === $this->process->execute(['git', 'branch'], $output, $dir)) {
$branches = $output; $branches = $output;
} }
if (0 === $this->process->execute('git tag', $output, $dir)) { if (0 === $this->process->execute(['git', 'tag'], $output, $dir)) {
$tags = $output; $tags = $output;
} }
@ -390,11 +445,23 @@ class Git
return ''; return '';
} }
/**
* @return list<string>
*/
public static function getNoShowSignatureFlags(ProcessExecutor $process): array
{
$flags = static::getNoShowSignatureFlag($process);
if ('' === $flags) {
return [];
}
return explode(' ', substr($flags, 1));
}
private function checkRefIsInMirror(string $dir, string $ref): bool private function checkRefIsInMirror(string $dir, string $ref): bool
{ {
if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { 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(['git', 'rev-parse', '--quiet', '--verify', $ref.'^{commit}'], $ignoredOutput, $dir);
$exitCode = $this->process->execute(sprintf('git rev-parse --quiet --verify %s', $escapedRef), $ignoredOutput, $dir);
if ($exitCode === 0) { if ($exitCode === 0) {
return true; return true;
} }
@ -439,15 +506,15 @@ class Git
try { try {
if ($isLocalPathRepository) { if ($isLocalPathRepository) {
$this->process->execute('git remote show origin', $output, $dir); $this->process->execute(['git', 'remote', 'show', 'origin'], $output, $dir);
} else { } else {
$commandCallable = static function ($url): string { $commands = [
$sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url); ['git', 'remote', 'set-url', 'origin', '--', '%url%'],
['git', 'remote', 'show', 'origin'],
['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'],
];
return sprintf('git remote set-url origin -- %s && git remote show origin && git remote set-url origin -- %s', ProcessExecutor::escape($url), ProcessExecutor::escape($sanitizedUrl)); $this->runCommands($commands, $url, $dir, false, $output);
};
$this->runCommand($commandCallable, $url, $dir, false, $output);
} }
$lines = $this->process->splitLines($output); $lines = $this->process->splitLines($output);
@ -513,7 +580,7 @@ class Git
// git might delete a directory when it fails and php will not know // git might delete a directory when it fails and php will not know
clearstatcache(); clearstatcache();
if (0 !== $this->process->execute('git --version', $ignoredOutput)) { if (0 !== $this->process->execute(['git', '--version'], $ignoredOutput)) {
throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()));
} }
@ -529,7 +596,7 @@ class Git
{ {
if (false === self::$version) { if (false === self::$version) {
self::$version = null; self::$version = null;
if (0 === $process->execute('git --version', $output) && Preg::isMatch('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) { if (0 === $process->execute(['git', '--version'], $output) && Preg::isMatch('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) {
self::$version = $matches[1]; self::$version = $matches[1];
} }
} }

View File

@ -61,7 +61,7 @@ class GitHub
} }
// if available use token from git config // if available use token from git config
if (0 === $this->process->execute('git config github.accesstoken', $output)) { if (0 === $this->process->execute(['git', 'config', 'github.accesstoken'], $output)) {
$this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic'); $this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic');
return true; return true;
@ -86,7 +86,7 @@ class GitHub
} }
$note = 'Composer'; $note = 'Composer';
if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) { if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute(['hostname'], $output)) {
$note .= ' on ' . trim($output); $note .= ' on ' . trim($output);
} }
$note .= ' ' . date('Y-m-d Hi'); $note .= ' ' . date('Y-m-d Hi');

View File

@ -65,14 +65,14 @@ class GitLab
} }
// if available use token from git config // if available use token from git config
if (0 === $this->process->execute('git config gitlab.accesstoken', $output)) { if (0 === $this->process->execute(['git', 'config', 'gitlab.accesstoken'], $output)) {
$this->io->setAuthentication($originUrl, trim($output), 'oauth2'); $this->io->setAuthentication($originUrl, trim($output), 'oauth2');
return true; return true;
} }
// if available use deploy token from git config // if available use deploy token from git config
if (0 === $this->process->execute('git config gitlab.deploytoken.user', $tokenUser) && 0 === $this->process->execute('git config gitlab.deploytoken.token', $tokenPassword)) { if (0 === $this->process->execute(['git', 'config', 'gitlab.deploytoken.user'], $tokenUser) && 0 === $this->process->execute(['git', 'config', 'gitlab.deploytoken.token'], $tokenPassword)) {
$this->io->setAuthentication($originUrl, trim($tokenUser), trim($tokenPassword)); $this->io->setAuthentication($originUrl, trim($tokenUser), trim($tokenPassword));
return true; return true;

View File

@ -111,7 +111,7 @@ class Hg
{ {
if (false === self::$version) { if (false === self::$version) {
self::$version = null; self::$version = null;
if (0 === $process->execute('hg --version', $output) && Preg::isMatch('/^.+? (\d+(?:\.\d+)+)(?:\+.*?)?\)?\r?\n/', $output, $matches)) { if (0 === $process->execute(['hg', '--version'], $output) && Preg::isMatch('/^.+? (\d+(?:\.\d+)+)(?:\+.*?)?\)?\r?\n/', $output, $matches)) {
self::$version = $matches[1]; self::$version = $matches[1];
} }
} }

View File

@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
/** /**
@ -81,7 +82,7 @@ class Perforce
public static function checkServerExists(string $url, ProcessExecutor $processExecutor): bool public static function checkServerExists(string $url, ProcessExecutor $processExecutor): bool
{ {
return 0 === $processExecutor->execute('p4 -p ' . ProcessExecutor::escape($url) . ' info -s', $ignoredOutput); return 0 === $processExecutor->execute(['p4', '-p', $url, 'info', '-s'], $ignoredOutput);
} }
/** /**
@ -248,7 +249,7 @@ class Perforce
} }
$this->p4User = $this->io->ask('Enter P4 User:'); $this->p4User = $this->io->ask('Enter P4 User:');
if ($this->windowsFlag) { if ($this->windowsFlag) {
$command = 'p4 set P4USER=' . $this->p4User; $command = $this->getP4Executable().' set P4USER=' . $this->p4User;
} else { } else {
$command = 'export P4USER=' . $this->p4User; $command = 'export P4USER=' . $this->p4User;
} }
@ -261,7 +262,7 @@ class Perforce
protected function getP4variable(string $name): ?string protected function getP4variable(string $name): ?string
{ {
if ($this->windowsFlag) { if ($this->windowsFlag) {
$command = 'p4 set'; $command = $this->getP4Executable().' set';
$this->executeCommand($command); $this->executeCommand($command);
$result = trim($this->commandResult); $result = trim($this->commandResult);
$resArray = explode(PHP_EOL, $result); $resArray = explode(PHP_EOL, $result);
@ -309,7 +310,7 @@ class Perforce
*/ */
public function generateP4Command(string $command, bool $useClient = true): string public function generateP4Command(string $command, bool $useClient = true): string
{ {
$p4Command = 'p4 '; $p4Command = $this->getP4Executable().' ';
$p4Command .= '-u ' . $this->getUser() . ' '; $p4Command .= '-u ' . $this->getUser() . ' ';
if ($useClient) { if ($useClient) {
$p4Command .= '-c ' . $this->getClient() . ' '; $p4Command .= '-c ' . $this->getClient() . ' ';
@ -620,4 +621,17 @@ class Perforce
{ {
$this->filesystem = $fs; $this->filesystem = $fs;
} }
private function getP4Executable(): string
{
static $p4Executable;
if ($p4Executable) {
return $p4Executable;
}
$finder = new ExecutableFinder();
return $p4Executable = $finder->find('p4') ?? 'p4';
}
} }

View File

@ -54,6 +54,19 @@ class Platform
return $cwd; return $cwd;
} }
/**
* Infallible realpath version that falls back on the given $path if realpath is not working
*/
public static function realpath(string $path): string
{
$realPath = realpath($path);
if ($realPath === false) {
return $path;
}
return $realPath;
}
/** /**
* getenv() equivalent but reads from the runtime global variables first * getenv() equivalent but reads from the runtime global variables first
* *
@ -308,7 +321,7 @@ class Platform
if (defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Linux') { if (defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Linux') {
$process = new ProcessExecutor(); $process = new ProcessExecutor();
try { try {
if (0 === $process->execute('lsmod | grep vboxguest', $ignoredOutput)) { if (0 === $process->execute(['lsmod'], $output) && str_contains($output, 'vboxguest')) {
return self::$isVirtualBoxGuest = true; return self::$isVirtualBoxGuest = true;
} }
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -20,6 +20,7 @@ use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Exception\RuntimeException;
use React\Promise\Promise; use React\Promise\Promise;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
use Symfony\Component\Process\ExecutableFinder;
/** /**
* @author Robert Schönthal <seroscho@googlemail.com> * @author Robert Schönthal <seroscho@googlemail.com>
@ -33,6 +34,14 @@ class ProcessExecutor
private const STATUS_FAILED = 4; private const STATUS_FAILED = 4;
private const STATUS_ABORTED = 5; private const STATUS_ABORTED = 5;
private const BUILTIN_CMD_COMMANDS = [
'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date',
'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto',
'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause',
'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set',
'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol',
];
private const GIT_CMDS_NEED_GIT_DIR = [ private const GIT_CMDS_NEED_GIT_DIR = [
['show'], ['show'],
['log'], ['log'],
@ -63,6 +72,9 @@ class ProcessExecutor
/** @var bool */ /** @var bool */
private $allowAsync = false; private $allowAsync = false;
/** @var array<string, string> */
private static $executables = [];
public function __construct(?IOInterface $io = null) public function __construct(?IOInterface $io = null)
{ {
$this->io = $io; $this->io = $io;
@ -71,7 +83,7 @@ class ProcessExecutor
/** /**
* runs a process on the commandline * runs a process on the commandline
* *
* @param string|list<string> $command the command to execute * @param string|non-empty-list<string> $command the command to execute
* @param mixed $output the output will be written into this var if passed by ref * @param mixed $output the output will be written into this var if passed by ref
* if a callable is passed it will be used as output handler * if a callable is passed it will be used as output handler
* @param null|string $cwd the working directory * @param null|string $cwd the working directory
@ -89,7 +101,7 @@ class ProcessExecutor
/** /**
* runs a process on the commandline in TTY mode * runs a process on the commandline in TTY mode
* *
* @param string|list<string> $command the command to execute * @param string|non-empty-list<string> $command the command to execute
* @param null|string $cwd the working directory * @param null|string $cwd the working directory
* @return int statuscode * @return int statuscode
*/ */
@ -103,15 +115,26 @@ class ProcessExecutor
} }
/** /**
* @param string|list<string> $command * @param string|non-empty-list<string> $command
* @param array<string, string>|null $env * @param array<string, string>|null $env
* @param mixed $output * @param mixed $output
*/ */
private function runProcess($command, ?string $cwd, ?array $env, bool $tty, &$output = null): ?int private function runProcess($command, ?string $cwd, ?array $env, bool $tty, &$output = null): ?int
{ {
// On Windows, we don't rely on the OS to find the executable if possible to avoid lookups
// in the current directory which could be untrusted. Instead we use the ExecutableFinder.
if (is_string($command)) { if (is_string($command)) {
if (Platform::isWindows() && Preg::isMatch('{^([^:/\\\\]++) }', $command, $match)) {
$command = substr_replace($command, self::escape(self::getExecutable($match[1])), 0, strlen($match[1]));
}
$process = Process::fromShellCommandline($command, $cwd, $env, null, static::getTimeout()); $process = Process::fromShellCommandline($command, $cwd, $env, null, static::getTimeout());
} else { } else {
if (Platform::isWindows() && \strlen($command[0]) === strcspn($command[0], ':/\\')) {
$command[0] = self::getExecutable($command[0]);
}
$process = new Process($command, $cwd, $env, null, static::getTimeout()); $process = new Process($command, $cwd, $env, null, static::getTimeout());
} }
@ -161,7 +184,7 @@ class ProcessExecutor
} }
/** /**
* @param string|list<string> $command * @param string|non-empty-list<string> $command
* @param mixed $output * @param mixed $output
*/ */
private function doExecute($command, ?string $cwd, bool $tty, &$output = null): int private function doExecute($command, ?string $cwd, bool $tty, &$output = null): int
@ -178,7 +201,7 @@ class ProcessExecutor
$isBareRepository = !is_dir(sprintf('%s/.git', rtrim($cwd, '/'))); $isBareRepository = !is_dir(sprintf('%s/.git', rtrim($cwd, '/')));
if ($isBareRepository) { if ($isBareRepository) {
$configValue = ''; $configValue = '';
$this->runProcess('git config safe.bareRepository', $cwd, ['GIT_DIR' => $cwd], $tty, $configValue); $this->runProcess(['git', 'config', 'safe.bareRepository'], $cwd, ['GIT_DIR' => $cwd], $tty, $configValue);
$configValue = trim($configValue); $configValue = trim($configValue);
if ($configValue === 'explicit') { if ($configValue === 'explicit') {
$env = ['GIT_DIR' => $cwd]; $env = ['GIT_DIR' => $cwd];
@ -550,4 +573,23 @@ class ProcessExecutor
return false; return false;
} }
/**
* Resolves executable paths on Windows
*/
private static function getExecutable(string $name): string
{
if (\in_array(strtolower($name), self::BUILTIN_CMD_COMMANDS, true)) {
return $name;
}
if (!isset(self::$executables[$name])) {
$path = (new ExecutableFinder())->find($name, $name);
if ($path !== null) {
self::$executables[$name] = $path;
}
}
return self::$executables[$name] ?? $name;
}
} }

View File

@ -90,7 +90,7 @@ class Svn
* Execute an SVN remote command and try to fix up the process with credentials * Execute an SVN remote command and try to fix up the process with credentials
* if necessary. * if necessary.
* *
* @param string $command SVN command to run * @param non-empty-list<string> $command SVN command to run
* @param string $url SVN url * @param string $url SVN url
* @param ?string $cwd Working directory * @param ?string $cwd Working directory
* @param ?string $path Target for a checkout * @param ?string $path Target for a checkout
@ -98,7 +98,7 @@ class Svn
* *
* @throws \RuntimeException * @throws \RuntimeException
*/ */
public function execute(string $command, string $url, ?string $cwd = null, ?string $path = null, bool $verbose = false): string public function execute(array $command, string $url, ?string $cwd = null, ?string $path = null, bool $verbose = false): string
{ {
// Ensure we are allowed to use this URL by config // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);
@ -110,20 +110,23 @@ class Svn
* Execute an SVN local command and try to fix up the process with credentials * Execute an SVN local command and try to fix up the process with credentials
* if necessary. * if necessary.
* *
* @param string $command SVN command to run * @param non-empty-list<string> $command SVN command to run
* @param string $path Path argument passed thru to the command * @param string $path Path argument passed thru to the command
* @param string $cwd Working directory * @param string $cwd Working directory
* @param bool $verbose Output all output to the user * @param bool $verbose Output all output to the user
* *
* @throws \RuntimeException * @throws \RuntimeException
*/ */
public function executeLocal(string $command, string $path, ?string $cwd = null, bool $verbose = false): string public function executeLocal(array $command, string $path, ?string $cwd = null, bool $verbose = false): string
{ {
// A local command has no remote url // A local command has no remote url
return $this->executeWithAuthRetry($command, $cwd, '', $path, $verbose); return $this->executeWithAuthRetry($command, $cwd, '', $path, $verbose);
} }
private function executeWithAuthRetry(string $svnCommand, ?string $cwd, string $url, ?string $path, bool $verbose): ?string /**
* @param non-empty-list<string> $svnCommand
*/
private function executeWithAuthRetry(array $svnCommand, ?string $cwd, string $url, ?string $path, bool $verbose): ?string
{ {
// Regenerate the command at each try, to use the newly user-provided credentials // Regenerate the command at each try, to use the newly user-provided credentials
$command = $this->getCommand($svnCommand, $url, $path); $command = $this->getCommand($svnCommand, $url, $path);
@ -209,22 +212,23 @@ class Svn
/** /**
* A method to create the svn commands run. * A method to create the svn commands run.
* *
* @param string $cmd Usually 'svn ls' or something like that. * @param non-empty-list<string> $cmd Usually 'svn ls' or something like that.
* @param string $url Repo URL. * @param string $url Repo URL.
* @param string $path Target for a checkout * @param string $path Target for a checkout
*
* @return non-empty-list<string>
*/ */
protected function getCommand(string $cmd, string $url, ?string $path = null): string protected function getCommand(array $cmd, string $url, ?string $path = null): array
{ {
$cmd = sprintf( $cmd = array_merge(
'%s %s%s -- %s',
$cmd, $cmd,
'--non-interactive ', ['--non-interactive'],
$this->getCredentialString(), $this->getCredentialArgs(),
ProcessExecutor::escape($url) ['--', $url]
); );
if ($path) { if ($path !== null) {
$cmd .= ' ' . ProcessExecutor::escape($path); $cmd[] = $path;
} }
return $cmd; return $cmd;
@ -234,18 +238,18 @@ class Svn
* Return the credential string for the svn command. * Return the credential string for the svn command.
* *
* Adds --no-auth-cache when credentials are present. * Adds --no-auth-cache when credentials are present.
*
* @return list<string>
*/ */
protected function getCredentialString(): string protected function getCredentialArgs(): array
{ {
if (!$this->hasAuth()) { if (!$this->hasAuth()) {
return ''; return [];
} }
return sprintf( return array_merge(
' %s--username %s --password %s ', $this->getAuthCacheArgs(),
$this->getAuthCache(), ['--username', $this->getUsername(), '--password', $this->getPassword()]
ProcessExecutor::escape($this->getUsername()),
ProcessExecutor::escape($this->getPassword())
); );
} }
@ -295,10 +299,12 @@ class Svn
/** /**
* Return the no-auth-cache switch. * Return the no-auth-cache switch.
*
* @return list<string>
*/ */
protected function getAuthCache(): string protected function getAuthCacheArgs(): array
{ {
return $this->cacheCredentials ? '' : '--no-auth-cache '; return $this->cacheCredentials ? [] : ['--no-auth-cache'];
} }
/** /**
@ -349,7 +355,7 @@ class Svn
public function binaryVersion(): ?string public function binaryVersion(): ?string
{ {
if (!self::$version) { if (!self::$version) {
if (0 === $this->process->execute('svn --version', $output)) { if (0 === $this->process->execute(['svn', '--version'], $output)) {
if (Preg::isMatch('{(\d+(?:\.\d+)+)}', $output, $match)) { if (Preg::isMatch('{(\d+(?:\.\d+)+)}', $output, $match)) {
self::$version = $match[1]; self::$version = $match[1];
} }

View File

@ -87,6 +87,7 @@ class AllFunctionalTest extends TestCase
} }
$proc = new Process([PHP_BINARY, '-dphar.readonly=0', './bin/compile'], $target); $proc = new Process([PHP_BINARY, '-dphar.readonly=0', './bin/compile'], $target);
$proc->setTimeout(300);
$exitcode = $proc->run(); $exitcode = $proc->run();
if ($exitcode !== 0 || trim($proc->getOutput()) !== '') { if ($exitcode !== 0 || trim($proc->getOutput()) !== '') {

View File

@ -76,9 +76,9 @@ class FossilDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
self::getCmd('fossil clone -- \'http://fossil.kd2.org/kd2fw/\' \''.$this->workingDir.'.fossil\''), ['fossil', 'clone', '--', 'http://fossil.kd2.org/kd2fw/', $this->workingDir.'.fossil'],
self::getCmd('fossil open --nested -- \''.$this->workingDir.'.fossil\''), ['fossil', 'open', '--nested', '--', $this->workingDir.'.fossil'],
self::getCmd('fossil update -- \'trunk\''), ['fossil', 'update', '--', 'trunk'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, null, $process); $downloader = $this->getDownloaderMock(null, null, $process);
@ -123,8 +123,9 @@ class FossilDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
self::getCmd("fossil changes"), ['fossil', 'changes'],
self::getCmd("fossil pull && fossil up 'trunk'"), ['fossil', 'pull'],
['fossil', 'up', 'trunk'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, null, $process); $downloader = $this->getDownloaderMock(null, null, $process);
@ -143,7 +144,7 @@ class FossilDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
self::getCmd('fossil changes'), ['fossil', 'changes'],
], true); ], true);
$filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock();

View File

@ -122,10 +122,16 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('dev-master')); ->will($this->returnValue('dev-master'));
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath';
$process->expects([ $process->expects([
$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'"), ['git', 'clone', '--no-checkout', '--', 'https://example.com/composer/composer', $expectedPath],
$this->winCompat("git branch -r"), ['git', 'remote', 'add', 'composer', '--', 'https://example.com/composer/composer'],
$this->winCompat("(git checkout 'master' -- || git checkout -B 'master' 'composer/master' --) && git reset --hard '1234567890123456789012345678901234567890' --"), ['git', 'fetch', 'composer'],
['git', 'remote', 'set-url', 'origin', '--', 'https://example.com/composer/composer'],
['git', 'remote', 'set-url', 'composer', '--', 'https://example.com/composer/composer'],
['git', 'branch', '-r'],
['git', 'checkout', 'master', '--'],
['git', 'reset', '--hard', '1234567890123456789012345678901234567890', '--'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, null, $process); $downloader = $this->getDownloaderMock(null, null, $process);
@ -160,16 +166,24 @@ class GitDownloaderTest extends TestCase
$filesystem = new \Composer\Util\Filesystem; $filesystem = new \Composer\Util\Filesystem;
$filesystem->removeDirectory($cachePath); $filesystem->removeDirectory($cachePath);
$expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath';
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
['cmd' => $this->winCompat(sprintf("git clone --mirror -- 'https://example.com/composer/composer' '%s'", $cachePath)), 'callback' => static function () use ($cachePath): void { [
@mkdir($cachePath, 0777, true); 'cmd' => ['git', 'clone', '--mirror', '--', 'https://example.com/composer/composer', $cachePath],
}], 'callback' => static function () use ($cachePath): void {
['cmd' => 'git rev-parse --git-dir', 'stdout' => '.'], @mkdir($cachePath, 0777, true);
$this->winCompat('git rev-parse --quiet --verify \'1234567890123456789012345678901234567890^{commit}\''), }
$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)), ],
'git branch -r', ['cmd' => ['git', 'rev-parse', '--git-dir'], 'stdout' => '.'],
$this->winCompat("(git checkout 'master' -- || git checkout -B 'master' 'composer/master' --) && git reset --hard '1234567890123456789012345678901234567890' --"), ['git', 'rev-parse', '--quiet', '--verify', '1234567890123456789012345678901234567890^{commit}'],
['git', 'clone', '--no-checkout', $cachePath, $expectedPath, '--dissociate', '--reference', $cachePath],
['git', 'remote', 'set-url', 'origin', '--', 'https://example.com/composer/composer'],
['git', 'remote', 'add', 'composer', '--', 'https://example.com/composer/composer'],
['git', 'branch', '-r'],
['cmd' => ['git', 'checkout', 'master', '--'], 'return' => 1],
['git', 'checkout', '-B', 'master', 'composer/master', '--'],
['git', 'reset', '--hard', '1234567890123456789012345678901234567890', '--'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, $config, $process); $downloader = $this->getDownloaderMock(null, $config, $process);
@ -197,17 +211,21 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath';
$process->expects([ $process->expects([
[ ['cmd' => ['git', 'clone', '--no-checkout', '--', 'https://github.com/mirrors/composer', $expectedPath], 'return' => 1, 'stderr' => 'Error1'],
'cmd' => $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'"),
'return' => 1, ['git', 'clone', '--no-checkout', '--', 'git@github.com:mirrors/composer', $expectedPath],
'stderr' => 'Error1', ['git', 'remote', 'add', 'composer', '--', 'git@github.com:mirrors/composer'],
], ['git', 'fetch', 'composer'],
$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'"), ['git', 'remote', 'set-url', 'origin', '--', 'git@github.com:mirrors/composer'],
$this->winCompat("git remote set-url origin -- 'https://github.com/composer/composer'"), ['git', 'remote', 'set-url', 'composer', '--', 'git@github.com:mirrors/composer'],
$this->winCompat("git remote set-url --push origin -- 'git@github.com:composer/composer.git'"),
'git branch -r', ['git', 'remote', 'set-url', 'origin', '--', 'https://github.com/composer/composer'],
$this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), ['git', 'remote', 'set-url', '--push', 'origin', '--', 'git@github.com:composer/composer.git'],
['git', 'branch', '-r'],
['git', 'checkout', 'ref', '--'],
['git', 'reset', '--hard', 'ref', '--'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, new Config(), $process); $downloader = $this->getDownloaderMock(null, new Config(), $process);
@ -250,11 +268,18 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath';
$process->expects([ $process->expects([
$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}'"), ['git', 'clone', '--no-checkout', '--', $url, $expectedPath],
$this->winCompat("git remote set-url --push origin -- '{$pushUrl}'"), ['git', 'remote', 'add', 'composer', '--', $url],
'git branch -r', ['git', 'fetch', 'composer'],
$this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), ['git', 'remote', 'set-url', 'origin', '--', $url],
['git', 'remote', 'set-url', 'composer', '--', $url],
['git', 'remote', 'set-url', '--push', 'origin', '--', $pushUrl],
['git', 'branch', '-r'],
['git', 'checkout', 'ref', '--'],
['git', 'reset', '--hard', 'ref', '--'],
], true); ], true);
$config = new Config(); $config = new Config();
@ -284,28 +309,21 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue('1.0.0')); ->will($this->returnValue('1.0.0'));
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath';
$process->expects([ $process->expects([
[ [
'cmd' => $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'"), 'cmd' => ['git', 'clone', '--no-checkout', '--', 'https://example.com/composer/composer', $expectedPath],
'return' => 1, 'return' => 1,
], ],
]); ]);
// not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe self::expectException('RuntimeException');
try { self::expectExceptionMessage('Failed to execute git clone --no-checkout -- https://example.com/composer/composer '.$expectedPath);
$downloader = $this->getDownloaderMock(null, null, $process); $downloader = $this->getDownloaderMock(null, null, $process);
$downloader->download($packageMock, 'composerPath'); $downloader->download($packageMock, 'composerPath');
$downloader->prepare('install', $packageMock, 'composerPath'); $downloader->prepare('install', $packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath');
$downloader->cleanup('install', $packageMock, 'composerPath'); $downloader->cleanup('install', $packageMock, 'composerPath');
$this->fail('This test should throw');
} catch (\RuntimeException $e) {
if ('RuntimeException' !== get_class($e)) {
throw $e;
}
self::assertEquals('RuntimeException', get_class($e));
}
} }
public function testUpdateforPackageWithoutSourceReference(): void public function testUpdateforPackageWithoutSourceReference(): void
@ -327,8 +345,6 @@ class GitDownloaderTest extends TestCase
public function testUpdate(): void public function testUpdate(): void
{ {
$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 = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
$packageMock->expects($this->any()) $packageMock->expects($this->any())
->method('getSourceReference') ->method('getSourceReference')
@ -345,13 +361,23 @@ class GitDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
$this->winCompat('git show-ref --head -d'), ['git', 'show-ref', '--head', '-d'],
$this->winCompat('git status --porcelain --untracked-files=no'), ['git', 'status', '--porcelain', '--untracked-files=no'],
$this->winCompat('git remote -v'), ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1],
$expectedGitUpdateCommand,
$this->winCompat('git branch -r'), // fallback commands for the above failing
$this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), ['git', 'remote', '-v'],
$this->winCompat('git remote -v'), ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'],
['git', 'fetch', 'composer'],
['git', 'fetch', '--tags', 'composer'],
['git', 'remote', '-v'],
['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'],
['git', 'branch', '-r'],
['git', 'checkout', 'ref', '--'],
['git', 'reset', '--hard', 'ref', '--'],
['git', 'remote', '-v'],
], true); ], true);
$this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
@ -364,8 +390,6 @@ class GitDownloaderTest extends TestCase
public function testUpdateWithNewRepoUrl(): void public function testUpdateWithNewRepoUrl(): void
{ {
$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 = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
$packageMock->expects($this->any()) $packageMock->expects($this->any())
->method('getSourceReference') ->method('getSourceReference')
@ -385,22 +409,26 @@ class GitDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
$this->winCompat("git show-ref --head -d"), ['git', 'show-ref', '--head', '-d'],
$this->winCompat("git status --porcelain --untracked-files=no"), ['git', 'status', '--porcelain', '--untracked-files=no'],
$this->winCompat("git remote -v"), ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 0],
$this->winCompat($expectedGitUpdateCommand),
'git branch -r', ['git', 'remote', '-v'],
$this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'],
['git', 'branch', '-r'],
['git', 'checkout', 'ref', '--'],
['git', 'reset', '--hard', 'ref', '--'],
[ [
'cmd' => $this->winCompat("git remote -v"), 'cmd' => ['git', 'remote', '-v'],
'stdout' => 'origin https://github.com/old/url (fetch) 'stdout' => 'origin https://github.com/old/url (fetch)
origin https://github.com/old/url (push) origin https://github.com/old/url (push)
composer https://github.com/old/url (fetch) composer https://github.com/old/url (fetch)
composer https://github.com/old/url (push) composer https://github.com/old/url (push)
', ',
], ],
$this->winCompat("git remote set-url origin -- 'https://github.com/composer/composer'"), ['git', 'remote', 'set-url', 'origin', '--', 'https://github.com/composer/composer'],
$this->winCompat("git remote set-url --push origin -- 'git@github.com:composer/composer.git'"), ['git', 'remote', 'set-url', '--push', 'origin', '--', 'git@github.com:composer/composer.git'],
], true); ], true);
$this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
@ -416,9 +444,6 @@ composer https://github.com/old/url (push)
*/ */
public function testUpdateThrowsRuntimeExceptionIfGitCommandFails(): void public function testUpdateThrowsRuntimeExceptionIfGitCommandFails(): void
{ {
$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 = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
$packageMock->expects($this->any()) $packageMock->expects($this->any())
->method('getSourceReference') ->method('getSourceReference')
@ -432,44 +457,38 @@ composer https://github.com/old/url (push)
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
$this->winCompat('git show-ref --head -d'), ['git', 'show-ref', '--head', '-d'],
$this->winCompat('git status --porcelain --untracked-files=no'), ['git', 'status', '--porcelain', '--untracked-files=no'],
$this->winCompat('git remote -v'),
[ // commit not yet in so we try to fetch
'cmd' => $expectedGitUpdateCommand, ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1],
'return' => 1,
], // fail first fetch
[ ['git', 'remote', '-v'],
'cmd' => $expectedGitUpdateCommand2, ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'],
'return' => 1, ['cmd' => ['git', 'fetch', 'composer'], 'return' => 1],
],
$this->winCompat('git --version'), // fail second fetch
['git', 'remote', 'set-url', 'composer', '--', 'git@github.com:composer/composer'],
['cmd' => ['git', 'fetch', 'composer'], 'return' => 1],
['git', '--version'],
], true); ], true);
$this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
// not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe self::expectException('RuntimeException');
try { self::expectExceptionMessage('Failed to clone https://github.com/composer/composer via https, ssh protocols, aborting.');
$downloader = $this->getDownloaderMock(null, new Config(), $process); self::expectExceptionMessageMatches('{git@github\.com:composer/composer}');
$downloader->download($packageMock, $this->workingDir, $packageMock); $downloader = $this->getDownloaderMock(null, new Config(), $process);
$downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->download($packageMock, $this->workingDir, $packageMock);
$downloader->update($packageMock, $packageMock, $this->workingDir); $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock);
$downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir);
$downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock);
$this->fail('This test should throw');
} catch (\RuntimeException $e) {
if ('RuntimeException' !== get_class($e)) {
throw $e;
}
self::assertEquals('RuntimeException', get_class($e));
}
} }
public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover(): void public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover(): void
{ {
$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 -- '".(Platform::isWindows() ? 'C:\\' : '/')."'");
$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 = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
$packageMock->expects($this->any()) $packageMock->expects($this->any())
->method('getSourceReference') ->method('getSourceReference')
@ -486,22 +505,33 @@ composer https://github.com/old/url (push)
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
$this->winCompat('git show-ref --head -d'), ['git', 'show-ref', '--head', '-d'],
$this->winCompat('git status --porcelain --untracked-files=no'), ['git', 'status', '--porcelain', '--untracked-files=no'],
$this->winCompat('git remote -v'),
[ // commit not yet in so we try to fetch
'cmd' => $expectedFirstGitUpdateCommand, ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1],
'return' => 1,
], // fail first source URL
$this->winCompat('git --version'), ['git', 'remote', '-v'],
$this->winCompat('git remote -v'), ['git', 'remote', 'set-url', 'composer', '--', Platform::isWindows() ? 'C:\\' : '/'],
[ ['cmd' => ['git', 'fetch', 'composer'], 'return' => 1],
'cmd' => $expectedSecondGitUpdateCommand, ['git', '--version'],
'return' => 0,
], // commit not yet in so we try to fetch
$this->winCompat('git branch -r'), ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1],
$this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"),
$this->winCompat('git remote -v'), // pass second source URL
['git', 'remote', '-v'],
['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'],
['cmd' => ['git', 'fetch', 'composer'], 'return' => 0],
['git', 'fetch', '--tags', 'composer'],
['git', 'remote', '-v'],
['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'],
['git', 'branch', '-r'],
['git', 'checkout', 'ref', '--'],
['git', 'reset', '--hard', 'ref', '--'],
['git', 'remote', '-v'],
], true); ], true);
$this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
@ -604,13 +634,11 @@ composer https://github.com/old/url (push)
public function testRemove(): void public function testRemove(): void
{ {
$expectedGitResetCommand = $this->winCompat("git status --porcelain --untracked-files=no");
$packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
'git show-ref --head -d', ['git', 'show-ref', '--head', '-d'],
$expectedGitResetCommand, ['git', 'status', '--porcelain', '--untracked-files=no'],
], true); ], true);
$this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
@ -633,16 +661,4 @@ composer https://github.com/old/url (push)
self::assertEquals('source', $downloader->getInstallationSource()); self::assertEquals('source', $downloader->getInstallationSource());
} }
private function winCompat(string $cmd): string
{
if (Platform::isWindows()) {
$cmd = str_replace('cd ', 'cd /D ', $cmd);
$cmd = str_replace('composerPath', Platform::getCwd().'/composerPath', $cmd);
return self::getCmd($cmd);
}
return $cmd;
}
} }

View File

@ -76,8 +76,8 @@ class HgDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
self::getCmd('hg clone -- \'https://mercurial.dev/l3l0/composer\' \''.$this->workingDir.'\''), ['hg', 'clone', '--', 'https://mercurial.dev/l3l0/composer', $this->workingDir],
self::getCmd('hg up -- \'ref\''), ['hg', 'up', '--', 'ref'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, null, $process); $downloader = $this->getDownloaderMock(null, null, $process);
@ -117,8 +117,9 @@ class HgDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
self::getCmd('hg st'), ['hg', 'st'],
self::getCmd("hg pull -- 'https://github.com/l3l0/composer' && hg up -- 'ref'"), ['hg', 'pull', '--', 'https://github.com/l3l0/composer'],
['hg', 'up', '--', 'ref'],
], true); ], true);
$downloader = $this->getDownloaderMock(null, null, $process); $downloader = $this->getDownloaderMock(null, null, $process);
@ -135,7 +136,7 @@ class HgDownloaderTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
self::getCmd('hg st'), ['hg', 'st'],
], true); ], true);
$filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock();

View File

@ -57,16 +57,16 @@ class ProcessExecutorMock extends ProcessExecutor
} }
/** /**
* @param array<string|array{cmd: string|list<string>, return?: int, stdout?: string, stderr?: string, callback?: callable}> $expectations * @param array<string|non-empty-list<string>|array{cmd: string|non-empty-list<string>, return?: int, stdout?: string, stderr?: string, callback?: callable}> $expectations
* @param bool $strict set to true if you want to provide *all* expected commands, and not just a subset you are interested in testing * @param bool $strict set to true if you want to provide *all* expected commands, and not just a subset you are interested in testing
* @param array{return: int, stdout?: string, stderr?: string} $defaultHandler default command handler for undefined commands if not in strict mode * @param array{return: int, stdout?: string, stderr?: string} $defaultHandler default command handler for undefined commands if not in strict mode
*/ */
public function expects(array $expectations, bool $strict = false, array $defaultHandler = ['return' => 0, 'stdout' => '', 'stderr' => '']): void public function expects(array $expectations, bool $strict = false, array $defaultHandler = ['return' => 0, 'stdout' => '', 'stderr' => '']): void
{ {
/** @var array{cmd: string|list<string>, return: int, stdout: string, stderr: string, callback: callable|null} $default */ /** @var array{cmd: string|non-empty-list<string>, return: int, stdout: string, stderr: string, callback: callable|null} $default */
$default = ['cmd' => '', 'return' => 0, 'stdout' => '', 'stderr' => '', 'callback' => null]; $default = ['cmd' => '', 'return' => 0, 'stdout' => '', 'stderr' => '', 'callback' => null];
$this->expectations = array_map(static function ($expect) use ($default): array { $this->expectations = array_map(static function ($expect) use ($default): array {
if (is_string($expect)) { if (is_string($expect) || array_is_list($expect)) {
$command = $expect; $command = $expect;
$expect = $default; $expect = $default;
$expect['cmd'] = $command; $expect['cmd'] = $command;

View File

@ -36,11 +36,11 @@ class VersionGuesserTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
['cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'return' => 128], ['cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'return' => 128],
['cmd' => 'git describe --exact-match --tags', 'return' => 128], ['cmd' => ['git', 'describe', '--exact-match', '--tags'], 'return' => 128],
['cmd' => 'git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($process), 'return' => 128], ['cmd' => array_merge(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], GitUtil::getNoShowSignatureFlags($process)), 'return' => 128],
['cmd' => 'hg branch', 'return' => 0, 'stdout' => $branch], ['cmd' => ['hg', 'branch'], 'return' => 0, 'stdout' => $branch],
['cmd' => 'hg branches', 'return' => 0], ['cmd' => ['hg', 'branches'], 'return' => 0],
['cmd' => 'hg bookmarks', 'return' => 0], ['cmd' => ['hg', 'bookmarks'], 'return' => 0],
], true); ], true);
GitUtil::getVersion(new ProcessExecutor); GitUtil::getVersion(new ProcessExecutor);
@ -201,7 +201,7 @@ class VersionGuesserTest extends TestCase
'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'],
'stdout' => "* (no branch) $commitHash Commit message\n", 'stdout' => "* (no branch) $commitHash Commit message\n",
], ],
'git describe --exact-match --tags', ['git', 'describe', '--exact-match', '--tags'],
], true); ], true);
$config = new Config; $config = new Config;
@ -223,7 +223,7 @@ class VersionGuesserTest extends TestCase
'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'],
'stdout' => "* (HEAD detached at FETCH_HEAD) $commitHash Commit message\n", 'stdout' => "* (HEAD detached at FETCH_HEAD) $commitHash Commit message\n",
], ],
'git describe --exact-match --tags', ['git', 'describe', '--exact-match', '--tags'],
], true); ], true);
$config = new Config; $config = new Config;
@ -245,7 +245,7 @@ class VersionGuesserTest extends TestCase
'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'],
'stdout' => "* (HEAD detached at 03a15d220) $commitHash Commit message\n", 'stdout' => "* (HEAD detached at 03a15d220) $commitHash Commit message\n",
], ],
'git describe --exact-match --tags', ['git', 'describe', '--exact-match', '--tags'],
], true); ], true);
$config = new Config; $config = new Config;
@ -266,7 +266,7 @@ class VersionGuesserTest extends TestCase
'stdout' => "* (HEAD detached at v2.0.5-alpha2) 433b98d4218c181bae01865901aac045585e8a1a Commit message\n", 'stdout' => "* (HEAD detached at v2.0.5-alpha2) 433b98d4218c181bae01865901aac045585e8a1a Commit message\n",
], ],
[ [
'cmd' => 'git describe --exact-match --tags', 'cmd' => ['git', 'describe', '--exact-match', '--tags'],
'stdout' => "v2.0.5-alpha2", 'stdout' => "v2.0.5-alpha2",
], ],
], true); ], true);
@ -289,7 +289,7 @@ class VersionGuesserTest extends TestCase
'stdout' => "* (HEAD detached at 1.0.0) c006f0c12bbbf197b5c071ffb1c0e9812bb14a4d Commit message\n", 'stdout' => "* (HEAD detached at 1.0.0) c006f0c12bbbf197b5c071ffb1c0e9812bb14a4d Commit message\n",
], ],
[ [
'cmd' => 'git describe --exact-match --tags', 'cmd' => ['git', 'describe', '--exact-match', '--tags'],
'stdout' => '1.0.0', 'stdout' => '1.0.0',
], ],
], true); ], true);

View File

@ -72,7 +72,7 @@ GIT;
$process $process
->expects([[ ->expects([[
'cmd' => 'git branch --no-color', 'cmd' => ['git', 'branch', '--no-color'],
'stdout' => $stdout, 'stdout' => $stdout,
]], true); ]], true);
@ -102,11 +102,17 @@ GIT;
$process $process
->expects([[ ->expects([[
'cmd' => 'git remote -v', 'cmd' => ['git', 'remote', '-v'],
'stdout' => '', 'stdout' => '',
], [ ], [
'cmd' => Platform::isWindows() ? "git remote set-url origin -- https://example.org/acme.git && git remote show origin && git remote set-url origin -- https://example.org/acme.git" : "git remote set-url origin -- 'https://example.org/acme.git' && git remote show origin && git remote set-url origin -- 'https://example.org/acme.git'", 'cmd' => ['git', 'remote', 'set-url', 'origin', '--', 'https://example.org/acme.git'],
'stdout' => '',
], [
'cmd' => ['git', 'remote', 'show', 'origin'],
'stdout' => $stdout, 'stdout' => $stdout,
], [
'cmd' => ['git', 'remote', 'set-url', 'origin', '--', 'https://example.org/acme.git'],
'stdout' => '',
]]); ]]);
self::assertSame('main', $driver->getRootIdentifier()); self::assertSame('main', $driver->getRootIdentifier());
@ -130,7 +136,7 @@ GIT;
$process $process
->expects([[ ->expects([[
'cmd' => 'git branch --no-color', 'cmd' => ['git', 'branch', '--no-color'],
'stdout' => $stdout, 'stdout' => $stdout,
]]); ]]);
@ -155,7 +161,7 @@ GIT;
$process $process
->expects([[ ->expects([[
'cmd' => 'git branch --no-color --no-abbrev -v', 'cmd' => ['git', 'branch', '--no-color', '--no-abbrev', '-v'],
'stdout' => $stdout, 'stdout' => $stdout,
]]); ]]);

View File

@ -303,18 +303,18 @@ class GitHubDriverTest extends TestCase
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$process->expects([ $process->expects([
['cmd' => 'git config github.accesstoken', 'return' => 1], ['cmd' => ['git', 'config', 'github.accesstoken'], 'return' => 1],
'git clone --mirror -- '.ProcessExecutor::escape($repoSshUrl).' '.ProcessExecutor::escape($this->config->get('cache-vcs-dir').'/git-github.com-composer-packagist.git/'), ['git', 'clone', '--mirror', '--', $repoSshUrl, $this->config->get('cache-vcs-dir').'/git-github.com-composer-packagist.git/'],
[ [
'cmd' => 'git show-ref --tags --dereference', 'cmd' => ['git', 'show-ref', '--tags', '--dereference'],
'stdout' => $sha.' refs/tags/'.$identifier, 'stdout' => $sha.' refs/tags/'.$identifier,
], ],
[ [
'cmd' => 'git branch --no-color --no-abbrev -v', 'cmd' => ['git', 'branch', '--no-color', '--no-abbrev', '-v'],
'stdout' => ' test_master edf93f1fccaebd8764383dc12016d0a1a9672d89 Fix test & behavior', 'stdout' => ' test_master edf93f1fccaebd8764383dc12016d0a1a9672d89 Fix test & behavior',
], ],
[ [
'cmd' => 'git branch --no-color', 'cmd' => ['git', 'branch', '--no-color'],
'stdout' => '* test_master', 'stdout' => '* test_master',
], ],
], true); ], true);

View File

@ -85,10 +85,10 @@ HG_BOOKMARKS;
$process $process
->expects([[ ->expects([[
'cmd' => 'hg branches', 'cmd' => ['hg', 'branches'],
'stdout' => $stdout, 'stdout' => $stdout,
], [ ], [
'cmd' => 'hg bookmarks', 'cmd' => ['hg', 'bookmarks'],
'stdout' => $stdout1, 'stdout' => $stdout1,
]]); ]]);

View File

@ -60,12 +60,7 @@ class SvnDriverTest extends TestCase
$output .= " rejected Basic challenge (https://corp.svn.local/)"; $output .= " rejected Basic challenge (https://corp.svn.local/)";
$process = $this->getProcessExecutorMock(); $process = $this->getProcessExecutorMock();
$authedCommand = sprintf( $authedCommand = ['svn', 'ls', '--verbose', '--non-interactive', '--username', 'till', '--password', 'secret', '--', 'https://till:secret@corp.svn.local/repo/trunk'];
'svn ls --verbose --non-interactive --username %s --password %s -- %s',
ProcessExecutor::escape('till'),
ProcessExecutor::escape('secret'),
ProcessExecutor::escape('https://till:secret@corp.svn.local/repo/trunk')
);
$process->expects([ $process->expects([
['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output],
['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output],
@ -73,7 +68,7 @@ class SvnDriverTest extends TestCase
['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output],
['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output],
['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output],
['cmd' => 'svn --version', 'return' => 0, 'stdout' => '1.2.3'], ['cmd' => ['svn', '--version'], 'return' => 0, 'stdout' => '1.2.3'],
], true); ], true);
$repoConfig = [ $repoConfig = [

View File

@ -57,6 +57,7 @@ class GitTest extends TestCase
$this->process->expects(['git command'], true); $this->process->expects(['git command'], true);
// @phpstan-ignore method.deprecated
$this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true); $this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true);
} }
@ -82,9 +83,10 @@ class GitTest extends TestCase
$this->process->expects([ $this->process->expects([
['cmd' => 'git command', 'return' => 1], ['cmd' => 'git command', 'return' => 1],
['cmd' => 'git --version', 'return' => 0], ['cmd' => ['git', '--version'], 'return' => 0],
], true); ], true);
// @phpstan-ignore method.deprecated
$this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true); $this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true);
} }
@ -124,6 +126,7 @@ class GitTest extends TestCase
->with($this->equalTo('github.com')) ->with($this->equalTo('github.com'))
->willReturn(['username' => 'token', 'password' => $gitHubToken]); ->willReturn(['username' => 'token', 'password' => $gitHubToken]);
// @phpstan-ignore method.deprecated
$this->git->runCommand($commandCallable, $gitUrl, null, true); $this->git->runCommand($commandCallable, $gitUrl, null, true);
} }
@ -152,7 +155,7 @@ class GitTest extends TestCase
// When we are testing what happens without auth saved, and URLs // When we are testing what happens without auth saved, and URLs
// with https, there will also be an attempt to find the token in // with https, there will also be an attempt to find the token in
// the git config for the folder and repo, locally. // the git config for the folder and repo, locally.
$additional_calls = array_fill(0, $bitbucket_git_auth_calls, ['cmd' => 'git config bitbucket.accesstoken', 'return' => 1]); $additional_calls = array_fill(0, $bitbucket_git_auth_calls, ['cmd' => ['git', 'config', 'bitbucket.accesstoken'], 'return' => 1]);
foreach ($additional_calls as $call) { foreach ($additional_calls as $call) {
$expectedCalls[] = $call; $expectedCalls[] = $call;
} }
@ -177,6 +180,7 @@ class GitTest extends TestCase
->with($this->equalTo('bitbucket.org')) ->with($this->equalTo('bitbucket.org'))
->willReturn(['username' => 'token', 'password' => $bitbucketToken]); ->willReturn(['username' => 'token', 'password' => $bitbucketToken]);
} }
// @phpstan-ignore method.deprecated
$this->git->runCommand($commandCallable, $gitUrl, null, true); $this->git->runCommand($commandCallable, $gitUrl, null, true);
} }
@ -202,7 +206,7 @@ class GitTest extends TestCase
if (count($initial_config) > 0) { if (count($initial_config) > 0) {
$expectedCalls[] = ['cmd' => 'git command failing', 'return' => 1]; $expectedCalls[] = ['cmd' => 'git command failing', 'return' => 1];
} else { } else {
$expectedCalls[] = ['cmd' => 'git config bitbucket.accesstoken', 'return' => 1]; $expectedCalls[] = ['cmd' => ['git', 'config', 'bitbucket.accesstoken'], 'return' => 1];
} }
$expectedCalls[] = ['cmd' => 'git command ok', 'return' => 0]; $expectedCalls[] = ['cmd' => 'git command ok', 'return' => 0];
$this->process->expects($expectedCalls, true); $this->process->expects($expectedCalls, true);
@ -259,6 +263,7 @@ class GitTest extends TestCase
['url' => 'https://bitbucket.org/site/oauth2/access_token', 'status' => 200, 'body' => '{"expires_in": 600, "access_token": "my-access-token"}'] ['url' => 'https://bitbucket.org/site/oauth2/access_token', 'status' => 200, 'body' => '{"expires_in": 600, "access_token": "my-access-token"}']
]); ]);
$this->git->setHttpDownloader($downloader_mock); $this->git->setHttpDownloader($downloader_mock);
// @phpstan-ignore method.deprecated
$this->git->runCommand($commandCallable, $gitUrl, null, true); $this->git->runCommand($commandCallable, $gitUrl, null, true);
} }

View File

@ -561,7 +561,9 @@ class PerforceTest extends TestCase
public function testCheckServerExists(): void public function testCheckServerExists(): void
{ {
$this->processExecutor->expects( $this->processExecutor->expects(
['p4 -p '.ProcessExecutor::escape('perforce.does.exist:port').' info -s'], [
['p4', '-p', 'perforce.does.exist:port', 'info', '-s']
],
true true
); );
@ -578,7 +580,7 @@ class PerforceTest extends TestCase
{ {
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$expectedCommand = 'p4 -p '.ProcessExecutor::escape('perforce.does.exist:port').' info -s'; $expectedCommand = ['p4', '-p', 'perforce.does.exist:port', 'info', '-s'];
$processExecutor->expects($this->once()) $processExecutor->expects($this->once())
->method('execute') ->method('execute')
->with($this->equalTo($expectedCommand), $this->equalTo(null)) ->with($this->equalTo($expectedCommand), $this->equalTo(null))

View File

@ -23,14 +23,14 @@ class SvnTest extends TestCase
* Test the credential string. * Test the credential string.
* *
* @param string $url The SVN url. * @param string $url The SVN url.
* @param string $expect The expectation for the test. * @param non-empty-list<string> $expect The expectation for the test.
* *
* @dataProvider urlProvider * @dataProvider urlProvider
*/ */
public function testCredentials(string $url, string $expect): void public function testCredentials(string $url, array $expect): void
{ {
$svn = new Svn($url, new NullIO, new Config()); $svn = new Svn($url, new NullIO, new Config());
$reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs');
$reflMethod->setAccessible(true); $reflMethod->setAccessible(true);
self::assertEquals($expect, $reflMethod->invoke($svn)); self::assertEquals($expect, $reflMethod->invoke($svn));
@ -39,9 +39,9 @@ class SvnTest extends TestCase
public static function urlProvider(): array public static function urlProvider(): array
{ {
return [ return [
['http://till:test@svn.example.org/', self::getCmd(" --username 'till' --password 'test' ")], ['http://till:test@svn.example.org/', ['--username', 'till', '--password', 'test']],
['http://svn.apache.org/', ''], ['http://svn.apache.org/', []],
['svn://johndoe@example.org', self::getCmd(" --username 'johndoe' --password '' ")], ['svn://johndoe@example.org', ['--username', 'johndoe', '--password', '']],
]; ];
} }
@ -54,8 +54,8 @@ class SvnTest extends TestCase
$reflMethod->setAccessible(true); $reflMethod->setAccessible(true);
self::assertEquals( self::assertEquals(
self::getCmd("svn ls --non-interactive -- 'http://svn.example.org'"), ['svn', 'ls', '--non-interactive', '--', 'http://svn.example.org'],
$reflMethod->invokeArgs($svn, ['svn ls', $url]) $reflMethod->invokeArgs($svn, [['svn', 'ls'], $url])
); );
} }
@ -73,10 +73,10 @@ class SvnTest extends TestCase
]); ]);
$svn = new Svn($url, new NullIO, $config); $svn = new Svn($url, new NullIO, $config);
$reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs');
$reflMethod->setAccessible(true); $reflMethod->setAccessible(true);
self::assertEquals(self::getCmd(" --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); self::assertEquals(['--username', 'foo', '--password', 'bar'], $reflMethod->invoke($svn));
} }
public function testCredentialsFromConfigWithCacheCredentialsTrue(): void public function testCredentialsFromConfigWithCacheCredentialsTrue(): void
@ -96,10 +96,10 @@ class SvnTest extends TestCase
$svn = new Svn($url, new NullIO, $config); $svn = new Svn($url, new NullIO, $config);
$svn->setCacheCredentials(true); $svn->setCacheCredentials(true);
$reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs');
$reflMethod->setAccessible(true); $reflMethod->setAccessible(true);
self::assertEquals(self::getCmd(" --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); self::assertEquals(['--username', 'foo', '--password', 'bar'], $reflMethod->invoke($svn));
} }
public function testCredentialsFromConfigWithCacheCredentialsFalse(): void public function testCredentialsFromConfigWithCacheCredentialsFalse(): void
@ -119,9 +119,9 @@ class SvnTest extends TestCase
$svn = new Svn($url, new NullIO, $config); $svn = new Svn($url, new NullIO, $config);
$svn->setCacheCredentials(false); $svn->setCacheCredentials(false);
$reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs');
$reflMethod->setAccessible(true); $reflMethod->setAccessible(true);
self::assertEquals(self::getCmd(" --no-auth-cache --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); self::assertEquals(['--no-auth-cache', '--username', 'foo', '--password', 'bar'], $reflMethod->invoke($svn));
} }
} }