function Git::runCommand
Parameters
callable|array<callable> $commandCallable:
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
Deprecated
Use runCommands with placeholders instead of callbacks for simplicity
1 call to Git::runCommand()
- Git::runCommands in vendor/
composer/ composer/ src/ Composer/ Util/ Git.php - Runs a set of commands using the $url or a variation of it (with auth, ssh, ..)
File
-
vendor/
composer/ composer/ src/ Composer/ Util/ Git.php, line 103
Class
- Git
- @author Jordi Boggiano <j.boggiano@seld.be>
Namespace
Composer\UtilCode
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
$this->config
->prohibitUrlByConfig($url, $this->io);
if ($initialClone) {
$origCwd = $cwd;
}
$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)) {
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) {
// capture username/password from URL if there is one and we have no auth configured yet
$this->process
->execute([
'git',
'remote',
'-v',
], $output, $cwd);
if (Preg::isMatchStrictGroups('{^(?:composer|origin)\\s+https?://(.+):(.+)@([^/]+)}im', $output, $match) && !$this->io
->hasAuthentication($match[3])) {
$this->io
->setAuthentication($match[3], rawurldecode($match[1]), rawurldecode($match[2]));
}
}
$protocols = $this->config
->get('github-protocols');
// public github, autoswitch protocols
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
if (Preg::isMatchStrictGroups('{^(?:https?|git)://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match)) {
$messages = [];
foreach ($protocols as $protocol) {
if ('ssh' === $protocol) {
$protoUrl = "git@" . $match[1] . ":" . $match[2];
}
else {
$protoUrl = $protocol . "://" . $match[1] . "/" . $match[2];
}
if (0 === $runCommands($protoUrl)) {
return;
}
$messages[] = '- ' . $protoUrl . "\n" . Preg::replace('#^#m', ' ', $this->process
->getErrorOutput());
if ($initialClone && isset($origCwd)) {
$this->filesystem
->removeDirectory($origCwd);
}
}
// failed to checkout, first check git accessibility
if (!$this->io
->hasAuthentication($match[1]) && !$this->io
->isInteractive()) {
$this->throwException('Failed to clone ' . $url . ' via ' . implode(', ', $protocols) . ' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url);
}
}
// 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);
$auth = null;
$credentials = [];
if ($bypassSshForGitHub || 0 !== $runCommands($url)) {
$errorMsg = $this->process
->getErrorOutput();
// private github repository without ssh key access, try https with auth
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
if (Preg::isMatchStrictGroups('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\\.git$}i', $url, $match) || Preg::isMatchStrictGroups('{^https?://' . self::getGitHubDomainsRegex($this->config) . '/(.*?)(?:\\.git)?$}i', $url, $match)) {
if (!$this->io
->hasAuthentication($match[1])) {
$gitHubUtil = new GitHub($this->io, $this->config, $this->process);
$message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos';
if (!$gitHubUtil->authorizeOAuth($match[1]) && $this->io
->isInteractive()) {
$gitHubUtil->authorizeOAuthInteractively($match[1], $message);
}
}
if ($this->io
->hasAuthentication($match[1])) {
$auth = $this->io
->getAuthentication($match[1]);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git';
if (0 === $runCommands($authUrl)) {
return;
}
$credentials = [
rawurlencode($auth['username']),
rawurlencode($auth['password']),
];
$errorMsg = $this->process
->getErrorOutput();
}
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
}
elseif (Preg::isMatchStrictGroups('{^(https?)://(bitbucket\\.org)/(.*?)(?:\\.git)?$}i', $url, $match) || Preg::isMatchStrictGroups('{^(git)@(bitbucket\\.org):(.+?\\.git)$}i', $url, $match)) {
//bitbucket either through oauth or app password, with fallback to ssh.
$bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->httpDownloader);
$domain = $match[2];
$repo_with_git_part = $match[3];
if (!str_ends_with($repo_with_git_part, '.git')) {
$repo_with_git_part .= '.git';
}
if (!$this->io
->hasAuthentication($domain)) {
$message = 'Enter your Bitbucket credentials to access private repos';
if (!$bitbucketUtil->authorizeOAuth($domain) && $this->io
->isInteractive()) {
$bitbucketUtil->authorizeOAuthInteractively($match[1], $message);
$accessToken = $bitbucketUtil->getToken();
$this->io
->setAuthentication($domain, 'x-token-auth', $accessToken);
}
}
// First we try to authenticate with whatever we have stored.
// This will be successful if there is for example an app
// password in there.
if ($this->io
->hasAuthentication($domain)) {
$auth = $this->io
->getAuthentication($domain);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part;
if (0 === $runCommands($authUrl)) {
// Well if that succeeded on our first try, let's just
// take the win.
return;
}
//We already have an access_token from a previous request.
if ($auth['username'] !== 'x-token-auth') {
$accessToken = $bitbucketUtil->requestToken($domain, $auth['username'], $auth['password']);
if (!empty($accessToken)) {
$this->io
->setAuthentication($domain, 'x-token-auth', $accessToken);
}
}
}
if ($this->io
->hasAuthentication($domain)) {
$auth = $this->io
->getAuthentication($domain);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part;
if (0 === $runCommands($authUrl)) {
return;
}
$credentials = [
rawurlencode($auth['username']),
rawurlencode($auth['password']),
];
}
//Falling back to ssh
$sshUrl = 'git@bitbucket.org:' . $repo_with_git_part;
$this->io
->writeError(' No bitbucket authentication configured. Falling back to ssh.');
if (0 === $runCommands($sshUrl)) {
return;
}
$errorMsg = $this->process
->getErrorOutput();
}
elseif (Preg::isMatchStrictGroups('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?\\.git)$}i', $url, $match) || Preg::isMatchStrictGroups('{^(https?)://' . self::getGitLabDomainsRegex($this->config) . '/(.*)}i', $url, $match)) {
if ($match[1] === 'git') {
$match[1] = 'https';
}
if (!$this->io
->hasAuthentication($match[2])) {
$gitLabUtil = new GitLab($this->io, $this->config, $this->process);
$message = 'Cloning failed, enter your GitLab credentials to access private repos';
if (!$gitLabUtil->authorizeOAuth($match[2]) && $this->io
->isInteractive()) {
$gitLabUtil->authorizeOAuthInteractively($match[1], $match[2], $message);
}
}
if ($this->io
->hasAuthentication($match[2])) {
$auth = $this->io
->getAuthentication($match[2]);
if ($auth['password'] === 'private-token' || $auth['password'] === 'oauth2' || $auth['password'] === 'gitlab-ci-token') {
$authUrl = $match[1] . '://' . rawurlencode($auth['password']) . ':' . rawurlencode((string) $auth['username']) . '@' . $match[2] . '/' . $match[3];
// swap username and password
}
else {
$authUrl = $match[1] . '://' . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . '/' . $match[3];
}
if (0 === $runCommands($authUrl)) {
return;
}
$credentials = [
rawurlencode((string) $auth['username']),
rawurlencode((string) $auth['password']),
];
$errorMsg = $this->process
->getErrorOutput();
}
}
elseif (null !== ($match = $this->getAuthenticationFailure($url))) {
// private non-github/gitlab/bitbucket repo that failed to authenticate
if (str_contains($match[2], '@')) {
[
$authParts,
$match[2],
] = explode('@', $match[2], 2);
}
$storeAuth = false;
if ($this->io
->hasAuthentication($match[2])) {
$auth = $this->io
->getAuthentication($match[2]);
}
elseif ($this->io
->isInteractive()) {
$defaultUsername = null;
if (isset($authParts) && $authParts !== '') {
if (str_contains($authParts, ':')) {
[
$defaultUsername,
] = explode(':', $authParts, 2);
}
else {
$defaultUsername = $authParts;
}
}
$this->io
->writeError(' Authentication required (<info>' . $match[2] . '</info>):');
$this->io
->writeError('<warning>' . trim($errorMsg) . '</warning>', true, IOInterface::VERBOSE);
$auth = [
'username' => $this->io
->ask(' Username: ', $defaultUsername),
'password' => $this->io
->askAndHideAnswer(' Password: '),
];
$storeAuth = $this->config
->get('store-auths');
}
if (null !== $auth) {
$authUrl = $match[1] . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . $match[3];
if (0 === $runCommands($authUrl)) {
$this->io
->setAuthentication($match[2], $auth['username'], $auth['password']);
$authHelper = new AuthHelper($this->io, $this->config);
$authHelper->storeAuth($match[2], $storeAuth);
return;
}
$credentials = [
rawurlencode((string) $auth['username']),
rawurlencode((string) $auth['password']),
];
$errorMsg = $this->process
->getErrorOutput();
}
}
if ($initialClone && isset($origCwd)) {
$this->filesystem
->removeDirectory($origCwd);
}
$lastCommand = implode(' ', $lastCommand);
if (count($credentials) > 0) {
$lastCommand = $this->maskCredentials($lastCommand, $credentials);
$errorMsg = $this->maskCredentials($errorMsg, $credentials);
}
$this->throwException('Failed to execute ' . $lastCommand . "\n\n" . $errorMsg, $url);
}
}