Преглед изворни кода

Merge remote-tracking branch 'sroze/feature/path-repository'

Jordi Boggiano пре 9 година
родитељ
комит
3a79313b63

+ 1 - 0
composer.json

@@ -29,6 +29,7 @@
         "symfony/console": "~2.5",
         "symfony/finder": "~2.2",
         "symfony/process": "~2.1",
+        "symfony/filesystem": "~2.5",
         "seld/phar-utils": "~1.0",
         "seld/cli-prompt": "~1.0"
     },

+ 51 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "6fbac2ddcd4e9064c84090f6d5514412",
+    "hash": "3024e89a7e808b8dece156112459a7ea",
     "packages": [
         {
             "name": "composer/spdx-licenses",
@@ -328,6 +328,56 @@
             "homepage": "https://symfony.com",
             "time": "2015-07-26 09:08:40"
         },
+        {
+            "name": "symfony/filesystem",
+            "version": "v2.6.11",
+            "target-dir": "Symfony/Component/Filesystem",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/Filesystem.git",
+                "reference": "823c035b1a5c13a4924e324d016eb07e70f94735"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/Filesystem/zipball/823c035b1a5c13a4924e324d016eb07e70f94735",
+                "reference": "823c035b1a5c13a4924e324d016eb07e70f94735",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "~2.7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.6-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Symfony\\Component\\Filesystem\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Filesystem Component",
+            "homepage": "https://symfony.com",
+            "time": "2015-07-08 05:59:48"
+        },
         {
             "name": "symfony/finder",
             "version": "v2.6.11",

+ 50 - 0
src/Composer/Downloader/PathDownloader.php

@@ -0,0 +1,50 @@
+<?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\Downloader;
+
+use Composer\Package\PackageInterface;
+use Symfony\Component\Filesystem\Exception\IOException;
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * Download a package from a local path.
+ *
+ * @author Samuel Roze <samuel.roze@gmail.com>
+ * @author Johann Reinke <johann.reinke@gmail.com>
+ */
+class PathDownloader extends FileDownloader
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function download(PackageInterface $package, $path)
+    {
+        $fileSystem = new Filesystem();
+        if ($fileSystem->exists($path)) {
+            $fileSystem->remove($path);
+        }
+
+        try {
+            $fileSystem->symlink($package->getDistUrl(), $path);
+        } catch (IOException $e) {
+            $fileSystem->mirror($package->getDistUrl(), $path);
+        }
+
+        $this->io->writeError(sprintf(
+            '    Downloaded <info>%s</info> (<comment>%s</comment>) from %s',
+            $package->getName(),
+            $package->getFullPrettyVersion(),
+            $package->getDistUrl()
+        ));
+    }
+}

+ 5 - 1
src/Composer/Factory.php

@@ -16,6 +16,7 @@ use Composer\Config\JsonConfigSource;
 use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
 use Composer\Package\Archiver;
+use Composer\Package\Version\VersionGuesser;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\WritableRepositoryInterface;
 use Composer\Util\ProcessExecutor;
@@ -264,7 +265,8 @@ class Factory
 
         // load package
         $parser = new VersionParser;
-        $loader  = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io));
+        $guesser = new VersionGuesser(new ProcessExecutor($io), $parser);
+        $loader  = new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser);
         $package = $loader->load($localConfig);
         $composer->setPackage($package);
 
@@ -331,6 +333,7 @@ class Factory
         $rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository');
+        $rm->setRepositoryClass('path', 'Composer\Repository\PathRepository');
 
         return $rm;
     }
@@ -403,6 +406,7 @@ class Factory
         $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache));
         $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache));
         $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache));
+        $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache));
 
         return $dm;
     }

+ 18 - 177
src/Composer/Package/Loader/RootPackageLoader.php

@@ -16,13 +16,10 @@ use Composer\Package\BasePackage;
 use Composer\Package\AliasPackage;
 use Composer\Config;
 use Composer\Factory;
+use Composer\Package\Version\VersionGuesser;
 use Composer\Package\Version\VersionParser;
 use Composer\Repository\RepositoryManager;
-use Composer\Repository\Vcs\HgDriver;
-use Composer\IO\NullIO;
 use Composer\Util\ProcessExecutor;
-use Composer\Util\Git as GitUtil;
-use Composer\Util\Svn as SvnUtil;
 
 /**
  * ArrayLoader built for the sole purpose of loading the root package
@@ -33,16 +30,28 @@ use Composer\Util\Svn as SvnUtil;
  */
 class RootPackageLoader extends ArrayLoader
 {
+    /**
+     * @var RepositoryManager
+     */
     private $manager;
+
+    /**
+     * @var Config
+     */
     private $config;
-    private $process;
 
-    public function __construct(RepositoryManager $manager, Config $config, VersionParser $parser = null, ProcessExecutor $process = null)
+    /**
+     * @var VersionGuesser
+     */
+    private $versionGuesser;
+
+    public function __construct(RepositoryManager $manager, Config $config, VersionParser $parser = null, VersionGuesser $versionGuesser = null)
     {
+        parent::__construct($parser);
+
         $this->manager = $manager;
         $this->config = $config;
-        $this->process = $process ?: new ProcessExecutor();
-        parent::__construct($parser);
+        $this->versionGuesser = $versionGuesser ?: new VersionGuesser(new ProcessExecutor(), $this->versionParser);
     }
 
     public function load(array $config, $class = 'Composer\Package\RootPackage')
@@ -56,7 +65,7 @@ class RootPackageLoader extends ArrayLoader
             if (getenv('COMPOSER_ROOT_VERSION')) {
                 $version = getenv('COMPOSER_ROOT_VERSION');
             } else {
-                $version = $this->guessVersion($config);
+                $version = $this->versionGuesser->guessVersion($this->config, $config);
             }
 
             if (!$version) {
@@ -176,172 +185,4 @@ class RootPackageLoader extends ArrayLoader
 
         return $references;
     }
-
-    private function guessVersion(array $config)
-    {
-        if (function_exists('proc_open')) {
-            $version = $this->guessGitVersion($config);
-            if (null !== $version) {
-                return $version;
-            }
-
-            $version = $this->guessHgVersion($config);
-            if (null !== $version) {
-                return $version;
-            }
-
-            return $this->guessSvnVersion($config);
-        }
-    }
-
-    private function guessGitVersion(array $config)
-    {
-        GitUtil::cleanEnv();
-
-        // try to fetch current version from git tags
-        if (0 === $this->process->execute('git describe --exact-match --tags', $output)) {
-            try {
-                return $this->versionParser->normalize(trim($output));
-            } catch (\Exception $e) {
-            }
-        }
-
-        // try to fetch current version from git branch
-        if (0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output)) {
-            $branches = array();
-            $isFeatureBranch = false;
-            $version = null;
-
-            // find current branch and collect all branch names
-            foreach ($this->process->splitLines($output) as $branch) {
-                if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
-                    if ($match[1] === '(no branch)' || substr($match[1], 0, 10) === '(detached ') {
-                        $version = 'dev-'.$match[2];
-                        $isFeatureBranch = true;
-                    } else {
-                        $version = $this->versionParser->normalizeBranch($match[1]);
-                        $isFeatureBranch = 0 === strpos($version, 'dev-');
-                        if ('9999999-dev' === $version) {
-                            $version = 'dev-'.$match[1];
-                        }
-                    }
-                }
-
-                if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
-                    if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
-                        $branches[] = $match[1];
-                    }
-                }
-            }
-
-            if (!$isFeatureBranch) {
-                return $version;
-            }
-
-            // try to find the best (nearest) version branch to assume this feature's version
-            $version = $this->guessFeatureVersion($config, $version, $branches, 'git rev-list %candidate%..%branch%');
-
-            return $version;
-        }
-    }
-
-    private function guessHgVersion(array $config)
-    {
-        // try to fetch current version from hg branch
-        if (0 === $this->process->execute('hg branch', $output)) {
-            $branch = trim($output);
-            $version = $this->versionParser->normalizeBranch($branch);
-            $isFeatureBranch = 0 === strpos($version, 'dev-');
-
-            if ('9999999-dev' === $version) {
-                $version = 'dev-'.$branch;
-            }
-
-            if (!$isFeatureBranch) {
-                return $version;
-            }
-
-            // re-use the HgDriver to fetch branches (this properly includes bookmarks)
-            $config = array('url' => getcwd());
-            $driver = new HgDriver($config, new NullIO(), $this->config, $this->process);
-            $branches = array_keys($driver->getBranches());
-
-            // try to find the best (nearest) version branch to assume this feature's version
-            $version = $this->guessFeatureVersion($config, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"');
-
-            return $version;
-        }
-    }
-
-    private function guessFeatureVersion(array $config, $version, array $branches, $scmCmdline)
-    {
-        // ignore feature branches if they have no branch-alias or self.version is used
-        // and find the branch they came from to use as a version instead
-        if ((isset($config['extra']['branch-alias']) && !isset($config['extra']['branch-alias'][$version]))
-            || strpos(json_encode($config), '"self.version"')
-        ) {
-            $branch = preg_replace('{^dev-}', '', $version);
-            $length = PHP_INT_MAX;
-
-            $nonFeatureBranches = '';
-            if (!empty($config['non-feature-branches'])) {
-                $nonFeatureBranches = implode('|', $config['non-feature-branches']);
-            }
-
-            foreach ($branches as $candidate) {
-                // return directly, if branch is configured to be non-feature branch
-                if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) {
-                    return $version;
-                }
-
-                // do not compare against other feature branches
-                if ($candidate === $branch || !preg_match('{^(master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) {
-                    continue;
-                }
-
-                $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
-                if (0 !== $this->process->execute($cmdLine, $output)) {
-                    continue;
-                }
-
-                if (strlen($output) < $length) {
-                    $length = strlen($output);
-                    $version = $this->versionParser->normalizeBranch($candidate);
-                    if ('9999999-dev' === $version) {
-                        $version = 'dev-'.$match[1];
-                    }
-                }
-            }
-        }
-
-        return $version;
-    }
-
-    private function guessSvnVersion(array $config)
-    {
-        SvnUtil::cleanEnv();
-
-        // try to fetch current version from svn
-        if (0 === $this->process->execute('svn info --xml', $output)) {
-            $trunkPath = isset($config['trunk-path']) ? preg_quote($config['trunk-path'], '#') : 'trunk';
-            $branchesPath = isset($config['branches-path']) ? preg_quote($config['branches-path'], '#') : 'branches';
-            $tagsPath = isset($config['tags-path']) ? preg_quote($config['tags-path'], '#') : 'tags';
-
-            $urlPattern = '#<url>.*/('.$trunkPath.'|('.$branchesPath.'|'. $tagsPath .')/(.*))</url>#';
-
-            if (preg_match($urlPattern, $output, $matches)) {
-                if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) {
-                    // we are in a branches path
-                    $version = $this->versionParser->normalizeBranch($matches[3]);
-                    if ('9999999-dev' === $version) {
-                        $version = 'dev-'.$matches[3];
-                    }
-
-                    return $version;
-                }
-
-                return $this->versionParser->normalize(trim($matches[1]));
-            }
-        }
-    }
 }

+ 224 - 0
src/Composer/Package/Version/VersionGuesser.php

@@ -0,0 +1,224 @@
+<?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\Package\Version;
+
+use Composer\Config;
+use Composer\Repository\Vcs\HgDriver;
+use Composer\IO\NullIO;
+use Composer\Util\Git as GitUtil;
+use Composer\Util\ProcessExecutor;
+use Composer\Util\Svn as SvnUtil;
+
+/**
+ * Try to guess the current version number based on different VCS configuration.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Samuel Roze <samuel.roze@gmail.com>
+ */
+class VersionGuesser
+{
+    /**
+     * @var ProcessExecutor
+     */
+    private $process;
+
+    /**
+     * @var VersionParser
+     */
+    private $versionParser;
+
+    /**
+     * @var null|string
+     */
+    private $cwd;
+
+    /**
+     * @param ProcessExecutor $process
+     * @param VersionParser   $versionParser
+     * @param string          $cwd
+     */
+    public function __construct(ProcessExecutor $process, VersionParser $versionParser, $cwd = null)
+    {
+        $this->process = $process;
+        $this->versionParser = $versionParser;
+        $this->cwd = $cwd ?: getcwd();
+    }
+
+    public function guessVersion(Config $config, array $packageConfig)
+    {
+        if (function_exists('proc_open')) {
+            $version = $this->guessGitVersion($packageConfig);
+            if (null !== $version) {
+                return $version;
+            }
+
+            $version = $this->guessHgVersion($config, $packageConfig);
+            if (null !== $version) {
+                return $version;
+            }
+
+            return $this->guessSvnVersion($packageConfig);
+        }
+    }
+
+    private function guessGitVersion(array $config)
+    {
+        GitUtil::cleanEnv();
+
+        // try to fetch current version from git tags
+        if (0 === $this->process->execute('git describe --exact-match --tags', $output, $this->cwd)) {
+            try {
+                return $this->versionParser->normalize(trim($output));
+            } catch (\Exception $e) {
+            }
+        }
+
+        // try to fetch current version from git branch
+        if (0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output, $this->cwd)) {
+            $branches = array();
+            $isFeatureBranch = false;
+            $version = null;
+
+            // find current branch and collect all branch names
+            foreach ($this->process->splitLines($output) as $branch) {
+                if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
+                    if ($match[1] === '(no branch)' || substr($match[1], 0, 10) === '(detached ') {
+                        $version = 'dev-'.$match[2];
+                        $isFeatureBranch = true;
+                    } else {
+                        $version = $this->versionParser->normalizeBranch($match[1]);
+                        $isFeatureBranch = 0 === strpos($version, 'dev-');
+                        if ('9999999-dev' === $version) {
+                            $version = 'dev-'.$match[1];
+                        }
+                    }
+                }
+
+                if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
+                    if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
+                        $branches[] = $match[1];
+                    }
+                }
+            }
+
+            if (!$isFeatureBranch) {
+                return $version;
+            }
+
+            // try to find the best (nearest) version branch to assume this feature's version
+            $version = $this->guessFeatureVersion($config, $version, $branches, 'git rev-list %candidate%..%branch%');
+
+            return $version;
+        }
+    }
+
+    private function guessHgVersion(Config $config, array $packageConfig)
+    {
+        // try to fetch current version from hg branch
+        if (0 === $this->process->execute('hg branch', $output, $this->cwd)) {
+            $branch = trim($output);
+            $version = $this->versionParser->normalizeBranch($branch);
+            $isFeatureBranch = 0 === strpos($version, 'dev-');
+
+            if ('9999999-dev' === $version) {
+                $version = 'dev-'.$branch;
+            }
+
+            if (!$isFeatureBranch) {
+                return $version;
+            }
+
+            // re-use the HgDriver to fetch branches (this properly includes bookmarks)
+            $packageConfig = array('url' => $this->cwd);
+            $driver = new HgDriver($packageConfig, new NullIO(), $config, $this->process);
+            $branches = array_keys($driver->getBranches());
+
+            // try to find the best (nearest) version branch to assume this feature's version
+            $version = $this->guessFeatureVersion($config, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"');
+
+            return $version;
+        }
+    }
+
+    private function guessFeatureVersion(array $config, $version, array $branches, $scmCmdline)
+    {
+        // ignore feature branches if they have no branch-alias or self.version is used
+        // and find the branch they came from to use as a version instead
+        if ((isset($config['extra']['branch-alias']) && !isset($config['extra']['branch-alias'][$version]))
+            || strpos(json_encode($config), '"self.version"')
+        ) {
+            $branch = preg_replace('{^dev-}', '', $version);
+            $length = PHP_INT_MAX;
+
+            $nonFeatureBranches = '';
+            if (!empty($config['non-feature-branches'])) {
+                $nonFeatureBranches = implode('|', $config['non-feature-branches']);
+            }
+
+            foreach ($branches as $candidate) {
+                // return directly, if branch is configured to be non-feature branch
+                if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) {
+                    return $version;
+                }
+
+                // do not compare against other feature branches
+                if ($candidate === $branch || !preg_match('{^(master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) {
+                    continue;
+                }
+
+                $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
+                if (0 !== $this->process->execute($cmdLine, $output, $this->cwd)) {
+                    continue;
+                }
+
+                if (strlen($output) < $length) {
+                    $length = strlen($output);
+                    $version = $this->versionParser->normalizeBranch($candidate);
+                    if ('9999999-dev' === $version) {
+                        $version = 'dev-'.$match[1];
+                    }
+                }
+            }
+        }
+
+        return $version;
+    }
+
+    private function guessSvnVersion(array $config)
+    {
+        SvnUtil::cleanEnv();
+
+        // try to fetch current version from svn
+        if (0 === $this->process->execute('svn info --xml', $output, $this->cwd)) {
+            $trunkPath = isset($config['trunk-path']) ? preg_quote($config['trunk-path'], '#') : 'trunk';
+            $branchesPath = isset($config['branches-path']) ? preg_quote($config['branches-path'], '#') : 'branches';
+            $tagsPath = isset($config['tags-path']) ? preg_quote($config['tags-path'], '#') : 'tags';
+
+            $urlPattern = '#<url>.*/('.$trunkPath.'|('.$branchesPath.'|'. $tagsPath .')/(.*))</url>#';
+
+            if (preg_match($urlPattern, $output, $matches)) {
+                if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) {
+                    // we are in a branches path
+                    $version = $this->versionParser->normalizeBranch($matches[3]);
+                    if ('9999999-dev' === $version) {
+                        $version = 'dev-'.$matches[3];
+                    }
+
+                    return $version;
+                }
+
+                return $this->versionParser->normalize(trim($matches[1]));
+            }
+        }
+    }
+}

+ 134 - 0
src/Composer/Repository/PathRepository.php

@@ -0,0 +1,134 @@
+<?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\Config;
+use Composer\IO\IOInterface;
+use Composer\Json\JsonFile;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\Loader\LoaderInterface;
+use Composer\Package\Version\VersionGuesser;
+use Composer\Package\Version\VersionParser;
+use Composer\Util\ProcessExecutor;
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * This repository allows installing local packages that are not necessarily under their own VCS.
+ *
+ * The local packages will be symlinked when possible, else they will be copied.
+ *
+ * @code
+ * "require": {
+ *     "<vendor>/<local-package>": "*"
+ * },
+ * "repositories": [
+ *     {
+ *         "type": "path",
+ *         "url": "../../relative/path/to/package/"
+ *     },
+ *     {
+ *         "type": "path",
+ *         "url": "/absolute/path/to/package/"
+ *     }
+ * ]
+ * @endcode
+ *
+ * @author Samuel Roze <samuel.roze@gmail.com>
+ * @author Johann Reinke <johann.reinke@gmail.com>
+ */
+class PathRepository extends ArrayRepository
+{
+    /**
+     * @var Config
+     */
+    private $config;
+
+    /**
+     * @var Filesystem
+     */
+    private $fileSystem;
+
+    /**
+     * @var ArrayLoader
+     */
+    private $loader;
+
+    /**
+     * @var VersionGuesser
+     */
+    private $versionGuesser;
+
+    /**
+     * @var array
+     */
+    private $packageConfig;
+
+    /**
+     * @var string
+     */
+    private $path;
+
+    /**
+     * Initializes path repository.
+     *
+     * @param array $packageConfig
+     * @param IOInterface $io
+     * @param Config $config
+     * @param LoaderInterface $loader
+     * @param Filesystem $filesystem
+     * @param VersionGuesser $versionGuesser
+     */
+    public function __construct(array $packageConfig, IOInterface $io, Config $config, LoaderInterface $loader = null, Filesystem $filesystem = null, VersionGuesser $versionGuesser = null)
+    {
+        if (!isset($packageConfig['url'])) {
+            throw new \RuntimeException('You must specify the `url` configuration for the path repository');
+        }
+
+        $this->fileSystem = $filesystem ?: new Filesystem();
+        $this->loader = $loader ?: new ArrayLoader();
+        $this->config = $config;
+        $this->packageConfig = $packageConfig;
+        $this->path = realpath(rtrim($packageConfig['url'], '/')) . '/';
+        $this->versionGuesser = $versionGuesser ?: new VersionGuesser(new ProcessExecutor($io), new VersionParser(), $this->path);
+    }
+
+    /**
+     * Initializes path repository.
+     *
+     * This method will basically read the folder and add the found package.
+     *
+     */
+    protected function initialize()
+    {
+        parent::initialize();
+
+        $composerFilePath = $this->path.'composer.json';
+        if (!$this->fileSystem->exists($composerFilePath)) {
+            throw new \RuntimeException(sprintf('No `composer.json` file found in path repository "%s"', $this->path));
+        }
+
+        $json = file_get_contents($composerFilePath);
+        $package = JsonFile::parseJson($json, $composerFilePath);
+        $package['dist'] = array(
+            'type' => 'path',
+            'url' => $this->path,
+        );
+
+        if (!isset($package['version'])) {
+            $package['version'] = $this->versionGuesser->guessVersion($this->config, $this->packageConfig) ?: 'dev-master';
+        }
+
+        $package = $this->loader->load($package);
+        $this->addPackage($package);
+    }
+}

+ 8 - 1
src/Composer/Util/Filesystem.php

@@ -508,7 +508,14 @@ class Filesystem
         return unlink($path);
     }
 
-    private function isSymlinkedDirectory($directory)
+    /**
+     * return true if that directory is a symlink.
+     *
+     * @param string $directory
+     *
+     * @return bool
+     */
+    public function isSymlinkedDirectory($directory)
     {
         if (!is_dir($directory)) {
             return false;

+ 25 - 158
tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php

@@ -15,144 +15,43 @@ namespace Composer\Test\Package\Loader;
 use Composer\Config;
 use Composer\Package\Loader\RootPackageLoader;
 use Composer\Package\BasePackage;
+use Composer\Package\Version\VersionGuesser;
+use Composer\Package\Version\VersionParser;
 
 class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase
 {
-    public function testDetachedHeadBecomesDevHash()
+    protected function loadPackage($data)
     {
-        if (!function_exists('proc_open')) {
-            $this->markTestSkipped('proc_open() is not available');
-        }
-
-        $commitHash = '03a15d220da53c52eddd5f32ffca64a7b3801bea';
-
         $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager')
             ->disableOriginalConstructor()
-            ->getMock()
-        ;
-
-        $executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
-            ->setMethods(array('execute'))
-            ->disableArgumentCloning()
-            ->disableOriginalConstructor()
-            ->getMock()
-        ;
-
-        $executor
-            ->expects($this->at(0))
-            ->method('execute')
-            ->with('git describe --exact-match --tags')
-            ->willReturn(1)
-        ;
-
-        $self = $this;
-
-        $executor
-            ->expects($this->at(1))
-            ->method('execute')
-            ->willReturnCallback(function ($command, &$output) use ($self, $commitHash) {
-                $self->assertEquals('git branch --no-color --no-abbrev -v', $command);
-                $output = "* (no branch) $commitHash Commit message\n";
-
-                return 0;
-            })
-        ;
+            ->getMock();
 
         $config = new Config;
         $config->merge(array('repositories' => array('packagist' => false)));
-        $loader = new RootPackageLoader($manager, $config, null, $executor);
-        $package = $loader->load(array());
-
-        $this->assertEquals("dev-$commitHash", $package->getVersion());
-    }
-
-    public function testTagBecomesVersion()
-    {
-        if (!function_exists('proc_open')) {
-            $this->markTestSkipped('proc_open() is not available');
-        }
 
-        $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager')
-            ->disableOriginalConstructor()
-            ->getMock()
-        ;
-
-        $executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
-            ->setMethods(array('execute'))
-            ->disableArgumentCloning()
-            ->disableOriginalConstructor()
-            ->getMock()
-        ;
-
-        $self = $this;
-
-        $executor
-            ->expects($this->at(0))
-            ->method('execute')
-            ->willReturnCallback(function ($command, &$output) use ($self) {
-                $self->assertEquals('git describe --exact-match --tags', $command);
-                $output = "v2.0.5-alpha2";
-
-                return 0;
-            })
-        ;
-
-        $config = new Config;
-        $config->merge(array('repositories' => array('packagist' => false)));
-        $loader = new RootPackageLoader($manager, $config, null, $executor);
-        $package = $loader->load(array());
+        $loader = new RootPackageLoader($manager, $config);
 
-        $this->assertEquals("2.0.5.0-alpha2", $package->getVersion());
+        return $loader->load($data);
     }
 
-    public function testInvalidTagBecomesVersion()
+    public function testStabilityFlagsParsing()
     {
-        if (!function_exists('proc_open')) {
-            $this->markTestSkipped('proc_open() is not available');
-        }
-
-        $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager')
-            ->disableOriginalConstructor()
-            ->getMock()
-        ;
-
-        $executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
-            ->setMethods(array('execute'))
-            ->disableArgumentCloning()
-            ->disableOriginalConstructor()
-            ->getMock()
-        ;
-
-        $self = $this;
-
-        $executor
-            ->expects($this->at(0))
-            ->method('execute')
-            ->willReturnCallback(function ($command, &$output) use ($self) {
-                $self->assertEquals('git describe --exact-match --tags', $command);
-                $output = "foo-bar";
-
-                return 0;
-            })
-        ;
-
-        $executor
-            ->expects($this->at(1))
-            ->method('execute')
-            ->willReturnCallback(function ($command, &$output) use ($self) {
-                $self->assertEquals('git branch --no-color --no-abbrev -v', $command);
-                $output = "* foo 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n";
-
-                return 0;
-            })
-        ;
-
-        $config = new Config;
-        $config->merge(array('repositories' => array('packagist' => false)));
-        $loader = new RootPackageLoader($manager, $config, null, $executor);
-        $package = $loader->load(array());
+        $package = $this->loadPackage(array(
+            'require' => array(
+                'foo/bar' => '~2.1.0-beta2',
+                'bar/baz' => '1.0.x-dev as 1.2.0',
+                'qux/quux' => '1.0.*@rc',
+                'zux/complex' => '~1.0,>=1.0.2@dev'
+            ),
+            'minimum-stability' => 'alpha',
+        ));
 
-        $this->assertEquals("dev-foo", $package->getVersion());
+        $this->assertEquals('alpha', $package->getMinimumStability());
+        $this->assertEquals(array(
+            'bar/baz' => BasePackage::STABILITY_DEV,
+            'qux/quux' => BasePackage::STABILITY_RC,
+            'zux/complex' => BasePackage::STABILITY_DEV,
+        ), $package->getStabilityFlags());
     }
 
     public function testNoVersionIsVisibleInPrettyVersion()
@@ -177,46 +76,14 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase
 
         $config = new Config;
         $config->merge(array('repositories' => array('packagist' => false)));
-        $loader = new RootPackageLoader($manager, $config, null, $executor);
+        $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($executor, new VersionParser()));
         $package = $loader->load(array());
 
         $this->assertEquals("1.0.0.0", $package->getVersion());
         $this->assertEquals("No version set (parsed as 1.0.0)", $package->getPrettyVersion());
     }
 
-    protected function loadPackage($data)
-    {
-        $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager')
-            ->disableOriginalConstructor()
-            ->getMock();
-
-        $config = new Config;
-        $config->merge(array('repositories' => array('packagist' => false)));
-
-        $loader = new RootPackageLoader($manager, $config);
-
-        return $loader->load($data);
-    }
 
-    public function testStabilityFlagsParsing()
-    {
-        $package = $this->loadPackage(array(
-            'require' => array(
-                'foo/bar' => '~2.1.0-beta2',
-                'bar/baz' => '1.0.x-dev as 1.2.0',
-                'qux/quux' => '1.0.*@rc',
-                'zux/complex' => '~1.0,>=1.0.2@dev'
-            ),
-            'minimum-stability' => 'alpha',
-        ));
-
-        $this->assertEquals('alpha', $package->getMinimumStability());
-        $this->assertEquals(array(
-            'bar/baz' => BasePackage::STABILITY_DEV,
-            'qux/quux' => BasePackage::STABILITY_RC,
-            'zux/complex' => BasePackage::STABILITY_DEV,
-        ), $package->getStabilityFlags());
-    }
 
     public function testFeatureBranchPrettyVersion()
     {
@@ -272,7 +139,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase
 
         $config = new Config;
         $config->merge(array('repositories' => array('packagist' => false)));
-        $loader = new RootPackageLoader($manager, $config, null, $executor);
+        $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($executor, new VersionParser()));
         $package = $loader->load(array('require' => array('foo/bar' => 'self.version')));
 
         $this->assertEquals("dev-master", $package->getPrettyVersion());
@@ -321,7 +188,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase
 
         $config = new Config;
         $config->merge(array('repositories' => array('packagist' => false)));
-        $loader = new RootPackageLoader($manager, $config, null, $executor);
+        $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($executor, new VersionParser()));
         $package = $loader->load(array('require' => array('foo/bar' => 'self.version'), "non-feature-branches" => array("latest-.*")));
 
         $this->assertEquals("dev-latest-production", $package->getPrettyVersion());

+ 137 - 0
tests/Composer/Test/Package/Version/VersionGuesserTest.php

@@ -0,0 +1,137 @@
+<?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\Test\Package\Version;
+
+use Composer\Config;
+use Composer\Package\Version\VersionGuesser;
+use Composer\Package\Version\VersionParser;
+
+class VersionGuesserTest extends \PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        if (!function_exists('proc_open')) {
+            $this->markTestSkipped('proc_open() is not available');
+        }
+    }
+
+    public function testDetachedHeadBecomesDevHash()
+    {
+        $commitHash = '03a15d220da53c52eddd5f32ffca64a7b3801bea';
+
+        $executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
+            ->setMethods(array('execute'))
+            ->disableArgumentCloning()
+            ->disableOriginalConstructor()
+            ->getMock()
+        ;
+
+        $executor
+            ->expects($this->at(0))
+            ->method('execute')
+            ->with('git describe --exact-match --tags')
+            ->willReturn(1)
+        ;
+
+        $self = $this;
+
+        $executor
+            ->expects($this->at(1))
+            ->method('execute')
+            ->willReturnCallback(function ($command, &$output) use ($self, $commitHash) {
+                $self->assertEquals('git branch --no-color --no-abbrev -v', $command);
+                $output = "* (no branch) $commitHash Commit message\n";
+
+                return 0;
+            })
+        ;
+
+        $config = new Config;
+        $config->merge(array('repositories' => array('packagist' => false)));
+        $guesser = new VersionGuesser($executor, new VersionParser());
+        $version = $guesser->guessVersion($config, array());
+
+        $this->assertEquals("dev-$commitHash", $version);
+    }
+
+    public function testTagBecomesVersion()
+    {
+        $executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
+            ->setMethods(array('execute'))
+            ->disableArgumentCloning()
+            ->disableOriginalConstructor()
+            ->getMock()
+        ;
+
+        $self = $this;
+
+        $executor
+            ->expects($this->at(0))
+            ->method('execute')
+            ->willReturnCallback(function ($command, &$output) use ($self) {
+                $self->assertEquals('git describe --exact-match --tags', $command);
+                $output = "v2.0.5-alpha2";
+
+                return 0;
+            })
+        ;
+
+        $config = new Config;
+        $config->merge(array('repositories' => array('packagist' => false)));
+        $guesser = new VersionGuesser($executor, new VersionParser());
+        $version = $guesser->guessVersion($config, array());
+
+        $this->assertEquals("2.0.5.0-alpha2", $version);
+    }
+
+    public function testInvalidTagBecomesVersion()
+    {
+        $executor = $this->getMockBuilder('\\Composer\\Util\\ProcessExecutor')
+            ->setMethods(array('execute'))
+            ->disableArgumentCloning()
+            ->disableOriginalConstructor()
+            ->getMock()
+        ;
+
+        $self = $this;
+
+        $executor
+            ->expects($this->at(0))
+            ->method('execute')
+            ->willReturnCallback(function ($command, &$output) use ($self) {
+                $self->assertEquals('git describe --exact-match --tags', $command);
+                $output = "foo-bar";
+
+                return 0;
+            })
+        ;
+
+        $executor
+            ->expects($this->at(1))
+            ->method('execute')
+            ->willReturnCallback(function ($command, &$output) use ($self) {
+                $self->assertEquals('git branch --no-color --no-abbrev -v', $command);
+                $output = "* foo 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n";
+
+                return 0;
+            })
+        ;
+
+        $config = new Config;
+        $config->merge(array('repositories' => array('packagist' => false)));
+        $guesser = new VersionGuesser($executor, new VersionParser());
+        $version = $guesser->guessVersion($config, array());
+
+        $this->assertEquals("dev-foo", $version);
+    }
+}

+ 4 - 0
tests/Composer/Test/Repository/Fixtures/path/with-version/composer.json

@@ -0,0 +1,4 @@
+{
+  "name": "test/path",
+  "version": "0.0.2"
+}

+ 3 - 0
tests/Composer/Test/Repository/Fixtures/path/without-version/composer.json

@@ -0,0 +1,3 @@
+{
+  "name": "test/path"
+}

+ 62 - 0
tests/Composer/Test/Repository/PathRepositoryTest.php

@@ -0,0 +1,62 @@
+<?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\Loader\ArrayLoader;
+use Composer\Package\Version\VersionParser;
+use Composer\TestCase;
+use Composer\IO\NullIO;
+use Composer\Config;
+use Composer\Package\BasePackage;
+
+class PathRepositoryTest extends TestCase
+{
+    public function testLoadPackageFromFileSystemWithVersion()
+    {
+        $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface')
+            ->getMock();
+
+        $config = new \Composer\Config();
+        $loader = new ArrayLoader(new VersionParser());
+        $versionGuesser = null;
+
+        $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', 'with-version'));
+        $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config, $loader);
+        $repository->getPackages();
+
+        $this->assertEquals(1, $repository->count());
+        $this->assertTrue($repository->hasPackage($this->getPackage('test/path', '0.0.2')));
+    }
+
+    public function testLoadPackageFromFileSystemWithoutVersion()
+    {
+        $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface')
+            ->getMock();
+
+        $config = new \Composer\Config();
+        $loader = new ArrayLoader(new VersionParser());
+        $versionGuesser = null;
+
+        $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', 'without-version'));
+        $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config, $loader);
+        $packages = $repository->getPackages();
+
+        $this->assertEquals(1, $repository->count());
+
+        $package = $packages[0];
+        $this->assertEquals('test/path', $package->getName());
+
+        $packageVersion = $package->getVersion();
+        $this->assertTrue(!empty($packageVersion));
+    }
+}