Browse Source

Add tests on GitLabDriver

Add an interactive prompt for gitlab token

Update doc for gitlab-domains

Add tests on GitLabDriver::supports

Update doc + CS

Optimize branch detection + fix typos

Fix test on GitLab support as it depends on SSL

Remove useless method + fix repository URL containing .git
Jérôme Tamarelle 10 years ago
parent
commit
c1edfbb65c

+ 2 - 0
doc/04-schema.md

@@ -798,6 +798,8 @@ The following options are supported:
 * **github-expose-hostname:** Defaults to `true`. If set to 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, so that they get notified whenever a package from
   that repository is installed. This option allows you to disable that behaviour.

+ 77 - 51
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -1,5 +1,15 @@
 <?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;
@@ -7,41 +17,76 @@ use Composer\Cache;
 use Composer\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Downloader\TransportException;
+use Composer\Util\RemoteFilesystem;
 
 /**
- * Simplistic driver for GitLab currently only supports the api, not local checkouts.
+ * 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
 {
-    protected $owner;
-    protected $repository;
-    protected $originUrl;
-    protected $cache;
-    protected $infoCache = array();
+    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;
 
-    protected $project;
-    protected $commits = array();
-    protected $tags;
-    protected $branches;
+    /**
+     * @var array List of branch => reference
+     */
+    private $branches;
 
     /**
      * Extracts information from the repository url.
+     * SSH urls are not supported in order to know the HTTP sheme to use.
      *
      * {@inheritDoc}
      */
     public function initialize()
     {
-        preg_match('#^(?:(https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $this->url, $match);
+        if (!preg_match('#^(https?)://([^/]+)/([^/]+)/([^/]+)(?:\.git|/)?$#', $this->url, $match)) {
+            throw new \InvalidArgumentException('The URL provided is invalid. It must be the HTTP URL of a GitLab project.');
+        }
 
-        $this->scheme       = $match[1];
-        $this->owner        = $match[4];
-        $this->repository   = $match[5];
-        $this->originUrl    = !empty($match[2]) ? $match[2] : $match[3];
-        $this->cache        = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository);
+        $this->scheme = $match[1];
+        $this->originUrl = $match[2];
+        $this->owner = $match[3];
+        $this->repository = preg_replace('#(\.git)$#', '', $match[4]);
+
+        $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.
      *
@@ -53,10 +98,9 @@ class GitLabDriver extends VcsDriver
     {
         // Convert the root identifier to a cachable commit id
         if (!preg_match('{[a-f0-9]{40}}i', $identifier)) {
-            foreach ($this->getBranches() as $ref => $id) {
-                if ($ref === $identifier) {
-                    $identifier = $id;
-                }
+            $branches = $this->getBranches();
+            if (isset($branches[$identifier])) {
+                $identifier = $branches[$identifier];
             }
         }
 
@@ -88,24 +132,6 @@ class GitLabDriver extends VcsDriver
         return $this->infoCache[$identifier] = $composer;
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    public function hasComposerFile($identifier)
-    {
-        try {
-            $this->getComposerInformation($identifier);
-
-            return true;
-        } catch (TransportException $e) {
-            if ($e->getCode() !== 404) {
-                throw $e;
-            }
-        }
-
-        return false;
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -173,31 +199,30 @@ class GitLabDriver extends VcsDriver
     }
 
     /**
-     * Fetches composer.json file from the repository through api
+     * 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';
+        $resource = $this->getApiUrl().'/repository/blobs/'.$identifier.'?filepath=composer.json';
 
         return JsonFile::parseJson($this->getContents($resource), $resource);
     }
 
     /**
-     * Root url
-     *
-     * {@inheritDoc}
+     * @return string Base URL for GitLab API v3
      */
-    protected function getApiUrl()
+    public 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;
+        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)
@@ -212,7 +237,7 @@ class GitLabDriver extends VcsDriver
             $references[$datum['name']] = $datum['commit']['id'];
 
             // Keep the last commit date of a reference to avoid
-            // unnecessary API call when retreiving the composer file.
+            // unnecessary API call when retrieving the composer file.
             $this->commits[$datum['commit']['id']] = $datum['commit'];
         }
 
@@ -228,24 +253,25 @@ class GitLabDriver extends VcsDriver
     }
 
     /**
-     * Uses the config `gitlab-domains` to see if the driver supports the url for the 
+     * 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)) {
+        if (!preg_match('#^(https?)://([^/]+)/([^/]+)/([^/]+)(?:\.git|/)?$#', $url, $match)) {
             return false;
         }
 
-        $originUrl = empty($matches[2]) ? $matches[3] : $matches[2];
+        $scheme = $match[1];
+        $originUrl = $match[2];
 
         if (!in_array($originUrl, (array) $config->get('gitlab-domains'))) {
             return false;
         }
 
-        if (!extension_loaded('openssl')) {
+        if ('https' === $scheme && !extension_loaded('openssl')) {
             if ($io->isVerbose()) {
                 $io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.');
             }

+ 10 - 9
src/Composer/Util/RemoteFilesystem.php

@@ -351,14 +351,15 @@ class RemoteFilesystem
             ) {
                 throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
             }
-        // } else if ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) {
-        //     $message = "\n".'Could not fetch '.$this->fileUrl.', enter your GitLab private tolen to access private repos';
-        //     $gitHubUtil = new GitHub($this->io, $this->config, null, $this);
-        //     if (!$gitHubUtil->authorizeOAuth($this->originUrl)
-        //         && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($this->originUrl, $message))
-        //     ) {
-        //         throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
-        //     }
+        } else if ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) {
+            if ($this->io->isInteractive()) {
+                $this->io->overwrite('Enter your GitLab private token to access API (<info>'.parse_url($this->fileUrl, PHP_URL_HOST).'</info>):');
+                $token = $this->io->askAndHideAnswer('      Private-Token: ');
+                $this->io->setAuthentication($this->originUrl, $token, 'gitlab-private-token');
+                $this->config->getAuthConfigSource()->addConfigSetting('gitlab-tokens.'.$this->originUrl, $token);
+            } else {
+                throw new TransportException("The GitLab URL requires authentication.\nYou must be using the interactive console to authenticate", $httpStatus);
+            }
         } else {
             // 404s are only handled for github
             if ($httpStatus === 404) {
@@ -422,7 +423,7 @@ class RemoteFilesystem
                 $options['github-token'] = $auth['username'];
             } elseif ($auth['password'] === 'gitlab-private-token') {
                 $headers[] = 'Private-Token: '.$auth['username'];
-            }else {
+            } else {
                 $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
                 $headers[] = 'Authorization: Basic '.$authStr;
             }

+ 184 - 10
tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php

@@ -12,29 +12,203 @@
 
 namespace Composer\Test\Repository\Vcs;
 
-use Composer\Downloader\TransportException;
 use Composer\Repository\Vcs\GitLabDriver;
-use Composer\Util\Filesystem;
 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 = 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 testInitialize()
+    {
+        $url = 'https://gitlab.com/mygroup/myproject';
+        $apiUrl = 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject';
+
+        // @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)
+        ;
 
-        $this->io = $this->getMock('Composer\IO\IOInterface');
+        $driver  = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal());
+        $driver->initialize();
 
-        $this->process = $this->getMock('Composer\Util\ProcessExecutor');
+        $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 testInterfaceIsComplete()
+    /**
+     * @depends testInitialize
+     */
+    public function testGetDist(GitLabDriver $driver)
     {
-        $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem')
-            ->setConstructorArgs(array($this->io))
-            ->getMock();
+        $reference = 'c3ebdbf9cceddb82cd2089aaef8c7b992e536363';
+        $expected = array(
+            'type' => 'zip',
+            'url' => 'https://gitlab.com/api/v3/projects/mygroup%2Fmyproject/repository/archive.zip?sha='.$reference,
+            'reference' => $reference,
+            'shasum' => '',
+        );
 
-        $driver  = new GitLabDriver(array('url' => 'http://google.com'), $this->io, $this->config, $this->process, $remoteFilesystem);
+        $this->assertEquals($expected, $driver->getDist($reference));
+    }
+
+    /**
+     * @depends testInitialize
+     */
+    public function testGetSource(GitLabDriver $driver)
+    {
+        $reference = 'c3ebdbf9cceddb82cd2089aaef8c7b992e536363';
+        $expected = array(
+            'type' => 'git',
+            'url' => 'git@gitlab.com:mygroup/myproject.git',
+            'reference' => $reference,
+        );
+
+        $this->assertEquals($expected, $driver->getSource($reference));
+    }
+
+    /**
+     * @depends testInitialize
+     */
+    public function testGetTags(GitLabDriver $driver)
+    {
+        $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');
+    }
+
+    /**
+     * @depends testInitialize
+     */
+    public function testGetBranches(GitLabDriver $driver)
+    {
+        $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', false),
+            array('http://example.com/foo/bar', false),
+        );
     }
 }