Browse Source

Merge pull request #87 from Seldaek/vcs

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

+ 1 - 0
bin/composer

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

+ 0 - 64
src/Composer/Repository/GitRepository.php

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

+ 178 - 0
src/Composer/Repository/Vcs/GitDriver.php

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

+ 151 - 0
src/Composer/Repository/Vcs/GitHubDriver.php

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

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

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

+ 177 - 0
src/Composer/Repository/VcsRepository.php

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