562 lines
25 KiB
PHP
562 lines
25 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of Composer.
|
|
*
|
|
* (c) Nils Adermann <naderman@naderman.de>
|
|
* Jordi Boggiano <j.boggiano@seld.be>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Composer\Util;
|
|
|
|
use Composer\Config;
|
|
use Composer\IO\IOInterface;
|
|
use Composer\Pcre\Preg;
|
|
|
|
/**
|
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
|
*/
|
|
class Git
|
|
{
|
|
/** @var string|false|null */
|
|
private static $version = false;
|
|
|
|
/** @var IOInterface */
|
|
protected $io;
|
|
/** @var Config */
|
|
protected $config;
|
|
/** @var ProcessExecutor */
|
|
protected $process;
|
|
/** @var Filesystem */
|
|
protected $filesystem;
|
|
/** @var HttpDownloader */
|
|
protected $httpDownloader;
|
|
|
|
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process, Filesystem $fs)
|
|
{
|
|
$this->io = $io;
|
|
$this->config = $config;
|
|
$this->process = $process;
|
|
$this->filesystem = $fs;
|
|
}
|
|
|
|
/**
|
|
* @param IOInterface|null $io If present, a warning is output there instead of throwing, so pass this in only for cases where this is a soft failure
|
|
*/
|
|
public static function checkForRepoOwnershipError(string $output, string $path, ?IOInterface $io = null): void
|
|
{
|
|
if (str_contains($output, 'fatal: detected dubious ownership')) {
|
|
$msg = 'The repository at "' . $path . '" does not have the correct ownership and git refuses to use it:' . PHP_EOL . PHP_EOL . $output;
|
|
if ($io === null) {
|
|
throw new \RuntimeException($msg);
|
|
}
|
|
$io->writeError('<warning>'.$msg.'</warning>');
|
|
}
|
|
}
|
|
|
|
public function setHttpDownloader(HttpDownloader $httpDownloader): void
|
|
{
|
|
$this->httpDownloader = $httpDownloader;
|
|
}
|
|
|
|
/**
|
|
* @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 runCommand(callable $commandCallable, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void
|
|
{
|
|
// Ensure we are allowed to use this URL by config
|
|
$this->config->prohibitUrlByConfig($url, $this->io);
|
|
|
|
if ($initialClone) {
|
|
$origCwd = $cwd;
|
|
$cwd = null;
|
|
}
|
|
|
|
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 === $this->process->execute($commandCallable($protoUrl), $commandOutput, $cwd)) {
|
|
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);
|
|
|
|
$command = $commandCallable($url);
|
|
|
|
$auth = null;
|
|
$credentials = [];
|
|
if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $commandOutput, $cwd)) {
|
|
$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)
|
|
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
|
|
|| 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';
|
|
$command = $commandCallable($authUrl);
|
|
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
|
|
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;
|
|
|
|
$command = $commandCallable($authUrl);
|
|
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
|
|
// 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;
|
|
$command = $commandCallable($authUrl);
|
|
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
|
|
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.');
|
|
$command = $commandCallable($sshUrl);
|
|
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
|
|
return;
|
|
}
|
|
|
|
$errorMsg = $this->process->getErrorOutput();
|
|
} elseif (
|
|
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
|
|
Preg::isMatchStrictGroups('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?\.git)$}i', $url, $match)
|
|
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
|
|
|| 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];
|
|
}
|
|
|
|
$command = $commandCallable($authUrl);
|
|
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
|
|
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];
|
|
|
|
$command = $commandCallable($authUrl);
|
|
if (0 === $this->process->execute($command, $commandOutput, $cwd)) {
|
|
$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);
|
|
}
|
|
|
|
if (count($credentials) > 0) {
|
|
$command = $this->maskCredentials($command, $credentials);
|
|
$errorMsg = $this->maskCredentials($errorMsg, $credentials);
|
|
}
|
|
$this->throwException('Failed to execute ' . $command . "\n\n" . $errorMsg, $url);
|
|
}
|
|
}
|
|
|
|
public function syncMirror(string $url, string $dir): bool
|
|
{
|
|
if ((bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK') && Platform::getEnv('COMPOSER_DISABLE_NETWORK') !== 'prime') {
|
|
$this->io->writeError('<warning>Aborting git mirror sync of '.$url.' as network is disabled</warning>');
|
|
|
|
return false;
|
|
}
|
|
|
|
// 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) === '.') {
|
|
try {
|
|
$commandCallable = static function ($url): string {
|
|
$sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url);
|
|
|
|
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->runCommand($commandCallable, $url, $dir);
|
|
} catch (\Exception $e) {
|
|
$this->io->writeError('<error>Sync mirror failed: ' . $e->getMessage() . '</error>', true, IOInterface::DEBUG);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
self::checkForRepoOwnershipError($this->process->getErrorOutput(), $dir);
|
|
|
|
// clean up directory and do a fresh clone into it
|
|
$this->filesystem->removeDirectory($dir);
|
|
|
|
$commandCallable = static function ($url) use ($dir): string {
|
|
return sprintf('git clone --mirror -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($dir));
|
|
};
|
|
|
|
$this->runCommand($commandCallable, $url, $dir, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function fetchRefOrSyncMirror(string $url, string $dir, string $ref, ?string $prettyVersion = null): bool
|
|
{
|
|
if ($this->checkRefIsInMirror($dir, $ref)) {
|
|
if (Preg::isMatch('{^[a-f0-9]{40}$}', $ref) && $prettyVersion !== null) {
|
|
$branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion);
|
|
$branches = null;
|
|
$tags = null;
|
|
if (0 === $this->process->execute('git branch', $output, $dir)) {
|
|
$branches = $output;
|
|
}
|
|
if (0 === $this->process->execute('git tag', $output, $dir)) {
|
|
$tags = $output;
|
|
}
|
|
|
|
// if the pretty version cannot be found as a branch (nor branch with 'v' in front of the branch as it may have been stripped when generating pretty name),
|
|
// nor as a tag, then we sync the mirror as otherwise it will likely fail during install.
|
|
// this can occur if a git tag gets created *after* the reference is already put into the cache, as the ref check above will then not sync the new tags
|
|
// see https://github.com/composer/composer/discussions/11002
|
|
if (null !== $branches && !Preg::isMatch('{^[\s*]*v?'.preg_quote($branch).'$}m', $branches)
|
|
&& null !== $tags && !Preg::isMatch('{^[\s*]*'.preg_quote($branch).'$}m', $tags)
|
|
) {
|
|
$this->syncMirror($url, $dir);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if ($this->syncMirror($url, $dir)) {
|
|
return $this->checkRefIsInMirror($dir, $ref);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static function getNoShowSignatureFlag(ProcessExecutor $process): string
|
|
{
|
|
$gitVersion = self::getVersion($process);
|
|
if ($gitVersion && version_compare($gitVersion, '2.10.0-rc0', '>=')) {
|
|
return ' --no-show-signature';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
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) === '.') {
|
|
$escapedRef = ProcessExecutor::escape($ref.'^{commit}');
|
|
$exitCode = $this->process->execute(sprintf('git rev-parse --quiet --verify %s', $escapedRef), $ignoredOutput, $dir);
|
|
if ($exitCode === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
self::checkForRepoOwnershipError($this->process->getErrorOutput(), $dir);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>|null
|
|
*/
|
|
private function getAuthenticationFailure(string $url): ?array
|
|
{
|
|
if (!Preg::isMatchStrictGroups('{^(https?://)([^/]+)(.*)$}i', $url, $match)) {
|
|
return null;
|
|
}
|
|
|
|
$authFailures = [
|
|
'fatal: Authentication failed',
|
|
'remote error: Invalid username or password.',
|
|
'error: 401 Unauthorized',
|
|
'fatal: unable to access',
|
|
'fatal: could not read Username',
|
|
];
|
|
|
|
$errorOutput = $this->process->getErrorOutput();
|
|
foreach ($authFailures as $authFailure) {
|
|
if (strpos($errorOutput, $authFailure) !== false) {
|
|
return $match;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function getMirrorDefaultBranch(string $url, string $dir, bool $isLocalPathRepository): ?string
|
|
{
|
|
if ((bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK')) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if ($isLocalPathRepository) {
|
|
$this->process->execute('git remote show origin', $output, $dir);
|
|
} else {
|
|
$commandCallable = static function ($url): string {
|
|
$sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url);
|
|
|
|
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->runCommand($commandCallable, $url, $dir, false, $output);
|
|
}
|
|
|
|
$lines = $this->process->splitLines($output);
|
|
foreach ($lines as $line) {
|
|
if (Preg::isMatch('{^\s*HEAD branch:\s(.+)\s*$}m', $line, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->io->writeError('<error>Failed to fetch root identifier from remote: ' . $e->getMessage() . '</error>', true, IOInterface::DEBUG);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static function cleanEnv(): void
|
|
{
|
|
// added in git 1.7.1, prevents prompting the user for username/password
|
|
if (Platform::getEnv('GIT_ASKPASS') !== 'echo') {
|
|
Platform::putEnv('GIT_ASKPASS', 'echo');
|
|
}
|
|
|
|
// clean up rogue git env vars in case this is running in a git hook
|
|
if (Platform::getEnv('GIT_DIR')) {
|
|
Platform::clearEnv('GIT_DIR');
|
|
}
|
|
if (Platform::getEnv('GIT_WORK_TREE')) {
|
|
Platform::clearEnv('GIT_WORK_TREE');
|
|
}
|
|
|
|
// Run processes with predictable LANGUAGE
|
|
if (Platform::getEnv('LANGUAGE') !== 'C') {
|
|
Platform::putEnv('LANGUAGE', 'C');
|
|
}
|
|
|
|
// clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940
|
|
Platform::clearEnv('DYLD_LIBRARY_PATH');
|
|
}
|
|
|
|
/**
|
|
* @return non-empty-string
|
|
*/
|
|
public static function getGitHubDomainsRegex(Config $config): string
|
|
{
|
|
return '(' . implode('|', array_map('preg_quote', $config->get('github-domains'))) . ')';
|
|
}
|
|
|
|
/**
|
|
* @return non-empty-string
|
|
*/
|
|
public static function getGitLabDomainsRegex(Config $config): string
|
|
{
|
|
return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')';
|
|
}
|
|
|
|
/**
|
|
* @param non-empty-string $message
|
|
*
|
|
* @return never
|
|
*/
|
|
private function throwException($message, string $url): void
|
|
{
|
|
// git might delete a directory when it fails and php will not know
|
|
clearstatcache();
|
|
|
|
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($message));
|
|
}
|
|
|
|
/**
|
|
* Retrieves the current git version.
|
|
*
|
|
* @return string|null The git version number, if present.
|
|
*/
|
|
public static function getVersion(ProcessExecutor $process): ?string
|
|
{
|
|
if (false === self::$version) {
|
|
self::$version = null;
|
|
if (0 === $process->execute('git --version', $output) && Preg::isMatch('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) {
|
|
self::$version = $matches[1];
|
|
}
|
|
}
|
|
|
|
return self::$version;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $credentials
|
|
*/
|
|
private function maskCredentials(string $error, array $credentials): string
|
|
{
|
|
$maskedCredentials = [];
|
|
|
|
foreach ($credentials as $credential) {
|
|
if (in_array($credential, ['private-token', 'x-token-auth', 'oauth2', 'gitlab-ci-token', 'x-oauth-basic'])) {
|
|
$maskedCredentials[] = $credential;
|
|
} elseif (strlen($credential) > 6) {
|
|
$maskedCredentials[] = substr($credential, 0, 3) . '...' . substr($credential, -3);
|
|
} elseif (strlen($credential) > 3) {
|
|
$maskedCredentials[] = substr($credential, 0, 3) . '...';
|
|
} else {
|
|
$maskedCredentials[] = 'XXX';
|
|
}
|
|
}
|
|
|
|
return str_replace($credentials, $maskedCredentials, $error);
|
|
}
|
|
}
|