Browse Source

Merge pull request #329 from Seldaek/new_dev

[BC Break] New dev handling
Nils Adermann 13 years ago
parent
commit
39aa5c0752

+ 49 - 0
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('<info>Updating dependencies</info>');
@@ -125,6 +127,7 @@ EOT
                 $request->install($link->getTarget(), $link->getConstraint());
             }
         } elseif ($composer->getLocker()->isLocked()) {
+            $installFromLock = true;
             $io->write('<info>Installing from lock file</info>');
 
             if (!$composer->getLocker()->isFresh()) {
@@ -185,13 +188,59 @@ EOT
         if (!$operations) {
             $io->write('<info>Nothing to install/update</info>');
         }
+
+        // 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);
             }
         }

+ 4 - 4
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();

+ 3 - 1
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);
+        }
     }
 
     /**

+ 2 - 0
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'])) {

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

+ 17 - 6
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);

+ 11 - 0
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;
     }
 
     /**

+ 7 - 0
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
      *

+ 8 - 3
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;
     }
 
     /**

+ 27 - 31
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 <info>' . $this->packageName . '</info> (<comment>' . $tag . '</comment>)', false);
+            $msg = 'Get composer info for <info>' . $this->packageName . '</info> (<comment>' . $tag . '</comment>)';
+            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 <info>' . $this->packageName . '</info> (<comment>' . $branch . '</comment>)', false);
+            $msg = 'Get composer info for <info>' . $this->packageName . '</info> (<comment>' . $branch . '</comment>)';
+            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) {

+ 4 - 4
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;

+ 2 - 3
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())

+ 10 - 5
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')),
         );
     }
 

+ 140 - 0
tests/Composer/Test/Repository/VcsRepositoryTest.php

@@ -0,0 +1,140 @@
+<?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\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)));
+    }
+}