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