From 3ebc869060e67bad68eff43be944d240ddfdf21c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 23 May 2014 18:48:10 +0200 Subject: [PATCH] Extract functionality from GitDownloader to make it more reusable --- src/Composer/Downloader/GitDownloader.php | 149 +++------------------- src/Composer/Repository/Vcs/GitDriver.php | 44 +------ src/Composer/Util/Git.php | 136 ++++++++++++++++++++ 3 files changed, 159 insertions(+), 170 deletions(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 2c4884320..e33c9d790 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -15,6 +15,10 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\GitHub; use Composer\Util\Git as GitUtil; +use Composer\Util\ProcessExecutor; +use Composer\IO\IOInterface; +use Composer\Util\Filesystem; +use Composer\Config; /** * @author Jordi Boggiano @@ -22,6 +26,13 @@ use Composer\Util\Git as GitUtil; class GitDownloader extends VcsDownloader { private $hasStashedChanges = false; + private $gitUtil; + + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) + { + parent::__construct($io, $config, $process, $fs); + $this->gitUtil = new GitUtil($this->io, $this->config, $this->process, $this->filesystem); + } /** * {@inheritDoc} @@ -40,7 +51,7 @@ class GitDownloader extends VcsDownloader return sprintf($command, escapeshellarg($url), escapeshellarg($path), escapeshellarg($ref)); }; - $this->runCommand($commandCallable, $url, $path, true); + $this->gitUtil->runCommand($commandCallable, $url, $path, true); $this->setPushUrl($path, $url); if ($newRef = $this->updateToCommit($path, $ref, $package->getPrettyVersion(), $package->getReleaseDate())) { @@ -66,17 +77,11 @@ class GitDownloader extends VcsDownloader $this->io->write(" Checking out ".$ref); $command = 'git remote set-url composer %s && git fetch composer && git fetch --tags composer'; - // capture username/password from URL if there is one - $this->process->execute('git remote -v', $output, $path); - if (preg_match('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match)) { - $this->io->setAuthentication($match[3], urldecode($match[1]), urldecode($match[2])); - } - $commandCallable = function($url) use ($command) { return sprintf($command, escapeshellarg($url)); }; - $this->runCommand($commandCallable, $url, $path); + $this->gitUtil->runCommand($commandCallable, $url, $path); if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { if ($target->getDistReference() === $target->getSourceReference()) { $target->setDistReference($newRef); @@ -273,7 +278,7 @@ class GitDownloader extends VcsDownloader if (empty($newReference)) { // no matching branch found, find the previous commit by date in all commits if (0 !== $this->process->execute(sprintf($guessTemplate, $date, '--all'), $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . GitUtil::sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); } $newReference = trim($output); } @@ -287,135 +292,13 @@ class GitDownloader extends VcsDownloader } } - throw new \RuntimeException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); - } - - /** - * Runs a command doing attempts for each protocol supported by github. - * - * @param callable $commandCallable A callable building the command for the given url - * @param string $url - * @param string $cwd - * @param bool $initialClone If true, the directory if cleared between every attempt - * @throws \InvalidArgumentException - * @throws \RuntimeException - */ - protected function runCommand($commandCallable, $url, $cwd, $initialClone = false) - { - if ($initialClone) { - $origCwd = $cwd; - $cwd = null; - } - - if (preg_match('{^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.'); - } - - // public github, autoswitch protocols - if (preg_match('{^(?:https?|git)://'.$this->getGitHubDomainsRegex().'/(.*)}', $url, $match)) { - $protocols = $this->config->get('github-protocols'); - if (!is_array($protocols)) { - throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols)); - } - $messages = array(); - foreach ($protocols as $protocol) { - if ('ssh' === $protocol) { - $url = "git@" . $match[1] . ":" . $match[2]; - } else { - $url = $protocol ."://" . $match[1] . "/" . $match[2]; - } - - if (0 === $this->process->execute(call_user_func($commandCallable, $url), $ignoredOutput, $cwd)) { - return; - } - $messages[] = '- ' . $url . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput()); - if ($initialClone) { - $this->filesystem->removeDirectory($origCwd); - } - } - - // failed to checkout, first check git accessibility - $this->throwException('Failed to clone ' . $this->sanitizeUrl($url) .' via '.implode(', ', $protocols).' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); - } - - $command = call_user_func($commandCallable, $url); - if (0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { - // private github repository without git access, try https with auth - if (preg_match('{^git@'.$this->getGitHubDomainsRegex().':(.+?)\.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]); - $url = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; - - $command = call_user_func($commandCallable, $url); - if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { - return; - } - } - } elseif ( // private non-github repo that failed to authenticate - $this->io->isInteractive() && - preg_match('{(https?://)([^/]+)(.*)$}i', $url, $match) && - strpos($this->process->getErrorOutput(), 'fatal: Authentication failed') !== false - ) { - // TODO this should use an auth manager class that prompts and stores in the config - if ($this->io->hasAuthentication($match[2])) { - $auth = $this->io->getAuthentication($match[2]); - } else { - $this->io->write($url.' requires Authentication'); - $auth = array( - 'username' => $this->io->ask('Username: '), - 'password' => $this->io->askAndHideAnswer('Password: '), - ); - } - - $url = $match[1].rawurlencode($auth['username']).':'.rawurlencode($auth['password']).'@'.$match[2].$match[3]; - - $command = call_user_func($commandCallable, $url); - if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { - $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); - - return; - } - } - - if ($initialClone) { - $this->filesystem->removeDirectory($origCwd); - } - $this->throwException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput(), $url); - } - } - - protected function getGitHubDomainsRegex() - { - return '('.implode('|', array_map('preg_quote', $this->config->get('github-domains'))).')'; - } - - protected function throwException($message, $url) - { - if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException('Failed to clone '.$this->sanitizeUrl($url).', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); - } - - throw new \RuntimeException($message); - } - - protected function sanitizeUrl($message) - { - return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); + throw new \RuntimeException('Failed to execute ' . GitUtil::sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); } protected function setPushUrl($path, $url) { // set push url for github projects - if (preg_match('{^(?:https?|git)://'.$this->getGitHubDomainsRegex().'/([^/]+)/([^/]+?)(?:\.git)?$}', $url, $match)) { + if (preg_match('{^(?:https?|git)://'.GitUtil::getGitHubDomainsRegex($this->config).'/([^/]+)/([^/]+?)(?:\.git)?$}', $url, $match)) { $protocols = $this->config->get('github-protocols'); $pushUrl = 'git@'.$match[1].':'.$match[2].'/'.$match[3].'.git'; if ($protocols[0] !== 'git') { diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index f8fbc715f..422868442 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -56,7 +56,7 @@ class GitDriver extends VcsDriver } // update the repo if it is a valid git repository - if (is_dir($this->repoDir) && 0 === $this->process->execute('git remote', $output, $this->repoDir)) { + if (is_dir($this->repoDir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $this->repoDir) && trim($output) === '.') { if (0 !== $this->process->execute('git remote update --prune origin', $output, $this->repoDir)) { $this->io->write('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); } @@ -64,43 +64,13 @@ class GitDriver extends VcsDriver // clean up directory and do a fresh clone into it $fs->removeDirectory($this->repoDir); - $command = sprintf('git clone --mirror %s %s', escapeshellarg($this->url), escapeshellarg($this->repoDir)); - if (0 !== $this->process->execute($command, $output)) { - $output = $this->process->getErrorOutput(); + $gitUtil = new GitUtil($this->io, $this->config, $this->process, $fs); + $repoDir = $this->repoDir; + $commandCallable = function($url) use ($repoDir) { + return sprintf('git clone --mirror %s %s', escapeshellarg($url), escapeshellarg($repoDir)); + }; - if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException('Failed to clone '.$this->url.', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); - } - - if ( - $this->io->isInteractive() && - preg_match('{(https?://)([^/]+)(.*)$}i', $this->url, $match) && - strpos($output, 'fatal: Authentication failed') !== false - ) { - if ($this->io->hasAuthentication($match[2])) { - $auth = $this->io->getAuthentication($match[2]); - } else { - $this->io->write($this->url.' requires Authentication'); - $auth = array( - 'username' => $this->io->ask('Username: '), - 'password' => $this->io->askAndHideAnswer('Password: '), - ); - } - - $url = $match[1].rawurlencode($auth['username']).':'.rawurlencode($auth['password']).'@'.$match[2].$match[3]; - - $command = sprintf('git clone --mirror %s %s', escapeshellarg($url), escapeshellarg($this->repoDir)); - - if (0 === $this->process->execute($command, $output)) { - $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); - } else { - $output = $this->process->getErrorOutput(); - throw new \RuntimeException('Failed to clone '.$this->url.', could not read packages from it' . "\n\n" .$output); - } - } else { - throw new \RuntimeException('Failed to clone '.$this->url.', could not read packages from it' . "\n\n" .$output); - } - } + $gitUtil->runCommand($commandCallable, $this->url, $this->repoDir, true); } } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index f15922a3e..0f61a53be 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -12,11 +12,128 @@ namespace Composer\Util; +use Composer\Config; +use Composer\IO\IOInterface; + /** * @author Jordi Boggiano */ class Git { + protected $io; + protected $config; + protected $process; + protected $filesystem; + + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process, Filesystem $fs) + { + $this->io = $io; + $this->config = $config; + $this->process = $process; + $this->filesystem = $fs; + } + + public function runCommand($commandCallable, $url, $cwd, $initialClone = false) + { + if ($initialClone) { + $origCwd = $cwd; + $cwd = null; + } + + if (preg_match('{^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 + $this->process->execute('git remote -v', $output, $path); + if (preg_match('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match)) { + $this->io->setAuthentication($match[3], urldecode($match[1]), urldecode($match[2])); + } + } + + // public github, autoswitch protocols + if (preg_match('{^(?:https?|git)://'.self::getGitHubDomainsRegex($this->config).'/(.*)}', $url, $match)) { + $protocols = $this->config->get('github-protocols'); + if (!is_array($protocols)) { + throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols)); + } + $messages = array(); + foreach ($protocols as $protocol) { + if ('ssh' === $protocol) { + $url = "git@" . $match[1] . ":" . $match[2]; + } else { + $url = $protocol ."://" . $match[1] . "/" . $match[2]; + } + + if (0 === $this->process->execute(call_user_func($commandCallable, $url), $ignoredOutput, $cwd)) { + return; + } + $messages[] = '- ' . $url . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput()); + if ($initialClone) { + $this->filesystem->removeDirectory($origCwd); + } + } + + // failed to checkout, first check git accessibility + $this->throwException('Failed to clone ' . self::sanitizeUrl($url) .' via '.implode(', ', $protocols).' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); + } + + $command = call_user_func($commandCallable, $url); + if (0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { + // private github repository without git access, try https with auth + if (preg_match('{^git@'.self::getGitHubDomainsRegex($this->config).':(.+?)\.git$}i', $url, $match)) { + 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]); + $url = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; + + $command = call_user_func($commandCallable, $url); + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { + return; + } + } + } elseif ( // private non-github repo that failed to authenticate + $this->io->isInteractive() && + preg_match('{(https?://)([^/]+)(.*)$}i', $url, $match) && + strpos($this->process->getErrorOutput(), 'fatal: Authentication failed') !== false + ) { + // TODO this should use an auth manager class that prompts and stores in the config + if ($this->io->hasAuthentication($match[2])) { + $auth = $this->io->getAuthentication($match[2]); + } else { + $this->io->write($url.' requires Authentication'); + $auth = array( + 'username' => $this->io->ask('Username: '), + 'password' => $this->io->askAndHideAnswer('Password: '), + ); + } + + $url = $match[1].rawurlencode($auth['username']).':'.rawurlencode($auth['password']).'@'.$match[2].$match[3]; + + $command = call_user_func($commandCallable, $url); + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { + $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); + + return; + } + } + + if ($initialClone) { + $this->filesystem->removeDirectory($origCwd); + } + $this->throwException('Failed to execute ' . self::sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput(), $url); + } + } + public static function cleanEnv() { if (ini_get('safe_mode') && false === strpos(ini_get('safe_mode_allowed_env_vars'), 'GIT_ASKPASS')) { @@ -36,4 +153,23 @@ class Git putenv('GIT_WORK_TREE'); } } + + public static function getGitHubDomainsRegex(Config $config) + { + return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')'; + } + + public static function sanitizeUrl($message) + { + return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); + } + + private function throwException($message, $url) + { + if (0 !== $this->process->execute('git --version', $ignoredOutput)) { + throw new \RuntimeException('Failed to clone '.self::sanitizeUrl($url).', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); + } + + throw new \RuntimeException($message); + } }