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