Merge remote-tracking branch 'GromNaN/gitlab'
commit
a10bffec0e
|
@ -141,6 +141,11 @@ used for GitHub Enterprise setups.
|
|||
Defaults to `true`. If `false`, the OAuth tokens created to access the
|
||||
github API will have a date instead of the machine hostname.
|
||||
|
||||
## gitlab-domains
|
||||
|
||||
Defaults to `["gitlab.com"]`. A list of domains of GitLab servers.
|
||||
This is used if you use the `gitlab` repository type.
|
||||
|
||||
## notify-on-install
|
||||
|
||||
Defaults to `true`. Composer allows repositories to define a notification URL,
|
||||
|
|
|
@ -44,6 +44,7 @@ class Config
|
|||
'prepend-autoloader' => true,
|
||||
'github-domains' => array('github.com'),
|
||||
'github-expose-hostname' => true,
|
||||
'gitlab-domains' => array('gitlab.com'),
|
||||
'store-auths' => 'prompt',
|
||||
'platform' => array(),
|
||||
'archive-format' => 'tar',
|
||||
|
|
|
@ -341,6 +341,7 @@ class Factory
|
|||
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');
|
||||
$rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository');
|
||||
$rm->setRepositoryClass('git', 'Composer\Repository\VcsRepository');
|
||||
$rm->setRepositoryClass('gitlab', 'Composer\Repository\VcsRepository');
|
||||
$rm->setRepositoryClass('svn', 'Composer\Repository\VcsRepository');
|
||||
$rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository');
|
||||
$rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository');
|
||||
|
|
|
@ -70,6 +70,12 @@ abstract class BaseIO implements IOInterface
|
|||
}
|
||||
}
|
||||
|
||||
if ($tokens = $config->get('gitlab-oauth')) {
|
||||
foreach ($tokens as $domain => $token) {
|
||||
$this->setAuthentication($domain, $token, 'oauth2');
|
||||
}
|
||||
}
|
||||
|
||||
// reload http basic credentials from config if available
|
||||
if ($creds = $config->get('http-basic')) {
|
||||
foreach ($creds as $domain => $cred) {
|
||||
|
|
|
@ -0,0 +1,367 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Repository\Vcs;
|
||||
|
||||
use Composer\Config;
|
||||
use Composer\Cache;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\Util\RemoteFilesystem;
|
||||
use Composer\Util\GitLab;
|
||||
/**
|
||||
* Driver for GitLab API, use the Git driver for local checkouts.
|
||||
*
|
||||
* @author Henrik Bjørnskov <henrik@bjrnskov.dk>
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class GitLabDriver extends VcsDriver
|
||||
{
|
||||
private $scheme;
|
||||
private $owner;
|
||||
private $repository;
|
||||
|
||||
private $cache;
|
||||
private $infoCache = array();
|
||||
|
||||
/**
|
||||
* @var array Project data returned by GitLab API
|
||||
*/
|
||||
private $project;
|
||||
|
||||
/**
|
||||
* @var array Keeps commits returned by GitLab API
|
||||
*/
|
||||
private $commits = array();
|
||||
|
||||
/**
|
||||
* @var array List of tag => reference
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
/**
|
||||
* @var array List of branch => reference
|
||||
*/
|
||||
private $branches;
|
||||
|
||||
/**
|
||||
* Git Driver
|
||||
*
|
||||
* @var GitDriver
|
||||
*/
|
||||
protected $gitDriver;
|
||||
|
||||
/**
|
||||
* Extracts information from the repository url.
|
||||
* SSH urls uses https by default.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
if (!preg_match('#^((https?)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $this->url, $match)) {
|
||||
throw new \InvalidArgumentException('The URL provided is invalid. It must be the HTTP URL of a GitLab project.');
|
||||
}
|
||||
|
||||
$this->scheme = !empty($match[2]) ? $match[2] : 'https';
|
||||
$this->originUrl = !empty($match[3]) ? $match[3] : $match[4];
|
||||
$this->owner = $match[5];
|
||||
$this->repository = preg_replace('#(\.git)$#', '', $match[6]);
|
||||
|
||||
$this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository);
|
||||
|
||||
$this->fetchProject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the RemoteFilesystem instance.
|
||||
* Mainly useful for tests.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function setRemoteFilesystem(RemoteFilesystem $remoteFilesystem)
|
||||
{
|
||||
$this->remoteFilesystem = $remoteFilesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
// Convert the root identifier to a cachable commit id
|
||||
if (!preg_match('{[a-f0-9]{40}}i', $identifier)) {
|
||||
$branches = $this->getBranches();
|
||||
if (isset($branches[$identifier])) {
|
||||
$identifier = $branches[$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);
|
||||
}
|
||||
|
||||
try {
|
||||
$composer = $this->fetchComposerFile($identifier);
|
||||
} catch (TransportException $e) {
|
||||
if ($e->getCode() !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
$composer = false;
|
||||
}
|
||||
|
||||
if ($composer && !isset($composer['time']) && isset($this->commits[$identifier])) {
|
||||
$composer['time'] = $this->commits[$identifier]['committed_date'];
|
||||
}
|
||||
|
||||
if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
|
||||
$this->cache->write($identifier, json_encode($composer));
|
||||
}
|
||||
|
||||
return $this->infoCache[$identifier] = $composer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRepositoryUrl()
|
||||
{
|
||||
return $this->project['ssh_url_to_repo'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->project['web_url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getDist($identifier)
|
||||
{
|
||||
$url = $this->getApiUrl().'/repository/archive.zip?sha='.$identifier;
|
||||
|
||||
return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => '');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSource($identifier)
|
||||
{
|
||||
return array('type' => 'git', 'url' => $this->getRepositoryUrl(), 'reference' => $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRootIdentifier()
|
||||
{
|
||||
return $this->project['default_branch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getBranches()
|
||||
{
|
||||
if (!$this->branches) {
|
||||
$this->branches = $this->getReferences('branches');
|
||||
}
|
||||
|
||||
return $this->branches;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
if (!$this->tags) {
|
||||
$this->tags = $this->getReferences('tags');
|
||||
}
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches composer.json file from the repository through api.
|
||||
*
|
||||
* @param string $identifier
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function fetchComposerFile($identifier)
|
||||
{
|
||||
$resource = $this->getApiUrl().'/repository/blobs/'.$identifier.'?filepath=composer.json';
|
||||
|
||||
return JsonFile::parseJson($this->getContents($resource), $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Base URL for GitLab API v3
|
||||
*/
|
||||
public function getApiUrl()
|
||||
{
|
||||
return $this->scheme.'://'.$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'];
|
||||
|
||||
// Keep the last commit date of a reference to avoid
|
||||
// unnecessary API call when retrieving the composer file.
|
||||
$this->commits[$datum['commit']['id']] = $datum['commit'];
|
||||
}
|
||||
|
||||
return $references;
|
||||
}
|
||||
|
||||
protected function fetchProject()
|
||||
{
|
||||
// we need to fetch the default branch from the api
|
||||
$resource = $this->getApiUrl();
|
||||
$this->project = JsonFile::parseJson($this->getContents($resource, true), $resource);
|
||||
}
|
||||
|
||||
protected function attemptCloneFallback()
|
||||
{
|
||||
|
||||
try {
|
||||
// If this repository may be private and we
|
||||
// cannot ask for authentication credentials (because we
|
||||
// are not interactive) then we fallback to GitDriver.
|
||||
$this->setupGitDriver($this->generateSshUrl());
|
||||
|
||||
return;
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->gitDriver = null;
|
||||
|
||||
$this->io->writeError('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your credentials</error>');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setupGitDriver($url)
|
||||
{
|
||||
$this->gitDriver = new GitDriver(
|
||||
array('url' => $url),
|
||||
$this->io,
|
||||
$this->config,
|
||||
$this->process,
|
||||
$this->remoteFilesystem
|
||||
);
|
||||
$this->gitDriver->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function getContents($url, $fetchingRepoData = false)
|
||||
{
|
||||
try {
|
||||
return parent::getContents($url);
|
||||
} catch (TransportException $e) {
|
||||
$gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->remoteFilesystem);
|
||||
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
case 404:
|
||||
// try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404
|
||||
if (!$fetchingRepoData) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($gitLabUtil->authorizeOAuth($this->originUrl)) {
|
||||
return parent::getContents($url);
|
||||
}
|
||||
|
||||
if (!$this->io->isInteractive()) {
|
||||
return $this->attemptCloneFallback();
|
||||
}
|
||||
$this->io->writeError('<warning>Failed to download ' . $this->owner . '/' . $this->repository . ':' . $e->getMessage() . '</warning>');
|
||||
$gitLabUtil->authorizeOAuthInteractively($this->originUrl, 'Your credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
|
||||
|
||||
return parent::getContents($url);
|
||||
|
||||
case 403:
|
||||
if (!$this->io->hasAuthentication($this->originUrl) && $gitLabUtil->authorizeOAuth($this->originUrl)) {
|
||||
return parent::getContents($url);
|
||||
}
|
||||
|
||||
if (!$this->io->isInteractive() && $fetchingRepoData) {
|
||||
return $this->attemptCloneFallback();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
|
||||
default:
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|/)?$#', $url, $match)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$scheme = !empty($match[2]) ? $match[2] : 'https';
|
||||
$originUrl = !empty($match[3]) ? $match[3] : $match[4];
|
||||
|
||||
if (!in_array($originUrl, (array) $config->get('gitlab-domains'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('https' === $scheme && !extension_loaded('openssl')) {
|
||||
if ($io->isVerbose()) {
|
||||
$io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -84,9 +84,11 @@ class Git
|
|||
$bypassSshForGitHub = preg_match('{^git@'.self::getGitHubDomainsRegex($this->config).':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true);
|
||||
|
||||
$command = call_user_func($commandCallable, $url);
|
||||
|
||||
if ($bypassSshForGitHub || 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';
|
||||
|
@ -99,7 +101,6 @@ class Git
|
|||
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;
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Roshan Gautam <roshan.gautam@hotmail.com>
|
||||
*
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Util;
|
||||
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Config;
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\Json\JsonFile;
|
||||
|
||||
/**
|
||||
* @author Roshan Gautam <roshan.gautam@hotmail.com>
|
||||
*/
|
||||
class GitLab
|
||||
{
|
||||
protected $io;
|
||||
protected $config;
|
||||
protected $process;
|
||||
protected $remoteFilesystem;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param IOInterface $io The IO instance
|
||||
* @param Config $config The composer configuration
|
||||
* @param ProcessExecutor $process Process instance, injectable for mocking
|
||||
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
|
||||
*/
|
||||
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
|
||||
{
|
||||
$this->io = $io;
|
||||
$this->config = $config;
|
||||
$this->process = $process ?: new ProcessExecutor();
|
||||
$this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authorize a GitLab domain via OAuth.
|
||||
*
|
||||
* @param string $originUrl The host this GitLab instance is located at
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function authorizeOAuth($originUrl)
|
||||
{
|
||||
if (!in_array($originUrl, $this->config->get('gitlab-domains'), true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if available use token from git config
|
||||
if (0 === $this->process->execute('git config gitlab.accesstoken', $output)) {
|
||||
$this->io->setAuthentication($originUrl, trim($output), 'oauth2');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorizes a GitLab domain interactively via OAuth.
|
||||
*
|
||||
* @param string $originUrl The host this GitLab instance is located at
|
||||
* @param string $message The reason this authorization is required
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @throws TransportException|\Exception
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function authorizeOAuthInteractively($scheme, $originUrl, $message = null)
|
||||
{
|
||||
if ($message) {
|
||||
$this->io->writeError($message);
|
||||
}
|
||||
|
||||
$this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName()));
|
||||
$this->io->writeError('To revoke access to this token you can visit '.$originUrl.'/profile/applications');
|
||||
|
||||
$attemptCounter = 0;
|
||||
|
||||
while ($attemptCounter++ < 5) {
|
||||
try {
|
||||
$response = $this->createToken($scheme, $originUrl);
|
||||
} catch (TransportException $e) {
|
||||
// 401 is bad credentials,
|
||||
// 403 is max login attempts exceeded
|
||||
if (in_array($e->getCode(), array(403, 401))) {
|
||||
if (401 === $e->getCode()) {
|
||||
$this->io->writeError('Bad credentials.');
|
||||
} else {
|
||||
$this->io->writeError('Maximum number of login attempts exceeded. Please try again later.');
|
||||
}
|
||||
|
||||
$this->io->writeError('You can also manually create a personal token at '.$scheme.'://'.$originUrl.'/profile/applications');
|
||||
$this->io->writeError('Add it using "composer config gitlab-oauth.'.$originUrl.' <token>"');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2');
|
||||
|
||||
// store value in user config in auth file
|
||||
$this->config->getAuthConfigSource()->addConfigSetting('gitlab-oauth.'.$originUrl, $response['access_token']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Invalid GitLab credentials 5 times in a row, aborting.');
|
||||
}
|
||||
|
||||
private function createToken($scheme, $originUrl)
|
||||
{
|
||||
$username = $this->io->ask('Username: ');
|
||||
$password = $this->io->askAndHideAnswer('Password: ');
|
||||
|
||||
$headers = array('Content-Type: application/x-www-form-urlencoded');
|
||||
|
||||
$apiUrl = $originUrl;
|
||||
$data = http_build_query(array(
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'grant_type' => 'password',
|
||||
));
|
||||
$options = array(
|
||||
'retry-auth-failure' => false,
|
||||
'http' => array(
|
||||
'method' => 'POST',
|
||||
'header' => $headers,
|
||||
'content' => $data,
|
||||
),
|
||||
);
|
||||
|
||||
$json = $this->remoteFilesystem->getContents($originUrl, $scheme.'://'.$apiUrl.'/oauth/token', false, $options);
|
||||
|
||||
$this->io->writeError('Token successfully created');
|
||||
|
||||
return JsonFile::parseJson($json);
|
||||
}
|
||||
}
|
|
@ -124,6 +124,7 @@ class RemoteFilesystem
|
|||
$originUrl = 'github.com';
|
||||
}
|
||||
|
||||
$this->scheme = parse_url($fileUrl, PHP_URL_SCHEME);
|
||||
$this->bytesMax = 0;
|
||||
$this->originUrl = $originUrl;
|
||||
$this->fileUrl = $fileUrl;
|
||||
|
@ -146,20 +147,35 @@ class RemoteFilesystem
|
|||
|
||||
$options = $this->getOptionsForUrl($originUrl, $additionalOptions);
|
||||
|
||||
if (isset($options['retry-auth-failure'])) {
|
||||
$this->retryAuthFailure = (bool) $options['retry-auth-failure'];
|
||||
|
||||
unset($options['retry-auth-failure']);
|
||||
}
|
||||
|
||||
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, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token'];
|
||||
unset($options['gitlab-token']);
|
||||
}
|
||||
|
||||
if (isset($options['http'])) {
|
||||
$options['http']['ignore_errors'] = true;
|
||||
}
|
||||
|
||||
if ($this->degradedMode && substr($fileUrl, 0, 21) === 'http://packagist.org/') {
|
||||
// access packagist using the resolved IPv4 instead of the hostname to force IPv4 protocol
|
||||
$fileUrl = 'http://' . gethostbyname('packagist.org') . substr($fileUrl, 20);
|
||||
}
|
||||
|
||||
$ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet')));
|
||||
|
||||
if ($this->progress) {
|
||||
|
@ -396,6 +412,14 @@ class RemoteFilesystem
|
|||
) {
|
||||
throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
|
||||
}
|
||||
} elseif ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) {
|
||||
$message = "\n".'Could not fetch '.$this->fileUrl.', enter your ' . $this->originUrl . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit');
|
||||
$gitLabUtil = new GitLab($this->io, $this->config, null);
|
||||
if (!$gitLabUtil->authorizeOAuth($this->originUrl)
|
||||
&& (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, $message))
|
||||
) {
|
||||
throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
|
||||
}
|
||||
} else {
|
||||
// 404s are only handled for github
|
||||
if ($httpStatus === 404) {
|
||||
|
@ -463,6 +487,10 @@ class RemoteFilesystem
|
|||
$auth = $this->io->getAuthentication($originUrl);
|
||||
if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) {
|
||||
$options['github-token'] = $auth['username'];
|
||||
} elseif ($this->config && in_array($originUrl, $this->config->get('gitlab-domains'), true)) {
|
||||
if($auth['password'] === 'oauth2') {
|
||||
$headers[] = 'Authorization: Bearer '.$auth['username'];
|
||||
}
|
||||
} else {
|
||||
$authStr = base64_encode($auth['username'] . ':' . $auth['password']);
|
||||
$headers[] = 'Authorization: Basic '.$authStr;
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Test\Repository\Vcs;
|
||||
|
||||
use Composer\Repository\Vcs\GitLabDriver;
|
||||
use Composer\Config;
|
||||
|
||||
/**
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class GitLabDriverTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function setUp()
|
||||
{
|
||||
$this->config = new Config();
|
||||
$this->config->merge(array(
|
||||
'config' => array(
|
||||
'home' => sys_get_temp_dir().'/composer-test',
|
||||
),
|
||||
));
|
||||
|
||||
$this->io = $this->prophesize('Composer\IO\IOInterface');
|
||||
|
||||
$this->process = $this->prophesize('Composer\Util\ProcessExecutor');
|
||||
|
||||
$this->remoteFilesystem = $this->prophesize('Composer\Util\RemoteFilesystem');
|
||||
}
|
||||
|
||||
public function getInitializeUrls()
|
||||
{
|
||||
return array(
|
||||
array('https://gitlab.com/mygroup/myproject', 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject'),
|
||||
array('http://gitlab.com/mygroup/myproject', 'http://gitlab.com/api/v3/projects/mygroup%2Fmyproject'),
|
||||
array('git@gitlab.com:mygroup/myproject', 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInitializeUrls
|
||||
*/
|
||||
public function testInitialize($url, $apiUrl)
|
||||
{
|
||||
// @link http://doc.gitlab.com/ce/api/projects.html#get-single-project
|
||||
$projectData = <<<JSON
|
||||
{
|
||||
"id": 17,
|
||||
"default_branch": "mymaster",
|
||||
"http_url_to_repo": "https://gitlab.com/mygroup/myproject.git",
|
||||
"ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git",
|
||||
"last_activity_at": "2014-12-01T09:17:51.000+01:00",
|
||||
"name": "My Project",
|
||||
"name_with_namespace": "My Group / My Project",
|
||||
"path": "myproject",
|
||||
"path_with_namespace": "mygroup/myproject",
|
||||
"web_url": "https://gitlab.com/mygroup/myproject"
|
||||
}
|
||||
JSON;
|
||||
|
||||
$this->remoteFilesystem
|
||||
->getContents('gitlab.com', $apiUrl, false)
|
||||
->willReturn($projectData)
|
||||
->shouldBeCalledTimes(1)
|
||||
;
|
||||
|
||||
$driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal());
|
||||
$driver->initialize();
|
||||
|
||||
$this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL');
|
||||
$this->assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab');
|
||||
$this->assertEquals('git@gitlab.com:mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default');
|
||||
$this->assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl());
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
public function testGetDist()
|
||||
{
|
||||
$driver = $this->testInitialize('https://gitlab.com/mygroup/myproject', 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject');
|
||||
|
||||
$reference = 'c3ebdbf9cceddb82cd2089aaef8c7b992e536363';
|
||||
$expected = array(
|
||||
'type' => 'zip',
|
||||
'url' => 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject/repository/archive.zip?sha='.$reference,
|
||||
'reference' => $reference,
|
||||
'shasum' => '',
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, $driver->getDist($reference));
|
||||
}
|
||||
|
||||
public function testGetSource()
|
||||
{
|
||||
$driver = $this->testInitialize('https://gitlab.com/mygroup/myproject', 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject');
|
||||
|
||||
$reference = 'c3ebdbf9cceddb82cd2089aaef8c7b992e536363';
|
||||
$expected = array(
|
||||
'type' => 'git',
|
||||
'url' => 'git@gitlab.com:mygroup/myproject.git',
|
||||
'reference' => $reference,
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, $driver->getSource($reference));
|
||||
}
|
||||
|
||||
public function testGetTags()
|
||||
{
|
||||
$driver = $this->testInitialize('https://gitlab.com/mygroup/myproject', 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject');
|
||||
|
||||
$apiUrl = 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject/repository/tags';
|
||||
|
||||
// @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-tags
|
||||
$tagData = <<<JSON
|
||||
[
|
||||
{
|
||||
"name": "v1.0.0",
|
||||
"commit": {
|
||||
"id": "092ed2c762bbae331e3f51d4a17f67310bf99a81",
|
||||
"committed_date": "2012-05-28T04:42:42-07:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "v2.0.0",
|
||||
"commit": {
|
||||
"id": "8e8f60b3ec86d63733db3bd6371117a758027ec6",
|
||||
"committed_date": "2014-07-06T12:59:11.000+02:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
JSON;
|
||||
|
||||
$this->remoteFilesystem
|
||||
->getContents('gitlab.com', $apiUrl, false)
|
||||
->willReturn($tagData)
|
||||
->shouldBeCalledTimes(1)
|
||||
;
|
||||
$driver->setRemoteFilesystem($this->remoteFilesystem->reveal());
|
||||
|
||||
$expected = array(
|
||||
'v1.0.0' => '092ed2c762bbae331e3f51d4a17f67310bf99a81',
|
||||
'v2.0.0' => '8e8f60b3ec86d63733db3bd6371117a758027ec6',
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, $driver->getTags());
|
||||
$this->assertEquals($expected, $driver->getTags(), 'Tags are cached');
|
||||
}
|
||||
|
||||
public function testGetBranches()
|
||||
{
|
||||
$driver = $this->testInitialize('https://gitlab.com/mygroup/myproject', 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject');
|
||||
|
||||
$apiUrl = 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject/repository/branches';
|
||||
|
||||
// @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-branches
|
||||
$branchData = <<<JSON
|
||||
[
|
||||
{
|
||||
"name": "mymaster",
|
||||
"commit": {
|
||||
"id": "97eda36b5c1dd953a3792865c222d4e85e5f302e",
|
||||
"committed_date": "2013-01-03T21:04:07.000+01:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "staging",
|
||||
"commit": {
|
||||
"id": "502cffe49f136443f2059803f2e7192d1ac066cd",
|
||||
"committed_date": "2013-03-09T16:35:23.000+01:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
JSON;
|
||||
|
||||
$this->remoteFilesystem
|
||||
->getContents('gitlab.com', $apiUrl, false)
|
||||
->willReturn($branchData)
|
||||
->shouldBeCalledTimes(1)
|
||||
;
|
||||
$driver->setRemoteFilesystem($this->remoteFilesystem->reveal());
|
||||
|
||||
$expected = array(
|
||||
'mymaster' => '97eda36b5c1dd953a3792865c222d4e85e5f302e',
|
||||
'staging' => '502cffe49f136443f2059803f2e7192d1ac066cd',
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, $driver->getBranches());
|
||||
$this->assertEquals($expected, $driver->getBranches(), 'Branches are cached');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataForTestSupports
|
||||
*/
|
||||
public function testSupports($url, $expected)
|
||||
{
|
||||
$this->assertSame($expected, GitLabDriver::supports($this->io->reveal(), $this->config, $url));
|
||||
}
|
||||
|
||||
public function dataForTestSupports()
|
||||
{
|
||||
return array(
|
||||
array('http://gitlab.com/foo/bar', true),
|
||||
array('http://gitlab.com/foo/bar/', true),
|
||||
array('http://gitlab.com/foo/bar.git', true),
|
||||
array('http://gitlab.com/foo/bar.baz.git', true),
|
||||
array('https://gitlab.com/foo/bar', extension_loaded('openssl')), // Platform requirement
|
||||
array('git@gitlab.com:foo/bar.git', extension_loaded('openssl')),
|
||||
array('git@example.com:foo/bar.git', false),
|
||||
array('http://example.com/foo/bar', false),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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\Test\Util;
|
||||
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\Util\GitLab;
|
||||
|
||||
/**
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class GitLabTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $username = 'username';
|
||||
private $password = 'password';
|
||||
private $authcode = 'authcode';
|
||||
private $message = 'mymessage';
|
||||
private $origin = 'gitlab.com';
|
||||
private $token = 'gitlabtoken';
|
||||
|
||||
public function testUsernamePasswordAuthenticationFlow()
|
||||
{
|
||||
$io = $this->getIOMock();
|
||||
$io
|
||||
->expects($this->at(0))
|
||||
->method('writeError')
|
||||
->with($this->message)
|
||||
;
|
||||
$io
|
||||
->expects($this->once())
|
||||
->method('ask')
|
||||
->with('Username: ')
|
||||
->willReturn($this->username)
|
||||
;
|
||||
$io
|
||||
->expects($this->once())
|
||||
->method('askAndHideAnswer')
|
||||
->with('Password: ')
|
||||
->willReturn($this->password)
|
||||
;
|
||||
|
||||
$rfs = $this->getRemoteFilesystemMock();
|
||||
$rfs
|
||||
->expects($this->once())
|
||||
->method('getContents')
|
||||
->with(
|
||||
$this->equalTo($this->origin),
|
||||
$this->equalTo(sprintf('http://%s/oauth/token', $this->origin)),
|
||||
$this->isFalse(),
|
||||
$this->anything()
|
||||
)
|
||||
->willReturn(sprintf('{"access_token": "%s", "token_type": "bearer", "expires_in": 7200}', $this->token))
|
||||
;
|
||||
|
||||
$config = $this->getConfigMock();
|
||||
$config
|
||||
->expects($this->exactly(2))
|
||||
->method('getAuthConfigSource')
|
||||
->willReturn($this->getAuthJsonMock())
|
||||
;
|
||||
|
||||
$gitLab = new GitLab($io, $config, null, $rfs);
|
||||
|
||||
$this->assertTrue($gitLab->authorizeOAuthInteractively('http', $this->origin, $this->message));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RuntimeException
|
||||
* @expectedExceptionMessage Invalid GitLab credentials 5 times in a row, aborting.
|
||||
*/
|
||||
public function testUsernamePasswordFailure()
|
||||
{
|
||||
$io = $this->getIOMock();
|
||||
$io
|
||||
->expects($this->exactly(5))
|
||||
->method('ask')
|
||||
->with('Username: ')
|
||||
->willReturn($this->username)
|
||||
;
|
||||
$io
|
||||
->expects($this->exactly(5))
|
||||
->method('askAndHideAnswer')
|
||||
->with('Password: ')
|
||||
->willReturn($this->password)
|
||||
;
|
||||
|
||||
$rfs = $this->getRemoteFilesystemMock();
|
||||
$rfs
|
||||
->expects($this->exactly(5))
|
||||
->method('getContents')
|
||||
->will($this->throwException(new TransportException('', 401)))
|
||||
;
|
||||
|
||||
$config = $this->getConfigMock();
|
||||
$config
|
||||
->expects($this->exactly(1))
|
||||
->method('getAuthConfigSource')
|
||||
->willReturn($this->getAuthJsonMock())
|
||||
;
|
||||
|
||||
$gitLab = new GitLab($io, $config, null, $rfs);
|
||||
|
||||
$gitLab->authorizeOAuthInteractively('https', $this->origin);
|
||||
}
|
||||
|
||||
private function getIOMock()
|
||||
{
|
||||
$io = $this
|
||||
->getMockBuilder('Composer\IO\ConsoleIO')
|
||||
->disableOriginalConstructor()
|
||||
->getMock()
|
||||
;
|
||||
|
||||
return $io;
|
||||
}
|
||||
|
||||
private function getConfigMock()
|
||||
{
|
||||
$config = $this->getMock('Composer\Config');
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function getRemoteFilesystemMock()
|
||||
{
|
||||
$rfs = $this
|
||||
->getMockBuilder('Composer\Util\RemoteFilesystem')
|
||||
->disableOriginalConstructor()
|
||||
->getMock()
|
||||
;
|
||||
|
||||
return $rfs;
|
||||
}
|
||||
|
||||
private function getAuthJsonMock()
|
||||
{
|
||||
$authjson = $this
|
||||
->getMockBuilder('Composer\Config\JsonConfigSource')
|
||||
->disableOriginalConstructor()
|
||||
->getMock()
|
||||
;
|
||||
$authjson
|
||||
->expects($this->atLeastOnce())
|
||||
->method('getName')
|
||||
->willReturn('auth.json')
|
||||
;
|
||||
|
||||
return $authjson;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue