Browse Source

Merge pull request #7936 from naderman/solve-without-installed

Separate Install & Update code, no longer use vendor dir as input to solver
Nils Adermann 5 years ago
parent
commit
8810c6467d
78 changed files with 1863 additions and 1143 deletions
  1. 1 1
      src/Composer/Command/CreateProjectCommand.php
  2. 1 1
      src/Composer/Command/InitCommand.php
  3. 2 2
      src/Composer/Command/ShowCommand.php
  4. 15 1
      src/Composer/Command/UpdateCommand.php
  5. 17 37
      src/Composer/DependencyResolver/DefaultPolicy.php
  6. 36 0
      src/Composer/DependencyResolver/LocalRepoTransaction.php
  7. 115 0
      src/Composer/DependencyResolver/LockTransaction.php
  8. 9 1
      src/Composer/DependencyResolver/Operation/InstallOperation.php
  9. 9 1
      src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php
  10. 9 1
      src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php
  11. 8 0
      src/Composer/DependencyResolver/Operation/OperationInterface.php
  12. 6 0
      src/Composer/DependencyResolver/Operation/SolverOperation.php
  13. 9 1
      src/Composer/DependencyResolver/Operation/UninstallOperation.php
  14. 9 1
      src/Composer/DependencyResolver/Operation/UpdateOperation.php
  15. 2 2
      src/Composer/DependencyResolver/PolicyInterface.php
  16. 0 7
      src/Composer/DependencyResolver/Pool.php
  17. 45 10
      src/Composer/DependencyResolver/PoolBuilder.php
  18. 10 3
      src/Composer/DependencyResolver/Problem.php
  19. 65 21
      src/Composer/DependencyResolver/Request.php
  20. 1 1
      src/Composer/DependencyResolver/Rule.php
  21. 29 18
      src/Composer/DependencyResolver/RuleSetGenerator.php
  22. 24 62
      src/Composer/DependencyResolver/Solver.php
  23. 217 152
      src/Composer/DependencyResolver/Transaction.php
  24. 3 2
      src/Composer/EventDispatcher/EventDispatcher.php
  25. 299 593
      src/Composer/Installer.php
  26. 7 7
      src/Composer/Installer/SuggestedPackagesReporter.php
  27. 5 0
      src/Composer/Package/AliasPackage.php
  28. 36 7
      src/Composer/Package/Locker.php
  29. 17 0
      src/Composer/Package/Package.php
  30. 9 0
      src/Composer/Package/PackageInterface.php
  31. 1 1
      src/Composer/Plugin/PluginManager.php
  32. 7 2
      src/Composer/Repository/RepositorySet.php
  33. 25 35
      tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php
  34. 3 17
      tests/Composer/Test/DependencyResolver/RequestTest.php
  35. 43 53
      tests/Composer/Test/DependencyResolver/SolverTest.php
  36. 7 3
      tests/Composer/Test/Fixtures/installer/abandoned-listed.test
  37. 39 0
      tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test
  38. 1 1
      tests/Composer/Test/Fixtures/installer/aliased-priority.test
  39. 2 2
      tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
  40. 1 1
      tests/Composer/Test/Fixtures/installer/circular-dependency2.test
  41. 2 2
      tests/Composer/Test/Fixtures/installer/github-issues-4319.test
  42. 25 3
      tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test
  43. 19 2
      tests/Composer/Test/Fixtures/installer/github-issues-4795.test
  44. 1 1
      tests/Composer/Test/Fixtures/installer/install-aliased-alias.test
  45. 2 2
      tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test
  46. 1 1
      tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test
  47. 1 1
      tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test
  48. 3 2
      tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test
  49. 1 1
      tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test
  50. 2 2
      tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test
  51. 5 19
      tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test
  52. 1 1
      tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test
  53. 20 5
      tests/Composer/Test/Fixtures/installer/solver-problems.test
  54. 7 3
      tests/Composer/Test/Fixtures/installer/suggest-installed.test
  55. 5 1
      tests/Composer/Test/Fixtures/installer/suggest-prod.test
  56. 7 3
      tests/Composer/Test/Fixtures/installer/suggest-replaced.test
  57. 6 2
      tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test
  58. 0 14
      tests/Composer/Test/Fixtures/installer/update-alias-lock.test
  59. 59 8
      tests/Composer/Test/Fixtures/installer/update-changes-url.test
  60. 204 0
      tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test
  61. 1 1
      tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test
  62. 10 2
      tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test
  63. 67 0
      tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test
  64. 17 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test
  65. 19 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test
  66. 18 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test
  67. 22 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test
  68. 17 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test
  69. 23 2
      tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test
  70. 2 2
      tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test
  71. 16 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test
  72. 16 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test
  73. 16 0
      tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test
  74. 18 0
      tests/Composer/Test/Fixtures/installer/update-whitelist.test
  75. 21 3
      tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test
  76. 1 1
      tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test
  77. 1 1
      tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test
  78. 63 11
      tests/Composer/Test/InstallerTest.php

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

@@ -293,7 +293,7 @@ EOT
             throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities)));
         }
 
-        $repositorySet = new RepositorySet(array(), $stability);
+        $repositorySet = new RepositorySet(array(), array(), $stability);
         $repositorySet->addRepository($sourceRepo);
 
         $phpVersion = null;

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

@@ -668,7 +668,7 @@ EOT
         $key = $minimumStability ?: 'default';
 
         if (!isset($this->repositorySets[$key])) {
-            $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), $minimumStability ?: $this->getMinimumStability($input));
+            $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), array(), $minimumStability ?: $this->getMinimumStability($input));
             $repositorySet->addRepository($this->getRepos());
         }
 

+ 2 - 2
src/Composer/Command/ShowCommand.php

@@ -544,7 +544,7 @@ EOT
         $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version;
 
         $policy = new DefaultPolicy();
-        $repositorySet = new RepositorySet(array(), 'dev');
+        $repositorySet = new RepositorySet(array(), array(), 'dev');
         $repositorySet->addRepository($repos);
 
         $matchedPackage = null;
@@ -1009,7 +1009,7 @@ EOT
     private function getRepositorySet(Composer $composer)
     {
         if (!$this->repositorySet) {
-            $this->repositorySet = new RepositorySet(array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags());
+            $this->repositorySet = new RepositorySet(array(), array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags());
             $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories()));
         }
 

+ 15 - 1
src/Composer/Command/UpdateCommand.php

@@ -121,6 +121,19 @@ EOT
             }
         }
 
+        // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead
+        // they are further mutually exclusive with listing actual package names
+        $filteredPackages = array_filter($packages, function ($package) {
+            return !in_array($package, array('lock', 'nothing', 'mirrors'), true);
+        });
+        $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages);
+        $packages = $filteredPackages;
+
+        if ($updateMirrors && !empty($packages)) {
+            $io->writeError('<error>You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.</error>');
+            return -1;
+        }
+
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
 
@@ -146,7 +159,8 @@ EOT
             ->setClassMapAuthoritative($authoritative)
             ->setApcuAutoloader($apcu)
             ->setUpdate(true)
-            ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages)
+            ->setUpdateMirrors($updateMirrors)
+            ->setUpdateWhitelist($packages)
             ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies'))
             ->setWhitelistAllDependencies($input->getOption('with-all-dependencies'))
             ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))

+ 17 - 37
src/Composer/DependencyResolver/DefaultPolicy.php

@@ -44,7 +44,7 @@ class DefaultPolicy implements PolicyInterface
         return $constraint->matchSpecific($version, true);
     }
 
-    public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false)
+    public function findUpdatePackages(Pool $pool, PackageInterface $package, $mustMatchName = false)
     {
         $packages = array();
 
@@ -57,36 +57,34 @@ class DefaultPolicy implements PolicyInterface
         return $packages;
     }
 
-    public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null)
+    public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null)
     {
-        $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals);
+        $packages = $this->groupLiteralsByName($pool, $literals);
 
-        foreach ($packages as &$literals) {
+        foreach ($packages as &$nameLiterals) {
             $policy = $this;
-            usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) {
-                return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
+            usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) {
+                return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
             });
         }
 
-        foreach ($packages as &$literals) {
-            $literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals);
-
-            $literals = $this->pruneToBestVersion($pool, $literals);
-
-            $literals = $this->pruneRemoteAliases($pool, $literals);
+        foreach ($packages as &$sortedLiterals) {
+            $sortedLiterals = $this->pruneToHighestPriority($pool, $sortedLiterals);
+            $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals);
+            $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals);
         }
 
         $selected = call_user_func_array('array_merge', $packages);
 
         // now sort the result across all packages to respect replaces across packages
-        usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) {
-            return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
+        usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) {
+            return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
         });
 
         return $selected;
     }
 
-    protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals)
+    protected function groupLiteralsByName(Pool $pool, $literals)
     {
         $packages = array();
         foreach ($literals as $literal) {
@@ -95,12 +93,7 @@ class DefaultPolicy implements PolicyInterface
             if (!isset($packages[$packageName])) {
                 $packages[$packageName] = array();
             }
-
-            if (isset($installedMap[abs($literal)])) {
-                array_unshift($packages[$packageName], $literal);
-            } else {
-                $packages[$packageName][] = $literal;
-            }
+            $packages[$packageName][] = $literal;
         }
 
         return $packages;
@@ -109,7 +102,7 @@ class DefaultPolicy implements PolicyInterface
     /**
      * @protected
      */
-    public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false)
+    public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false)
     {
         if ($a->getRepository() === $b->getRepository()) {
             // prefer aliases to the original package
@@ -155,14 +148,6 @@ class DefaultPolicy implements PolicyInterface
             return ($a->id < $b->id) ? -1 : 1;
         }
 
-        if (isset($installedMap[$a->id])) {
-            return -1;
-        }
-
-        if (isset($installedMap[$b->id])) {
-            return 1;
-        }
-
         return ($pool->getPriority($a->id) > $pool->getPriority($b->id)) ? -1 : 1;
     }
 
@@ -214,9 +199,9 @@ class DefaultPolicy implements PolicyInterface
     }
 
     /**
-     * Assumes that installed packages come first and then all highest priority packages
+     * Assumes that highest priority packages come first
      */
-    protected function pruneToHighestPriorityOrInstalled(Pool $pool, array $installedMap, array $literals)
+    protected function pruneToHighestPriority(Pool $pool, array $literals)
     {
         $selected = array();
 
@@ -225,11 +210,6 @@ class DefaultPolicy implements PolicyInterface
         foreach ($literals as $literal) {
             $package = $pool->literalToPackage($literal);
 
-            if (isset($installedMap[$package->id])) {
-                $selected[] = $literal;
-                continue;
-            }
-
             if (null === $priority) {
                 $priority = $pool->getPriority($package->id);
             }

+ 36 - 0
src/Composer/DependencyResolver/LocalRepoTransaction.php

@@ -0,0 +1,36 @@
+<?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\DependencyResolver;
+
+use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
+use Composer\DependencyResolver\Operation\UninstallOperation;
+use Composer\Package\AliasPackage;
+use Composer\Package\Link;
+use Composer\Package\PackageInterface;
+use Composer\Repository\PlatformRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Semver\Constraint\Constraint;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class LocalRepoTransaction extends Transaction
+{
+    public function __construct(RepositoryInterface $lockedRepository, $localRepository)
+    {
+        parent::__construct(
+            $localRepository->getPackages(),
+            $lockedRepository->getPackages()
+        );
+    }
+}

+ 115 - 0
src/Composer/DependencyResolver/LockTransaction.php

@@ -0,0 +1,115 @@
+<?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\DependencyResolver;
+
+use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\Package\AliasPackage;
+use Composer\Package\RootAliasPackage;
+use Composer\Package\RootPackageInterface;
+use Composer\Repository\ArrayRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Test\Repository\ArrayRepositoryTest;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class LockTransaction extends Transaction
+{
+    /**
+     * packages in current lock file, platform repo or otherwise present
+     * @var array
+     */
+    protected $presentMap;
+
+    /**
+     * Packages which cannot be mapped, platform repo, root package, other fixed repos
+     * @var array
+     */
+    protected $unlockableMap;
+
+    /**
+     * @var array
+     */
+    protected $resultPackages;
+
+    public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions)
+    {
+        $this->presentMap = $presentMap;
+        $this->unlockableMap = $unlockableMap;
+
+        $this->setResultPackages($pool, $decisions);
+        parent::__construct($this->presentMap, $this->resultPackages['all']);
+
+    }
+
+    // TODO make this a bit prettier instead of the two text indexes?
+    public function setResultPackages(Pool $pool, Decisions $decisions)
+    {
+        $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array());
+        foreach ($decisions as $i => $decision) {
+            $literal = $decision[Decisions::DECISION_LITERAL];
+
+            if ($literal > 0) {
+                $package = $pool->literalToPackage($literal);
+                $this->resultPackages['all'][] = $package;
+                if (!isset($this->unlockableMap[$package->id])) {
+                    $this->resultPackages['non-dev'][] = $package;
+                }
+            }
+        }
+    }
+
+    public function setNonDevPackages(LockTransaction $extractionResult)
+    {
+        $packages = $extractionResult->getNewLockPackages(false);
+
+        $this->resultPackages['dev'] = $this->resultPackages['non-dev'];
+        $this->resultPackages['non-dev'] = array();
+
+        foreach ($packages as $package) {
+            foreach ($this->resultPackages['dev'] as $i => $resultPackage) {
+                // TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible?
+                if ($package->getName() == $resultPackage->getName()) {
+                    $this->resultPackages['non-dev'][] = $resultPackage;
+                    unset($this->resultPackages['dev'][$i]);
+                }
+            }
+        }
+    }
+
+    // TODO additionalFixedRepository needs to be looked at here as well?
+    public function getNewLockPackages($devMode, $updateMirrors = false)
+    {
+        $packages = array();
+        foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) {
+            if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) {
+                // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is
+                // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed
+                if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) {
+                    foreach ($this->presentMap as $presentPackage) {
+                        if ($package->getName() == $presentPackage->getName() &&
+                            $package->getVersion() == $presentPackage->getVersion() &&
+                            $presentPackage->getSourceReference() &&
+                            $presentPackage->getSourceType() === $package->getSourceType()
+                        ) {
+                            $package->setSourceDistReferences($presentPackage->getSourceReference());
+                        }
+                    }
+                }
+                $packages[] = $package;
+            }
+        }
+
+        return $packages;
+    }
+}

+ 9 - 1
src/Composer/DependencyResolver/Operation/InstallOperation.php

@@ -56,11 +56,19 @@ class InstallOperation extends SolverOperation
         return 'install';
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function show($lock)
+    {
+        return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
+    }
+
     /**
      * {@inheritDoc}
      */
     public function __toString()
     {
-        return 'Installing '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
+        return $this->show(false);
     }
 }

+ 9 - 1
src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php

@@ -60,8 +60,16 @@ class MarkAliasInstalledOperation extends SolverOperation
     /**
      * {@inheritDoc}
      */
-    public function __toString()
+    public function show($lock)
     {
         return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')';
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function __toString()
+    {
+        return $this->show(false);
+    }
 }

+ 9 - 1
src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php

@@ -60,8 +60,16 @@ class MarkAliasUninstalledOperation extends SolverOperation
     /**
      * {@inheritDoc}
      */
-    public function __toString()
+    public function show($lock)
     {
         return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')';
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function __toString()
+    {
+        return $this->show(false);
+    }
 }

+ 8 - 0
src/Composer/DependencyResolver/Operation/OperationInterface.php

@@ -33,6 +33,14 @@ interface OperationInterface
      */
     public function getReason();
 
+    /**
+     * Serializes the operation in a human readable format
+     *
+     * @param $lock bool Whether this is an operation on the lock file
+     * @return string
+     */
+    public function show($lock);
+
     /**
      * Serializes the operation in a human readable format
      *

+ 6 - 0
src/Composer/DependencyResolver/Operation/SolverOperation.php

@@ -43,6 +43,12 @@ abstract class SolverOperation implements OperationInterface
         return $this->reason;
     }
 
+    /**
+     * @param $lock bool Whether this is an operation on the lock file
+    * @return string
+    */
+    abstract public function show($lock);
+
     protected function formatVersion(PackageInterface $package)
     {
         return $package->getFullPrettyVersion();

+ 9 - 1
src/Composer/DependencyResolver/Operation/UninstallOperation.php

@@ -59,8 +59,16 @@ class UninstallOperation extends SolverOperation
     /**
      * {@inheritDoc}
      */
-    public function __toString()
+    public function show($lock)
     {
         return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function __toString()
+    {
+        return $this->show(false);
+    }
 }

+ 9 - 1
src/Composer/DependencyResolver/Operation/UpdateOperation.php

@@ -72,9 +72,17 @@ class UpdateOperation extends SolverOperation
     /**
      * {@inheritDoc}
      */
-    public function __toString()
+    public function show($lock)
     {
         return 'Updating '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '.
             $this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')';
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function __toString()
+    {
+        return $this->show(false);
+    }
 }

+ 2 - 2
src/Composer/DependencyResolver/PolicyInterface.php

@@ -21,7 +21,7 @@ interface PolicyInterface
 {
     public function versionCompare(PackageInterface $a, PackageInterface $b, $operator);
 
-    public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package);
+    public function findUpdatePackages(Pool $pool, PackageInterface $package);
 
-    public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null);
+    public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null);
 }

+ 0 - 7
src/Composer/DependencyResolver/Pool.php

@@ -12,18 +12,11 @@
 
 namespace Composer\DependencyResolver;
 
-use Composer\Package\BasePackage;
 use Composer\Package\AliasPackage;
 use Composer\Package\Version\VersionParser;
-use Composer\Repository\RepositorySet;
 use Composer\Semver\Constraint\ConstraintInterface;
 use Composer\Semver\Constraint\Constraint;
 use Composer\Semver\Constraint\EmptyConstraint;
-use Composer\Repository\RepositoryInterface;
-use Composer\Repository\CompositeRepository;
-use Composer\Repository\ComposerRepository;
-use Composer\Repository\InstalledRepositoryInterface;
-use Composer\Repository\PlatformRepository;
 use Composer\Package\PackageInterface;
 
 /**

+ 45 - 10
src/Composer/DependencyResolver/PoolBuilder.php

@@ -14,11 +14,11 @@ namespace Composer\DependencyResolver;
 
 use Composer\Package\AliasPackage;
 use Composer\Package\BasePackage;
+use Composer\Package\Package;
 use Composer\Package\PackageInterface;
 use Composer\Repository\AsyncRepositoryInterface;
 use Composer\Repository\ComposerRepository;
 use Composer\Repository\InstalledRepositoryInterface;
-use Composer\Repository\LockArrayRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Semver\Constraint\Constraint;
 use Composer\Semver\Constraint\MultiConstraint;
@@ -31,6 +31,7 @@ class PoolBuilder
     private $isPackageAcceptableCallable;
     private $filterRequires;
     private $rootAliases;
+    private $rootReferences;
 
     private $aliasMap = array();
     private $nameConstraints = array();
@@ -46,22 +47,45 @@ class PoolBuilder
         $this->filterRequires = $filterRequires;
     }
 
-    public function buildPool(array $repositories, array $rootAliases, Request $request)
+    public function buildPool(array $repositories, array $rootAliases, array $rootReferences, Request $request)
     {
         $pool = new Pool($this->filterRequires);
         $this->rootAliases = $rootAliases;
+        $this->rootReferences = $rootReferences;
 
         // TODO do we really want the request here? kind of want a root requirements thingy instead
         $loadNames = array();
+        foreach ($request->getFixedPackages() as $package) {
+            // TODO can actually use very specific constraint
+            $loadNames[$package->getName()] = null;
+        }
+
         foreach ($request->getJobs() as $job) {
             switch ($job['cmd']) {
                 case 'install':
-                    $loadNames[$job['packageName']] = $job['constraint'];
-                    $this->nameConstraints[$job['packageName']] = $job['constraint'] ? new MultiConstraint(array($job['constraint']), false) : null;
+                    // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints
+                    // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints?
+                    // also see the solver-problems.test test case
+                    $constraint = array_key_exists($job['packageName'], $loadNames) ? null : $job['constraint'];
+                    $loadNames[$job['packageName']] = $constraint;
+                    $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($constraint), false) : null;
                     break;
             }
         }
 
+        // packages from the locked repository only get loaded if they are explicitly fixed
+        foreach ($repositories as $key => $repository) {
+            if ($repository === $request->getLockedRepository()) {
+                foreach ($repository->getPackages() as $lockedPackage) {
+                    foreach ($request->getFixedPackages() as $package) {
+                        if ($package === $lockedPackage) {
+                            $loadNames += $this->loadPackage($request, $package, $key);
+                        }
+                    }
+                }
+            }
+        }
+
         while (!empty($loadNames)) {
             $loadIds = array();
             foreach ($repositories as $key => $repository) {
@@ -76,7 +100,7 @@ class PoolBuilder
 
             $newLoadNames = array();
             foreach ($repositories as $key => $repository) {
-                if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) {
+                if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface || $repository === $request->getLockedRepository()) {
                     continue;
                 }
 
@@ -90,7 +114,7 @@ class PoolBuilder
 
                 foreach ($packages as $package) {
                     if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) {
-                        $newLoadNames += $this->loadPackage($package, $key);
+                        $newLoadNames += $this->loadPackage($request, $package, $key);
                     }
                 }
             }
@@ -99,7 +123,7 @@ class PoolBuilder
         }
 
         foreach ($this->packages as $i => $package) {
-            // we check all alias related packages at once, so no need ot check individual aliases
+            // we check all alias related packages at once, so no need to check individual aliases
             // isset also checks non-null value
             if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) {
                 $constraint = $this->nameConstraints[$package->getName()];
@@ -128,7 +152,7 @@ class PoolBuilder
             if ($repository instanceof PlatformRepository ||
                 $repository instanceof InstalledRepositoryInterface) {
                 foreach ($repository->getPackages() as $package) {
-                    $this->loadPackage($package, $key);
+                    $this->loadPackage($request, $package, $key);
                 }
             }
         }
@@ -142,7 +166,7 @@ class PoolBuilder
         return $pool;
     }
 
-    private function loadPackage(PackageInterface $package, $repoIndex)
+    private function loadPackage(Request $request, PackageInterface $package, $repoIndex)
     {
         $index = count($this->packages);
         $this->packages[] = $package;
@@ -152,8 +176,18 @@ class PoolBuilder
             $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package;
         }
 
-        // handle root package aliases
         $name = $package->getName();
+
+        // we're simply setting the root references on all versions for a name here and rely on the solver to pick the
+        // right version. It'd be more work to figure out which versions and which aliases of those versions this may
+        // apply to
+        if (isset($this->rootReferences[$name])) {
+            // do not modify the references on already locked packages
+            if (!$request->isFixedPackage($package)) {
+                $package->setSourceDistReferences($this->rootReferences[$name]);
+            }
+        }
+
         if (isset($this->rootAliases[$name][$package->getVersion()])) {
             $alias = $this->rootAliases[$name][$package->getVersion()];
             if ($package instanceof AliasPackage) {
@@ -183,6 +217,7 @@ class PoolBuilder
                     // TODO addConstraint function?
                     $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false);
                 }
+                // else it is null and should stay null
             } else {
                 $this->nameConstraints[$require] = null;
             }

+ 10 - 3
src/Composer/DependencyResolver/Problem.php

@@ -68,7 +68,7 @@ class Problem
     /**
      * A human readable textual representation of the problem's reasons
      *
-     * @param  array  $installedMap A map of all installed packages
+     * @param  array  $installedMap A map of all present packages
      * @return string
      */
     public function getPrettyString(array $installedMap = array(), array $learnedPool = array())
@@ -90,7 +90,7 @@ class Problem
                 $packages = array();
             }
 
-            if ($job && $job['cmd'] === 'install' && empty($packages)) {
+            if ($job && ($job['cmd'] === 'install' || $job['cmd'] === 'fix') && empty($packages)) {
 
                 // handle php/hhvm
                 if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
@@ -208,6 +208,13 @@ class Problem
         $packageName = $job['packageName'];
         $constraint = $job['constraint'];
         switch ($job['cmd']) {
+            case 'fix':
+                $package = $job['package'];
+                if ($job['lockable']) {
+                    return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.';
+                }
+
+                return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer';
             case 'install':
                 $packages = $this->pool->whatProvides($packageName, $constraint);
                 if (!$packages) {
@@ -224,7 +231,7 @@ class Problem
         if (isset($constraint)) {
             $packages = $this->pool->whatProvides($packageName, $constraint);
         } else {
-            $packages = array();
+            $packages = $this->pool->whatProvides($job['packageName'], null);
         }
 
         return 'Job(cmd='.$job['cmd'].', target='.$packageName.', packages=['.$this->getPackageList($packages).'])';

+ 65 - 21
src/Composer/DependencyResolver/Request.php

@@ -12,6 +12,10 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\Package\Package;
+use Composer\Package\PackageInterface;
+use Composer\Package\RootAliasPackage;
+use Composer\Repository\RepositoryInterface;
 use Composer\Semver\Constraint\ConstraintInterface;
 
 /**
@@ -19,11 +23,14 @@ use Composer\Semver\Constraint\ConstraintInterface;
  */
 class Request
 {
-    protected $jobs;
+    protected $lockedRepository;
+    protected $jobs = array();
+    protected $fixedPackages = array();
+    protected $unlockables = array();
 
-    public function __construct()
+    public function __construct(RepositoryInterface $lockedRepository = null)
     {
-        $this->jobs = array();
+        $this->lockedRepository = $lockedRepository;
     }
 
     public function install($packageName, ConstraintInterface $constraint = null)
@@ -31,11 +38,6 @@ class Request
         $this->addJob($packageName, 'install', $constraint);
     }
 
-    public function update($packageName, ConstraintInterface $constraint = null)
-    {
-        $this->addJob($packageName, 'update', $constraint);
-    }
-
     public function remove($packageName, ConstraintInterface $constraint = null)
     {
         $this->addJob($packageName, 'remove', $constraint);
@@ -43,18 +45,21 @@ class Request
 
     /**
      * Mark an existing package as being installed and having to remain installed
-     *
-     * These jobs will not be tempered with by the solver
-     *
-     * @param string                   $packageName
-     * @param ConstraintInterface|null $constraint
      */
-    public function fix($packageName, ConstraintInterface $constraint = null)
+    public function fixPackage(PackageInterface $package, $lockable = true)
     {
-        $this->addJob($packageName, 'install', $constraint, true);
+        if ($package instanceof RootAliasPackage) {
+            $package = $package->getAliasOf();
+        }
+
+        $this->fixedPackages[spl_object_hash($package)] = $package;
+
+        if (!$lockable) {
+            $this->unlockables[] = $package;
+        }
     }
 
-    protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null, $fixed = false)
+    protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null)
     {
         $packageName = strtolower($packageName);
 
@@ -62,17 +67,56 @@ class Request
             'cmd' => $cmd,
             'packageName' => $packageName,
             'constraint' => $constraint,
-            'fixed' => $fixed,
         );
     }
 
-    public function updateAll()
+    public function getJobs()
     {
-        $this->jobs[] = array('cmd' => 'update-all');
+        return $this->jobs;
     }
 
-    public function getJobs()
+    public function getFixedPackages()
     {
-        return $this->jobs;
+        return $this->fixedPackages;
+    }
+
+    public function isFixedPackage(PackageInterface $package)
+    {
+        return isset($this->fixedPackages[spl_object_hash($package)]);
+    }
+
+    // TODO look into removing the packageIds option, the only place true is used is for the installed map in the solver problems
+    // some locked packages may not be in the pool so they have a package->id of -1
+    public function getPresentMap($packageIds = false)
+    {
+        $presentMap = array();
+
+        if ($this->lockedRepository) {
+            foreach ($this->lockedRepository->getPackages() as $package) {
+                $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package;
+            }
+        }
+
+        foreach ($this->fixedPackages as $package) {
+            $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package;
+        }
+
+        return $presentMap;
+    }
+
+    public function getUnlockableMap()
+    {
+        $unlockableMap = array();
+
+        foreach ($this->unlockables as $package) {
+            $unlockableMap[$package->id] = $package;
+        }
+
+        return $unlockableMap;
+    }
+
+    public function getLockedRepository()
+    {
+        return $this->lockedRepository;
     }
 }

+ 1 - 1
src/Composer/DependencyResolver/Rule.php

@@ -231,7 +231,7 @@ abstract class Rule
             case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
                 return $ruleText;
             case self::RULE_PACKAGE_SAME_NAME:
-                return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.';
+                return 'Same name, can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.';
             case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
                 return $ruleText;
             case self::RULE_LEARNED:

+ 29 - 18
src/Composer/DependencyResolver/RuleSetGenerator.php

@@ -12,9 +12,11 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\Package\LinkConstraint\VersionConstraint;
 use Composer\Package\PackageInterface;
 use Composer\Package\AliasPackage;
 use Composer\Repository\PlatformRepository;
+use Composer\Semver\Constraint\Constraint;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
@@ -24,8 +26,6 @@ class RuleSetGenerator
     protected $policy;
     protected $pool;
     protected $rules;
-    protected $jobs;
-    protected $installedMap;
     protected $addedMap;
     protected $conflictAddedMap;
     protected $addedPackages;
@@ -222,8 +222,6 @@ class RuleSetGenerator
             }
 
             // check obsoletes and implicit obsoletes of a package
-            $isInstalled = isset($this->installedMap[$package->id]);
-
             foreach ($package->getReplaces() as $link) {
                 if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
                     continue;
@@ -236,7 +234,7 @@ class RuleSetGenerator
                     }
 
                     if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
-                        $reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
+                        $reason = Rule::RULE_PACKAGE_OBSOLETES;
                         $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link));
                     }
                 }
@@ -258,21 +256,39 @@ class RuleSetGenerator
         return $impossible;
     }
 
-    protected function addRulesForJobs($ignorePlatformReqs)
+    protected function addRulesForRequest(Request $request, $ignorePlatformReqs)
     {
-        foreach ($this->jobs as $job) {
+        $unlockableMap = $request->getUnlockableMap();
+
+        foreach ($request->getFixedPackages() as $package) {
+            if ($package->id == -1) {
+                throw new \RuntimeException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool.");
+            }
+
+            $this->addRulesForPackage($package, $ignorePlatformReqs);
+
+            $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_JOB_INSTALL, array(
+                'cmd' => 'fix',
+                'packageName' => $package->getName(),
+                'constraint' => null,
+                'package' => $package,
+                'lockable' => !isset($unlockableMap[$package->id]),
+                'fixed' => true
+            ));
+            $this->addRule(RuleSet::TYPE_JOB, $rule);
+        }
+
+        foreach ($request->getJobs() as $job) {
             switch ($job['cmd']) {
                 case 'install':
-                    if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
+                    if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
                         break;
                     }
 
                     $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
                     if ($packages) {
                         foreach ($packages as $package) {
-                            if (!isset($this->installedMap[$package->id])) {
-                                $this->addRulesForPackage($package, $ignorePlatformReqs);
-                            }
+                            $this->addRulesForPackage($package, $ignorePlatformReqs);
                         }
 
                         $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job);
@@ -292,21 +308,16 @@ class RuleSetGenerator
         }
     }
 
-    public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false)
+    public function getRulesFor(Request $request, $ignorePlatformReqs = false)
     {
-        $this->jobs = $jobs;
         $this->rules = new RuleSet;
-        $this->installedMap = $installedMap;
 
         $this->addedMap = array();
         $this->conflictAddedMap = array();
         $this->addedPackages = array();
         $this->addedPackagesByNames = array();
-        foreach ($this->installedMap as $package) {
-            $this->addRulesForPackage($package, $ignorePlatformReqs);
-        }
 
-        $this->addRulesForJobs($ignorePlatformReqs);
+        $this->addRulesForRequest($request, $ignorePlatformReqs);
 
         $this->addConflictRules($ignorePlatformReqs);
 

+ 24 - 62
src/Composer/DependencyResolver/Solver.php

@@ -30,23 +30,18 @@ class Solver
     protected $policy;
     /** @var Pool */
     protected $pool = null;
-    /** @var RepositoryInterface */
-    protected $installed;
+
     /** @var RuleSet */
     protected $rules;
     /** @var RuleSetGenerator */
     protected $ruleSetGenerator;
-    /** @var array */
-    protected $jobs;
 
-    /** @var int[] */
-    protected $updateMap = array();
     /** @var RuleWatchGraph */
     protected $watchGraph;
     /** @var Decisions */
     protected $decisions;
     /** @var PackageInterface[] */
-    protected $installedMap;
+    protected $fixedMap;
 
     /** @var int */
     protected $propagateIndex;
@@ -68,15 +63,13 @@ class Solver
     /**
      * @param PolicyInterface     $policy
      * @param Pool                $pool
-     * @param RepositoryInterface $installed
      * @param IOInterface         $io
      */
-    public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io)
+    public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io)
     {
         $this->io = $io;
         $this->policy = $policy;
         $this->pool = $pool;
-        $this->installed = $installed;
     }
 
     /**
@@ -154,7 +147,6 @@ class Solver
                 if (abs($literal) !== abs($assertRuleLiteral)) {
                     continue;
                 }
-
                 $problem->addRule($assertRule);
                 $this->disableProblem($assertRule);
             }
@@ -165,36 +157,22 @@ class Solver
         }
     }
 
-    protected function setupInstalledMap()
+    protected function setupFixedMap(Request $request)
     {
-        $this->installedMap = array();
-        foreach ($this->installed->getPackages() as $package) {
-            $this->installedMap[$package->id] = $package;
+        $this->fixedMap = array();
+        foreach ($request->getFixedPackages() as $package) {
+            $this->fixedMap[$package->id] = $package;
         }
     }
 
     /**
+     * @param  Request $request
      * @param bool $ignorePlatformReqs
      */
-    protected function checkForRootRequireProblems($ignorePlatformReqs)
+    protected function checkForRootRequireProblems($request, $ignorePlatformReqs)
     {
-        foreach ($this->jobs as $job) {
+        foreach ($request->getJobs() as $job) {
             switch ($job['cmd']) {
-                case 'update':
-                    $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
-                    foreach ($packages as $package) {
-                        if (isset($this->installedMap[$package->id])) {
-                            $this->updateMap[$package->id] = true;
-                        }
-                    }
-                    break;
-
-                case 'update-all':
-                    foreach ($this->installedMap as $package) {
-                        $this->updateMap[$package->id] = true;
-                    }
-                    break;
-
                 case 'install':
                     if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
                         break;
@@ -213,18 +191,16 @@ class Solver
     /**
      * @param  Request $request
      * @param  bool    $ignorePlatformReqs
-     * @return array
+     * @return LockTransaction
      */
     public function solve(Request $request, $ignorePlatformReqs = false)
     {
-        $this->jobs = $request->getJobs();
-
-        $this->setupInstalledMap();
+        $this->setupFixedMap($request);
 
         $this->io->writeError('Generating rules', true, IOInterface::DEBUG);
         $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool);
-        $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs);
-        $this->checkForRootRequireProblems($ignorePlatformReqs);
+        $this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs);
+        $this->checkForRootRequireProblems($request, $ignorePlatformReqs);
         $this->decisions = new Decisions($this->pool);
         $this->watchGraph = new RuleWatchGraph;
 
@@ -241,20 +217,11 @@ class Solver
         $this->io->writeError('', true, IOInterface::DEBUG);
         $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE);
 
-        // decide to remove everything that's installed and undecided
-        foreach ($this->installedMap as $packageId => $void) {
-            if ($this->decisions->undecided($packageId)) {
-                $this->decisions->decide(-$packageId, 1, null);
-            }
-        }
-
         if ($this->problems) {
-            throw new SolverProblemsException($this->problems, $this->installedMap, $this->learnedPool);
+            throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool);
         }
 
-        $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
-
-        return $transaction->getOperations();
+        return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
     }
 
     /**
@@ -393,7 +360,7 @@ class Solver
     private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule)
     {
         // choose best package to install from decisionQueue
-        $literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage());
+        $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage());
 
         $selectedLiteral = array_shift($literals);
 
@@ -729,19 +696,14 @@ class Solver
                         }
 
                         if ($noneSatisfied && count($decisionQueue)) {
-                            // prune all update packages until installed version
-                            // except for requested updates
-                            if (count($this->installed) != count($this->updateMap)) {
-                                $prunedQueue = array();
-                                foreach ($decisionQueue as $literal) {
-                                    if (isset($this->installedMap[abs($literal)])) {
-                                        $prunedQueue[] = $literal;
-                                        if (isset($this->updateMap[abs($literal)])) {
-                                            $prunedQueue = $decisionQueue;
-                                            break;
-                                        }
-                                    }
+                            // if any of the options in the decision queue are fixed, only use those
+                            $prunedQueue = array();
+                            foreach ($decisionQueue as $literal) {
+                                if (isset($this->fixedMap[abs($literal)])) {
+                                    $prunedQueue[] = $literal;
                                 }
+                            }
+                            if (!empty($prunedQueue)) {
                                 $decisionQueue = $prunedQueue;
                             }
                         }

+ 217 - 152
src/Composer/DependencyResolver/Transaction.php

@@ -12,162 +12,209 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
+use Composer\DependencyResolver\Operation\UninstallOperation;
 use Composer\Package\AliasPackage;
+use Composer\Package\Link;
+use Composer\Package\PackageInterface;
+use Composer\Repository\PlatformRepository;
+use Composer\Repository\RepositoryInterface;
+use Composer\Semver\Constraint\Constraint;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
  */
 class Transaction
 {
-    protected $policy;
-    protected $pool;
-    protected $installedMap;
-    protected $decisions;
-    protected $transaction;
-
-    public function __construct($policy, $pool, $installedMap, $decisions)
+    /**
+     * @var array
+     */
+    protected $operations;
+
+    /**
+     * Packages present at the beginning of the transaction
+     * @var array
+     */
+    protected $presentPackages;
+
+    /**
+     * Package set resulting from this transaction
+     * @var array
+     */
+    protected $resultPackageMap;
+
+    /**
+     * @var array
+     */
+    protected $resultPackagesByName = array();
+
+    public function __construct($presentPackages, $resultPackages)
     {
-        $this->policy = $policy;
-        $this->pool = $pool;
-        $this->installedMap = $installedMap;
-        $this->decisions = $decisions;
-        $this->transaction = array();
+        $this->presentPackages = $presentPackages;
+        $this->setResultPackageMaps($resultPackages);
+        $this->operations = $this->calculateOperations();
     }
 
     public function getOperations()
     {
-        $installMeansUpdateMap = $this->findUpdates();
-
-        $updateMap = array();
-        $installMap = array();
-        $uninstallMap = array();
-
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $reason = $decision[Decisions::DECISION_REASON];
-
-            $package = $this->pool->literalToPackage($literal);
+        return $this->operations;
+    }
 
-            // wanted & installed || !wanted & !installed
-            if (($literal > 0) == isset($this->installedMap[$package->id])) {
-                continue;
+    private function setResultPackageMaps($resultPackages)
+    {
+        $packageSort = function (PackageInterface $a, PackageInterface $b) {
+            // sort alias packages by the same name behind their non alias version
+            if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) {
+                return $a instanceof AliasPackage ? -1 : 1;
             }
-
-            if ($literal > 0) {
-                if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) {
-                    $source = $installMeansUpdateMap[abs($literal)];
-
-                    $updateMap[$package->id] = array(
-                        'package' => $package,
-                        'source' => $source,
-                        'reason' => $reason,
-                    );
-
-                    // avoid updates to one package from multiple origins
-                    unset($installMeansUpdateMap[abs($literal)]);
-                    $ignoreRemove[$source->id] = true;
-                } else {
-                    $installMap[$package->id] = array(
-                        'package' => $package,
-                        'reason' => $reason,
-                    );
-                }
+            return strcmp($b->getName(), $a->getName());
+        };
+
+        $this->resultPackageMap = array();
+        foreach ($resultPackages as $package) {
+            $this->resultPackageMap[spl_object_hash($package)] = $package;
+            foreach ($package->getNames() as $name) {
+                $this->resultPackagesByName[$name][] = $package;
             }
         }
 
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $reason = $decision[Decisions::DECISION_REASON];
-            $package = $this->pool->literalToPackage($literal);
-
-            if ($literal <= 0 &&
-                isset($this->installedMap[$package->id]) &&
-                !isset($ignoreRemove[$package->id])) {
-                $uninstallMap[$package->id] = array(
-                    'package' => $package,
-                    'reason' => $reason,
-                );
-            }
+        uasort($this->resultPackageMap, $packageSort);
+        foreach ($this->resultPackagesByName as $name => $packages) {
+            uasort($this->resultPackagesByName[$name], $packageSort);
         }
-
-        $this->transactionFromMaps($installMap, $updateMap, $uninstallMap);
-
-        return $this->transaction;
     }
 
-    protected function transactionFromMaps($installMap, $updateMap, $uninstallMap)
+    protected function calculateOperations()
     {
-        $queue = array_map(
-            function ($operation) {
-                return $operation['package'];
-            },
-            $this->findRootPackages($installMap, $updateMap)
-        );
+        $operations = array();
+
+        $presentPackageMap = array();
+        $removeMap = array();
+        $presentAliasMap = array();
+        $removeAliasMap = array();
+        foreach ($this->presentPackages as $package) {
+            if ($package instanceof AliasPackage) {
+                $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
+                $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
+            } else {
+                $presentPackageMap[$package->getName()] = $package;
+                $removeMap[$package->getName()] = $package;
+            }
+        }
+
+        $stack = $this->getRootPackages();
 
         $visited = array();
+        $processed = array();
+
+        while (!empty($stack)) {
+            $package = array_pop($stack);
 
-        while (!empty($queue)) {
-            $package = array_pop($queue);
-            $packageId = $package->id;
+            if (isset($processed[spl_object_hash($package)])) {
+                continue;
+            }
 
-            if (!isset($visited[$packageId])) {
-                $queue[] = $package;
+            if (!isset($visited[spl_object_hash($package)])) {
+                $visited[spl_object_hash($package)] = true;
 
+                $stack[] = $package;
                 if ($package instanceof AliasPackage) {
-                    $queue[] = $package->getAliasOf();
+                    $stack[] = $package->getAliasOf();
                 } else {
                     foreach ($package->getRequires() as $link) {
-                        $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+                        $possibleRequires = $this->getProvidersInResult($link);
 
                         foreach ($possibleRequires as $require) {
-                            $queue[] = $require;
+                            $stack[] = $require;
                         }
                     }
                 }
+            } elseif (!isset($processed[spl_object_hash($package)])) {
+                $processed[spl_object_hash($package)] = true;
 
-                $visited[$package->id] = true;
-            } else {
-                if (isset($installMap[$packageId])) {
-                    $this->install(
-                        $installMap[$packageId]['package'],
-                        $installMap[$packageId]['reason']
-                    );
-                    unset($installMap[$packageId]);
-                }
-                if (isset($updateMap[$packageId])) {
-                    $this->update(
-                        $updateMap[$packageId]['source'],
-                        $updateMap[$packageId]['package'],
-                        $updateMap[$packageId]['reason']
-                    );
-                    unset($updateMap[$packageId]);
+                if ($package instanceof AliasPackage) {
+                    $aliasKey = $package->getName().'::'.$package->getVersion();
+                    if (isset($presentAliasMap[$aliasKey])) {
+                        unset($removeAliasMap[$aliasKey]);
+                    } else {
+                        $operations[] = new Operation\MarkAliasInstalledOperation($package);
+                    }
+                } else {
+                    if (isset($presentPackageMap[$package->getName()])) {
+                        $source = $presentPackageMap[$package->getName()];
+
+                        // do we need to update?
+                        // TODO different for lock?
+                        if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion()) {
+                            $operations[] = new Operation\UpdateOperation($source, $package);
+                        } elseif ($package->isDev() && $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()) {
+                            $operations[] = new Operation\UpdateOperation($source, $package);
+                        }
+                        unset($removeMap[$package->getName()]);
+                    } else {
+                        $operations[] = new Operation\InstallOperation($package);
+                        unset($removeMap[$package->getName()]);
+                    }
                 }
             }
         }
 
-        foreach ($uninstallMap as $uninstall) {
-            $this->uninstall($uninstall['package'], $uninstall['reason']);
+        foreach ($removeMap as $name => $package) {
+            array_unshift($operations, new Operation\UninstallOperation($package, null));
         }
+        foreach ($removeAliasMap as $nameVersion => $package) {
+            $operations[] = new Operation\MarkAliasUninstalledOperation($package, null);
+        }
+
+        $operations = $this->movePluginsToFront($operations);
+        // TODO fix this:
+        // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls
+        $operations = $this->moveUninstallsToFront($operations);
+
+        // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place?
+        /*
+        if ('update' === $jobType) {
+            $targetPackage = $operation->getTargetPackage();
+            if ($targetPackage->isDev()) {
+                $initialPackage = $operation->getInitialPackage();
+                if ($targetPackage->getVersion() === $initialPackage->getVersion()
+                    && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference())
+                    && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference())
+                ) {
+                    $this->io->writeError('  - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG);
+                    $this->io->writeError('', true, IOInterface::DEBUG);
+
+                    continue;
+                }
+            }
+        }*/
+
+        return $this->operations = $operations;
     }
 
-    protected function findRootPackages($installMap, $updateMap)
+    /**
+     * Determine which packages in the result are not required by any other packages in it.
+     *
+     * These serve as a starting point to enumerate packages in a topological order despite potential cycles.
+     * If there are packages with a cycle on the top level the package with the lowest name gets picked
+     *
+     * @return array
+     */
+    protected function getRootPackages()
     {
-        $packages = $installMap + $updateMap;
-        $roots = $packages;
-
-        foreach ($packages as $packageId => $operation) {
-            $package = $operation['package'];
+        $roots = $this->resultPackageMap;
 
-            if (!isset($roots[$packageId])) {
+        foreach ($this->resultPackageMap as $packageHash => $package) {
+            if (!isset($roots[$packageHash])) {
                 continue;
             }
 
             foreach ($package->getRequires() as $link) {
-                $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+                $possibleRequires = $this->getProvidersInResult($link);
 
                 foreach ($possibleRequires as $require) {
                     if ($require !== $package) {
-                        unset($roots[$require->id]);
+                        unset($roots[spl_object_hash($require)]);
                     }
                 }
             }
@@ -176,69 +223,87 @@ class Transaction
         return $roots;
     }
 
-    protected function findUpdates()
+    protected function getProvidersInResult(Link $link)
     {
-        $installMeansUpdateMap = array();
-
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $package = $this->pool->literalToPackage($literal);
+        if (!isset($this->resultPackagesByName[$link->getTarget()])) {
+            return array();
+        }
+        return $this->resultPackagesByName[$link->getTarget()];
+    }
 
-            if ($package instanceof AliasPackage) {
+    /**
+     * Workaround: if your packages depend on plugins, we must be sure
+     * that those are installed / updated first; else it would lead to packages
+     * being installed multiple times in different folders, when running Composer
+     * twice.
+     *
+     * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147,
+     * it at least fixes the symptoms and makes usage of composer possible (again)
+     * in such scenarios.
+     *
+     * @param  Operation\OperationInterface[] $operations
+     * @return Operation\OperationInterface[] reordered operation list
+     */
+    private function movePluginsToFront(array $operations)
+    {
+        $pluginsNoDeps = array();
+        $pluginsWithDeps = array();
+        $pluginRequires = array();
+
+        foreach (array_reverse($operations, true) as $idx => $op) {
+            if ($op instanceof Operation\InstallOperation) {
+                $package = $op->getPackage();
+            } elseif ($op instanceof Operation\UpdateOperation) {
+                $package = $op->getTargetPackage();
+            } else {
                 continue;
             }
 
-            // !wanted & installed
-            if ($literal <= 0 && isset($this->installedMap[$package->id])) {
-                $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
+            // is this package a plugin?
+            $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
 
-                $literals = array($package->id);
+            // is this a plugin or a dependency of a plugin?
+            if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) {
+                // get the package's requires, but filter out any platform requirements or 'composer-plugin-api'
+                $requires = array_filter(array_keys($package->getRequires()), function ($req) {
+                    return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req);
+                });
 
-                foreach ($updates as $update) {
-                    $literals[] = $update->id;
+                // is this a plugin with no meaningful dependencies?
+                if ($isPlugin && !count($requires)) {
+                    // plugins with no dependencies go to the very front
+                    array_unshift($pluginsNoDeps, $op);
+                } else {
+                    // capture the requirements for this package so those packages will be moved up as well
+                    $pluginRequires = array_merge($pluginRequires, $requires);
+                    // move the operation to the front
+                    array_unshift($pluginsWithDeps, $op);
                 }
 
-                foreach ($literals as $updateLiteral) {
-                    if ($updateLiteral !== $literal) {
-                        $installMeansUpdateMap[abs($updateLiteral)] = $package;
-                    }
-                }
+                unset($operations[$idx]);
             }
         }
 
-        return $installMeansUpdateMap;
+        return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations);
     }
 
-    protected function install($package, $reason)
+    /**
+     * Removals of packages should be executed before installations in
+     * case two packages resolve to the same path (due to custom installers)
+     *
+     * @param  Operation\OperationInterface[] $operations
+     * @return Operation\OperationInterface[] reordered operation list
+     */
+    private function moveUninstallsToFront(array $operations)
     {
-        if ($package instanceof AliasPackage) {
-            return $this->markAliasInstalled($package, $reason);
-        }
-
-        $this->transaction[] = new Operation\InstallOperation($package, $reason);
-    }
-
-    protected function update($from, $to, $reason)
-    {
-        $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason);
-    }
-
-    protected function uninstall($package, $reason)
-    {
-        if ($package instanceof AliasPackage) {
-            return $this->markAliasUninstalled($package, $reason);
+        $uninstOps = array();
+        foreach ($operations as $idx => $op) {
+            if ($op instanceof UninstallOperation) {
+                $uninstOps[] = $op;
+                unset($operations[$idx]);
+            }
         }
 
-        $this->transaction[] = new Operation\UninstallOperation($package, $reason);
-    }
-
-    protected function markAliasInstalled($package, $reason)
-    {
-        $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason);
-    }
-
-    protected function markAliasUninstalled($package, $reason)
-    {
-        $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason);
+        return array_merge($uninstOps, $operations);
     }
 }

+ 3 - 2
src/Composer/EventDispatcher/EventDispatcher.php

@@ -19,6 +19,7 @@ use Composer\IO\IOInterface;
 use Composer\Composer;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\Repository\CompositeRepository;
+use Composer\Repository\RepositoryInterface;
 use Composer\Repository\RepositorySet;
 use Composer\Script;
 use Composer\Installer\PackageEvent;
@@ -130,9 +131,9 @@ class EventDispatcher
      * @return int return code of the executed script if any, for php scripts a false return
      *             value is changed to 1, anything else to 0
      */
-    public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations = array())
+    public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $lockedRepo, Request $request, array $operations = array())
     {
-        return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations));
+        return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $lockedRepo, $request, $operations));
     }
 
     /**

File diff suppressed because it is too large
+ 299 - 593
src/Composer/Installer.php


+ 7 - 7
src/Composer/Installer/SuggestedPackagesReporter.php

@@ -96,21 +96,21 @@ class SuggestedPackagesReporter
      * @param  RepositoryInterface       $installedRepo Installed packages
      * @return SuggestedPackagesReporter
      */
-    public function output(RepositoryInterface $installedRepo = null)
+    public function output(RepositoryInterface $lockedRepo = null)
     {
         $suggestedPackages = $this->getPackages();
-        $installedPackages = array();
-        if (null !== $installedRepo && ! empty($suggestedPackages)) {
-            foreach ($installedRepo->getPackages() as $package) {
-                $installedPackages = array_merge(
-                    $installedPackages,
+        $lockedPackages = array();
+        if (null !== $lockedRepo && ! empty($suggestedPackages)) {
+            foreach ($lockedRepo->getPackages() as $package) {
+                $lockedPackages = array_merge(
+                    $lockedPackages,
                     $package->getNames()
                 );
             }
         }
 
         foreach ($suggestedPackages as $suggestion) {
-            if (in_array($suggestion['target'], $installedPackages)) {
+            if (in_array($suggestion['target'], $lockedPackages)) {
                 continue;
             }
 

+ 5 - 0
src/Composer/Package/AliasPackage.php

@@ -411,4 +411,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
     {
         return $this->aliasOf->setDistType($type);
     }
+
+    public function setSourceDistReferences($reference)
+    {
+        return $this->aliasOf->setSourceDistReferences($reference);
+    }
 }

+ 36 - 7
src/Composer/Package/Locker.php

@@ -46,6 +46,7 @@ class Locker
     /** @var ProcessExecutor */
     private $process;
     private $lockDataCache;
+    private $virtualFileWritten;
 
     /**
      * Initializes packages locker.
@@ -112,7 +113,7 @@ class Locker
      */
     public function isLocked()
     {
-        if (!$this->lockFile->exists()) {
+        if (!$this->virtualFileWritten && !$this->lockFile->exists()) {
             return false;
         }
 
@@ -161,7 +162,7 @@ class Locker
             if (isset($lockData['packages-dev'])) {
                 $lockedPackages = array_merge($lockedPackages, $lockData['packages-dev']);
             } else {
-                throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or run update to install those packages.');
+                throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or delete it and run composer update to generate a new lock file.');
             }
         }
 
@@ -170,8 +171,24 @@ class Locker
         }
 
         if (isset($lockedPackages[0]['name'])) {
+            $packageByName = array();
             foreach ($lockedPackages as $info) {
-                $packages->addPackage($this->loader->load($info));
+                $package = $this->loader->load($info);
+                $packages->addPackage($package);
+                $packageByName[$package->getName()] = $package;
+
+                if ($package instanceof AliasPackage) {
+                    $packages->addPackage($package->getAliasOf());
+                    $packageByName[$package->getAliasOf()->getName()] = $package->getAliasOf();
+                }
+            }
+
+            if (isset($lockData['aliases'])) {
+                foreach ($lockData['aliases'] as $alias) {
+                    if (isset($packageByName[$alias['package']])) {
+                        $packages->addPackage(new AliasPackage($packageByName[$alias['package']], $alias['alias_normalized'], $alias['alias']));
+                    }
+                }
             }
 
             return $packages;
@@ -286,10 +303,11 @@ class Locker
      * @param bool   $preferStable
      * @param bool   $preferLowest
      * @param array  $platformOverrides
+     * @param bool   $write             Whether to actually write data to disk, useful in tests and for --dry-run
      *
      * @return bool
      */
-    public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides)
+    public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides, $write = true)
     {
         $lock = array(
             '_readme' => array('This file locks the dependencies of your project to a known state',
@@ -329,7 +347,11 @@ class Locker
 
         if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) {
             if ($this->lockFile->exists()) {
-                unlink($this->lockFile->getPath());
+                if ($write) {
+                    unlink($this->lockFile->getPath());
+                } else {
+                    $this->virtualFileWritten = false;
+                }
             }
 
             return false;
@@ -341,8 +363,15 @@ class Locker
             $isLocked = false;
         }
         if (!$isLocked || $lock !== $this->getLockData()) {
-            $this->lockFile->write($lock);
-            $this->lockDataCache = null;
+            if ($write) {
+                $this->lockFile->write($lock);
+//                $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT));
+                $this->lockDataCache = null;
+                $this->virtualFileWritten = false;
+            } else {
+                $this->virtualFileWritten = true;
+                $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT));
+            }
 
             return true;
         }

+ 17 - 0
src/Composer/Package/Package.php

@@ -569,6 +569,23 @@ class Package extends BasePackage
         return $this->archiveExcludes;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function setSourceDistReferences($reference)
+    {
+        $this->setSourceReference($reference);
+
+        // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL
+        // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this?
+        if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $this->getDistUrl())) {
+            $this->setDistReference($reference);
+            $this->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $this->getDistUrl()));
+        } elseif ($this->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it
+            $this->setDistReference($reference);
+        }
+    }
+
     /**
      * Replaces current version and pretty version with passed values.
      * It also sets stability.

+ 9 - 0
src/Composer/Package/PackageInterface.php

@@ -386,4 +386,13 @@ interface PackageInterface
      * @return void
      */
     public function setDistReference($reference);
+
+    /**
+     * Set dist and source references and update dist URL for ones that contain a reference
+     *
+     * @param string $reference
+     *
+     * @return void
+     */
+    public function setSourceDistReferences($reference);
 }

+ 1 - 1
src/Composer/Plugin/PluginManager.php

@@ -158,7 +158,7 @@ class PluginManager
         $localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
         $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
 
-        $repositorySet = new RepositorySet(array(), 'dev');
+        $repositorySet = new RepositorySet(array(), array(), 'dev');
         $repositorySet->addRepository($localRepo);
         if ($globalRepo) {
             $repositorySet->addRepository($globalRepo);

+ 7 - 2
src/Composer/Repository/RepositorySet.php

@@ -29,6 +29,8 @@ class RepositorySet
 {
     /** @var array */
     private $rootAliases;
+    /** @var array */
+    private $rootReferences;
 
     /** @var RepositoryInterface[] */
     private $repositories = array();
@@ -40,9 +42,10 @@ class RepositorySet
     /** @var Pool */
     private $pool;
 
-    public function __construct(array $rootAliases = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array())
+    public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array())
     {
         $this->rootAliases = $rootAliases;
+        $this->rootReferences = $rootReferences;
 
         $this->acceptableStabilities = array();
         foreach (BasePackage::$stabilities as $stability => $value) {
@@ -101,6 +104,8 @@ class RepositorySet
     /**
      * Find packages providing or matching a name and optionally meeting a constraint in all repositories
      *
+     * Returned in the order of repositories, matching priority
+     *
      * @param string $name
      * @param ConstraintInterface|null $constraint
      * @param bool $exactMatch
@@ -149,7 +154,7 @@ class RepositorySet
     {
         $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->filterRequires);
 
-        return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $request);
+        return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $this->rootReferences, $request);
     }
 
     // TODO unify this with above in some simpler version without "request"?

+ 25 - 35
tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php

@@ -29,15 +29,15 @@ class DefaultPolicyTest extends TestCase
     /** @var ArrayRepository */
     protected $repo;
     /** @var ArrayRepository */
-    protected $repoInstalled;
+    protected $repoLocked;
     /** @var DefaultPolicy */
     protected $policy;
 
     public function setUp()
     {
-        $this->repositorySet = new RepositorySet(array(), 'dev');
+        $this->repositorySet = new RepositorySet(array(), array(), 'dev');
         $this->repo = new ArrayRepository;
-        $this->repoInstalled = new ArrayRepository;
+        $this->repoLocked = new ArrayRepository;
 
         $this->policy = new DefaultPolicy;
     }
@@ -52,7 +52,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA->getId());
         $expected = array($packageA->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -68,7 +68,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA1->getId(), $packageA2->getId());
         $expected = array($packageA2->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -84,7 +84,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA1->getId(), $packageA2->getId());
         $expected = array($packageA2->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -101,7 +101,7 @@ class DefaultPolicyTest extends TestCase
         $expected = array($packageA1->getId());
 
         $policy = new DefaultPolicy(true);
-        $selected = $policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -117,24 +117,24 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA1->getId(), $packageA2->getId());
         $expected = array($packageA2->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
 
-    public function testSelectNewestOverInstalled()
+    public function testSelectNewestOverLocked()
     {
         $this->repo->addPackage($packageA = $this->getPackage('A', '2.0'));
-        $this->repoInstalled->addPackage($packageAInstalled = $this->getPackage('A', '1.0'));
-        $this->repositorySet->addRepository($this->repoInstalled);
+        $this->repoLocked->addPackage($packageAInstalled = $this->getPackage('A', '1.0'));
         $this->repositorySet->addRepository($this->repo);
+        $this->repositorySet->addRepository($this->repoLocked);
 
         $pool = $this->repositorySet->createPoolForPackage('A');
 
         $literals = array($packageA->getId(), $packageAInstalled->getId());
         $expected = array($packageA->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, $this->mapFromRepo($this->repoInstalled), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -146,16 +146,16 @@ class DefaultPolicyTest extends TestCase
         $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
         $otherRepository->addPackage($packageAImportant = $this->getPackage('A', '1.0'));
 
-        $this->repositorySet->addRepository($this->repoInstalled);
         $this->repositorySet->addRepository($otherRepository);
         $this->repositorySet->addRepository($this->repo);
+        $this->repositorySet->addRepository($this->repoLocked);
 
         $pool = $this->repositorySet->createPoolForPackage('A');
 
         $literals = array($packageA->getId(), $packageAImportant->getId());
         $expected = array($packageAImportant->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -177,18 +177,18 @@ class DefaultPolicyTest extends TestCase
 
         $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId());
         $expected = array($package2->getId());
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
 
-        $this->repositorySet = new RepositorySet(array(), 'dev');
+        $this->repositorySet = new RepositorySet(array(), array(), 'dev');
         $this->repositorySet->addRepository($repo2);
         $this->repositorySet->addRepository($repo1);
 
         $pool = $this->repositorySet->createPoolForPackage('A');
 
         $expected = array($package4->getId());
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -205,9 +205,9 @@ class DefaultPolicyTest extends TestCase
         $repoImportant->addPackage($packageA2AliasImportant = new AliasPackage($packageA2Important, '2.1.9999999.9999999-dev', '2.1.x-dev'));
         $packageAAliasImportant->setRootPackageAlias(true);
 
-        $this->repositorySet->addRepository($this->repoInstalled);
         $this->repositorySet->addRepository($repoImportant);
         $this->repositorySet->addRepository($this->repo);
+        $this->repositorySet->addRepository($this->repoLocked);
 
         $pool = $this->repositorySet->createPoolForPackage('A');
 
@@ -219,7 +219,7 @@ class DefaultPolicyTest extends TestCase
 
         $expected = array($packageAAliasImportant->getId());
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -239,7 +239,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA->getId(), $packageB->getId());
         $expected = $literals;
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -258,7 +258,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA->getId(), $packageB->getId());
         $expected = $literals;
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $this->policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }
@@ -279,7 +279,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA->getId(), $packageB->getId());
         $expected = $literals;
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package');
+        $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package');
         $this->assertEquals($expected, $selected);
 
         // test with reversed order in repo
@@ -287,7 +287,7 @@ class DefaultPolicyTest extends TestCase
         $repo->addPackage($packageA = clone $packageA);
         $repo->addPackage($packageB = clone $packageB);
 
-        $repositorySet = new RepositorySet(array(), 'dev');
+        $repositorySet = new RepositorySet(array(), array(), 'dev');
         $repositorySet->addRepository($this->repo);
 
         $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer'));
@@ -295,20 +295,10 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA->getId(), $packageB->getId());
         $expected = $literals;
 
-        $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package');
+        $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package');
         $this->assertSame($expected, $selected);
     }
 
-    protected function mapFromRepo(RepositoryInterface $repo)
-    {
-        $map = array();
-        foreach ($repo->getPackages() as $package) {
-            $map[$package->getId()] = true;
-        }
-
-        return $map;
-    }
-
     public function testSelectLowest()
     {
         $policy = new DefaultPolicy(false, true);
@@ -322,7 +312,7 @@ class DefaultPolicyTest extends TestCase
         $literals = array($packageA1->getId(), $packageA2->getId());
         $expected = array($packageA1->getId());
 
-        $selected = $policy->selectPreferredPackages($pool, array(), $literals);
+        $selected = $policy->selectPreferredPackages($pool, $literals);
 
         $this->assertSame($expected, $selected);
     }

+ 3 - 17
tests/Composer/Test/DependencyResolver/RequestTest.php

@@ -31,14 +31,12 @@ class RequestTest extends TestCase
 
         $request = new Request();
         $request->install('foo');
-        $request->fix('bar');
         $request->remove('foobar');
 
         $this->assertEquals(
             array(
-                array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null, 'fixed' => false),
-                array('cmd' => 'install', 'packageName' => 'bar', 'constraint' => null, 'fixed' => true),
-                array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null, 'fixed' => false),
+                array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null),
+                array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null),
             ),
             $request->getJobs()
         );
@@ -60,21 +58,9 @@ class RequestTest extends TestCase
 
         $this->assertEquals(
             array(
-                    array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint, 'fixed' => false),
+                    array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint),
             ),
             $request->getJobs()
         );
     }
-
-    public function testUpdateAll()
-    {
-        $request = new Request();
-
-        $request->updateAll();
-
-        $this->assertEquals(
-            array(array('cmd' => 'update-all')),
-            $request->getJobs()
-        );
-    }
 }

+ 43 - 53
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -29,7 +29,7 @@ class SolverTest extends TestCase
 {
     protected $repoSet;
     protected $repo;
-    protected $repoInstalled;
+    protected $repoLocked;
     protected $request;
     protected $policy;
     protected $solver;
@@ -38,9 +38,9 @@ class SolverTest extends TestCase
     {
         $this->repoSet = new RepositorySet(array());
         $this->repo = new ArrayRepository;
-        $this->repoInstalled = new InstalledArrayRepository;
+        $this->repoLocked = new ArrayRepository;
 
-        $this->request = new Request();
+        $this->request = new Request($this->repoLocked);
         $this->policy = new DefaultPolicy;
     }
 
@@ -56,9 +56,9 @@ class SolverTest extends TestCase
         ));
     }
 
-    public function testSolverRemoveIfNotInstalled()
+    public function testSolverRemoveIfNotRequested()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->reposComplete();
 
         $this->checkSolverResult(array(
@@ -93,7 +93,6 @@ class SolverTest extends TestCase
         $repo1->addPackage($foo1 = $this->getPackage('foo', '1'));
         $repo2->addPackage($foo2 = $this->getPackage('foo', '1'));
 
-        $this->repoSet->addRepository($this->repoInstalled);
         $this->repoSet->addRepository($repo1);
         $this->repoSet->addRepository($repo2);
 
@@ -175,30 +174,30 @@ class SolverTest extends TestCase
         ));
     }
 
-    public function testSolverInstallInstalled()
+    public function testSolverFixLocked()
     {
-        $this->repoInstalled->addPackage($this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->reposComplete();
 
-        $this->request->install('A');
+        $this->request->fixPackage($packageA);
 
         $this->checkSolverResult(array());
     }
 
-    public function testSolverInstallInstalledWithAlternative()
+    public function testSolverFixLockedWithAlternative()
     {
         $this->repo->addPackage($this->getPackage('A', '1.0'));
-        $this->repoInstalled->addPackage($this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->reposComplete();
 
-        $this->request->install('A');
+        $this->request->fixPackage($packageA);
 
         $this->checkSolverResult(array());
     }
 
     public function testSolverRemoveSingle()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->reposComplete();
 
         $this->request->remove('A');
@@ -220,17 +219,15 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateDoesOnlyUpdate()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
-        $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
         $this->reposComplete();
 
         $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires')));
 
-        $this->request->install('A', $this->getVersionConstraint('=', '1.0.0.0'));
+        $this->request->fixPackage($packageA);
         $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0'));
-        $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0'));
-        $this->request->update('B', $this->getVersionConstraint('=', '1.0.0.0'));
 
         $this->checkSolverResult(array(
             array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB),
@@ -239,12 +236,11 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateSingle()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1'));
         $this->reposComplete();
 
         $this->request->install('A');
-        $this->request->update('A');
 
         $this->checkSolverResult(array(
             array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA),
@@ -253,8 +249,8 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateAll()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
-        $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1'));
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
 
@@ -264,7 +260,6 @@ class SolverTest extends TestCase
         $this->reposComplete();
 
         $this->request->install('A');
-        $this->request->updateAll();
 
         $this->checkSolverResult(array(
             array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB),
@@ -274,28 +269,26 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateCurrent()
     {
-        $this->repoInstalled->addPackage($this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($this->getPackage('A', '1.0'));
         $this->repo->addPackage($this->getPackage('A', '1.0'));
         $this->reposComplete();
 
         $this->request->install('A');
-        $this->request->update('A');
 
         $this->checkSolverResult(array());
     }
 
     public function testSolverUpdateOnlyUpdatesSelectedPackage()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
-        $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($packageAnewer = $this->getPackage('A', '1.1'));
         $this->repo->addPackage($packageBnewer = $this->getPackage('B', '1.1'));
 
         $this->reposComplete();
 
         $this->request->install('A');
-        $this->request->install('B');
-        $this->request->update('A');
+        $this->request->fixPackage($packageB);
 
         $this->checkSolverResult(array(
             array('job' => 'update', 'from' => $packageA, 'to' => $packageAnewer),
@@ -304,13 +297,12 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateConstrained()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2'));
         $this->repo->addPackage($this->getPackage('A', '2.0'));
         $this->reposComplete();
 
         $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0'));
-        $this->request->update('A');
 
         $this->checkSolverResult(array(array(
             'job' => 'update',
@@ -321,13 +313,12 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateFullyConstrained()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2'));
         $this->repo->addPackage($this->getPackage('A', '2.0'));
         $this->reposComplete();
 
         $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0'));
-        $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0'));
 
         $this->checkSolverResult(array(array(
             'job' => 'update',
@@ -338,32 +329,31 @@ class SolverTest extends TestCase
 
     public function testSolverUpdateFullyConstrainedPrunesInstalledPackages()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
-        $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2'));
         $this->repo->addPackage($this->getPackage('A', '2.0'));
         $this->reposComplete();
 
         $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0'));
-        $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0'));
 
         $this->checkSolverResult(array(
+            array(
+                'job' => 'remove',
+                'package' => $packageB,
+            ),
             array(
                 'job' => 'update',
                 'from' => $packageA,
                 'to' => $newPackageA,
             ),
-            array(
-                'job' => 'remove',
-                'package' => $packageB,
-            ),
         ));
     }
 
     public function testSolverAllJobs()
     {
-        $this->repoInstalled->addPackage($packageD = $this->getPackage('D', '1.0'));
-        $this->repoInstalled->addPackage($oldPackageC = $this->getPackage('C', '1.0'));
+        $this->repoLocked->addPackage($packageD = $this->getPackage('D', '1.0'));
+        $this->repoLocked->addPackage($oldPackageC = $this->getPackage('C', '1.0'));
 
         $this->repo->addPackage($packageA = $this->getPackage('A', '2.0'));
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
@@ -376,14 +366,13 @@ class SolverTest extends TestCase
 
         $this->request->install('A');
         $this->request->install('C');
-        $this->request->update('C');
         $this->request->remove('D');
 
         $this->checkSolverResult(array(
-            array('job' => 'update',  'from' => $oldPackageC, 'to' => $packageC),
+            array('job' => 'remove',  'package' => $packageD),
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageA),
-            array('job' => 'remove',  'package' => $packageD),
+            array('job' => 'update',  'from' => $oldPackageC, 'to' => $packageC),
         ));
     }
 
@@ -408,7 +397,7 @@ class SolverTest extends TestCase
 
     public function testSolverObsolete()
     {
-        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
         $packageB->setReplaces(array('a' => new Link('B', 'A', new MultiConstraint(array()))));
 
@@ -417,7 +406,8 @@ class SolverTest extends TestCase
         $this->request->install('B');
 
         $this->checkSolverResult(array(
-            array('job' => 'update', 'from' => $packageA, 'to' => $packageB),
+            array('job' => 'remove', 'package' => $packageA),
+            array('job' => 'install', 'package' => $packageB),
         ));
     }
 
@@ -581,9 +571,9 @@ class SolverTest extends TestCase
         $this->request->install('C');
 
         $this->checkSolverResult(array(
+            array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageC),
-            array('job' => 'install', 'package' => $packageB),
         ));
     }
 
@@ -763,7 +753,7 @@ class SolverTest extends TestCase
             $msg .= "    - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n";
             $msg .= "    - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n";
             $msg .= "    - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n";
-            $msg .= "    - Can only install one of: B[0.9, 1.0].\n";
+            $msg .= "    - Same name, can only install one of: B[0.9, 1.0].\n";
             $msg .= "    - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n";
             $msg .= "    - Installation request for a -> satisfiable by A[1.0].\n";
             $this->assertEquals($msg, $e->getMessage());
@@ -817,8 +807,8 @@ class SolverTest extends TestCase
         $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0'));
 
         $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $packageA2),
             array('job' => 'install', 'package' => $packageB),
+            array('job' => 'install', 'package' => $packageA2),
             array('job' => 'install', 'package' => $packageA2Alias),
         ));
     }
@@ -921,13 +911,13 @@ class SolverTest extends TestCase
 
     protected function reposComplete()
     {
-        $this->repoSet->addRepository($this->repoInstalled);
         $this->repoSet->addRepository($this->repo);
+        $this->repoSet->addRepository($this->repoLocked);
     }
 
     protected function createSolver()
     {
-        $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), $this->repoInstalled, new NullIO());
+        $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO());
     }
 
     protected function checkSolverResult(array $expected)
@@ -936,7 +926,7 @@ class SolverTest extends TestCase
         $transaction = $this->solver->solve($this->request);
 
         $result = array();
-        foreach ($transaction as $operation) {
+        foreach ($transaction->getOperations() as $operation) {
             if ('update' === $operation->getJobType()) {
                 $result[] = array(
                     'job' => 'update',

+ 7 - 3
tests/Composer/Test/Fixtures/installer/abandoned-listed.test

@@ -22,14 +22,18 @@ Abandoned packages are flagged
     }
 }
 --RUN--
-install
+update
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
+Updating dependencies
+Lock file operations: 2 installs, 0 updates, 0 removals
+  - Locking a/a (1.0.0)
+  - Locking c/c (1.0.0)
+Writing lock file
+Installing dependencies from lock file (including require-dev)
 Package operations: 2 installs, 0 updates, 0 removals
 <warning>Package a/a is abandoned, you should avoid using it. No replacement was suggested.</warning>
 <warning>Package c/c is abandoned, you should avoid using it. Use b/b instead.</warning>
-Writing lock file
 Generating autoload files
 
 --EXPECT--

+ 39 - 0
tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test

@@ -43,6 +43,45 @@ Aliases take precedence over default package even if default is selected
     },
     "minimum-stability": "dev"
 }
+--EXPECT-LOCK--
+{
+    "packages": [
+        {
+            "name": "a/a", "version": "dev-master",
+            "require": { "a/req": "dev-master" },
+            "type": "library"
+        },
+        {
+            "name": "a/b", "version": "dev-master",
+            "require": { "a/req": "dev-master" },
+            "type": "library"
+        },
+        {
+            "name": "a/req", "version": "dev-feature-foo",
+            "source": { "reference": "feat.f", "type": "git", "url": "" },
+            "type": "library"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [
+        {
+            "alias": "dev-master",
+            "alias_normalized": "9999999-dev",
+            "version": "dev-feature-foo",
+            "package": "a/req"
+        }
+    ],
+    "minimum-stability": "dev",
+    "stability-flags": {
+        "a/a": 20,
+        "a/b": 20,
+        "a/req": 20
+    },
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 install
 --EXPECT--

+ 1 - 1
tests/Composer/Test/Fixtures/installer/aliased-priority.test

@@ -51,6 +51,6 @@ install
 Installing a/c (dev-feature-foo feat.f)
 Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f)
 Installing a/b (dev-master forked)
+Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)
 Installing a/a (dev-master master)
 Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master)
-Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)

+ 2 - 2
tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test

@@ -19,10 +19,10 @@ Broken dependencies should not lead to a replacer being installed which is not m
     }
 }
 --RUN--
-install
+update
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
+Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 
   Problem 1

+ 1 - 1
tests/Composer/Test/Fixtures/installer/circular-dependency2.test

@@ -32,5 +32,5 @@ Circular dependencies are possible between packages
 --RUN--
 update -v
 --EXPECT--
-Installing require/itself (1.0.0)
 Installing regular/pkg (1.0.0)
+Installing require/itself (1.0.0)

+ 2 - 2
tests/Composer/Test/Fixtures/installer/github-issues-4319.test

@@ -28,11 +28,11 @@ Present a clear error message when config.platform.php version results in a conf
 }
 
 --RUN--
-install
+update
 
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
+Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 
   Problem 1

+ 25 - 3
tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test

@@ -29,15 +29,37 @@ that are also a root package, when that root package is also explicitly whitelis
     { "name": "a/a", "version": "1.0.0" },
     { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }
 ]
-
+--LOCK--
+{
+    "packages": [
+        {
+            "name": "a/a", "version": "1.0.0"
+        },
+        {
+            "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" }
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update a/a b/b --with-dependencies
 
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
-Package operations: 0 installs, 2 updates, 0 removals
+Updating dependencies
+Lock file operations: 0 installs, 2 updates, 0 removals
+  - Updating a/a (1.0.0) to a/a (1.1.0)
+  - Updating b/b (1.0.0) to b/b (1.1.0)
 Writing lock file
+Installing dependencies from lock file (including require-dev)
+Package operations: 0 installs, 2 updates, 0 removals
 Generating autoload files
 
 --EXPECT--

+ 19 - 2
tests/Composer/Test/Fixtures/installer/github-issues-4795.test

@@ -30,15 +30,32 @@ dependency of one the requirements that is whitelisted for update.
     { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }
 ]
 
+--LOCK--
+{
+    "packages": [
+        { "name": "a/a", "version": "1.0.0" },
+        { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update b/b --with-dependencies
 
 --EXPECT-OUTPUT--
 <warning>Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring.</warning>
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
-Nothing to install or update
+Updating dependencies
+Nothing to modify in lock file
 Writing lock file
+Installing dependencies from lock file (including require-dev)
+Nothing to install, update or remove
 Generating autoload files
 
 --EXPECT--

+ 1 - 1
tests/Composer/Test/Fixtures/installer/install-aliased-alias.test

@@ -32,5 +32,5 @@ install
 --EXPECT--
 Installing b/b (dev-foo)
 Marking b/b (dev-master) as installed, alias of b/b (dev-foo)
-Installing a/a (dev-master)
 Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo)
+Installing a/a (dev-master)

+ 2 - 2
tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test

@@ -21,7 +21,7 @@ Requirements from the composer file are not installed if the lock file is presen
     "packages": [
         { "name": "required", "version": "1.0.0" }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": [],
@@ -31,4 +31,4 @@ Requirements from the composer file are not installed if the lock file is presen
 --RUN--
 install
 --EXPECT--
-Installing required (1.0.0)
+Installing required (1.0.0)

+ 1 - 1
tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test

@@ -25,7 +25,7 @@ Install from a lock file that deleted a package
         { "name": "whitelisted", "version": "1.1.0" },
         { "name": "fixed-dependency", "version": "1.0.0" }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "dev",
     "stability-flags": [],

+ 1 - 1
tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test

@@ -29,7 +29,7 @@ Installing an old alias that doesn't exist anymore from a lock is possible
             "type": "library"
         }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "dev",
     "stability-flags": [],

+ 3 - 2
tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test

@@ -53,7 +53,7 @@ update c/uptodate
     "packages": [
         { "name": "a/old", "version": "1.0.0", "type": "library" },
         { "name": "b/unstable", "version": "1.0.0", "type": "library" },
-        { "name": "c/uptodate", "version": "2.0.0", "type": "library" },
+        { "name": "c/uptodate", "version": "1.0.0", "type": "library" },
         { "name": "d/removed", "version": "1.0.0", "type": "library" }
     ],
     "packages-dev": [],
@@ -66,6 +66,7 @@ update c/uptodate
     "platform-dev": []
 }
 --EXPECT--
-Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0)
 Updating a/old (0.9.0) to a/old (1.0.0)
+Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0)
+Updating c/uptodate (2.0.0) to c/uptodate (1.0.0)
 Installing d/removed (1.0.0)

+ 1 - 1
tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test

@@ -74,8 +74,8 @@ update b/unstable
     "platform-dev": []
 }
 --EXPECT--
-Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0)
 Updating a/old (0.9.0) to a/old (1.0.0)
+Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0)
 Updating c/uptodate (2.0.0) to c/uptodate (1.0.0)
 Installing d/removed (1.0.0)
 Installing e/newreq (1.0.0)

+ 2 - 2
tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test

@@ -98,8 +98,8 @@ update b/b
 }
 --EXPECT--
 Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a)
-Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2)
 Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a)
+Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2)
 Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2)
-Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b)
 Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a)
+Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b)

+ 5 - 19
tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test

@@ -1,5 +1,5 @@
 --TEST--
-Partial update without lock file should update everything whitelisted, remove overly unstable packages
+Partial update without lock file should error
 --COMPOSER--
 {
     "repositories": [
@@ -30,22 +30,8 @@ Partial update without lock file should update everything whitelisted, remove ov
 ]
 --RUN--
 update b/unstable
---EXPECT-LOCK--
-{
-    "packages": [
-        { "name": "a/old", "version": "1.0.0", "type": "library" },
-        { "name": "b/unstable", "version": "1.0.0", "type": "library" },
-        { "name": "c/uptodate", "version": "1.0.0", "type": "library" },
-        { "name": "d/removed", "version": "1.0.0", "type": "library" }
-    ],
-    "packages-dev": [],
-    "aliases": [],
-    "minimum-stability": "stable",
-    "stability-flags": [],
-    "prefer-stable": false,
-    "prefer-lowest": false,
-    "platform": [],
-    "platform-dev": []
-}
+--EXPECT-OUTPUT--
+Cannot update only a partial set of packages without a lock file present.
+--EXPECT-EXIT-CODE--
+1
 --EXPECT--
-Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0)

+ 1 - 1
tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test

@@ -23,7 +23,7 @@ The locked version will not get overwritten by an install
         { "name": "foo/bar", "version": "1.0.0" },
         { "name": "foo/baz", "version": "2.0.0" }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": [],

+ 20 - 5
tests/Composer/Test/Fixtures/installer/solver-problems.test

@@ -30,6 +30,21 @@ Test the error output of solver problems.
     { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }
 ]
 
+--LOCK--
+{
+    "packages": [
+        { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
+
 --RUN--
 update unstable/package requirer/pkg dependency/pkg
 
@@ -38,7 +53,7 @@ update unstable/package requirer/pkg dependency/pkg
 
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
+Updating dependencies
 Your requirements could not be resolved to an installable set of packages.
 
   Problem 1
@@ -46,12 +61,12 @@ Your requirements could not be resolved to an installable set of packages.
   Problem 2
     - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name.
   Problem 3
-    - The requested package stable-requiree-excluded/pkg 1.0.1 exists as stable-requiree-excluded/pkg[1.0.0] but these are rejected by your constraint.
-  Problem 4
-    - The requested package stable-requiree-excluded/pkg (installed at 1.0.0, required as 1.0.1) is satisfiable by stable-requiree-excluded/pkg[1.0.0] but these conflict with your requirements or minimum-stability.
-  Problem 5
     - Installation request for requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0].
     - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found.
+  Problem 4
+    - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested.
+    - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.1, 1.0.0].
+    - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1].
 
 Potential causes:
  - A typo in the package name

+ 7 - 3
tests/Composer/Test/Fixtures/installer/suggest-installed.test

@@ -17,12 +17,16 @@ Suggestions are not displayed for installed packages
     }
 }
 --RUN--
-install
+update
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
-Package operations: 2 installs, 0 updates, 0 removals
+Updating dependencies
+Lock file operations: 2 installs, 0 updates, 0 removals
+  - Locking a/a (1.0.0)
+  - Locking b/b (1.0.0)
 Writing lock file
+Installing dependencies from lock file (including require-dev)
+Package operations: 2 installs, 0 updates, 0 removals
 Generating autoload files
 
 --EXPECT--

+ 5 - 1
tests/Composer/Test/Fixtures/installer/suggest-prod.test

@@ -17,10 +17,14 @@ Suggestions are not displayed in non-dev mode
 --RUN--
 install --no-dev
 --EXPECT-OUTPUT--
+<warning>No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.</warning>
 Loading composer repositories with package information
 Updating dependencies
-Package operations: 1 install, 0 updates, 0 removals
+Lock file operations: 1 install, 0 updates, 0 removals
+  - Locking a/a (1.0.0)
 Writing lock file
+Installing dependencies from lock file
+Package operations: 1 install, 0 updates, 0 removals
 Generating autoload files
 
 --EXPECT--

+ 7 - 3
tests/Composer/Test/Fixtures/installer/suggest-replaced.test

@@ -17,12 +17,16 @@ Suggestions are not displayed for packages if they are replaced
     }
 }
 --RUN--
-install
+update
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
-Package operations: 2 installs, 0 updates, 0 removals
+Updating dependencies
+Lock file operations: 2 installs, 0 updates, 0 removals
+  - Locking c/c (1.0.0)
+  - Locking a/a (1.0.0)
 Writing lock file
+Installing dependencies from lock file (including require-dev)
+Package operations: 2 installs, 0 updates, 0 removals
 Generating autoload files
 
 --EXPECT--

+ 6 - 2
tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test

@@ -17,11 +17,15 @@ Suggestions are displayed
 --RUN--
 install
 --EXPECT-OUTPUT--
+<warning>No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.</warning>
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
+Updating dependencies
+Lock file operations: 1 install, 0 updates, 0 removals
+  - Locking a/a (1.0.0)
+Writing lock file
+Installing dependencies from lock file (including require-dev)
 Package operations: 1 install, 0 updates, 0 removals
 a/a suggests installing b/b (an obscure reason)
-Writing lock file
 Generating autoload files
 
 --EXPECT--

+ 0 - 14
tests/Composer/Test/Fixtures/installer/update-alias-lock.test

@@ -29,20 +29,6 @@ Update aliased package does not mess up the lock file
     },
     "minimum-stability": "dev"
 }
---LOCK--
-{
-    "_": "outdated lock file, should not have to be loaded in an update",
-    "packages": [
-        { "package": "a/a", "version": "dev-master", "source-reference": "1234" },
-        { "package": "a/a", "version": "dev-master", "alias-pretty-version": "1.0.x-dev", "alias-version": "1.0.9999999.9999999-dev" }
-    ],
-    "packages-dev": null,
-    "aliases": [],
-    "minimum-stability": "dev",
-    "stability-flags": [],
-    "prefer-stable": false,
-    "prefer-lowest": false
-}
 --INSTALLED--
 [
     {

+ 59 - 8
tests/Composer/Test/Fixtures/installer/update-changes-url.test

@@ -3,10 +3,10 @@ Update updates URLs for updated packages if they have changed
 
 a/a is dev and gets everything updated as it updates to a new ref
 b/b is a tag and gets everything updated by updating the package URL directly
-c/c is a tag and not whitelisted and gets the new URL but keeps its old ref
+c/c is a tag and not whitelisted and remains unchanged
 d/d is dev but with a #ref so it should get URL updated but not the reference
 e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref
-e/e is dev but not whitelisted and gets the new URL but keeps its old ref
+f/f is dev but not whitelisted and remains unchanged
 g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref
 --COMPOSER--
 {
@@ -98,6 +98,57 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
         "transport-options": { "foo": "bar" }
     }
 ]
+--LOCK--
+{
+    "packages": [
+        {
+            "name": "a/a", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library"
+        },
+        {
+            "name": "b/b", "version": "2.0.3",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library"
+        },
+        {
+            "name": "c/c", "version": "1.0.0",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library"
+        },
+        {
+            "name": "d/d", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library"
+        },
+        {
+            "name": "f/f", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
+            "type": "library",
+            "transport-options": { "foo": "bar" }
+        },
+        {
+            "name": "g/g", "version": "dev-master",
+            "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
+            "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip", "shasum": "oldsum" },
+            "type": "library",
+            "transport-options": { "foo": "bar" }
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --EXPECT-LOCK--
 {
     "packages": [
@@ -115,8 +166,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
         },
         {
             "name": "c/c", "version": "1.0.0",
-            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library"
         },
         {
@@ -133,10 +184,10 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
         },
         {
             "name": "f/f", "version": "dev-master",
-            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" },
-            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/tarball/1111111111111111111111111111111111111111", "type": "tar", "shasum": "newsum" },
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip", "shasum": "oldsum" },
             "type": "library",
-            "transport-options": { "foo": "bar2" }
+            "transport-options": { "foo": "bar" }
         },
         {
             "name": "g/g", "version": "dev-master",
@@ -163,6 +214,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an
 --RUN--
 update a/a b/b d/d g/g
 --EXPECT--
-Installing e/e (dev-master 1111111)
 Updating a/a (dev-master 1111111) to a/a (dev-master 2222222)
+Installing e/e (dev-master 1111111)
 Updating g/g (dev-master 0000000) to g/g (dev-master 1111111)

+ 204 - 0
tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test

@@ -0,0 +1,204 @@
+--TEST--
+Update mirrors updates URLs for all packages if they have changed without updating versions
+
+a/a is dev and gets everything updated as it updates to a new ref
+b/b is a tag and gets everything updated by updating the package URL directly
+c/c is a tag and not whitelisted and gets the new URL but keeps its old ref
+d/d is dev but with a #ref so it should get URL updated but not the reference
+e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref
+e/e is dev but not whitelisted and gets the new URL but keeps its old ref
+g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                {
+                    "name": "a/a", "version": "dev-master",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                },
+                {
+                    "name": "b/b", "version": "2.0.3",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                },
+                {
+                    "name": "c/c", "version": "1.0.0",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/c/newc", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/c/newc/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                },
+                {
+                    "name": "d/d", "version": "dev-master",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/d/newd", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                },
+                {
+                    "name": "e/e", "version": "dev-master",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/e/newe", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                },
+                {
+                    "name": "f/f", "version": "dev-master",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/f/newf", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                },
+                {
+                    "name": "g/g", "version": "dev-master",
+                    "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" },
+                    "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/zipball/2222222222222222222222222222222222222222", "type": "zip" }
+                }
+            ]
+        }
+    ],
+    "require": {
+        "a/a": "dev-master",
+        "b/b": "2.0.3",
+        "c/c": "1.0.0",
+        "d/d": "dev-master#1111111111111111111111111111111111111111",
+        "e/e": "dev-master#1111111111111111111111111111111111111111",
+        "f/f": "dev-master",
+        "g/g": "dev-master#1111111111111111111111111111111111111111"
+    }
+}
+--INSTALLED--
+[
+    {
+        "name": "a/a", "version": "dev-master",
+        "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+    },
+    {
+        "name": "b/b", "version": "2.0.3",
+        "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" },
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+    },
+    {
+        "name": "c/c", "version": "1.0.0",
+        "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+    },
+    {
+        "name": "d/d", "version": "dev-master",
+        "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+    },
+    {
+        "name": "f/f", "version": "dev-master",
+        "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
+        "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }
+    },
+    {
+        "name": "g/g", "version": "dev-master",
+        "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
+        "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" }
+    }
+]
+--LOCK--
+{
+    "packages": [
+        {
+            "name": "a/a", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "b/b", "version": "2.0.3",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "c/c", "version": "1.0.0",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "d/d", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "f/f", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "g/g", "version": "dev-master",
+            "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" },
+            "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" },
+            "type": "library"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
+--EXPECT-LOCK--
+{
+    "packages": [
+        {
+            "name": "a/a", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/newa", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/newa/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "b/b", "version": "2.0.3",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/newb", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/newb/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "c/c", "version": "1.0.0",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "d/d", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/newd", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "f/f", "version": "dev-master",
+            "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" },
+            "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/zipball/1111111111111111111111111111111111111111", "type": "zip" },
+            "type": "library"
+        },
+        {
+            "name": "g/g", "version": "dev-master",
+            "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/newg", "type": "git" },
+            "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/newg/zipball/0000000000000000000000000000000000000000", "type": "zip" },
+            "type": "library"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {
+        "a/a": 20,
+        "d/d": 20,
+        "e/e": 20,
+        "f/f": 20,
+        "g/g": 20
+    },
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
+--RUN--
+update mirrors
+--EXPECT--

+ 1 - 1
tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test

@@ -62,7 +62,7 @@ update --no-dev
 --EXPECT--
 Uninstalling a/b (1.0.0)
 Updating a/a (1.0.0) to a/a (1.0.1)
-Updating dev/pkg (dev-master old) to dev/pkg (dev-master new)
 Installing a/c (1.0.0)
+Updating dev/pkg (dev-master old) to dev/pkg (dev-master new)
 Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new)
 Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old)

+ 10 - 2
tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test

@@ -31,10 +31,18 @@ Converting from one VCS type to another (including an URL change) should update
             "name": "a/a", "version": "1.0.0",
             "source": { "reference": "old-hg-ref", "type": "hg", "url": "old-hg-url" }
         }
-    ]
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
 }
 --RUN--
-update
+update mirrors
 --EXPECT-LOCK--
 {
     "packages": [

+ 67 - 0
tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test

@@ -0,0 +1,67 @@
+--TEST--
+A composer update should remove unused locked dependencies from the lock file and remove unused installed deps from disk
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "a/a", "version": "1.0.0" },
+                { "name": "b/b", "version": "1.0.0" }
+            ]
+        }
+    ],
+    "require": {
+        "a/a": "*"
+    }
+}
+--LOCK--
+{
+    "packages": [
+        { "name": "a/a", "version": "1.0.0" },
+        { "name": "b/b", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
+--INSTALLED--
+[
+    { "name": "a/a", "version": "1.0.0" },
+    { "name": "b/b", "version": "1.0.0" },
+    { "name": "c/c", "version": "1.0.0" }
+]
+--RUN--
+update
+--EXPECT-LOCK--
+{
+    "packages": [
+        { "name": "a/a", "version": "1.0.0", "type": "library" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Updating dependencies
+Lock file operations: 0 installs, 0 updates, 1 removal
+  - Uninstalling b/b (1.0.0)
+Writing lock file
+Installing dependencies from lock file (including require-dev)
+Package operations: 0 installs, 0 updates, 2 removals
+Generating autoload files
+
+--EXPECT--
+Uninstalling c/c (1.0.0)
+Uninstalling b/b (1.0.0)

+ 17 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test

@@ -29,6 +29,23 @@ Update with a package whitelist only updates those packages if they are not pres
     { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } },
     { "name": "fixed-sub-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } },
+        { "name": "fixed-sub-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update whitelisted dependency
 --EXPECT--

+ 19 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test

@@ -38,6 +38,25 @@ Update with a package whitelist pattern and all-dependencies flag updates packag
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted-component1", "version": "1.0.0" },
+        { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update whitelisted-* --with-all-dependencies
 --EXPECT--

+ 18 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test

@@ -41,6 +41,24 @@ Update with a package whitelist only updates those packages and their dependenci
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted-component1", "version": "1.0.0" },
+        { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+        { "name": "root-dependency", "version": "1.0.0" },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": {"a/a":20},
+    "prefer-stable": false,
+    "prefer-lowest": false
+}
 --RUN--
 update whitelisted-* --with-dependencies
 --EXPECT--

+ 22 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test

@@ -47,6 +47,28 @@ Update with a package whitelist only updates those packages and their dependenci
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } },
+        { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+        { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } },
+        { "name": "whitelisted-component4", "version": "1.0.0" },
+        { "name": "whitelisted-component5", "version": "1.0.0" },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update whitelisted-* --with-dependencies
 --EXPECT--

+ 17 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test

@@ -37,6 +37,23 @@ Update with a package whitelist only updates those packages matching the pattern
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted-component1", "version": "1.0.0" },
+        { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": {"a/a":20},
+    "prefer-stable": false,
+    "prefer-lowest": false
+}
 --RUN--
 update whitelisted-*
 --EXPECT--

+ 23 - 2
tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test

@@ -39,10 +39,31 @@ Update with a package whitelist only updates those corresponding to the pattern
     { "name": "another/another", "version": "1.0" },
     { "name": "no/regexp", "version": "1.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "vendor/Test-Package", "version": "1.0" },
+        { "name": "vendor/NotMe", "version": "1.0" },
+        { "name": "exact/Test-Package", "version": "1.0" },
+        { "name": "notexact/TestPackage", "version": "1.0" },
+        { "name": "all/Package1", "version": "1.0" },
+        { "name": "all/Package2", "version": "1.0" },
+        { "name": "another/another", "version": "1.0" },
+        { "name": "no/regexp", "version": "1.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp
 --EXPECT--
-Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0)
-Updating exact/Test-Package (1.0) to exact/Test-Package (2.0)
 Updating all/Package1 (1.0) to all/Package1 (2.0)
 Updating all/Package2 (1.0) to all/Package2 (2.0)
+Updating exact/Test-Package (1.0) to exact/Test-Package (2.0)
+Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0)

+ 2 - 2
tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test

@@ -28,7 +28,7 @@ Limited update takes rules from lock if available, and not from the installed re
         { "name": "toupdate/installed", "version": "1.0.0" },
         { "name": "toupdate/notinstalled", "version": "1.0.0" }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": [],
@@ -43,6 +43,6 @@ Limited update takes rules from lock if available, and not from the installed re
 --RUN--
 update toupdate/installed
 --EXPECT--
-Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0)
 Updating old/installed (0.9.0) to old/installed (1.0.0)
+Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0)
 Installing toupdate/notinstalled (1.0.0)

+ 16 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test

@@ -25,6 +25,22 @@ Update with a package whitelist removes unused packages
     { "name": "fixed-dependency", "version": "1.0.0" },
     { "name": "old-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } },
+        { "name": "fixed-dependency", "version": "1.0.0" },
+        { "name": "old-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update --with-dependencies whitelisted
 --EXPECT--

+ 16 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test

@@ -33,6 +33,22 @@ Update with a package whitelist only updates those packages and their dependenci
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": {"a/a":20},
+    "prefer-stable": false,
+    "prefer-lowest": false
+}
 --RUN--
 update whitelisted --with-dependencies
 --EXPECT--

+ 16 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test

@@ -33,6 +33,22 @@ Update with a package whitelist only updates whitelisted packages if no dependen
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": {"a/a":20},
+    "prefer-stable": false,
+    "prefer-lowest": false
+}
 --RUN--
 update whitelisted
 --EXPECT--

+ 18 - 0
tests/Composer/Test/Fixtures/installer/update-whitelist.test

@@ -33,6 +33,24 @@ Update with a package whitelist only updates those packages listed as command ar
     { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
     { "name": "unrelated-dependency", "version": "1.0.0" }
 ]
+--LOCK--
+{
+    "packages": [
+        { "name": "fixed", "version": "1.0.0" },
+        { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } },
+        { "name": "dependency", "version": "1.0.0" },
+        { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
+        { "name": "unrelated-dependency", "version": "1.0.0" }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update whitelisted
 --EXPECT--

+ 21 - 3
tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test

@@ -28,15 +28,33 @@ When `--with-all-dependencies` is used, Composer\Installer::whitelistUpdateDepen
     { "name": "a/a", "version": "1.0.0" },
     { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }
 ]
-
+--LOCK--
+{
+    "packages": [
+        { "name": "a/a", "version": "1.0.0" },
+        { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}
 --RUN--
 update b/b --with-all-dependencies
 
 --EXPECT-OUTPUT--
 Loading composer repositories with package information
-Updating dependencies (including require-dev)
-Package operations: 0 installs, 2 updates, 0 removals
+Updating dependencies
+Lock file operations: 0 installs, 2 updates, 0 removals
+  - Updating a/a (1.0.0) to a/a (1.1.0)
+  - Updating b/b (1.0.0) to b/b (1.1.0)
 Writing lock file
+Installing dependencies from lock file (including require-dev)
+Package operations: 0 installs, 2 updates, 0 removals
 Generating autoload files
 
 --EXPECT--

+ 1 - 1
tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test

@@ -16,7 +16,7 @@ Installing locked dev packages should remove old dependencies
             "require": {}
         }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "dev",
     "stability-flags": [],

+ 1 - 1
tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test

@@ -28,7 +28,7 @@ Updating a dev package for new reference updates the url and reference
             "dist": { "reference": "oldref", "url": "oldurl", "type": "zip", "shasum": "" }
         }
     ],
-    "packages-dev": null,
+    "packages-dev": [],
     "aliases": [],
     "minimum-stability": "dev",
     "stability-flags": {"a/a":20},

+ 63 - 11
tests/Composer/Test/InstallerTest.php

@@ -16,6 +16,7 @@ use Composer\Installer;
 use Composer\Console\Application;
 use Composer\IO\BufferIO;
 use Composer\Json\JsonFile;
+use Composer\Package\Dumper\ArrayDumper;
 use Composer\Util\Filesystem;
 use Composer\Repository\ArrayRepository;
 use Composer\Repository\RepositoryManager;
@@ -74,10 +75,30 @@ class InstallerTest extends TestCase
         foreach ($repositories as $repository) {
             $repositoryManager->addRepository($repository);
         }
-
-        $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock();
         $installationManager = new InstallationManagerMock();
 
+        // emulate a writable lock file
+        $lockData = null;
+        $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock();
+        $lockJsonMock->expects($this->any())
+            ->method('read')
+            ->will($this->returnCallback(function() use (&$lockData) {
+                return json_decode($lockData, true);
+            }));
+        $lockJsonMock->expects($this->any())
+            ->method('exists')
+            ->will($this->returnCallback(function () use (&$lockData) {
+                return $lockData !== null;
+            }));
+        $lockJsonMock->expects($this->any())
+            ->method('write')
+            ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) {
+                $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT);
+            }));
+
+        $tempLockData = null;
+        $locker = new Locker($io, $lockJsonMock, $installationManager, '{}');
+
         $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
 
         $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator);
@@ -91,7 +112,7 @@ class InstallerTest extends TestCase
         $expectedUninstalled = isset($options['uninstall']) ? $options['uninstall'] : array();
 
         $installed = $installationManager->getInstalledPackages();
-        $this->assertSame($expectedInstalled, $installed);
+        $this->assertEquals($this->makePackagesComparable($expectedInstalled), $this->makePackagesComparable($installed));
 
         $updated = $installationManager->getUpdatedPackages();
         $this->assertSame($expectedUpdated, $updated);
@@ -100,6 +121,17 @@ class InstallerTest extends TestCase
         $this->assertSame($expectedUninstalled, $uninstalled);
     }
 
+    protected function makePackagesComparable($packages)
+    {
+        $dumper = new ArrayDumper();
+
+        $comparable = array();
+        foreach ($packages as $package) {
+            $comparable[] = $dumper->dump($package);
+        }
+        return $comparable;
+    }
+
     public function provideInstaller()
     {
         $cases = array();
@@ -109,11 +141,11 @@ class InstallerTest extends TestCase
 
         $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage');
         $a->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')),
+            'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()),
         ));
         $b = $this->getPackage('B', '1.0.0');
         $b->setRequires(array(
-            new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')),
+            'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()),
         ));
 
         $cases[] = array(
@@ -129,11 +161,11 @@ class InstallerTest extends TestCase
 
         $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage');
         $a->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')),
+            'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()),
         ));
         $b = $this->getPackage('B', '1.0.0');
         $b->setRequires(array(
-            new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')),
+            'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()),
         ));
 
         $cases[] = array(
@@ -144,6 +176,7 @@ class InstallerTest extends TestCase
             ),
         );
 
+        // TODO why are there not more cases with uninstall/update?
         return $cases;
     }
 
@@ -182,13 +215,24 @@ class InstallerTest extends TestCase
         $repositoryManager = $composer->getRepositoryManager();
         $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock));
 
+        // emulate a writable lock file
+        $lockData = $lock ? json_encode($lock, JsonFile::JSON_PRETTY_PRINT): null;
         $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock();
         $lockJsonMock->expects($this->any())
             ->method('read')
-            ->will($this->returnValue($lock));
+            ->will($this->returnCallback(function() use (&$lockData) {
+                return json_decode($lockData, true);
+            }));
         $lockJsonMock->expects($this->any())
             ->method('exists')
-            ->will($this->returnValue(true));
+            ->will($this->returnCallback(function () use (&$lockData) {
+                return $lockData !== null;
+            }));
+        $lockJsonMock->expects($this->any())
+            ->method('write')
+            ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) {
+                $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT);
+            }));
 
         if ($expectLock) {
             $actualLock = array();
@@ -228,11 +272,19 @@ class InstallerTest extends TestCase
         });
 
         $application->get('update')->setCode(function ($input, $output) use ($installer) {
+            $packages = $input->getArgument('packages');
+            $filteredPackages = array_filter($packages, function ($package) {
+                return !in_array($package, array('lock', 'nothing', 'mirrors'), true);
+            });
+            $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages);
+            $packages = $filteredPackages;
+
             $installer
                 ->setDevMode(!$input->getOption('no-dev'))
                 ->setUpdate(true)
                 ->setDryRun($input->getOption('dry-run'))
-                ->setUpdateWhitelist($input->getArgument('packages'))
+                ->setUpdateMirrors($updateMirrors)
+                ->setUpdateWhitelist($packages)
                 ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies'))
                 ->setWhitelistAllDependencies($input->getOption('with-all-dependencies'))
                 ->setPreferStable($input->getOption('prefer-stable'))
@@ -248,7 +300,7 @@ class InstallerTest extends TestCase
 
         $application->setAutoExit(false);
         $appOutput = fopen('php://memory', 'w+');
-        $input = new StringInput($run);
+        $input = new StringInput($run.' -vvv');
         $input->setInteractive(false);
         $result = $application->run($input, new StreamOutput($appOutput));
         fseek($appOutput, 0);

Some files were not shown because too many files changed in this diff