From 4c2e8596aa7dc3fb3b96e97cc88d28571edcf6d1 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 3 Nov 2011 19:22:38 +0100 Subject: [PATCH] Add VCS Repository and Git + GitHub drivers --- bin/composer | 1 + src/Composer/Repository/GitRepository.php | 64 ------- src/Composer/Repository/Vcs/GitDriver.php | 166 ++++++++++++++++ src/Composer/Repository/Vcs/GitHubDriver.php | 139 ++++++++++++++ .../Repository/Vcs/VcsDriverInterface.php | 80 ++++++++ src/Composer/Repository/VcsRepository.php | 177 ++++++++++++++++++ 6 files changed, 563 insertions(+), 64 deletions(-) delete mode 100644 src/Composer/Repository/GitRepository.php create mode 100644 src/Composer/Repository/Vcs/GitDriver.php create mode 100644 src/Composer/Repository/Vcs/GitHubDriver.php create mode 100644 src/Composer/Repository/Vcs/VcsDriverInterface.php create mode 100644 src/Composer/Repository/VcsRepository.php diff --git a/bin/composer b/bin/composer index a12fe3ea3..b75292ba9 100755 --- a/bin/composer +++ b/bin/composer @@ -17,6 +17,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'); diff --git a/src/Composer/Repository/GitRepository.php b/src/Composer/Repository/GitRepository.php deleted file mode 100644 index d78c71e98..000000000 --- a/src/Composer/Repository/GitRepository.php +++ /dev/null @@ -1,64 +0,0 @@ - - * Jordi Boggiano - * - * 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 - */ -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)); - } -} diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php new file mode 100644 index 000000000..8c3e54d40 --- /dev/null +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -0,0 +1,166 @@ + + */ +class GitDriver implements VcsDriverInterface +{ + protected $url; + protected $tags; + protected $branches; + 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() + { + return 'master'; + } + + /** + * {@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; + } +} diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php new file mode 100644 index 000000000..85527f299 --- /dev/null +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -0,0 +1,139 @@ + + */ +class GitHubDriver implements VcsDriverInterface +{ + protected $owner; + protected $repository; + protected $tags; + protected $branches; + 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() + { + $this->getTags(); + $this->getBranches(); + } + + /** + * {@inheritDoc} + */ + public function getRootIdentifier() + { + return 'master'; + } + + /** + * {@inheritDoc} + */ + public function getUrl() + { + return 'http://github.com/'.$this->owner.'/'.$this->repository.'.git'; + } + + /** + * {@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) + { + $label = array_search($identifier, (array) $this->tags) ?: $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('http://github.com/api/v2/json/commits/show/'.$this->owner.'/'.$this->repository.'/'.$identifier), true); + $composer['time'] = $commit['commit']['committed_date']; + } + $this->infoCache[$identifier] = $composer; + } + + return $this->infoCache[$identifier]; + } + + /** + * {@inheritDoc} + */ + public function getTags() + { + if (null === $this->tags) { + $tagsData = json_decode(file_get_contents('http://github.com/api/v2/json/repos/show/'.$this->owner.'/'.$this->repository.'/tags'), true); + $this->tags = $tagsData['tags']; + } + return $this->tags; + } + + /** + * {@inheritDoc} + */ + public function getBranches() + { + if (null === $this->branches) { + $branchesData = json_decode(file_get_contents('http://github.com/api/v2/json/repos/show/'.$this->owner.'/'.$this->repository.'/branches'), true); + $this->branches = $branchesData['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) + { + return preg_match('#^(?:https?|git)://github\.com/([^/]+)/(.+?)(?:\.git)?$#', $url, $match); + } +} diff --git a/src/Composer/Repository/Vcs/VcsDriverInterface.php b/src/Composer/Repository/Vcs/VcsDriverInterface.php new file mode 100644 index 000000000..b8e6d1269 --- /dev/null +++ b/src/Composer/Repository/Vcs/VcsDriverInterface.php @@ -0,0 +1,80 @@ + + */ +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); +} diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php new file mode 100644 index 000000000..78241cfd9 --- /dev/null +++ b/src/Composer/Repository/VcsRepository.php @@ -0,0 +1,177 @@ + + */ +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; + } +} \ No newline at end of file