1
0
Fork 0

Merge pull request #5467 from bohwaz/fossil

Fossil VCS support for Composer
pull/5473/head
Jordi Boggiano 2016-06-26 15:27:40 +02:00 committed by GitHub
commit 664ba44901
14 changed files with 624 additions and 10 deletions

View File

@ -37,7 +37,7 @@ flags are also required, but when using the installer you will be warned about
any incompatibilities.
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,
Linux and OSX.

View File

@ -57,7 +57,7 @@ available platform packages.
## Specifying 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
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
[`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.
## Publishing to packagist

View File

@ -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
using the `options` parameter.
* **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
project.
* **package:** If you depend on a project that does not have any support for

View File

@ -222,7 +222,7 @@ for more information.
### VCS
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.
#### Loading a package from a VCS repository
@ -300,6 +300,7 @@ The following are supported:
* **Git:** [git-scm.com](https://git-scm.com)
* **Subversion:** [subversion.apache.org](https://subversion.apache.org)
* **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
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)
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
repository as it would with any other git repository instead of using the

View File

@ -149,7 +149,7 @@ Defaults to `$cache-dir/files`. Stores the zip archives of packages.
## cache-repo-dir
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

View File

@ -200,7 +200,7 @@ EOT
) {
$finder = new Finder();
$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);
}

View File

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

View File

@ -460,6 +460,7 @@ class Factory
$dm->setDownloader('git', new Downloader\GitDownloader($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('perforce', new Downloader\PerforceDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));

View File

@ -74,6 +74,11 @@ class VersionGuesser
return $versionData;
}
$versionData = $this->guessFossilVersion($packageConfig, $path);
if (null !== $versionData) {
return $versionData;
}
return $this->guessSvnVersion($packageConfig, $path);
}
}
@ -212,6 +217,31 @@ class VersionGuesser
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)
{
SvnUtil::cleanEnv();

View File

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

View File

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

View File

@ -53,6 +53,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
'hg-bitbucket' => 'Composer\Repository\Vcs\HgBitbucketDriver',
'hg' => 'Composer\Repository\Vcs\HgDriver',
'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' => 'Composer\Repository\Vcs\SvnDriver',
);

View File

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

View File

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