diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php
index d6f60028b..41ab72a5c 100644
--- a/src/Composer/Command/InstallCommand.php
+++ b/src/Composer/Command/InstallCommand.php
@@ -30,6 +30,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Solver;
use Composer\IO\IOInterface;
@@ -113,6 +114,7 @@ EOT
}
// creating requirements request
+ $installFromLock = false;
$request = new Request($pool);
if ($update) {
$io->write('Updating dependencies');
@@ -125,6 +127,7 @@ EOT
$request->install($link->getTarget(), $link->getConstraint());
}
} elseif ($composer->getLocker()->isLocked()) {
+ $installFromLock = true;
$io->write('Installing from lock file');
if (!$composer->getLocker()->isFresh()) {
@@ -185,13 +188,59 @@ EOT
if (!$operations) {
$io->write('Nothing to install/update');
}
+
+ // force dev packages to be updated to latest reference on update
+ if ($update) {
+ foreach ($localRepo->getPackages() as $package) {
+ // skip non-dev packages
+ if (!$package->isDev()) {
+ continue;
+ }
+
+ // skip packages that will be updated/uninstalled
+ foreach ($operations as $operation) {
+ if (('update' === $operation->getJobType() && $package === $operation->getInitialPackage())
+ || ('uninstall' === $operation->getJobType() && $package === $operation->getPackage())
+ ) {
+ continue 2;
+ }
+ }
+
+ // force update
+ $newPackage = $composer->getRepositoryManager()->findPackage($package->getName(), $package->getVersion());
+ if ($newPackage->getSourceReference() !== $package->getSourceReference()) {
+ $operations[] = new UpdateOperation($package, $newPackage);
+ }
+ }
+ }
+
foreach ($operations as $operation) {
if ($verbose) {
$io->write((string) $operation);
}
if (!$dryRun) {
$eventDispatcher->dispatchPackageEvent(constant('Composer\Script\ScriptEvents::PRE_PACKAGE_'.strtoupper($operation->getJobType())), $operation);
+
+ // if installing from lock, restore dev packages' references to their locked state
+ if ($installFromLock) {
+ $package = null;
+ if ('update' === $operation->getJobType()) {
+ $package = $operation->getTargetPackage();
+ } elseif ('install' === $operation->getJobType()) {
+ $package = $operation->getPackage();
+ }
+ if ($package && $package->isDev()) {
+ $lockData = $composer->getLocker()->getLockData();
+ foreach ($lockData['packages'] as $lockedPackage) {
+ if (!empty($lockedPackage['source-reference']) && strtolower($lockedPackage['package']) === $package->getName()) {
+ $package->setSourceReference($lockedPackage['source-reference']);
+ break;
+ }
+ }
+ }
+ }
$installationManager->execute($operation);
+
$eventDispatcher->dispatchPackageEvent(constant('Composer\Script\ScriptEvents::POST_PACKAGE_'.strtoupper($operation->getJobType())), $operation);
}
}
diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php
index e466bbb09..e1441f182 100644
--- a/src/Composer/Downloader/DownloadManager.php
+++ b/src/Composer/Downloader/DownloadManager.php
@@ -125,14 +125,14 @@ class DownloadManager
$sourceType = $package->getSourceType();
$distType = $package->getDistType();
- if (!($preferSource && $sourceType) && $distType) {
+ if (!$package->isDev() && !($preferSource && $sourceType) && $distType) {
$package->setInstallationSource('dist');
} elseif ($sourceType) {
$package->setInstallationSource('source');
+ } elseif ($package->isDev()) {
+ throw new \InvalidArgumentException('Dev package '.$package.' must have a source specified');
} else {
- throw new \InvalidArgumentException(
- 'Package '.$package.' should have source or dist specified'
- );
+ throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
}
$fs = new Filesystem();
diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php
index a3f0b2b74..8eef0d7ed 100644
--- a/src/Composer/Installer/LibraryInstaller.php
+++ b/src/Composer/Installer/LibraryInstaller.php
@@ -109,7 +109,9 @@ class LibraryInstaller implements InstallerInterface
$this->downloadManager->update($initial, $target, $downloadPath);
$this->installBinaries($target);
$this->repository->removePackage($initial);
- $this->repository->addPackage(clone $target);
+ if (!$this->repository->hasPackage($target)) {
+ $this->repository->addPackage(clone $target);
+ }
}
/**
diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php
index 6d6f4f15d..7c992392c 100644
--- a/src/Composer/Package/Loader/ArrayLoader.php
+++ b/src/Composer/Package/Loader/ArrayLoader.php
@@ -122,6 +122,8 @@ class ArrayLoader
$package->setSourceType($config['source']['type']);
$package->setSourceUrl($config['source']['url']);
$package->setSourceReference($config['source']['reference']);
+ } elseif ($package->isDev()) {
+ throw new \UnexpectedValueException('Dev package '.$package.' must have a source specified');
}
if (isset($config['dist'])) {
diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php
index 294dd3f42..208bc4fcf 100644
--- a/src/Composer/Package/Loader/RootPackageLoader.php
+++ b/src/Composer/Package/Loader/RootPackageLoader.php
@@ -38,7 +38,7 @@ class RootPackageLoader extends ArrayLoader
$config['name'] = '__root__';
}
if (!isset($config['version'])) {
- $config['version'] = '1.0.0-dev';
+ $config['version'] = '1.0.0';
}
$package = parent::load($config);
diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php
index 21e09eaf3..24aafae8f 100644
--- a/src/Composer/Package/Locker.php
+++ b/src/Composer/Package/Locker.php
@@ -69,11 +69,7 @@ class Locker
*/
public function getLockedPackages()
{
- if (!$this->isLocked()) {
- throw new \LogicException('No lockfile found. Unable to read locked packages');
- }
-
- $lockList = $this->lockFile->read();
+ $lockList = $this->getLockData();
$packages = array();
foreach ($lockList['packages'] as $info) {
$package = $this->repositoryManager->getLocalRepository()->findPackage($info['package'], $info['version']);
@@ -95,6 +91,15 @@ class Locker
return $packages;
}
+ public function getLockData()
+ {
+ if (!$this->isLocked()) {
+ throw new \LogicException('No lockfile found. Unable to read locked packages');
+ }
+
+ return $this->lockFile->read();
+ }
+
/**
* Locks provided packages into lockfile.
*
@@ -116,7 +121,13 @@ class Locker
));
}
- $lock['packages'][] = array('package' => $name, 'version' => $version);
+ $spec = array('package' => $name, 'version' => $version);
+
+ if ($package->isDev()) {
+ $spec['source-reference'] = $package->getSourceReference();
+ }
+
+ $lock['packages'][] = $spec;
}
$this->lockFile->write($lock);
diff --git a/src/Composer/Package/MemoryPackage.php b/src/Composer/Package/MemoryPackage.php
index d1e63e102..a57f2b6f6 100644
--- a/src/Composer/Package/MemoryPackage.php
+++ b/src/Composer/Package/MemoryPackage.php
@@ -41,6 +41,7 @@ class MemoryPackage extends BasePackage
protected $extra = array();
protected $binaries = array();
protected $scripts = array();
+ protected $dev;
protected $requires = array();
protected $conflicts = array();
@@ -63,6 +64,16 @@ class MemoryPackage extends BasePackage
$this->version = $version;
$this->prettyVersion = $prettyVersion;
+
+ $this->dev = 'dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isDev()
+ {
+ return $this->dev;
}
/**
diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php
index c8f92b581..1f19a835f 100644
--- a/src/Composer/Package/PackageInterface.php
+++ b/src/Composer/Package/PackageInterface.php
@@ -68,6 +68,13 @@ interface PackageInterface
*/
function matches($name, LinkConstraintInterface $constraint);
+ /**
+ * Returns whether the package is a development virtual package or a concrete one
+ *
+ * @return Boolean
+ */
+ function isDev();
+
/**
* Returns the package type, e.g. library
*
diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php
index 7bd941d93..716c24731 100644
--- a/src/Composer/Package/Version/VersionParser.php
+++ b/src/Composer/Package/Version/VersionParser.php
@@ -34,10 +34,15 @@ class VersionParser
{
$version = trim($version);
- if (preg_match('{^(?:master|trunk|default)(?:[.-]?dev)?$}i', $version)) {
+ // match master-like branches
+ if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
return '9999999-dev';
}
+ if ('dev-' === strtolower(substr($version, 0, 4))) {
+ return strtolower($version);
+ }
+
// match classical versioning
if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?'.$this->modifierRegex.'$}i', $version, $matches)) {
$version = $matches[1]
@@ -53,7 +58,7 @@ class VersionParser
// add version modifiers if a version was matched
if (isset($index)) {
if (!empty($matches[$index])) {
- $mod = array('{^pl?$}', '{^rc$}');
+ $mod = array('{^pl?$}i', '{^rc$}i');
$modNormalized = array('patch', 'RC');
$version .= '-'.preg_replace($mod, $modNormalized, strtolower($matches[$index]))
. (!empty($matches[$index+1]) ? $matches[$index+1] : '');
@@ -97,7 +102,7 @@ class VersionParser
return str_replace('x', '9999999', $version).'-dev';
}
- throw new \UnexpectedValueException('Invalid branch name '.$name);
+ return 'dev-'.$name;
}
/**
diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php
index a2506d2df..2c4175fa0 100644
--- a/src/Composer/Repository/VcsRepository.php
+++ b/src/Composer/Repository/VcsRepository.php
@@ -76,20 +76,22 @@ class VcsRepository extends ArrayRepository
}
foreach ($driver->getTags() as $tag => $identifier) {
- $this->io->overwrite('Get composer of ' . $this->packageName . ' (' . $tag . ')', false);
+ $msg = 'Get composer info for ' . $this->packageName . ' (' . $tag . ')';
+ if ($debug) {
+ $this->io->write($msg);
+ } else {
+ $this->io->overwrite($msg, false);
+ }
+
$parsedTag = $this->validateTag($versionParser, $tag);
if ($parsedTag && $driver->hasComposerFile($identifier)) {
try {
$data = $driver->getComposerInformation($identifier);
} catch (\Exception $e) {
- if (strpos($e->getMessage(), 'JSON Parse Error') !== false) {
- if ($debug) {
- $this->io->write('Skipped tag '.$tag.', '.$e->getMessage());
- }
- continue;
- } else {
- throw $e;
+ if ($debug) {
+ $this->io->write('Skipped tag '.$tag.', '.$e->getMessage());
}
+ continue;
}
// manually versioned package
@@ -103,7 +105,7 @@ class VcsRepository extends ArrayRepository
// make sure tag packages have no -dev flag
$data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']);
- $data['version_normalized'] = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']);
+ $data['version_normalized'] = preg_replace('{(^dev-|[.-]?dev$)}i', '', $data['version_normalized']);
// broken package, version doesn't match tag
if ($data['version_normalized'] !== $parsedTag) {
@@ -126,39 +128,33 @@ class VcsRepository extends ArrayRepository
$this->io->overwrite('', false);
foreach ($driver->getBranches() as $branch => $identifier) {
- $this->io->overwrite('Get composer of ' . $this->packageName . ' (' . $branch . ')', false);
+ $msg = 'Get composer info for ' . $this->packageName . ' (' . $branch . ')';
+ if ($debug) {
+ $this->io->write($msg);
+ } else {
+ $this->io->overwrite($msg, false);
+ }
+
$parsedBranch = $this->validateBranch($versionParser, $branch);
if ($driver->hasComposerFile($identifier)) {
$data = $driver->getComposerInformation($identifier);
- // manually versioned package
- if (isset($data['version'])) {
- $data['version_normalized'] = $versionParser->normalize($data['version']);
- } elseif ($parsedBranch) {
- // auto-versionned package, read value from branch name
- $data['version'] = $branch;
- $data['version_normalized'] = $parsedBranch;
- } else {
+ if (!$parsedBranch) {
if ($debug) {
$this->io->write('Skipped branch '.$branch.', invalid name and no composer file was found');
}
continue;
}
- // make sure branch packages have a -dev flag
- $normalizedStableVersion = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']);
- $data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']) . '-dev';
- $data['version_normalized'] = $normalizedStableVersion . '-dev';
+ // branches are always auto-versionned, read value from branch name
+ $data['version'] = $branch;
+ $data['version_normalized'] = $parsedBranch;
- // Skip branches that contain a version that has been tagged already
- foreach ($this->getPackages() as $package) {
- if ($normalizedStableVersion === $package->getVersion()) {
- if ($debug) {
- $this->io->write('Skipped branch '.$branch.', already tagged');
- }
-
- continue 2;
- }
+ // make sure branch packages have a dev flag
+ if ('dev-' === substr($parsedBranch, 0, 4) || '9999999-dev' === $parsedBranch) {
+ $data['version'] = 'dev-' . $data['version'];
+ } else {
+ $data['version'] = $data['version'] . '-dev';
}
if ($debug) {
diff --git a/tests/Composer/Test/Installer/InstallerInstallerTest.php b/tests/Composer/Test/Installer/InstallerInstallerTest.php
index 4e2f8c732..eab2d948a 100644
--- a/tests/Composer/Test/Installer/InstallerInstallerTest.php
+++ b/tests/Composer/Test/Installer/InstallerInstallerTest.php
@@ -67,9 +67,9 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
->method('getPackages')
->will($this->returnValue(array($this->packages[0])));
$this->repository
- ->expects($this->once())
+ ->expects($this->exactly(2))
->method('hasPackage')
- ->will($this->returnValue(true));
+ ->will($this->onConsecutiveCalls(true, false));
$installer = new InstallerInstallerMock(__DIR__.'/Fixtures/', __DIR__.'/Fixtures/bin', $this->dm, $this->repository, $this->io, $this->im);
$test = $this;
@@ -90,9 +90,9 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
->method('getPackages')
->will($this->returnValue(array($this->packages[1])));
$this->repository
- ->expects($this->once())
+ ->expects($this->exactly(2))
->method('hasPackage')
- ->will($this->returnValue(true));
+ ->will($this->onConsecutiveCalls(true, false));
$installer = new InstallerInstallerMock(__DIR__.'/Fixtures/', __DIR__.'/Fixtures/bin', $this->dm, $this->repository, $this->io, $this->im);
$test = $this;
diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php
index 3399345f0..ba86954e3 100644
--- a/tests/Composer/Test/Installer/LibraryInstallerTest.php
+++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php
@@ -128,10 +128,9 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue('package1'));
$this->repository
- ->expects($this->exactly(2))
+ ->expects($this->exactly(3))
->method('hasPackage')
- ->with($initial)
- ->will($this->onConsecutiveCalls(true, false));
+ ->will($this->onConsecutiveCalls(true, false, false));
$this->dm
->expects($this->once())
diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php
index faca00dc9..64a3e79f4 100644
--- a/tests/Composer/Test/Package/Version/VersionParserTest.php
+++ b/tests/Composer/Test/Package/Version/VersionParserTest.php
@@ -49,9 +49,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
'parses datetime' => array('20100102-203040', '20100102-203040'),
'parses dt+number' => array('20100102203040-10', '20100102203040-10'),
'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'),
- 'parses master' => array('master', '9999999-dev'),
- 'parses trunk' => array('trunk', '9999999-dev'),
- 'parses trunk/2' => array('trunk-dev', '9999999-dev'),
+ 'parses master' => array('dev-master', '9999999-dev'),
+ 'parses trunk' => array('dev-trunk', '9999999-dev'),
+ 'parses arbitrary' => array('dev-feature-foo', 'dev-feature-foo'),
+ 'parses arbitrary2' => array('DEV-FOOBAR', 'dev-foobar'),
);
}
@@ -72,6 +73,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
'invalid chars' => array('a'),
'invalid type' => array('1.0.0-meh'),
'too many bits' => array('1.0.0.0.0'),
+ 'non-dev arbitrary' => array('feature-foo'),
);
}
@@ -97,6 +99,8 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
'parses long digits/2' => array('2.4.4', '2.4.4.9999999-dev'),
'parses master' => array('master', '9999999-dev'),
'parses trunk' => array('trunk', '9999999-dev'),
+ 'parses arbitrary' => array('feature-a', 'dev-feature-a'),
+ 'parses arbitrary/2' => array('foobar', 'dev-foobar'),
);
}
@@ -121,8 +125,9 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
'no op means eq' => array('1.2.3', new VersionConstraint('=', '1.2.3.0')),
'completes version' => array('=1.0', new VersionConstraint('=', '1.0.0.0')),
'accepts spaces' => array('>= 1.2.3', new VersionConstraint('>=', '1.2.3.0')),
- 'accepts master' => array('>=master-dev', new VersionConstraint('>=', '9999999-dev')),
- 'accepts master/2' => array('master-dev', new VersionConstraint('=', '9999999-dev')),
+ 'accepts master' => array('>=dev-master', new VersionConstraint('>=', '9999999-dev')),
+ 'accepts master/2' => array('dev-master', new VersionConstraint('=', '9999999-dev')),
+ 'accepts arbitrary' => array('dev-feature-a', new VersionConstraint('=', 'dev-feature-a')),
);
}
diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php
new file mode 100644
index 000000000..4ea24725b
--- /dev/null
+++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php
@@ -0,0 +1,140 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Json;
+
+use Symfony\Component\Process\ExecutableFinder;
+use Composer\Package\Dumper\ArrayDumper;
+use Composer\Repository\VcsRepository;
+use Composer\Repository\Vcs\GitDriver;
+use Composer\Util\Filesystem;
+use Composer\Util\ProcessExecutor;
+use Composer\IO\NullIO;
+
+class VcsRepositoryTest extends \PHPUnit_Framework_TestCase
+{
+ private static $gitRepo;
+ private static $skipped;
+
+ public static function setUpBeforeClass()
+ {
+ $oldCwd = getcwd();
+ self::$gitRepo = sys_get_temp_dir() . '/composer-git-'.rand().'/';
+
+ $locator = new ExecutableFinder();
+ if (!$locator->find('git')) {
+ self::$skipped = 'This test needs a git binary in the PATH to be able to run';
+ return;
+ }
+ if (!mkdir(self::$gitRepo) || !chdir(self::$gitRepo)) {
+ self::$skipped = 'Could not create and move into the temp git repo '.self::$gitRepo;
+ return;
+ }
+
+ // init
+ $process = new ProcessExecutor;
+ $process->execute('git init', $null);
+ touch('foo');
+ $process->execute('git add foo', $null);
+ $process->execute('git commit -m init', $null);
+
+ // non-composed tag & branch
+ $process->execute('git tag 0.5.0', $null);
+ $process->execute('git branch oldbranch', $null);
+
+ // add composed tag & master branch
+ $composer = array('name' => 'a/b');
+ file_put_contents('composer.json', json_encode($composer));
+ $process->execute('git add composer.json', $null);
+ $process->execute('git commit -m addcomposer', $null);
+ $process->execute('git tag 0.6.0', $null);
+
+ // add feature-a branch
+ $process->execute('git checkout -b feature-a', $null);
+ file_put_contents('foo', 'bar feature');
+ $process->execute('git add foo', $null);
+ $process->execute('git commit -m change-a', $null);
+
+ // add version to composer.json
+ $process->execute('git checkout master', $null);
+ $composer['version'] = '1.0.0';
+ file_put_contents('composer.json', json_encode($composer));
+ $process->execute('git add composer.json', $null);
+ $process->execute('git commit -m addversion', $null);
+
+ // create tag with wrong version in it
+ $process->execute('git tag 0.9.0', $null);
+ // create tag with correct version in it
+ $process->execute('git tag 1.0.0', $null);
+
+ // add feature-b branch
+ $process->execute('git checkout -b feature-b', $null);
+ file_put_contents('foo', 'baz feature');
+ $process->execute('git add foo', $null);
+ $process->execute('git commit -m change-b', $null);
+
+ // add 1.0 branch
+ $process->execute('git checkout master', $null);
+ $process->execute('git branch 1.0', $null);
+
+ // add 1.0.x branch
+ $process->execute('git branch 1.0.x', $null);
+
+ // update master to 2.0
+ $composer['version'] = '2.0.0';
+ file_put_contents('composer.json', json_encode($composer));
+ $process->execute('git add composer.json', $null);
+ $process->execute('git commit -m bump-version', $null);
+
+ chdir($oldCwd);
+ }
+
+ public function setUp()
+ {
+ if (self::$skipped) {
+ $this->markTestSkipped(self::$skipped);
+ }
+ }
+
+ public static function tearDownAfterClass()
+ {
+ $fs = new Filesystem;
+ $fs->removeDirectory(self::$gitRepo);
+ }
+
+ public function testLoadVersions()
+ {
+ $expected = array(
+ '0.6.0' => true,
+ '1.0.0' => true,
+ '1.0-dev' => true,
+ '1.0.x-dev' => true,
+ 'dev-feature-b' => true,
+ 'dev-feature-a' => true,
+ 'dev-master' => true,
+ );
+
+ $repo = new VcsRepository(array('url' => self::$gitRepo), new NullIO);
+ $packages = $repo->getPackages();
+ $dumper = new ArrayDumper();
+
+ foreach ($packages as $package) {
+ if (isset($expected[$package->getPrettyVersion()])) {
+ unset($expected[$package->getPrettyVersion()]);
+ } else {
+ $this->fail('Unexpected version '.$package->getPrettyVersion().' in '.json_encode($dumper->dump($package)));
+ }
+ }
+
+ $this->assertEmpty($expected, 'Missing versions: '.implode(', ', array_keys($expected)));
+ }
+}