Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. Git.php

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\Util

Code

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

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal