From 782c6303bcfc5abd61f7eae7f5db9237dd8ca4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Bj=C3=B8rnskov?= Date: Tue, 21 Oct 2014 10:25:24 +0200 Subject: [PATCH] Initial GitLab Driver This is a proof of concept, and mostly done to gather feedback on the structure of the driver and to see if this is something that Composer should include in core. Various review changes based on Stof comments. * Remove cleanup() as it is implemented by the abstract class. * Remove wrong comment in getReferences * Implement getSource (as GitHubDriver does) * Finish phpDocs for methods. --- src/Composer/IO/BaseIO.php | 6 + src/Composer/Repository/Vcs/GitLabDriver.php | 236 ++++++++++++++++++ src/Composer/Repository/VcsRepository.php | 1 + src/Composer/Util/RemoteFilesystem.php | 11 +- .../Test/Repository/Vcs/GitLabDriverTest.php | 40 +++ 5 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 src/Composer/Repository/Vcs/GitLabDriver.php create mode 100644 tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 8d684833e..1b6b0e94e 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -69,6 +69,12 @@ abstract class BaseIO implements IOInterface } } + if ($tokens = $config->get('gitlab-tokens')) { + foreach ($tokens as $domain => $token) { + $this->setAuthentication($domain, $token, 'gitlab-private-token'); + } + } + // reload http basic credentials from config if available if ($creds = $config->get('http-basic')) { foreach ($creds as $domain => $cred) { diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php new file mode 100644 index 000000000..42d11287b --- /dev/null +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -0,0 +1,236 @@ +url, $match); + + $this->owner = $match[3]; + $this->repository = $match[4]; + $this->originUrl = !empty($match[1]) ? $match[1] : $match[2]; + $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository); + + $this->fetchRootIdentifier(); + } + + /** + * Fetches the composer.json file from the project by a identifier. + * + * if specific keys arent present it will try and infer them by default values. + * + * {@inheritDoc} + */ + public function getComposerInformation($identifier) + { + if (isset($this->infoCache[$identifier])) { + return $this->infoCache[$identifier]; + } + + if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) { + return $this->infoCache[$identifier] = JsonFile::parseJson($res, $res); + } + + $composer = $this->fetchComposerFile($identifier); + + if (empty($composer['content']) || $composer['encoding'] !== 'base64' || !($composer = base64_decode($composer['content']))) { + throw new \RuntimeException('Could not retrieve composer.json from GitLab#'.$identifier); + } + + $composer = JsonFile::parseJson($composer); + + if (!isset($composer['time'])) { + $resource = $this->getApiUrl().'/repository/commits/'.urlencode($identifier); + $commit = JsonFile::parseJson($this->getContents($resource), $resource); + + $composer['time'] = $commit['committed_date']; + } + + if (preg_match('{[a-f0-9]{40}}i', $identifier)) { + $this->cache->write($identifier, json_encode($composer)); + } + + $this->infoCache[$identifier] = $composer; + } + + /** + * {@inheritDoc} + */ + public function hasComposerFile($identifier) + { + try { + $this->fetchComposerFile($identifier); + + return true; + } catch (TransportException $e) { + if ($e->getCode() !== 404) { + throw $e; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function getRepositoryUrl() + { + return 'https://'.$this->originUrl.'/'.$this->owner.'/'.$this->repository; + } + + /** + * {@inheritDoc} + */ + public function getUrl() + { + return $this->getRepositoryUrl() . '.git'; + } + + /** + * {@inheritDoc} + */ + public function getDist($identifier) + { + $url = $this->getApiUrl().'/repository/archive?sha='.$identifier; + + return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); + } + + /** + * {@inheritDoc} + */ + public function getSource($identifier) + { + return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier); + } + + /** + * {@inheritDoc} + */ + public function getRootIdentifier() + { + return $this->rootIdentifier; + } + + /** + * {@inheritDoc} + */ + public function getBranches() + { + return $this->getReferences('branches'); + } + + /** + * {@inheritDoc} + */ + public function getTags() + { + return $this->getReferences('tags'); + } + + /** + * Fetches composer.json file from the repository through api + * + * @param string $identifier + * @return array + */ + protected function fetchComposerFile($identifier) + { + $resource = $this->getApiUrl() . '/repository/files?file_path=composer.json&ref='.$identifier; + + return JsonFile::parseJson($this->getContents($resource), $resource); + } + + /** + * Root url + * + * {@inheritDoc} + */ + protected function getApiUrl() + { + // this needs to be https, but our install is running http + return 'http://'.$this->originUrl.'/api/v3/projects/'.$this->owner.'%2F'.$this->repository; + } + + /** + * @param string $type + * @return string[] where keys are named references like tags or branches and the value a sha + */ + protected function getReferences($type) + { + $resource = $this->getApiUrl().'/repository/'.$type; + + $data = JsonFile::parseJson($this->getContents($resource), $resource); + + $references = array(); + + foreach ($data as $datum) { + $references[$datum['name']] = $datum['commit']['id']; + } + + return $references; + } + + protected function fetchRootIdentifier() + { + // we need to fetch the default branch from the api + $resource = $this->getApiUrl(); + + $project = JsonFile::parseJson($this->getContents($resource), $resource); + + $this->rootIdentifier = $project['default_branch']; + } + + /** + * Uses the config `gitlab-domains` to see if the driver supports the url for the + * repository given. + * + * {@inheritDoc} + */ + public static function supports(IOInterface $io, Config $config, $url, $deep = false) + { + if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $url, $matches)) { + return false; + } + + $originUrl = empty($matches[2]) ? $matches[3] : $matches[2]; + + if (!in_array($originUrl, (array) $config->get('gitlab-domains'))) { + return false; + } + + if (!extension_loaded('openssl')) { + if ($io->isVerbose()) { + $io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.'); + } + + return false; + } + + return true; + } +} diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 034260d0c..b6461e436 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -43,6 +43,7 @@ class VcsRepository extends ArrayRepository { $this->drivers = $drivers ?: array( 'github' => 'Composer\Repository\Vcs\GitHubDriver', + 'gitlab' => 'Composer\Repository\Vcs\GitLabDriver', 'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', 'git' => 'Composer\Repository\Vcs\GitDriver', 'hg-bitbucket' => 'Composer\Repository\Vcs\HgBitbucketDriver', diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 9e6b50d74..78a2df45e 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -148,10 +148,17 @@ class RemoteFilesystem if ($this->io->isDebug()) { $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl); } + if (isset($options['github-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token']; unset($options['github-token']); } + + if (isset($options['gitlab-token'])) { + $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'private_token='.$options['gitlab-token']; + unset($options['gitlab-token']); + } + if (isset($options['http'])) { $options['http']['ignore_errors'] = true; } @@ -410,7 +417,9 @@ class RemoteFilesystem $auth = $this->io->getAuthentication($originUrl); if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) { $options['github-token'] = $auth['username']; - } else { + } elseif ($auth['password'] === 'gitlab-private-token') { + $options['gitlab-token'] = $auth['username']; + }else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; } diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php new file mode 100644 index 000000000..96c809358 --- /dev/null +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -0,0 +1,40 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Repository\Vcs; + +use Composer\Downloader\TransportException; +use Composer\Repository\Vcs\GitLabDriver; +use Composer\Util\Filesystem; +use Composer\Config; + +class GitLabDriverTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->config = new Config; + + $this->io = $this->getMock('Composer\IO\IOInterface'); + + $this->process = $this->getMock('Composer\Util\ProcessExecutor'); + + } + + public function testInterfaceIsComplete() + { + $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + ->setConstructorArgs(array($this->io)) + ->getMock(); + + $driver = new GitLabDriver(array('url' => 'http://google.com'), $this->io, $this->config, $this->process, $remoteFilesystem); + } +}