Browse Source

add getFileContent function

This function is very similar to a part from getComposerInformation - so we can use this function in getComposerInformation too. And because it is almost everywhere the same we can put it to abstract class.

By implementing getComposerInformation in abstract class we need to add the getChangeDate to interface too. Only Problem: perforce seems not to support a ChangeDate. For this we use 'now' to have at least something.
Thomas Flori 8 years ago
parent
commit
597f834ae9

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

@@ -0,0 +1,184 @@
+<?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 GitDriver
+     */
+    protected $sshDriver;
+
+    /**
+     * {@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 getComposerInformation($identifier)
+    {
+        if ($this->sshDriver) {
+            return $this->sshDriver->getComposerInformation($identifier);
+        }
+
+        if (!isset($this->infoCache[$identifier])) {
+
+            $composer = parent::getComposerInformation($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;
+        }
+
+        return $this->infoCache[$identifier];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFileContent($file, $identifier) {
+        if ($this->sshDriver) {
+            return $this->sshDriver->getFileContent($file, $identifier);
+        }
+
+        if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier . ':' . $file)) {
+            return $res;
+        }
+
+        $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;
+        }
+
+        if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
+            $this->cache->write($identifier . ':' . $file, $fileData['data']);
+        }
+
+        return $fileData['data'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier) {
+        if ($this->sshDriver) {
+            return $this->sshDriver->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);
+
+            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;
+            }
+        }
+    }
+
+    /**
+     * Generate an SSH URL
+     *
+     * @return string
+     */
+    protected function generateSshUrl()
+    {
+        return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git';
+    }
+
+    protected function attemptCloneFallback()
+    {
+        try {
+            $this->setupSshDriver($this->generateSshUrl());
+
+            return;
+        } catch (\RuntimeException $e) {
+            $this->sshDriver = 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 setupSshDriver($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'));
     }
 
     /**

+ 22 - 133
src/Composer/Repository/Vcs/GitBitbucketDriver.php

@@ -22,43 +22,18 @@ 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->sshDriver) {
+            return $this->sshDriver->getRootIdentifier();
         }
 
         if (null === $this->rootIdentifier) {
@@ -76,8 +51,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
      */
     public function getUrl()
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getUrl();
+        if ($this->sshDriver) {
+            return $this->sshDriver->getUrl();
         }
 
         return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git';
@@ -88,8 +63,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
      */
     public function getSource($identifier)
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getSource($identifier);
+        if ($this->sshDriver) {
+            return $this->sshDriver->getSource($identifier);
         }
 
         return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier);
@@ -105,76 +80,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->sshDriver) {
+            return $this->sshDriver->getTags();
         }
 
         if (null === $this->tags) {
@@ -194,8 +107,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
      */
     public function getBranches()
     {
-        if ($this->gitDriver) {
-            return $this->gitDriver->getBranches();
+        if ($this->sshDriver) {
+            return $this->sshDriver->getBranches();
         }
 
         if (null === $this->branches) {
@@ -228,28 +141,19 @@ 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
+     * @param string $url
      */
-    private function generateSshUrl()
+    protected function setupSshDriver($url)
     {
-        return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git';
+        $this->sshDriver = new GitDriver(
+            array('url' => $url),
+            $this->io,
+            $this->config,
+            $this->process,
+            $this->remoteFilesystem
+        );
+        $this->sshDriver->initialize();
     }
 
     /**
@@ -284,19 +188,4 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
             }
         }
     }
-
-    /**
-     * @param string $url
-     */
-    private function setupGitDriver($url)
-    {
-        $this->gitDriver = new GitDriver(
-            array('url' => $url),
-            $this->io,
-            $this->config,
-            $this->process,
-            $this->remoteFilesystem
-        );
-        $this->gitDriver->initialize();
-    }
 }

+ 20 - 24
src/Composer/Repository/Vcs/GitDriver.php

@@ -120,38 +120,34 @@ 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 (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier . ':' . $file)) {
+            return $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));
-            }
+        if (!trim($content)) {
+            return null;
+        }
 
-            $this->infoCache[$identifier] = $composer;
+        if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
+            $this->cache->write($identifier . ':' . $file, $content);
         }
 
-        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'));
     }
 
     /**

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

@@ -146,57 +146,79 @@ 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;
-                }
+            $composer = parent::getComposerInformation($identifier);
+
+            // 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) {
-                $composer = JsonFile::parseJson($composer, $resource);
+            $this->infoCache[$identifier] = $composer;
+        }
+
+        return $this->infoCache[$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'];
+    /**
+     * {@inheritdoc}
+     */
+    public function getFileContent($file, $identifier)
+    {
+        if ($this->gitDriver) {
+            return $this->gitDriver->getFileContent($file, $identifier);
+        }
+
+        if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier . ':' . $file)) {
+            return $res;
+        }
+
+        $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);
                 }
-                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 ($content && preg_match('{[a-f0-9]{40}}i', $identifier)) {
+                    $this->cache->write($identifier . ':' . $file, $content);
                 }
-                if (!isset($composer['support']['issues']) && $this->hasIssues) {
-                    $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository);
+
+                return $content;
+            } catch (TransportException $e) {
+                if (404 !== $e->getCode()) {
+                    throw $e;
                 }
-            }
 
-            if ($composer && preg_match('{[a-f0-9]{40}}i', $identifier)) {
-                $this->cache->write($identifier, json_encode($composer));
+                // TODO should be removed when possible
+                // retry fetching if github returns a 404 since they happen randomly
+                $notFoundRetries--;
+                return null;
             }
+        }
 
-            $this->infoCache[$identifier] = $composer;
+        return null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier) {
+        if ($this->gitDriver) {
+            return $this->gitDriver->getChangeDate($identifier);
         }
 
-        return $this->infoCache[$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']);
     }
 
     /**

+ 48 - 1
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -33,7 +33,6 @@ class GitLabDriver extends VcsDriver
     private $repository;
 
     private $cache;
-    private $infoCache = array();
 
     /**
      * @var array Project data returned by GitLab API
@@ -143,6 +142,54 @@ class GitLabDriver extends VcsDriver
         return $this->infoCache[$identifier] = $composer;
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getFileContent($file, $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 (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier . ':' . $file)) {
+            return $res;
+        }
+
+        $resource = $this->getApiUrl().'/repository/blobs/'.$identifier.'?filepath=' . $file;
+
+        try {
+            $content = $this->getContents($resource);
+        } catch (TransportException $e) {
+            if ($e->getCode() !== 404) {
+                throw $e;
+            }
+            return null;
+        }
+
+        if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
+            $this->cache->write($identifier . ':' . $file, $content);
+        }
+
+        return $content;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier)
+    {
+        if (isset($this->commits[$identifier])) {
+            return new \DateTime($this->commits[$identifier]['committed_date']);
+        }
+
+        return new \DateTime();
+    }
+
+
     /**
      * {@inheritDoc}
      */

+ 13 - 60
src/Composer/Repository/Vcs/HgBitbucketDriver.php

@@ -20,27 +20,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 +34,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 +67,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 +119,15 @@ class HgBitbucketDriver extends VcsDriver
 
         return true;
     }
+
+    protected function setupSshDriver($url) {
+        $this->sshDriver = new HgDriver(
+            array('url' => $url),
+            $this->io,
+            $this->config,
+            $this->process,
+            $this->remoteFilesystem
+        );
+        $this->sshDriver->initialize();
+    }
 }

+ 17 - 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,27 @@ 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'));
     }
 
     /**

+ 16 - 0
src/Composer/Repository/Vcs/PerforceDriver.php

@@ -24,6 +24,7 @@ class PerforceDriver extends VcsDriver
 {
     protected $depot;
     protected $branch;
+    /** @var Perforce */
     protected $perforce;
     protected $composerInfo;
     protected $composerInfoIdentifier;
@@ -74,6 +75,21 @@ class PerforceDriver extends VcsDriver
         return $composer_info;
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getFileContent($file, $identifier)
+    {
+        return $this->perforce->getFileContent($file, $identifier);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier) {
+        return new \DateTime();
+    }
+
     /**
      * {@inheritDoc}
      */

+ 59 - 0
src/Composer/Repository/Vcs/SvnDriver.php

@@ -166,6 +166,65 @@ class SvnDriver extends VcsDriver
         return $this->infoCache[$identifier];
     }
 
+    /**
+     * @param string $file
+     * @param string $identifier
+     */
+    public function getFileContent($file, $identifier)
+    {
+        $identifier = '/' . trim($identifier, '/') . '/';
+
+        if ($res = $this->cache->read($identifier . ':' . $file)) {
+            return $res;
+        }
+
+        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());
+        }
+
+        $this->cache->write($identifier . ':' . $file, $output);
+
+        return $output;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getChangeDate($identifier) {
+        $identifier = '/' . trim($identifier, '/') . '/';
+
+        preg_match('{^(.+?)(@\d+)?/$}', $identifier, $match);
+        if (!empty($match[2])) {
+            $path = $match[1];
+            $rev = $match[2];
+        } else {
+            $path = $identifier;
+            $rev = '';
+        }
+
+        $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'));
+            }
+        }
+    }
+
     /**
      * {@inheritDoc}
      */

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

@@ -16,6 +16,7 @@ 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 +42,8 @@ abstract class VcsDriver implements VcsDriverInterface
     protected $process;
     /** @var RemoteFilesystem */
     protected $remoteFilesystem;
+    /** @var array */
+    protected $infoCache = array();
 
     /**
      * Constructor.
@@ -66,6 +69,31 @@ abstract class VcsDriver implements VcsDriverInterface
         $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getComposerInformation($identifier)
+    {
+        if (!isset($this->infoCache[$identifier])) {
+            $composerFileContent = $this->getFileContent('composer.json', $identifier);
+
+            if (!$composerFileContent) {
+                return null;
+            }
+
+            $composer = JsonFile::parseJson($composerFileContent, $identifier . ':composer.json');
+
+            if (empty($composer['time'])) {
+                $composer['time'] = $this->getChangeDate($identifier)->format('Y-m-d H:i:s');
+            }
+
+            $this->infoCache[$identifier] = $composer;
+        }
+
+
+        return $this->infoCache[$identifier];
+    }
+
     /**
      * {@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 ..)
      *

+ 42 - 0
src/Composer/Util/Perforce.php

@@ -407,6 +407,48 @@ class Perforce
         return $this->getComposerInformationFromLabel($identifier, $index);
     }
 
+    public function getFileContent($file, $identifier) {
+        $path = $this->getFilePath($file, $identifier);
+
+        $command = $this->generateP4Command(' print ' . $path);
+        $this->executeCommand($command);
+        $result = $this->commandResult;
+
+        if (!trim($result)) {
+            return null;
+        }
+
+        return $result;
+    }
+
+    public function getFilePath($file, $identifier) {
+        $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);
+                    $id = $fields[1];
+                    $path = substr($identifier, 0, $index) . '/' . $file . '@' . $id;
+
+                    return $path;
+                }
+            }
+        }
+
+        return null;
+    }
+
     public function getComposerInformationFromPath($composerJson)
     {
         $command = $this->generateP4Command(' print ' . $composerJson);