Browse Source

Merge pull request #5467 from bohwaz/fossil

Fossil VCS support for Composer
Jordi Boggiano 8 years ago
parent
commit
664ba44901

+ 1 - 1
doc/00-intro.md

@@ -37,7 +37,7 @@ flags are also required, but when using the installer you will be warned about
 any incompatibilities.
 any incompatibilities.
 
 
 To install packages from sources instead of simple zip archives, you will need
 To install packages from sources instead of simple zip archives, you will need
-git, svn or hg depending on how the package is version-controlled.
+git, svn, fossil or hg depending on how the package is version-controlled.
 
 
 Composer is multi-platform and we strive to make it run equally well on Windows,
 Composer is multi-platform and we strive to make it run equally well on Windows,
 Linux and OSX.
 Linux and OSX.

+ 3 - 3
doc/02-libraries.md

@@ -57,7 +57,7 @@ available platform packages.
 ## Specifying the version
 ## Specifying the version
 
 
 When you publish your package on Packagist, it is able to infer the version
 When you publish your package on Packagist, it is able to infer the version
-from the VCS (git, svn, hg) information. This means you don't have to
+from the VCS (git, svn, hg, fossil) information. This means you don't have to
 explicitly declare it. Read [tags](#tags) and [branches](#branches) to see how
 explicitly declare it. Read [tags](#tags) and [branches](#branches) to see how
 version numbers are extracted from these.
 version numbers are extracted from these.
 
 
@@ -183,8 +183,8 @@ available, see [Repositories](05-repositories.md).
 That's all. You can now install the dependencies by running Composer's
 That's all. You can now install the dependencies by running Composer's
 [`install`](03-cli.md#install) command!
 [`install`](03-cli.md#install) command!
 
 
-**Recap:** Any git/svn/hg repository containing a `composer.json` can be added
-to your project by specifying the package repository and declaring the
+**Recap:** Any git/svn/hg/fossil repository containing a `composer.json` can be
+added to your project by specifying the package repository and declaring the
 dependency in the [`require`](04-schema.md#require) field.
 dependency in the [`require`](04-schema.md#require) field.
 
 
 ## Publishing to packagist
 ## Publishing to packagist

+ 1 - 1
doc/04-schema.md

@@ -702,7 +702,7 @@ The following repository types are supported:
   file is loaded using a PHP stream. You can set extra options on that stream
   file is loaded using a PHP stream. You can set extra options on that stream
   using the `options` parameter.
   using the `options` parameter.
 * **vcs:** The version control system repository can fetch packages from git,
 * **vcs:** The version control system repository can fetch packages from git,
-  svn and hg repositories.
+  svn, fossil and hg repositories.
 * **pear:** With this you can import any pear repository into your Composer
 * **pear:** With this you can import any pear repository into your Composer
   project.
   project.
 * **package:** If you depend on a project that does not have any support for
 * **package:** If you depend on a project that does not have any support for

+ 4 - 3
doc/05-repositories.md

@@ -222,7 +222,7 @@ for more information.
 ### VCS
 ### VCS
 
 
 VCS stands for version control system. This includes versioning systems like
 VCS stands for version control system. This includes versioning systems like
-git, svn or hg. Composer has a repository type for installing packages from
+git, svn, fossil or hg. Composer has a repository type for installing packages from
 these systems.
 these systems.
 
 
 #### Loading a package from a VCS repository
 #### Loading a package from a VCS repository
@@ -300,6 +300,7 @@ The following are supported:
 * **Git:** [git-scm.com](https://git-scm.com)
 * **Git:** [git-scm.com](https://git-scm.com)
 * **Subversion:** [subversion.apache.org](https://subversion.apache.org)
 * **Subversion:** [subversion.apache.org](https://subversion.apache.org)
 * **Mercurial:** [mercurial.selenic.com](http://mercurial.selenic.com)
 * **Mercurial:** [mercurial.selenic.com](http://mercurial.selenic.com)
+* **Fossil**: [fossil-scm.org](https://www.fossil-scm.org/)
 
 
 To get packages from these systems you need to have their respective clients
 To get packages from these systems you need to have their respective clients
 installed. That can be inconvenient. And for this reason there is special
 installed. That can be inconvenient. And for this reason there is special
@@ -311,8 +312,8 @@ VCS repository provides `dist`s for them that fetch the packages as zips.
 * **BitBucket:** [bitbucket.org](https://bitbucket.org) (Git and Mercurial)
 * **BitBucket:** [bitbucket.org](https://bitbucket.org) (Git and Mercurial)
 
 
 The VCS driver to be used is detected automatically based on the URL. However,
 The VCS driver to be used is detected automatically based on the URL. However,
-should you need to specify one for whatever reason, you can use `git`, `svn` or
-`hg` as the repository type instead of `vcs`.
+should you need to specify one for whatever reason, you can use `fossil`, `git`, 
+`svn` or `hg` as the repository type instead of `vcs`.
 
 
 If you set the `no-api` key to `true` on a github repository it will clone the
 If you set the `no-api` key to `true` on a github repository it will clone the
 repository as it would with any other git repository instead of using the
 repository as it would with any other git repository instead of using the

+ 1 - 1
doc/06-config.md

@@ -149,7 +149,7 @@ Defaults to `$cache-dir/files`. Stores the zip archives of packages.
 ## cache-repo-dir
 ## cache-repo-dir
 
 
 Defaults to `$cache-dir/repo`. Stores repository metadata for the `composer`
 Defaults to `$cache-dir/repo`. Stores repository metadata for the `composer`
-type and the VCS repos of type `svn`, `github` and `bitbucket`.
+type and the VCS repos of type `svn`, `fossil`, `github` and `bitbucket`.
 
 
 ## cache-vcs-dir
 ## cache-vcs-dir
 
 

+ 1 - 1
src/Composer/Command/CreateProjectCommand.php

@@ -200,7 +200,7 @@ EOT
         ) {
         ) {
             $finder = new Finder();
             $finder = new Finder();
             $finder->depth(0)->directories()->in(getcwd())->ignoreVCS(false)->ignoreDotFiles(false);
             $finder->depth(0)->directories()->in(getcwd())->ignoreVCS(false)->ignoreDotFiles(false);
-            foreach (array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg') as $vcsName) {
+            foreach (array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg', '.fslckout', '_FOSSIL_') as $vcsName) {
                 $finder->name($vcsName);
                 $finder->name($vcsName);
             }
             }
 
 

+ 117 - 0
src/Composer/Downloader/FossilDownloader.php

@@ -0,0 +1,117 @@
+<?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 Composer\Util\ProcessExecutor;
+
+/**
+ * @author BohwaZ <http://bohwaz.net/>
+ */
+class FossilDownloader extends VcsDownloader
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function doDownload(PackageInterface $package, $path, $url)
+    {
+        // Ensure we are allowed to use this URL by config
+        $this->config->prohibitUrlByConfig($url, $this->io);
+
+        $url = ProcessExecutor::escape($url);
+        $ref = ProcessExecutor::escape($package->getSourceReference());
+        $repoFile = $path . '.fossil';
+        $this->io->writeError("    Cloning ".$package->getSourceReference($repoFile));
+        $command = sprintf('fossil clone %s %s', $url, ProcessExecutor::escape($repoFile));
+        if (0 !== $this->process->execute($command, $ignoredOutput)) {
+            throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
+        }
+        $command = sprintf('fossil open %s', ProcessExecutor::escape($repoFile));
+        if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) {
+            throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
+        }
+        $command = sprintf('fossil update %s', $ref);
+        if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) {
+            throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
+    {
+        // Ensure we are allowed to use this URL by config
+        $this->config->prohibitUrlByConfig($url, $this->io);
+
+        $url = ProcessExecutor::escape($url);
+        $ref = ProcessExecutor::escape($target->getSourceReference());
+        $this->io->writeError("    Updating to ".$target->getSourceReference());
+
+        if (!$this->hasMetadataRepository($path)) {
+            throw new \RuntimeException('The .fslckout file is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
+        }
+
+        $command = sprintf('fossil pull && fossil up %s', $ref);
+        if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) {
+            throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getLocalChanges(PackageInterface $package, $path)
+    {
+        if (!$this->hasMetadataRepository($path)) {
+            return null;
+        }
+
+        $this->process->execute('fossil changes', $output, realpath($path));
+
+        return trim($output) ?: null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getCommitLogs($fromReference, $toReference, $path)
+    {
+        $command = sprintf('fossil timeline -t ci -W 0 -n 0 before %s', $toReference);
+
+        if (0 !== $this->process->execute($command, $output, realpath($path))) {
+            throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
+        }
+
+        $log = '';
+        $match = '/\d\d:\d\d:\d\d\s+\[' . $toReference . '\]/';
+
+        foreach ($this->process->splitLines($output) as $line) {
+            if (preg_match($match, $line))
+            {
+                break;
+            }
+            $log .= $line;
+        }
+
+        return $log;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function hasMetadataRepository($path)
+    {
+        return is_file($path . '/.fslckout') || is_file($path . '/_FOSSIL_');
+    }
+}

+ 1 - 0
src/Composer/Factory.php

@@ -460,6 +460,7 @@ class Factory
 
 
         $dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $executor, $fs));
+        $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
         $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
         $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
         $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
         $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));

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

@@ -74,6 +74,11 @@ class VersionGuesser
                 return $versionData;
                 return $versionData;
             }
             }
 
 
+            $versionData = $this->guessFossilVersion($packageConfig, $path);
+            if (null !== $versionData) {
+                return $versionData;
+            }
+
             return $this->guessSvnVersion($packageConfig, $path);
             return $this->guessSvnVersion($packageConfig, $path);
         }
         }
     }
     }
@@ -212,6 +217,31 @@ class VersionGuesser
         return $version;
         return $version;
     }
     }
 
 
+    private function guessFossilVersion(array $packageConfig, $path)
+    {
+        $version = null;
+
+        // try to fetch current version from fossil
+        if (0 === $this->process->execute('fossil branch list', $output, $path)) {
+            $branch = trim($output);
+            $version = $this->versionParser->normalizeBranch($branch);
+
+            if ('9999999-dev' === $version) {
+                $version = 'dev-' . $branch;
+            }
+        }
+
+        // try to fetch current version from fossil tags
+        if (0 === $this->process->execute('fossil tag list', $output, $path)) {
+            try {
+                return $this->versionParser->normalize(trim($output));
+            } catch (\Exception $e) {
+            }
+        }
+
+        return array('version' => $version, 'commit' => '');
+    }
+
     private function guessSvnVersion(array $packageConfig, $path)
     private function guessSvnVersion(array $packageConfig, $path)
     {
     {
         SvnUtil::cleanEnv();
         SvnUtil::cleanEnv();

+ 1 - 0
src/Composer/Repository/RepositoryFactory.php

@@ -121,6 +121,7 @@ class RepositoryFactory
         $rm->setRepositoryClass('git', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('git', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('gitlab', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('gitlab', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('svn', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('svn', 'Composer\Repository\VcsRepository');
+        $rm->setRepositoryClass('fossil', 'Composer\Repository\FossilRepository');
         $rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository');
         $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository');

+ 220 - 0
src/Composer/Repository/Vcs/FossilDriver.php

@@ -0,0 +1,220 @@
+<?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\Vcs;
+
+use Composer\Config;
+use Composer\Json\JsonFile;
+use Composer\Util\ProcessExecutor;
+use Composer\Util\Filesystem;
+use Composer\IO\IOInterface;
+
+/**
+ * @author BohwaZ <http://bohwaz.net/>
+ */
+class FossilDriver extends VcsDriver
+{
+    protected $tags;
+    protected $branches;
+    protected $rootIdentifier;
+    protected $repoFile;
+    protected $checkoutDir;
+    protected $infoCache = array();
+
+    /**
+     * {@inheritDoc}
+     */
+    public function initialize()
+    {
+        if (Filesystem::isLocalPath($this->url)) {
+            $this->checkoutDir = $this->url;
+        } else {
+            $this->repoFile = $this->config->get('cache-repo-dir') . '/' . preg_replace('{[^a-z0-9]}i', '-', $this->url) . '.fossil';
+            $this->checkoutDir = $this->config->get('cache-vcs-dir') . '/' . preg_replace('{[^a-z0-9]}i', '-', $this->url) . '/';
+
+            $fs = new Filesystem();
+            $fs->ensureDirectoryExists($this->checkoutDir);
+
+            if (!is_writable(dirname($this->checkoutDir))) {
+                throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.$this->checkoutDir.'" directory is not writable by the current user.');
+            }
+
+            // Ensure we are allowed to use this URL by config
+            $this->config->prohibitUrlByConfig($this->url, $this->io);
+
+            // update the repo if it is a valid fossil repository
+            if (is_file($this->repoFile) && is_dir($this->checkoutDir) && 0 === $this->process->execute('fossil info', $output, $this->checkoutDir)) {
+                if (0 !== $this->process->execute('fossil pull', $output, $this->checkoutDir)) {
+                    $this->io->writeError('<error>Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')</error>');
+                }
+            } else {
+                // clean up directory and do a fresh clone into it
+                $fs->removeDirectory($this->checkoutDir);
+                $fs->remove($this->repoFile);
+
+                $fs->ensureDirectoryExists($this->checkoutDir);
+
+                if (0 !== $this->process->execute(sprintf('fossil clone %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoFile)), $output)) {
+                    $output = $this->process->getErrorOutput();
+
+                    if (0 !== $this->process->execute('fossil version', $ignoredOutput)) {
+                        throw new \RuntimeException('Failed to clone '.$this->url.', fossil was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput());
+                    }
+
+                    throw new \RuntimeException('Failed to clone '.$this->url.' to repository ' . $this->repoFile . "\n\n" .$output);
+                }
+
+                if (0 !== $this->process->execute(sprintf('fossil open %s', ProcessExecutor::escape($this->repoFile)), $output, $this->checkoutDir)) {
+                    $output = $this->process->getErrorOutput();
+
+                    throw new \RuntimeException('Failed to open repository '.$this->repoFile.' in ' . $this->checkoutDir . "\n\n" .$output);
+                }
+            }
+        }
+
+        $this->getTags();
+        $this->getBranches();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getRootIdentifier()
+    {
+        if (null === $this->rootIdentifier) {
+            $this->rootIdentifier = 'trunk';
+        }
+
+        return $this->rootIdentifier;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getUrl()
+    {
+        return $this->url;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getSource($identifier)
+    {
+        return array('type' => 'fossil', 'url' => $this->getUrl(), 'reference' => $identifier);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getDist($identifier)
+    {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getComposerInformation($identifier)
+    {
+        if (!isset($this->infoCache[$identifier])) {
+            $command = sprintf('fossil cat -r %s composer.json', ProcessExecutor::escape($identifier));
+            $this->process->execute($command, $composer, $this->checkoutDir);
+
+            if (trim($composer) === '') {
+                return;
+            }
+
+            $composer = JsonFile::parseJson(trim($composer), $identifier);
+
+            if (empty($composer['time'])) {
+                $this->process->execute(sprintf('fossil finfo composer.json | head -n 2 | tail -n 1 | awk \'{print $1}\''), $output, $this->checkoutDir);
+                $date = new \DateTime(trim($output), new \DateTimeZone('UTC'));
+                $composer['time'] = $date->format('Y-m-d H:i:s');
+            }
+            $this->infoCache[$identifier] = $composer;
+        }
+
+        return $this->infoCache[$identifier];
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getTags()
+    {
+        if (null === $this->tags) {
+            $tags = array();
+
+            $this->process->execute('fossil tag list', $output, $this->checkoutDir);
+            foreach ($this->process->splitLines($output) as $tag) {
+                $tags[$tag] = $tag;
+            }
+
+            $this->tags = $tags;
+        }
+
+        return $this->tags;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getBranches()
+    {
+        if (null === $this->branches) {
+            $branches = array();
+            $bookmarks = array();
+
+            $this->process->execute('fossil branch list', $output, $this->checkoutDir);
+            foreach ($this->process->splitLines($output) as $branch) {
+                $branch = trim(preg_replace('/^\*/', '', trim($branch)));
+                $branches[$branch] = $branch;
+            }
+
+            $this->branches = $branches;
+        }
+
+        return $this->branches;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public static function supports(IOInterface $io, Config $config, $url, $deep = false)
+    {
+        if (preg_match('#(^(?:https?|ssh)://(?:[^@]@)?(?:chiselapp\.com|fossil\.))#i', $url)) {
+            return true;
+        }
+
+        if (preg_match('!/fossil/|\.fossil!', $url))
+        {
+            return true;
+        }
+
+        // local filesystem
+        if (Filesystem::isLocalPath($url)) {
+            $url = Filesystem::getPlatformPath($url);
+            if (!is_dir($url)) {
+                return false;
+            }
+
+            $process = new ProcessExecutor();
+            // check whether there is a fossil repo in that path
+            if ($process->execute('fossil info', $output, $url) === 0) {
+                return true;
+            }
+        }
+
+        return false;
+   }
+}

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

@@ -53,6 +53,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
             'hg-bitbucket'  => 'Composer\Repository\Vcs\HgBitbucketDriver',
             'hg-bitbucket'  => 'Composer\Repository\Vcs\HgBitbucketDriver',
             'hg'            => 'Composer\Repository\Vcs\HgDriver',
             'hg'            => 'Composer\Repository\Vcs\HgDriver',
             'perforce'      => 'Composer\Repository\Vcs\PerforceDriver',
             'perforce'      => 'Composer\Repository\Vcs\PerforceDriver',
+            'fossil'        => 'Composer\Repository\Vcs\FossilDriver',
             // svn must be last because identifying a subversion server for sure is practically impossible
             // svn must be last because identifying a subversion server for sure is practically impossible
             'svn'           => 'Composer\Repository\Vcs\SvnDriver',
             'svn'           => 'Composer\Repository\Vcs\SvnDriver',
         );
         );

+ 173 - 0
tests/Composer/Test/Downloader/FossilDownloaderTest.php

@@ -0,0 +1,173 @@
+<?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\Downloader;
+
+use Composer\Downloader\FossilDownloader;
+use Composer\TestCase;
+use Composer\Util\Filesystem;
+use Composer\Util\Platform;
+
+class FossilDownloaderTest extends TestCase
+{
+    /** @var string */
+    private $workingDir;
+
+    protected function setUp()
+    {
+        $this->workingDir = $this->getUniqueTmpDirectory();
+    }
+
+    protected function tearDown()
+    {
+        if (is_dir($this->workingDir)) {
+            $fs = new Filesystem;
+            $fs->removeDirectory($this->workingDir);
+        }
+    }
+
+    protected function getDownloaderMock($io = null, $config = null, $executor = null, $filesystem = null)
+    {
+        $io = $io ?: $this->getMock('Composer\IO\IOInterface');
+        $config = $config ?: $this->getMock('Composer\Config');
+        $executor = $executor ?: $this->getMock('Composer\Util\ProcessExecutor');
+        $filesystem = $filesystem ?: $this->getMock('Composer\Util\Filesystem');
+
+        return new FossilDownloader($io, $config, $executor, $filesystem);
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testDownloadForPackageWithoutSourceReference()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->once())
+            ->method('getSourceReference')
+            ->will($this->returnValue(null));
+
+        $downloader = $this->getDownloaderMock();
+        $downloader->download($packageMock, '/path');
+    }
+
+    public function testDownload()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getSourceReference')
+            ->will($this->returnValue('trunk'));
+        $packageMock->expects($this->once())
+            ->method('getSourceUrls')
+            ->will($this->returnValue(array('http://fossil.kd2.org/kd2fw/')));
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+
+        $expectedFossilCommand = $this->getCmd('fossil clone \'http://fossil.kd2.org/kd2fw/\' \'repo.fossil\'');
+        $processExecutor->expects($this->at(0))
+            ->method('execute')
+            ->with($this->equalTo($expectedFossilCommand))
+            ->will($this->returnValue(0));
+
+        $expectedFossilCommand = $this->getCmd('fossil open \'repo.fossil\'');
+        $processExecutor->expects($this->at(1))
+            ->method('execute')
+            ->with($this->equalTo($expectedFossilCommand))
+            ->will($this->returnValue(0));
+
+        $expectedFossilCommand = $this->getCmd('fossil update \'trunk\'');
+        $processExecutor->expects($this->at(2))
+            ->method('execute')
+            ->with($this->equalTo($expectedFossilCommand))
+            ->will($this->returnValue(0));
+
+        $downloader = $this->getDownloaderMock(null, null, $processExecutor);
+        $downloader->download($packageMock, 'repo');
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testUpdateforPackageWithoutSourceReference()
+    {
+        $initialPackageMock = $this->getMock('Composer\Package\PackageInterface');
+        $sourcePackageMock = $this->getMock('Composer\Package\PackageInterface');
+        $sourcePackageMock->expects($this->once())
+            ->method('getSourceReference')
+            ->will($this->returnValue(null));
+
+        $downloader = $this->getDownloaderMock();
+        $downloader->update($initialPackageMock, $sourcePackageMock, '/path');
+    }
+
+    public function testUpdate()
+    {
+        // Ensure file exists
+        $file = $this->workingDir . '/.fslckout';
+
+        if (!file_exists($file)) {
+            touch($file);
+        }
+
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getSourceReference')
+            ->will($this->returnValue('trunk'));
+        $packageMock->expects($this->any())
+            ->method('getSourceUrls')
+            ->will($this->returnValue(array('http://fossil.kd2.org/kd2fw/')));
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+
+        $expectedFossilCommand = $this->getCmd("fossil changes");
+        $processExecutor->expects($this->at(0))
+            ->method('execute')
+            ->with($this->equalTo($expectedFossilCommand))
+            ->will($this->returnValue(0));
+        $expectedFossilCommand = $this->getCmd("fossil pull && fossil up 'trunk'");
+        $processExecutor->expects($this->at(1))
+            ->method('execute')
+            ->with($this->equalTo($expectedFossilCommand))
+            ->will($this->returnValue(0));
+
+        $downloader = $this->getDownloaderMock(null, null, $processExecutor);
+        $downloader->update($packageMock, $packageMock, $this->workingDir);
+    }
+
+    public function testRemove()
+    {
+        $expectedResetCommand = $this->getCmd('cd \'composerPath\' && fossil status');
+
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->any())
+            ->method('execute')
+            ->with($this->equalTo($expectedResetCommand));
+        $filesystem = $this->getMock('Composer\Util\Filesystem');
+        $filesystem->expects($this->any())
+            ->method('removeDirectory')
+            ->with($this->equalTo('composerPath'))
+            ->will($this->returnValue(true));
+
+        $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem);
+        $downloader->remove($packageMock, 'composerPath');
+    }
+
+    public function testGetInstallationSource()
+    {
+        $downloader = $this->getDownloaderMock(null);
+
+        $this->assertEquals('source', $downloader->getInstallationSource());
+    }
+
+    private function getCmd($cmd)
+    {
+        return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd;
+    }
+}

+ 70 - 0
tests/Composer/Test/Repository/Vcs/FossilDriverTest.php

@@ -0,0 +1,70 @@
+<?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\Repository\Vcs;
+
+use Composer\Repository\Vcs\FossilDriver;
+use Composer\Config;
+use Composer\TestCase;
+use Composer\Util\Filesystem;
+use Composer\Util\Platform;
+
+class FossilDriverTest extends TestCase
+{
+    protected $home;
+    protected $config;
+
+    public function setUp()
+    {
+        $this->home = $this->getUniqueTmpDirectory();
+        $this->config = new Config();
+        $this->config->merge(array(
+            'config' => array(
+                'home' => $this->home,
+            ),
+        ));
+    }
+
+    public function tearDown()
+    {
+        $fs = new Filesystem();
+        $fs->removeDirectory($this->home);
+    }
+
+    private function getCmd($cmd)
+    {
+        if (Platform::isWindows()) {
+            return strtr($cmd, "'", '"');
+        }
+
+        return $cmd;
+    }
+
+    public static function supportProvider()
+    {
+        return array(
+            array('http://fossil.kd2.org/kd2fw/', true),
+            array('https://chiselapp.com/user/rkeene/repository/flint/index', true),
+            array('ssh://fossil.kd2.org/kd2fw.fossil', true),
+        );
+    }
+
+    /**
+     * @dataProvider supportProvider
+     */
+    public function testSupport($url, $assertion)
+    {
+        $config = new Config();
+        $result = FossilDriver::supports($this->getMock('Composer\IO\IOInterface'), $config, $url);
+        $this->assertEquals($assertion, $result);
+    }
+}