Quellcode durchsuchen

Merge remote-tracking branch 'tflori/feature-getFileContent'

Jordi Boggiano vor 8 Jahren
Ursprung
Commit
b2e1d4cb9d

+ 205 - 0
src/Composer/Repository/Vcs/BitbucketDriver.php

@@ -0,0 +1,205 @@
+<?php
+
+namespace Composer\Repository\Vcs;
+
+use Composer\Cache;
+use Composer\Downloader\TransportException;
+use Composer\Json\JsonFile;
+use Composer\Util\Bitbucket;
+
+abstract class BitbucketDriver extends VcsDriver
+{
+    /** @var Cache */
+    protected $cache;
+    protected $owner;
+    protected $repository;
+    protected $hasIssues;
+    protected $rootIdentifier;
+    protected $tags;
+    protected $branches;
+    protected $infoCache = array();
+
+    /**
+     * @var VcsDriver
+     */
+    protected $fallbackDriver;
+
+    /**
+     * {@inheritDoc}
+     */
+    public function initialize()
+    {
+        preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+?)(\.git|/?)$#', $this->url, $match);
+        $this->owner = $match[1];
+        $this->repository = $match[2];
+        $this->originUrl = 'bitbucket.org';
+        $this->cache = new Cache(
+            $this->io,
+            implode('/', array(
+                $this->config->get('cache-repo-dir'),
+                $this->originUrl,
+                $this->owner,
+                $this->repository
+            ))
+        );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getComposerInformation($identifier)
+    {
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getComposerInformation($identifier);
+        }
+
+        if (!isset($this->infoCache[$identifier])) {
+            if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) {
+                return $this->infoCache[$identifier] = JsonFile::parseJson($res);
+            }
+
+            $composer = $this->getBaseComposerInformation($identifier);
+
+            // specials for bitbucket
+            if (!isset($composer['support']['source'])) {
+                $label = array_search(
+                    $identifier,
+                    $this->getTags()
+                ) ?: array_search(
+                    $identifier,
+                    $this->getBranches()
+                ) ?: $identifier;
+
+                if (array_key_exists($label, $tags = $this->getTags())) {
+                    $hash = $tags[$label];
+                } elseif (array_key_exists($label, $branches = $this->getBranches())) {
+                    $hash = $branches[$label];
+                }
+
+                if (! isset($hash)) {
+                    $composer['support']['source'] = sprintf(
+                        'https://%s/%s/%s/src',
+                        $this->originUrl,
+                        $this->owner,
+                        $this->repository
+                    );
+                } else {
+                    $composer['support']['source'] = sprintf(
+                        'https://%s/%s/%s/src/%s/?at=%s',
+                        $this->originUrl,
+                        $this->owner,
+                        $this->repository,
+                        $hash,
+                        $label
+                    );
+                }
+            }
+            if (!isset($composer['support']['issues']) && $this->hasIssues) {
+                $composer['support']['issues'] = sprintf(
+                    'https://%s/%s/%s/issues',
+                    $this->originUrl,
+                    $this->owner,
+                    $this->repository
+                );
+            }
+
+            $this->infoCache[$identifier] = $composer;
+
+            if ($this->shouldCache($identifier)) {
+                $this->cache->write($identifier, json_encode($composer));
+            }
+        }
+
+        return $this->infoCache[$identifier];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFileContent($file, $identifier)
+    {
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getFileContent($file, $identifier);
+        }
+
+        $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'
+                    . $this->owner . '/' . $this->repository . '/src/' . $identifier . '/' . $file;
+        $fileData = JsonFile::parseJson($this->getContents($resource), $resource);
+        if (!is_array($fileData) || ! array_key_exists('data', $fileData)) {
+            return null;
+        }
+
+        return $fileData['data'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getChangeDate($identifier);
+        }
+
+        $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'
+                    . $this->owner . '/' . $this->repository . '/changesets/' . $identifier;
+        $changeset = JsonFile::parseJson($this->getContents($resource), $resource);
+
+        return new \DateTime($changeset['timestamp']);
+    }
+
+    /**
+     * Get the remote content.
+     *
+     * @param string $url The URL of content
+     * @param bool $fetchingRepoData
+     *
+     * @return mixed The result
+     */
+    protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false)
+    {
+        try {
+            return parent::getContents($url);
+        } catch (TransportException $e) {
+            $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem);
+
+            if (403 === $e->getCode()) {
+                if (!$this->io->hasAuthentication($this->originUrl)
+                    && $bitbucketUtil->authorizeOAuth($this->originUrl)
+                ) {
+                    return parent::getContents($url);
+                }
+
+                if (!$this->io->isInteractive() && $fetchingRepoData) {
+                    return $this->attemptCloneFallback();
+                }
+            }
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Generate an SSH URL
+     *
+     * @return string
+     */
+    abstract protected function generateSshUrl();
+
+    protected function attemptCloneFallback()
+    {
+        try {
+            $this->setupFallbackDriver($this->generateSshUrl());
+        } catch (\RuntimeException $e) {
+            $this->fallbackDriver = null;
+
+            $this->io->writeError(
+                '<error>Failed to clone the ' . $this->generateSshUrl() . ' repository, try running in interactive mode'
+                    . ' so that you can enter your Bitbucket OAuth consumer credentials</error>'
+            );
+            throw $e;
+        }
+    }
+
+    abstract protected function setupFallbackDriver($url);
+}

+ 16 - 18
src/Composer/Repository/Vcs/FossilDriver.php

@@ -122,29 +122,27 @@ class FossilDriver extends VcsDriver
     }
 
     /**
-     * {@inheritDoc}
+     * {@inheritdoc}
      */
-    public function getComposerInformation($identifier)
+    public function getFileContent($file, $identifier)
     {
-        if (!isset($this->infoCache[$identifier])) {
-            $command = sprintf('fossil cat -r %s composer.json', ProcessExecutor::escape($identifier));
-            $this->process->execute($command, $composer, $this->checkoutDir);
-
-            if (trim($composer) === '') {
-                return;
-            }
+        $command = sprintf('fossil cat -r %s %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file));
+        $this->process->execute($command, $content, $this->checkoutDir);
 
-            $composer = JsonFile::parseJson(trim($composer), $identifier);
-
-            if (empty($composer['time'])) {
-                $this->process->execute(sprintf('fossil finfo composer.json | head -n 2 | tail -n 1 | awk \'{print $1}\''), $output, $this->checkoutDir);
-                $date = new \DateTime(trim($output), new \DateTimeZone('UTC'));
-                $composer['time'] = $date->format('Y-m-d H:i:s');
-            }
-            $this->infoCache[$identifier] = $composer;
+        if (!trim($content)) {
+            return null;
         }
 
-        return $this->infoCache[$identifier];
+        return $content;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        $this->process->execute(sprintf('fossil finfo composer.json | head -n 2 | tail -n 1 | awk \'{print $1}\''), $output, $this->checkoutDir);
+        return new \DateTime(trim($output), new \DateTimeZone('UTC'));
     }
 
     /**

+ 23 - 162
src/Composer/Repository/Vcs/GitBitbucketDriver.php

@@ -12,53 +12,25 @@
 
 namespace Composer\Repository\Vcs;
 
-use Composer\Cache;
 use Composer\Config;
-use Composer\Downloader\TransportException;
 use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
-use Composer\Util\Bitbucket;
 
 /**
  * @author Per Bernhardt <plb@webfactory.de>
  */
-class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
+class GitBitbucketDriver extends BitbucketDriver implements VcsDriverInterface
 {
-    /**
-     * @var Cache
-     */
-    protected $cache;
-    protected $owner;
-    protected $repository;
-    protected $tags;
-    protected $branches;
-    protected $rootIdentifier;
-    protected $infoCache = array();
-    private $hasIssues;
-    /**
-     * @var GitDriver
-     */
-    private $gitDriver;
 
-    /**
-     * {@inheritDoc}
-     */
-    public function initialize()
-    {
-        preg_match('#^https?://bitbucket\.org/([^/]+)/(.+?)\.git$#', $this->url, $match);
-        $this->owner = $match[1];
-        $this->repository = $match[2];
-        $this->originUrl = 'bitbucket.org';
-        $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository);
-    }
+
 
     /**
      * {@inheritDoc}
      */
     public function getRootIdentifier()
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getRootIdentifier();
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getRootIdentifier();
         }
 
         if (null === $this->rootIdentifier) {
@@ -76,8 +48,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
      */
     public function getUrl()
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getUrl();
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getUrl();
         }
 
         return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git';
@@ -88,8 +60,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
      */
     public function getSource($identifier)
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getSource($identifier);
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getSource($identifier);
         }
 
         return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier);
@@ -105,76 +77,14 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
         return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => '');
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    public function getComposerInformation($identifier)
-    {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getComposerInformation($identifier);
-        }
-
-        if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) {
-            $this->infoCache[$identifier] = JsonFile::parseJson($res);
-        }
-
-        if (!isset($this->infoCache[$identifier])) {
-            $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/src/'.$identifier.'/composer.json';
-            $file = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource);
-            if (!is_array($file) || ! array_key_exists('data', $file)) {
-                return array();
-            }
-
-            $composer = JsonFile::parseJson($file['data'], $resource);
-
-            if (empty($composer['time'])) {
-                $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier;
-                $changeset = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource);
-                $composer['time'] = $changeset['timestamp'];
-            }
-            if (!isset($composer['support']['source'])) {
-                $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier;
-
-                if (array_key_exists($label, $tags = $this->getTags())) {
-                    $hash = $tags[$label];
-                } elseif (array_key_exists($label, $branches = $this->getBranches())) {
-                    $hash = $branches[$label];
-                }
-
-                if (! isset($hash)) {
-                    $composer['support']['source'] = sprintf('https://%s/%s/%s/src', $this->originUrl, $this->owner, $this->repository);
-                } else {
-                    $composer['support']['source'] = sprintf(
-                        'https://%s/%s/%s/src/%s/?at=%s',
-                        $this->originUrl,
-                        $this->owner,
-                        $this->repository,
-                        $hash,
-                        $label
-                    );
-                }
-            }
-            if (!isset($composer['support']['issues']) && $this->hasIssues) {
-                $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository);
-            }
-
-            if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
-                $this->cache->write($identifier, json_encode($composer));
-            }
-
-            $this->infoCache[$identifier] = $composer;
-        }
-
-        return $this->infoCache[$identifier];
-    }
 
     /**
      * {@inheritDoc}
      */
     public function getTags()
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getTags();
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getTags();
         }
 
         if (null === $this->tags) {
@@ -194,8 +104,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
      */
     public function getBranches()
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getBranches();
+        if ($this->fallbackDriver) {
+            return $this->fallbackDriver->getBranches();
         }
 
         if (null === $this->branches) {
@@ -228,75 +138,26 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
         return true;
     }
 
-    protected function attemptCloneFallback()
-    {
-        try {
-            $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 Bitbucket OAuth consumer credentials</error>');
-            throw $e;
-        }
-    }
-
-    /**
-     * Generate an SSH URL
-     *
-     * @return string
-     */
-    private function generateSshUrl()
-    {
-        return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git';
-    }
-
-    /**
-     * Get the remote content.
-     *
-     * @param string $url The URL of content
-     * @param bool $fetchingRepoData
-     *
-     * @return mixed The result
-     */
-    protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false)
-    {
-        try {
-            return parent::getContents($url);
-        } catch (TransportException $e) {
-            $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem);
-
-            switch ($e->getCode()) {
-                case 403:
-                    if (!$this->io->hasAuthentication($this->originUrl) && $bitbucketUtil->authorizeOAuth($this->originUrl)) {
-                        return parent::getContents($url);
-                    }
-
-                    if (!$this->io->isInteractive() && $fetchingRepoData) {
-                        return $this->attemptCloneFallback();
-                    }
-
-                    throw $e;
-
-                default:
-                    throw $e;
-            }
-        }
-    }
-
     /**
      * @param string $url
      */
-    private function setupGitDriver($url)
+    protected function setupFallbackDriver($url)
     {
-        $this->gitDriver = new GitDriver(
+        $this->fallbackDriver = new GitDriver(
             array('url' => $url),
             $this->io,
             $this->config,
             $this->process,
             $this->remoteFilesystem
         );
-        $this->gitDriver->initialize();
+        $this->fallbackDriver->initialize();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function generateSshUrl()
+    {
+        return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git';
     }
 }

+ 19 - 27
src/Composer/Repository/Vcs/GitDriver.php

@@ -120,38 +120,30 @@ class GitDriver extends VcsDriver
     }
 
     /**
-     * {@inheritDoc}
+     * {@inheritdoc}
      */
-    public function getComposerInformation($identifier)
+    public function getFileContent($file, $identifier)
     {
-        if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) {
-            $this->infoCache[$identifier] = JsonFile::parseJson($res);
-        }
-
-        if (!isset($this->infoCache[$identifier])) {
-            $resource = sprintf('%s:composer.json', ProcessExecutor::escape($identifier));
-            $this->process->execute(sprintf('git show %s', $resource), $composer, $this->repoDir);
-
-            if (!trim($composer)) {
-                return;
-            }
-
-            $composer = JsonFile::parseJson($composer, $resource);
-
-            if (empty($composer['time'])) {
-                $this->process->execute(sprintf('git log -1 --format=%%at %s', ProcessExecutor::escape($identifier)), $output, $this->repoDir);
-                $date = new \DateTime('@'.trim($output), new \DateTimeZone('UTC'));
-                $composer['time'] = $date->format('Y-m-d H:i:s');
-            }
+        $resource = sprintf('%s:%s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file));
+        $this->process->execute(sprintf('git show %s', $resource), $content, $this->repoDir);
 
-            if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
-                $this->cache->write($identifier, json_encode($composer));
-            }
-
-            $this->infoCache[$identifier] = $composer;
+        if (!trim($content)) {
+            return null;
         }
 
-        return $this->infoCache[$identifier];
+        return $content;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        $this->process->execute(sprintf(
+            'git log -1 --format=%%at %s',
+            ProcessExecutor::escape($identifier)
+        ), $output, $this->repoDir);
+        return new \DateTime('@'.trim($output), new \DateTimeZone('UTC'));
     }
 
     /**

+ 59 - 38
src/Composer/Repository/Vcs/GitHubDriver.php

@@ -146,50 +146,23 @@ class GitHubDriver extends VcsDriver
             return $this->gitDriver->getComposerInformation($identifier);
         }
 
-        if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) {
-            $this->infoCache[$identifier] = JsonFile::parseJson($res);
-        }
-
         if (!isset($this->infoCache[$identifier])) {
-            $notFoundRetries = 2;
-            while ($notFoundRetries) {
-                try {
-                    $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/composer.json?ref='.urlencode($identifier);
-                    $resource = JsonFile::parseJson($this->getContents($resource));
-                    if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($composer = base64_decode($resource['content']))) {
-                        throw new \RuntimeException('Could not retrieve composer.json for '.$identifier);
-                    }
-                    break;
-                } catch (TransportException $e) {
-                    if (404 !== $e->getCode()) {
-                        throw $e;
-                    }
-
-                    // TODO should be removed when possible
-                    // retry fetching if github returns a 404 since they happen randomly
-                    $notFoundRetries--;
-                    $composer = null;
-                }
+            if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) {
+                return $this->infoCache[$identifier] = JsonFile::parseJson($res);
             }
 
-            if ($composer) {
-                $composer = JsonFile::parseJson($composer, $resource);
+            $composer = $this->getBaseComposerInformation($identifier);
 
-                if (empty($composer['time'])) {
-                    $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier);
-                    $commit = JsonFile::parseJson($this->getContents($resource), $resource);
-                    $composer['time'] = $commit['commit']['committer']['date'];
-                }
-                if (!isset($composer['support']['source'])) {
-                    $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier;
-                    $composer['support']['source'] = sprintf('https://%s/%s/%s/tree/%s', $this->originUrl, $this->owner, $this->repository, $label);
-                }
-                if (!isset($composer['support']['issues']) && $this->hasIssues) {
-                    $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository);
-                }
+            // specials for github
+            if (!isset($composer['support']['source'])) {
+                $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier;
+                $composer['support']['source'] = sprintf('https://%s/%s/%s/tree/%s', $this->originUrl, $this->owner, $this->repository, $label);
+            }
+            if (!isset($composer['support']['issues']) && $this->hasIssues) {
+                $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository);
             }
 
-            if ($composer && preg_match('{[a-f0-9]{40}}i', $identifier)) {
+            if ($this->shouldCache($identifier)) {
                 $this->cache->write($identifier, json_encode($composer));
             }
 
@@ -199,6 +172,54 @@ class GitHubDriver extends VcsDriver
         return $this->infoCache[$identifier];
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getFileContent($file, $identifier)
+    {
+        if ($this->gitDriver) {
+            return $this->gitDriver->getFileContent($file, $identifier);
+        }
+
+        $notFoundRetries = 2;
+        while ($notFoundRetries) {
+            try {
+
+                $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier);
+                $resource = JsonFile::parseJson($this->getContents($resource));
+                if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) {
+                    throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier);
+                }
+
+                return $content;
+            } catch (TransportException $e) {
+                if (404 !== $e->getCode()) {
+                    throw $e;
+                }
+
+                // TODO should be removed when possible
+                // retry fetching if github returns a 404 since they happen randomly
+                $notFoundRetries--;
+                return null;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier) {
+        if ($this->gitDriver) {
+            return $this->gitDriver->getChangeDate($identifier);
+        }
+
+        $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier);
+        $commit = JsonFile::parseJson($this->getContents($resource), $resource);
+        return new \DateTime($commit['commit']['committer']['date']);
+    }
+
     /**
      * {@inheritDoc}
      */

+ 16 - 38
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -32,9 +32,6 @@ class GitLabDriver extends VcsDriver
     private $owner;
     private $repository;
 
-    private $cache;
-    private $infoCache = array();
-
     /**
      * @var array Project data returned by GitLab API
      */
@@ -99,13 +96,9 @@ class GitLabDriver extends VcsDriver
     }
 
     /**
-     * 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}
+     * {@inheritdoc}
      */
-    public function getComposerInformation($identifier)
+    public function getFileContent($file, $identifier)
     {
         // Convert the root identifier to a cachable commit id
         if (!preg_match('{[a-f0-9]{40}}i', $identifier)) {
@@ -115,34 +108,33 @@ class GitLabDriver extends VcsDriver
             }
         }
 
-        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);
-        }
+        $resource = $this->getApiUrl().'/repository/blobs/'.$identifier.'?filepath=' . $file;
 
         try {
-            $composer = $this->fetchComposerFile($identifier);
+            $content = $this->getContents($resource);
         } catch (TransportException $e) {
             if ($e->getCode() !== 404) {
                 throw $e;
             }
-            $composer = false;
+            return null;
         }
 
-        if ($composer && !isset($composer['time']) && isset($this->commits[$identifier])) {
-            $composer['time'] = $this->commits[$identifier]['committed_date'];
-        }
+        return $content;
+    }
 
-        if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
-            $this->cache->write($identifier, json_encode($composer));
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        if (isset($this->commits[$identifier])) {
+            return new \DateTime($this->commits[$identifier]['committed_date']);
         }
 
-        return $this->infoCache[$identifier] = $composer;
+        return new \DateTime();
     }
 
+
     /**
      * {@inheritDoc}
      */
@@ -209,20 +201,6 @@ class GitLabDriver extends VcsDriver
         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
      */

+ 22 - 61
src/Composer/Repository/Vcs/HgBitbucketDriver.php

@@ -12,7 +12,6 @@
 
 namespace Composer\Repository\Vcs;
 
-use Composer\Cache;
 use Composer\Config;
 use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
@@ -20,27 +19,8 @@ use Composer\IO\IOInterface;
 /**
  * @author Per Bernhardt <plb@webfactory.de>
  */
-class HgBitbucketDriver extends VcsDriver
+class HgBitbucketDriver extends BitbucketDriver
 {
-    protected $cache;
-    protected $owner;
-    protected $repository;
-    protected $tags;
-    protected $branches;
-    protected $rootIdentifier;
-    protected $infoCache = array();
-
-    /**
-     * {@inheritDoc}
-     */
-    public function initialize()
-    {
-        preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+)/?$#', $this->url, $match);
-        $this->owner = $match[1];
-        $this->repository = $match[2];
-        $this->originUrl = 'bitbucket.org';
-        $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository);
-    }
 
     /**
      * {@inheritDoc}
@@ -53,6 +33,7 @@ class HgBitbucketDriver extends VcsDriver
             if (array() === $repoData || !isset($repoData['tip'])) {
                 throw new \RuntimeException($this->url.' does not appear to be a mercurial repository, use '.$this->url.'.git if this is a git bitbucket repository');
             }
+            $this->hasIssues = !empty($repoData['has_issues']);
             $this->rootIdentifier = $repoData['tip']['raw_node'];
         }
 
@@ -85,46 +66,6 @@ class HgBitbucketDriver extends VcsDriver
         return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => '');
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    public function getComposerInformation($identifier)
-    {
-        if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) {
-            $this->infoCache[$identifier] = JsonFile::parseJson($res);
-        }
-
-        if (!isset($this->infoCache[$identifier])) {
-            $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/src/'.$identifier.'/composer.json';
-            $repoData = JsonFile::parseJson($this->getContents($resource), $resource);
-
-            // Bitbucket does not send different response codes for found and
-            // not found files, so we have to check the response structure.
-            // found:     {node: ..., data: ..., size: ..., ...}
-            // not found: {node: ..., files: [...], directories: [...], ...}
-
-            if (!array_key_exists('data', $repoData)) {
-                return;
-            }
-
-            $composer = JsonFile::parseJson($repoData['data'], $resource);
-
-            if (empty($composer['time'])) {
-                $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier;
-                $changeset = JsonFile::parseJson($this->getContents($resource), $resource);
-                $composer['time'] = $changeset['timestamp'];
-            }
-
-            if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
-                $this->cache->write($identifier, json_encode($composer));
-            }
-
-            $this->infoCache[$identifier] = $composer;
-        }
-
-        return $this->infoCache[$identifier];
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -177,4 +118,24 @@ class HgBitbucketDriver extends VcsDriver
 
         return true;
     }
+
+    protected function setupFallbackDriver($url)
+    {
+        $this->fallbackDriver = new HgDriver(
+            array('url' => $url),
+            $this->io,
+            $this->config,
+            $this->process,
+            $this->remoteFilesystem
+        );
+        $this->fallbackDriver->initialize();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function generateSshUrl()
+    {
+        return 'hg@' . $this->originUrl . '/' . $this->owner.'/'.$this->repository;
+    }
 }

+ 24 - 17
src/Composer/Repository/Vcs/HgDriver.php

@@ -17,6 +17,7 @@ use Composer\Json\JsonFile;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\Filesystem;
 use Composer\IO\IOInterface;
+use Symfony\Component\Process\Process;
 
 /**
  * @author Per Bernhardt <plb@webfactory.de>
@@ -114,28 +115,34 @@ class HgDriver extends VcsDriver
     }
 
     /**
-     * {@inheritDoc}
+     * {@inheritdoc}
      */
-    public function getComposerInformation($identifier)
+    public function getFileContent($file, $identifier)
     {
-        if (!isset($this->infoCache[$identifier])) {
-            $this->process->execute(sprintf('hg cat -r %s composer.json', ProcessExecutor::escape($identifier)), $composer, $this->repoDir);
-
-            if (!trim($composer)) {
-                return;
-            }
+        $resource = sprintf('hg cat -r %s %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file));
+        $this->process->execute(sprintf('hg cat -r %s', $resource), $content, $this->repoDir);
 
-            $composer = JsonFile::parseJson($composer, $identifier);
-
-            if (empty($composer['time'])) {
-                $this->process->execute(sprintf('hg log --template "{date|rfc3339date}" -r %s', ProcessExecutor::escape($identifier)), $output, $this->repoDir);
-                $date = new \DateTime(trim($output), new \DateTimeZone('UTC'));
-                $composer['time'] = $date->format('Y-m-d H:i:s');
-            }
-            $this->infoCache[$identifier] = $composer;
+        if (!trim($content)) {
+            return;
         }
 
-        return $this->infoCache[$identifier];
+        return $content;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        $this->process->execute(
+            sprintf(
+                'hg log --template "{date|rfc3339date}" -r %s',
+                ProcessExecutor::escape($identifier)
+            ),
+            $output,
+            $this->repoDir
+        );
+        return new \DateTime(trim($output), new \DateTimeZone('UTC'));
     }
 
     /**

+ 15 - 15
src/Composer/Repository/Vcs/PerforceDriver.php

@@ -24,10 +24,8 @@ class PerforceDriver extends VcsDriver
 {
     protected $depot;
     protected $branch;
+    /** @var Perforce */
     protected $perforce;
-    protected $composerInfo;
-    protected $composerInfoIdentifier;
-
     /**
      * {@inheritDoc}
      */
@@ -59,19 +57,21 @@ class PerforceDriver extends VcsDriver
         $this->perforce = Perforce::create($repoConfig, $this->getUrl(), $repoDir, $this->process, $this->io);
     }
 
+
     /**
-     * {@inheritDoc}
+     * {@inheritdoc}
      */
-    public function getComposerInformation($identifier)
+    public function getFileContent($file, $identifier)
     {
-        if (!empty($this->composerInfoIdentifier)) {
-            if (strcmp($identifier, $this->composerInfoIdentifier) === 0) {
-                return $this->composerInfo;
-            }
-        }
-        $composer_info = $this->perforce->getComposerInformation($identifier);
+        return $this->perforce->getFileContent($file, $identifier);
+    }
 
-        return $composer_info;
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        return null;
     }
 
     /**
@@ -138,10 +138,10 @@ class PerforceDriver extends VcsDriver
      */
     public function hasComposerFile($identifier)
     {
-        $this->composerInfo = $this->perforce->getComposerInformation('//' . $this->depot . '/' . $identifier);
-        $this->composerInfoIdentifier = $identifier;
+        $composerInfo = $this->perforce->getComposerInformation('//' . $this->depot . '/' . $identifier);
+        $composerInfoIdentifier = $identifier;
 
-        return !empty($this->composerInfo);
+        return !empty($composerInfo);
     }
 
     /**

+ 60 - 34
src/Composer/Repository/Vcs/SvnDriver.php

@@ -116,54 +116,80 @@ class SvnDriver extends VcsDriver
     }
 
     /**
-     * {@inheritDoc}
+     * {@inheritdoc}
      */
     public function getComposerInformation($identifier)
+    {
+        if (!isset($this->infoCache[$identifier])) {
+            if ($res = $this->cache->read($identifier.'.json')) {
+                return $this->infoCache[$identifier] = JsonFile::parseJson($res);
+            }
+
+            $composer = $this->getBaseComposerInformation($identifier);
+
+            $this->cache->write($identifier.'.json', json_encode($composer));
+
+            $this->infoCache[$identifier] = $composer;
+        }
+
+
+        return $this->infoCache[$identifier];
+    }
+
+    /**
+     * @param string $file
+     * @param string $identifier
+     */
+    public function getFileContent($file, $identifier)
     {
         $identifier = '/' . trim($identifier, '/') . '/';
 
-        if ($res = $this->cache->read($identifier.'.json')) {
-            $this->infoCache[$identifier] = JsonFile::parseJson($res);
+        preg_match('{^(.+?)(@\d+)?/$}', $identifier, $match);
+        if (!empty($match[2])) {
+            $path = $match[1];
+            $rev = $match[2];
+        } else {
+            $path = $identifier;
+            $rev = '';
         }
 
-        if (!isset($this->infoCache[$identifier])) {
-            preg_match('{^(.+?)(@\d+)?/$}', $identifier, $match);
-            if (!empty($match[2])) {
-                $path = $match[1];
-                $rev = $match[2];
-            } else {
-                $path = $identifier;
-                $rev = '';
+        try {
+            $resource = $path.$file;
+            $output = $this->execute('svn cat', $this->baseUrl . $resource . $rev);
+            if (!trim($output)) {
+                return null;
             }
+        } catch (\RuntimeException $e) {
+            throw new TransportException($e->getMessage());
+        }
 
-            try {
-                $resource = $path.'composer.json';
-                $output = $this->execute('svn cat', $this->baseUrl . $resource . $rev);
-                if (!trim($output)) {
-                    return;
-                }
-            } catch (\RuntimeException $e) {
-                throw new TransportException($e->getMessage());
-            }
+        return $output;
+    }
 
-            $composer = JsonFile::parseJson($output, $this->baseUrl . $resource . $rev);
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        $identifier = '/' . trim($identifier, '/') . '/';
 
-            if (empty($composer['time'])) {
-                $output = $this->execute('svn info', $this->baseUrl . $path . $rev);
-                foreach ($this->process->splitLines($output) as $line) {
-                    if ($line && preg_match('{^Last Changed Date: ([^(]+)}', $line, $match)) {
-                        $date = new \DateTime($match[1], new \DateTimeZone('UTC'));
-                        $composer['time'] = $date->format('Y-m-d H:i:s');
-                        break;
-                    }
-                }
-            }
+        preg_match('{^(.+?)(@\d+)?/$}', $identifier, $match);
+        if (!empty($match[2])) {
+            $path = $match[1];
+            $rev = $match[2];
+        } else {
+            $path = $identifier;
+            $rev = '';
+        }
 
-            $this->cache->write($identifier.'.json', json_encode($composer));
-            $this->infoCache[$identifier] = $composer;
+        $output = $this->execute('svn info', $this->baseUrl . $path . $rev);
+        foreach ($this->process->splitLines($output) as $line) {
+            if ($line && preg_match('{^Last Changed Date: ([^(]+)}', $line, $match)) {
+                return new \DateTime($match[1], new \DateTimeZone('UTC'));
+            }
         }
 
-        return $this->infoCache[$identifier];
+        return null;
     }
 
     /**

+ 57 - 0
src/Composer/Repository/Vcs/VcsDriver.php

@@ -12,10 +12,12 @@
 
 namespace Composer\Repository\Vcs;
 
+use Composer\Cache;
 use Composer\Downloader\TransportException;
 use Composer\Config;
 use Composer\Factory;
 use Composer\IO\IOInterface;
+use Composer\Json\JsonFile;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\Util\Filesystem;
@@ -41,6 +43,10 @@ abstract class VcsDriver implements VcsDriverInterface
     protected $process;
     /** @var RemoteFilesystem */
     protected $remoteFilesystem;
+    /** @var array */
+    protected $infoCache = array();
+    /** @var Cache */
+    protected $cache;
 
     /**
      * Constructor.
@@ -66,6 +72,57 @@ abstract class VcsDriver implements VcsDriverInterface
         $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
     }
 
+    /**
+     * Returns whether or not the given $identifier should be cached or not.
+     *
+     * @param string $identifier
+     * @return boolean
+     */
+    protected function shouldCache($identifier)
+    {
+        return $this->cache && preg_match('{[a-f0-9]{40}}i', $identifier);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getComposerInformation($identifier)
+    {
+        if (!isset($this->infoCache[$identifier])) {
+            if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) {
+                return $this->infoCache[$identifier] = JsonFile::parseJson($res);
+            }
+
+            $composer = $this->getBaseComposerInformation($identifier);
+
+            if ($this->shouldCache($identifier)) {
+                $this->cache->write($identifier, json_encode($composer));
+            }
+
+            $this->infoCache[$identifier] = $composer;
+        }
+
+
+        return $this->infoCache[$identifier];
+    }
+
+    protected function getBaseComposerInformation($identifier)
+    {
+        $composerFileContent = $this->getFileContent('composer.json', $identifier);
+
+        if (!$composerFileContent) {
+            return null;
+        }
+
+        $composer = JsonFile::parseJson($composerFileContent, $identifier . ':composer.json');
+
+        if (empty($composer['time']) && $changeDate = $this->getChangeDate($identifier)) {
+            $composer['time'] = $changeDate->format('Y-m-d H:i:s');
+        }
+
+        return $composer;
+    }
+
     /**
      * {@inheritDoc}
      */

+ 17 - 0
src/Composer/Repository/Vcs/VcsDriverInterface.php

@@ -33,6 +33,23 @@ interface VcsDriverInterface
      */
     public function getComposerInformation($identifier);
 
+    /**
+     * Return the content of $file or null if the file does not exist.
+     *
+     * @param string $file
+     * @param string $identifier
+     * @return string
+     */
+    public function getFileContent($file, $identifier);
+
+    /**
+     * Get the changedate for $identifier.
+     *
+     * @param string $identifier
+     * @return \DateTime
+     */
+    public function getChangeDate($identifier);
+
     /**
      * Return the root identifier (trunk, master, default/tip ..)
      *

+ 34 - 33
src/Composer/Util/Perforce.php

@@ -304,7 +304,9 @@ class Perforce
 
     public function connectClient()
     {
-        $p4CreateClientCommand = $this->generateP4Command('client -i < ' . str_replace(" ", "\\ ", $this->getP4ClientSpec()));
+        $p4CreateClientCommand = $this->generateP4Command(
+            'client -i < ' . str_replace(" ", "\\ ", $this->getP4ClientSpec())
+        );
         $this->executeCommand($p4CreateClientCommand);
     }
 
@@ -397,55 +399,54 @@ class Perforce
 
     public function getComposerInformation($identifier)
     {
-        $index = strpos($identifier, '@');
-        if ($index === false) {
-            $composerJson = $identifier. '/composer.json';
+        $composerFileContent = $this->getFileContent('composer.json', $identifier);
 
-            return $this->getComposerInformationFromPath($composerJson);
+        if (!$composerFileContent) {
+            return;
         }
 
-        return $this->getComposerInformationFromLabel($identifier, $index);
+        return json_decode($composerFileContent, true);
     }
 
-    public function getComposerInformationFromPath($composerJson)
+    public function getFileContent($file, $identifier)
     {
-        $command = $this->generateP4Command(' print ' . $composerJson);
+        $path = $this->getFilePath($file, $identifier);
+
+        $command = $this->generateP4Command(' print ' . $path);
         $this->executeCommand($command);
         $result = $this->commandResult;
-        $index = strpos($result, '{');
-        if ($index === false) {
-            return '';
-        }
-        if ($index >= 0) {
-            $rawData = substr($result, $index);
-            $composer_info = json_decode($rawData, true);
 
-            return $composer_info;
+        if (!trim($result)) {
+            return null;
         }
 
-        return '';
+        return $result;
     }
 
-    public function getComposerInformationFromLabel($identifier, $index)
+    public function getFilePath($file, $identifier)
     {
-        $composerJsonPath = substr($identifier, 0, $index) . '/composer.json' . substr($identifier, $index);
-        $command = $this->generateP4Command(' files ' . $composerJsonPath, false);
-        $this->executeCommand($command);
-        $result = $this->commandResult;
-        $index2 = strpos($result, 'no such file(s).');
-        if ($index2 === false) {
-            $index3 = strpos($result, 'change');
-            if (!($index3 === false)) {
-                $phrase = trim(substr($result, $index3));
-                $fields = explode(' ', $phrase);
-                $id = $fields[1];
-                $composerJson = substr($identifier, 0, $index) . '/composer.json@' . $id;
-
-                return $this->getComposerInformationFromPath($composerJson);
+        $index = strpos($identifier, '@');
+        if ($index === false) {
+            $path = $identifier. '/' . $file;
+
+            return $path;
+        } else {
+            $path = substr($identifier, 0, $index) . '/' . $file . substr($identifier, $index);
+            $command = $this->generateP4Command(' files ' . $path, false);
+            $this->executeCommand($command);
+            $result = $this->commandResult;
+            $index2 = strpos($result, 'no such file(s).');
+            if ($index2 === false) {
+                $index3 = strpos($result, 'change');
+                if ($index3 !== false) {
+                    $phrase = trim(substr($result, $index3));
+                    $fields = explode(' ', $phrase);
+                    return substr($identifier, 0, $index) . '/' . $file . '@' . $fields[1];
+                }
             }
         }
 
-        return "";
+        return null;
     }
 
     public function getBranches()