Merge pull request #87 from Seldaek/vcs

Add VCS Repository and Git + GitHub drivers
main
Jordi Boggiano 13 years ago
commit 4002cab25b

@ -22,6 +22,7 @@ $vendorPath = 'vendor';
$rm = new Repository\RepositoryManager();
$rm->setLocalRepository(new Repository\FilesystemRepository(new JsonFile($vendorPath.'/.composer/installed.json')));
$rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
$rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
$rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository');
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');

@ -1,64 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Repository;
use Composer\Package\MemoryPackage;
use Composer\Package\BasePackage;
use Composer\Package\Link;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\Loader\ArrayLoader;
use Composer\Json\JsonFile;
/**
* FIXME This is majorly broken and incomplete, it was an experiment
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class GitRepository extends ArrayRepository
{
protected $url;
public function __construct(array $url)
{
if (!filter_var($config['url'], FILTER_VALIDATE_URL)) {
throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$url);
}
$this->url = $url;
}
protected function initialize()
{
parent::initialize();
if (preg_match('#^(?:https?|git(?:\+ssh)?|ssh)://#', $this->url)) {
// check if the repo is on github.com, read the composer.json file & branch/tags out of it
// otherwise, maybe we have to clone the repo to figure out what's in it
throw new \Exception('Not implemented yet');
} elseif (file_exists($this->url)) {
if (!file_exists($this->url.'/composer.json')) {
throw new \InvalidArgumentException('The repository at url '.$this->url.' does not contain a composer.json file.');
}
$json = new JsonFile($this->url.'/composer.json');
$config = $json->read();
if (!$config) {
throw new \UnexpectedValueException('Config file could not be parsed: '.$this->url.'/composer.json. Probably a JSON syntax error.');
}
} else {
throw new \InvalidArgumentException('Could not find repository at url '.$this->url);
}
$loader = new ArrayLoader($this->repositoryManager);
$this->addPackage($loader->load($config));
}
}

@ -0,0 +1,178 @@
<?php
namespace Composer\Repository\Vcs;
use Composer\Json\JsonFile;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class GitDriver implements VcsDriverInterface
{
protected $url;
protected $tags;
protected $branches;
protected $rootIdentifier;
protected $infoCache = array();
public function __construct($url)
{
$this->url = $url;
$this->tmpDir = sys_get_temp_dir() . '/composer-' . preg_replace('{[^a-z0-9]}i', '-', $url) . '/';
}
/**
* {@inheritDoc}
*/
public function initialize()
{
$url = escapeshellarg($this->url);
$tmpDir = escapeshellarg($this->tmpDir);
if (is_dir($this->tmpDir)) {
exec(sprintf('cd %s && git fetch origin', $tmpDir), $output);
} else {
exec(sprintf('git clone %s %s', $url, $tmpDir), $output);
}
$this->getTags();
$this->getBranches();
}
/**
* {@inheritDoc}
*/
public function getRootIdentifier()
{
if (null === $this->rootIdentifier) {
$this->rootIdentifier = 'master';
exec(sprintf('cd %s && git branch --no-color -r', escapeshellarg($this->tmpDir)), $output);
foreach ($output as $key => $branch) {
if ($branch && preg_match('{/HEAD +-> +[^/]+/(\S+)}', $branch, $match)) {
$this->rootIdentifier = $match[1];
break;
}
}
}
return $this->rootIdentifier;
}
/**
* {@inheritDoc}
*/
public function getUrl()
{
return $this->url;
}
/**
* {@inheritDoc}
*/
public function getSource($identifier)
{
$label = array_search($identifier, (array) $this->tags) ?: $identifier;
return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label);
}
/**
* {@inheritDoc}
*/
public function getDist($identifier)
{
return null;
}
/**
* {@inheritDoc}
*/
public function getComposerInformation($identifier)
{
if (!isset($this->infoCache[$identifier])) {
exec(sprintf('cd %s && git show %s:composer.json', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output);
$composer = implode("\n", $output);
unset($output);
if (!$composer) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl());
}
$composer = JsonFile::parseJson($composer);
if (!isset($composer['time'])) {
exec(sprintf('cd %s && git log -1 --format=\'%%at\' %s', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output);
$date = new \DateTime('@'.$output[0]);
$composer['time'] = $date->format('Y-m-d H:i:s');
}
$this->infoCache[$identifier] = $composer;
}
return $this->infoCache[$identifier];
}
/**
* {@inheritDoc}
*/
public function getTags()
{
if (null === $this->tags) {
exec(sprintf('cd %s && git tag', escapeshellarg($this->tmpDir)), $output);
$this->tags = array_combine($output, $output);
}
return $this->tags;
}
/**
* {@inheritDoc}
*/
public function getBranches()
{
if (null === $this->branches) {
$branches = array();
exec(sprintf('cd %s && git branch --no-color -rv', escapeshellarg($this->tmpDir)), $output);
foreach ($output as $key => $branch) {
if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
preg_match('{^ *[^/]+/(\S+) *([a-f0-9]+) .*$}', $branch, $match);
$branches[$match[1]] = $match[2];
}
}
$this->branches = $branches;
}
return $this->branches;
}
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/**
* {@inheritDoc}
*/
public static function supports($url, $deep = false)
{
if (preg_match('#(^git://|\.git$|git@|//git\.)#i', $url)) {
return true;
}
if (!$deep) {
return false;
}
// TODO try to connect to the server
return false;
}
}

@ -0,0 +1,151 @@
<?php
namespace Composer\Repository\Vcs;
use Composer\Json\JsonFile;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class GitHubDriver implements VcsDriverInterface
{
protected $owner;
protected $repository;
protected $tags;
protected $branches;
protected $rootIdentifier;
protected $infoCache = array();
public function __construct($url)
{
preg_match('#^(?:https?|git)://github\.com/([^/]+)/(.+?)(?:\.git)?$#', $url, $match);
$this->owner = $match[1];
$this->repository = $match[2];
}
/**
* {@inheritDoc}
*/
public function initialize()
{
}
/**
* {@inheritDoc}
*/
public function getRootIdentifier()
{
if (null === $this->rootIdentifier) {
$repoData = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository), true);
$this->rootIdentifier = $repoData['master_branch'] ?: 'master';
}
return $this->rootIdentifier;
}
/**
* {@inheritDoc}
*/
public function getUrl()
{
return 'http://github.com/'.$this->owner.'/'.$this->repository.'.git';
}
/**
* {@inheritDoc}
*/
public function getSource($identifier)
{
$label = array_search($identifier, $this->getTags()) ?: $identifier;
return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label);
}
/**
* {@inheritDoc}
*/
public function getDist($identifier)
{
$label = array_search($identifier, $this->getTags()) ?: $identifier;
$url = 'http://github.com/'.$this->owner.'/'.$this->repository.'/zipball/'.$label;
return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => '');
}
/**
* {@inheritDoc}
*/
public function getComposerInformation($identifier)
{
if (!isset($this->infoCache[$identifier])) {
$composer = @file_get_contents('https://raw.github.com/'.$this->owner.'/'.$this->repository.'/'.$identifier.'/composer.json');
if (!$composer) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl());
}
$composer = JsonFile::parseJson($composer);
if (!isset($composer['time'])) {
$commit = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.$identifier), true);
$composer['time'] = $commit['commit']['committer']['date'];
}
$this->infoCache[$identifier] = $composer;
}
return $this->infoCache[$identifier];
}
/**
* {@inheritDoc}
*/
public function getTags()
{
if (null === $this->tags) {
$tagsData = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/tags'), true);
$this->tags = array();
foreach ($tagsData as $tag) {
$this->tags[$tag['name']] = $tag['commit']['sha'];
}
}
return $this->tags;
}
/**
* {@inheritDoc}
*/
public function getBranches()
{
if (null === $this->branches) {
$branchData = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/branches'), true);
$this->branches = array();
foreach ($branchData as $branch) {
$this->branches[$branch['name']] = $branch['commit']['sha'];
}
}
return $this->branches;
}
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/**
* {@inheritDoc}
*/
public static function supports($url, $deep = false)
{
return preg_match('#^(?:https?|git)://github\.com/([^/]+)/(.+?)(?:\.git)?$#', $url, $match);
}
}

@ -0,0 +1,80 @@
<?php
namespace Composer\Repository\Vcs;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface VcsDriverInterface
{
/**
* Initializes the driver (git clone, svn checkout, fetch info etc)
*/
function initialize();
/**
* Return the composer.json file information
*
* @param string $identifier Any identifier to a specific branch/tag/commit
* @return array containing all infos from the composer.json file
*/
function getComposerInformation($identifier);
/**
* Return the root identifier (trunk, master, ..)
*
* @return string Identifier
*/
function getRootIdentifier();
/**
* Return list of branches in the repository
*
* @return array Branch names as keys, identifiers as values
*/
function getBranches();
/**
* Return list of tags in the repository
*
* @return array Tag names as keys, identifiers as values
*/
function getTags();
/**
* @param string $identifier Any identifier to a specific branch/tag/commit
* @return array With type, url reference and shasum keys.
*/
function getDist($identifier);
/**
* @param string $identifier Any identifier to a specific branch/tag/commit
* @return array With type, url and reference keys.
*/
function getSource($identifier);
/**
* Return the URL of the repository
*
* @return string
*/
function getUrl();
/**
* Return true if the repository has a composer file for a given identifier,
* false otherwise.
*
* @param string $identifier Any identifier to a specific branch/tag/commit
* @return boolean Whether the repository has a composer file for a given identifier.
*/
function hasComposerFile($identifier);
/**
* Checks if this driver can handle a given url
*
* @param string $url
* @param Boolean $shallow unless true, only shallow checks (url matching typically) should be done
* @return Boolean
*/
static function supports($url, $deep = false);
}

@ -0,0 +1,177 @@
<?php
namespace Composer\Repository;
use Composer\Repository\Vcs\VcsDriverInterface;
use Composer\Package\Version\VersionParser;
use Composer\Package\Loader\ArrayLoader;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class VcsRepository extends ArrayRepository
{
protected $url;
protected $packageName;
public function __construct(array $config)
{
if (!filter_var($config['url'], FILTER_VALIDATE_URL)) {
throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$config['url']);
}
$this->url = $config['url'];
}
protected function initialize()
{
parent::initialize();
$debug = false;
$drivers = array(
'Composer\Repository\Vcs\GitHubDriver',
'Composer\Repository\Vcs\GitDriver',
'Composer\Repository\Vcs\SvnDriver',
);
foreach ($drivers as $driver) {
if ($driver::supports($this->url)) {
$driver = new $driver($this->url);
$driver->initialize();
break;
}
}
$versionParser = new VersionParser;
$loader = new ArrayLoader($this->repositoryManager);
$versions = array();
if ($driver->hasComposerFile($driver->getRootIdentifier())) {
$data = $driver->getComposerInformation($driver->getRootIdentifier());
$this->packageName = !empty($data['name']) ? $data['name'] : null;
}
foreach ($driver->getTags() as $tag => $identifier) {
$parsedTag = $this->validateTag($versionParser, $tag);
if ($parsedTag && $driver->hasComposerFile($identifier)) {
try {
$data = $driver->getComposerInformation($identifier);
} catch (\Exception $e) {
if (strpos($e->getMessage(), 'JSON Parse Error') !== false) {
if ($debug) {
echo 'Skipped tag '.$tag.', '.$e->getMessage().PHP_EOL;
}
continue;
} else {
throw $e;
}
}
// manually versioned package
if (isset($data['version'])) {
$data['version_normalized'] = $versionParser->normalize($data['version']);
} else {
// auto-versionned package, read value from tag
$data['version'] = $tag;
$data['version_normalized'] = $parsedTag;
}
// make sure tag packages have no -dev flag
$data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']);
$data['version_normalized'] = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']);
// broken package, version doesn't match tag
if ($data['version_normalized'] !== $parsedTag) {
if ($debug) {
echo 'Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json'.PHP_EOL;
}
continue;
}
if ($debug) {
echo 'Importing tag '.$tag.PHP_EOL;
}
$this->addPackage($loader->load($this->preProcess($driver, $data, $identifier)));
} elseif ($debug) {
echo 'Skipped tag '.$tag.', invalid name or no composer file'.PHP_EOL;
}
}
foreach ($driver->getBranches() as $branch => $identifier) {
$parsedBranch = $this->validateBranch($versionParser, $branch);
if ($parsedBranch && $driver->hasComposerFile($identifier)) {
$data = $driver->getComposerInformation($identifier);
// manually versioned package
if (isset($data['version'])) {
$data['version_normalized'] = $versionParser->normalize($data['version']);
} else {
// auto-versionned package, read value from branch name
$data['version'] = $branch;
$data['version_normalized'] = $parsedBranch;
}
// make sure branch packages have a -dev flag
$normalizedStableVersion = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']);
$data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']) . '-dev';
$data['version_normalized'] = $normalizedStableVersion . '-dev';
// Skip branches that contain a version that has been tagged already
foreach ($this->getPackages() as $package) {
if ($normalizedStableVersion === $package->getVersion()) {
if ($debug) {
echo 'Skipped branch '.$branch.', already tagged'.PHP_EOL;
}
continue 2;
}
}
if ($debug) {
echo 'Importing branch '.$branch.PHP_EOL;
}
$this->addPackage($loader->load($this->preProcess($driver, $data, $identifier)));
} elseif ($debug) {
echo 'Skipped branch '.$branch.', invalid name or no composer file'.PHP_EOL;
}
}
}
private function preProcess(VcsDriverInterface $driver, array $data, $identifier)
{
// keep the name of the main identifier for all packages
$data['name'] = $this->packageName ?: $data['name'];
if (!isset($data['dist'])) {
$data['dist'] = $driver->getDist($identifier);
}
if (!isset($data['source'])) {
$data['source'] = $driver->getSource($identifier);
}
return $data;
}
private function validateBranch($versionParser, $branch)
{
try {
return $versionParser->normalizeBranch($branch);
} catch (\Exception $e) {
}
return false;
}
private function validateTag($versionParser, $version)
{
try {
return $versionParser->normalize($version);
} catch (\Exception $e) {
}
return false;
}
}
Loading…
Cancel
Save