1
0
Fork 0

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

pull/5886/head
Jordi Boggiano 2016-11-18 19:37:33 +01:00
commit b2e1d4cb9d
13 changed files with 586 additions and 462 deletions

View File

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

View File

@ -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 %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file));
$command = sprintf('fossil cat -r %s composer.json', ProcessExecutor::escape($identifier)); $this->process->execute($command, $content, $this->checkoutDir);
$this->process->execute($command, $composer, $this->checkoutDir);
if (trim($composer) === '') { if (!trim($content)) {
return; return null;
}
$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;
} }
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'));
} }
/** /**

View File

@ -12,53 +12,25 @@
namespace Composer\Repository\Vcs; namespace Composer\Repository\Vcs;
use Composer\Cache;
use Composer\Config; use Composer\Config;
use Composer\Downloader\TransportException;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Bitbucket;
/** /**
* @author Per Bernhardt <plb@webfactory.de> * @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} * {@inheritDoc}
*/ */
public function getRootIdentifier() public function getRootIdentifier()
{ {
if ($this->gitDriver) { if ($this->fallbackDriver) {
return $this->gitDriver->getRootIdentifier(); return $this->fallbackDriver->getRootIdentifier();
} }
if (null === $this->rootIdentifier) { if (null === $this->rootIdentifier) {
@ -76,8 +48,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
*/ */
public function getUrl() public function getUrl()
{ {
if ($this->gitDriver) { if ($this->fallbackDriver) {
return $this->gitDriver->getUrl(); return $this->fallbackDriver->getUrl();
} }
return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git';
@ -88,8 +60,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
*/ */
public function getSource($identifier) public function getSource($identifier)
{ {
if ($this->gitDriver) { if ($this->fallbackDriver) {
return $this->gitDriver->getSource($identifier); return $this->fallbackDriver->getSource($identifier);
} }
return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $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' => ''); 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} * {@inheritDoc}
*/ */
public function getTags() public function getTags()
{ {
if ($this->gitDriver) { if ($this->fallbackDriver) {
return $this->gitDriver->getTags(); return $this->fallbackDriver->getTags();
} }
if (null === $this->tags) { if (null === $this->tags) {
@ -194,8 +104,8 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
*/ */
public function getBranches() public function getBranches()
{ {
if ($this->gitDriver) { if ($this->fallbackDriver) {
return $this->gitDriver->getBranches(); return $this->fallbackDriver->getBranches();
} }
if (null === $this->branches) { if (null === $this->branches) {
@ -228,75 +138,26 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
return true; 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 * @param string $url
*/ */
private function setupGitDriver($url) protected function setupFallbackDriver($url)
{ {
$this->gitDriver = new GitDriver( $this->fallbackDriver = new GitDriver(
array('url' => $url), array('url' => $url),
$this->io, $this->io,
$this->config, $this->config,
$this->process, $this->process,
$this->remoteFilesystem $this->remoteFilesystem
); );
$this->gitDriver->initialize(); $this->fallbackDriver->initialize();
}
/**
* {@inheritdoc}
*/
protected function generateSshUrl()
{
return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git';
} }
} }

View File

@ -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)) { $resource = sprintf('%s:%s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file));
$this->infoCache[$identifier] = JsonFile::parseJson($res); $this->process->execute(sprintf('git show %s', $resource), $content, $this->repoDir);
if (!trim($content)) {
return null;
} }
if (!isset($this->infoCache[$identifier])) { return $content;
$resource = sprintf('%s:composer.json', ProcessExecutor::escape($identifier)); }
$this->process->execute(sprintf('git show %s', $resource), $composer, $this->repoDir);
if (!trim($composer)) { /**
return; * {@inheritdoc}
} */
public function getChangeDate($identifier)
$composer = JsonFile::parseJson($composer, $resource); {
$this->process->execute(sprintf(
if (empty($composer['time'])) { 'git log -1 --format=%%at %s',
$this->process->execute(sprintf('git log -1 --format=%%at %s', ProcessExecutor::escape($identifier)), $output, $this->repoDir); ProcessExecutor::escape($identifier)
$date = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); ), $output, $this->repoDir);
$composer['time'] = $date->format('Y-m-d H:i:s'); return new \DateTime('@'.trim($output), new \DateTimeZone('UTC'));
}
if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
$this->cache->write($identifier, json_encode($composer));
}
$this->infoCache[$identifier] = $composer;
}
return $this->infoCache[$identifier];
} }
/** /**

View File

@ -146,50 +146,23 @@ class GitHubDriver extends VcsDriver
return $this->gitDriver->getComposerInformation($identifier); 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])) { if (!isset($this->infoCache[$identifier])) {
$notFoundRetries = 2; if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) {
while ($notFoundRetries) { return $this->infoCache[$identifier] = JsonFile::parseJson($res);
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 ($composer) { $composer = $this->getBaseComposerInformation($identifier);
$composer = JsonFile::parseJson($composer, $resource);
if (empty($composer['time'])) { // specials for github
$resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); if (!isset($composer['support']['source'])) {
$commit = JsonFile::parseJson($this->getContents($resource), $resource); $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier;
$composer['time'] = $commit['commit']['committer']['date']; $composer['support']['source'] = sprintf('https://%s/%s/%s/tree/%s', $this->originUrl, $this->owner, $this->repository, $label);
} }
if (!isset($composer['support']['source'])) { if (!isset($composer['support']['issues']) && $this->hasIssues) {
$label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier; $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository);
$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)); $this->cache->write($identifier, json_encode($composer));
} }
@ -199,6 +172,54 @@ class GitHubDriver extends VcsDriver
return $this->infoCache[$identifier]; 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} * {@inheritDoc}
*/ */

View File

@ -32,9 +32,6 @@ class GitLabDriver extends VcsDriver
private $owner; private $owner;
private $repository; private $repository;
private $cache;
private $infoCache = array();
/** /**
* @var array Project data returned by GitLab API * @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. * {@inheritdoc}
*
* if specific keys arent present it will try and infer them by default values.
*
* {@inheritDoc}
*/ */
public function getComposerInformation($identifier) public function getFileContent($file, $identifier)
{ {
// Convert the root identifier to a cachable commit id // Convert the root identifier to a cachable commit id
if (!preg_match('{[a-f0-9]{40}}i', $identifier)) { if (!preg_match('{[a-f0-9]{40}}i', $identifier)) {
@ -115,34 +108,33 @@ class GitLabDriver extends VcsDriver
} }
} }
if (isset($this->infoCache[$identifier])) { $resource = $this->getApiUrl().'/repository/blobs/'.$identifier.'?filepath=' . $file;
return $this->infoCache[$identifier];
}
if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) {
return $this->infoCache[$identifier] = JsonFile::parseJson($res, $res);
}
try { try {
$composer = $this->fetchComposerFile($identifier); $content = $this->getContents($resource);
} catch (TransportException $e) { } catch (TransportException $e) {
if ($e->getCode() !== 404) { if ($e->getCode() !== 404) {
throw $e; throw $e;
} }
$composer = false; return null;
} }
if ($composer && !isset($composer['time']) && isset($this->commits[$identifier])) { return $content;
$composer['time'] = $this->commits[$identifier]['committed_date'];
}
if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
$this->cache->write($identifier, json_encode($composer));
}
return $this->infoCache[$identifier] = $composer;
} }
/**
* {@inheritdoc}
*/
public function getChangeDate($identifier)
{
if (isset($this->commits[$identifier])) {
return new \DateTime($this->commits[$identifier]['committed_date']);
}
return new \DateTime();
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -209,20 +201,6 @@ class GitLabDriver extends VcsDriver
return $this->tags; return $this->tags;
} }
/**
* Fetches composer.json file from the repository through api.
*
* @param string $identifier
*
* @return array
*/
protected function fetchComposerFile($identifier)
{
$resource = $this->getApiUrl().'/repository/blobs/'.$identifier.'?filepath=composer.json';
return JsonFile::parseJson($this->getContents($resource), $resource);
}
/** /**
* @return string Base URL for GitLab API v3 * @return string Base URL for GitLab API v3
*/ */

View File

@ -12,7 +12,6 @@
namespace Composer\Repository\Vcs; namespace Composer\Repository\Vcs;
use Composer\Cache;
use Composer\Config; use Composer\Config;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
@ -20,27 +19,8 @@ use Composer\IO\IOInterface;
/** /**
* @author Per Bernhardt <plb@webfactory.de> * @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} * {@inheritDoc}
@ -53,6 +33,7 @@ class HgBitbucketDriver extends VcsDriver
if (array() === $repoData || !isset($repoData['tip'])) { 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'); 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']; $this->rootIdentifier = $repoData['tip']['raw_node'];
} }
@ -85,46 +66,6 @@ class HgBitbucketDriver extends VcsDriver
return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); 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} * {@inheritDoc}
*/ */
@ -177,4 +118,24 @@ class HgBitbucketDriver extends VcsDriver
return true; 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;
}
} }

View File

@ -17,6 +17,7 @@ use Composer\Json\JsonFile;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Symfony\Component\Process\Process;
/** /**
* @author Per Bernhardt <plb@webfactory.de> * @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])) { $resource = sprintf('hg cat -r %s %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file));
$this->process->execute(sprintf('hg cat -r %s composer.json', ProcessExecutor::escape($identifier)), $composer, $this->repoDir); $this->process->execute(sprintf('hg cat -r %s', $resource), $content, $this->repoDir);
if (!trim($composer)) { if (!trim($content)) {
return; return;
}
$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;
} }
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'));
} }
/** /**

View File

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

View File

@ -116,56 +116,82 @@ class SvnDriver extends VcsDriver
} }
/** /**
* {@inheritDoc} * {@inheritdoc}
*/ */
public function getComposerInformation($identifier) public function getComposerInformation($identifier)
{ {
$identifier = '/' . trim($identifier, '/') . '/';
if ($res = $this->cache->read($identifier.'.json')) {
$this->infoCache[$identifier] = JsonFile::parseJson($res);
}
if (!isset($this->infoCache[$identifier])) { if (!isset($this->infoCache[$identifier])) {
preg_match('{^(.+?)(@\d+)?/$}', $identifier, $match); if ($res = $this->cache->read($identifier.'.json')) {
if (!empty($match[2])) { return $this->infoCache[$identifier] = JsonFile::parseJson($res);
$path = $match[1];
$rev = $match[2];
} else {
$path = $identifier;
$rev = '';
} }
try { $composer = $this->getBaseComposerInformation($identifier);
$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());
}
$composer = JsonFile::parseJson($output, $this->baseUrl . $resource . $rev);
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;
}
}
}
$this->cache->write($identifier.'.json', json_encode($composer)); $this->cache->write($identifier.'.json', json_encode($composer));
$this->infoCache[$identifier] = $composer; $this->infoCache[$identifier] = $composer;
} }
return $this->infoCache[$identifier]; return $this->infoCache[$identifier];
} }
/**
* @param string $file
* @param string $identifier
*/
public function getFileContent($file, $identifier)
{
$identifier = '/' . trim($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());
}
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'));
}
}
return null;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -12,10 +12,12 @@
namespace Composer\Repository\Vcs; namespace Composer\Repository\Vcs;
use Composer\Cache;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\Config; use Composer\Config;
use Composer\Factory; use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
@ -41,6 +43,10 @@ abstract class VcsDriver implements VcsDriverInterface
protected $process; protected $process;
/** @var RemoteFilesystem */ /** @var RemoteFilesystem */
protected $remoteFilesystem; protected $remoteFilesystem;
/** @var array */
protected $infoCache = array();
/** @var Cache */
protected $cache;
/** /**
* Constructor. * Constructor.
@ -66,6 +72,57 @@ abstract class VcsDriver implements VcsDriverInterface
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); $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} * {@inheritDoc}
*/ */

View File

@ -33,6 +33,23 @@ interface VcsDriverInterface
*/ */
public function getComposerInformation($identifier); 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 ..) * Return the root identifier (trunk, master, default/tip ..)
* *

View File

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