Bläddra i källkod

Reunify lock and local repo transaction code and apply the same sorting

Nils Adermann 5 år sedan
förälder
incheckning
25de5218c3

+ 5 - 270
src/Composer/DependencyResolver/LocalRepoTransaction.php

@@ -24,278 +24,13 @@ use Composer\Semver\Constraint\Constraint;
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
  */
  */
-class LocalRepoTransaction
+class LocalRepoTransaction extends Transaction
 {
 {
-    /** @var array */
-    protected $lockedPackages;
-    protected $lockedPackagesByName = array();
-
-    /** @var RepositoryInterface */
-    protected $localRepository;
-
-    /** @var array */
-    protected $operations;
-
-    /**
-     * Reassigns ids for all packages in the lockedrepository
-     */
     public function __construct(RepositoryInterface $lockedRepository, $localRepository)
     public function __construct(RepositoryInterface $lockedRepository, $localRepository)
     {
     {
-        $this->localRepository = $localRepository;
-        $this->setLockedPackageMaps($lockedRepository);
-        $this->operations = $this->calculateOperations();
-    }
-
-    private function setLockedPackageMaps($lockedRepository)
-    {
-        $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;
-            }
-            return strcmp($b->getName(), $a->getName());
-        };
-
-        $id = 1;
-        $this->lockedPackages = array();
-        foreach ($lockedRepository->getPackages() as $package) {
-            $package->id = $id++;
-            $this->lockedPackages[$package->id] = $package;
-            foreach ($package->getNames() as $name) {
-                $this->lockedPackagesByName[$name][] = $package;
-            }
-        }
-
-        uasort($this->lockedPackages, $packageSort);
-        foreach ($this->lockedPackagesByName as $name => $packages) {
-            uasort($this->lockedPackagesByName[$name], $packageSort);
-        }
-    }
-
-    public function getOperations()
-    {
-        return $this->operations;
-    }
-
-    protected function calculateOperations()
-    {
-        $operations = array();
-
-        $localPackageMap = array();
-        $removeMap = array();
-        $localAliasMap = array();
-        $removeAliasMap = array();
-        foreach ($this->localRepository->getPackages() as $package) {
-            if ($package instanceof AliasPackage) {
-                $localAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
-                $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
-            } else {
-                $localPackageMap[$package->getName()] = $package;
-                $removeMap[$package->getName()] = $package;
-            }
-        }
-
-        $stack = $this->getRootPackages();
-
-        $visited = array();
-        $processed = array();
-
-        while (!empty($stack)) {
-            $package = array_pop($stack);
-
-            if (isset($processed[$package->id])) {
-                continue;
-            }
-
-            if (!isset($visited[$package->id])) {
-                $visited[$package->id] = true;
-
-                $stack[] = $package;
-                if ($package instanceof AliasPackage) {
-                    $stack[] = $package->getAliasOf();
-                } else {
-                    foreach ($package->getRequires() as $link) {
-                        $possibleRequires = $this->getLockedProviders($link);
-
-                        foreach ($possibleRequires as $require) {
-                            $stack[] = $require;
-                        }
-                    }
-                }
-            } elseif (!isset($processed[$package->id])) {
-                $processed[$package->id] = true;
-
-                if ($package instanceof AliasPackage) {
-                    $aliasKey = $package->getName().'::'.$package->getVersion();
-                    if (isset($localAliasMap[$aliasKey])) {
-                        unset($removeAliasMap[$aliasKey]);
-                    } else {
-                        $operations[] = new Operation\MarkAliasInstalledOperation($package);
-                    }
-                } else {
-                    if (isset($localPackageMap[$package->getName()])) {
-                        $source = $localPackageMap[$package->getName()];
-
-                        // do we need to update?
-                        if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) {
-                            $operations[] = new Operation\UpdateOperation($source, $package);
-                        } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) {
-                            $operations[] = new Operation\UpdateOperation($source, $package);
-                        }
-                        unset($removeMap[$package->getName()]);
-                    } else {
-                        $operations[] = new Operation\InstallOperation($package);
-                        unset($removeMap[$package->getName()]);
-                    }
-                }
-            }
-        }
-
-        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 $operations;
-    }
-
-    /**
-     * Determine which packages in the lock file are not required by any other packages in the lock file.
-     *
-     * 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
-     */
-    private function getRootPackages()
-    {
-        $roots = $this->lockedPackages;
-
-        foreach ($this->lockedPackages as $packageId => $package) {
-            if (!isset($roots[$packageId])) {
-                continue;
-            }
-
-            foreach ($package->getRequires() as $link) {
-                $possibleRequires = $this->getLockedProviders($link);
-
-                foreach ($possibleRequires as $require) {
-                    if ($require !== $package) {
-                        unset($roots[$require->id]);
-                    }
-                }
-            }
-        }
-
-        return $roots;
-    }
-
-    private function getLockedProviders(Link $link)
-    {
-        if (!isset($this->lockedPackagesByName[$link->getTarget()])) {
-            return array();
-        }
-        return $this->lockedPackagesByName[$link->getTarget()];
-    }
-
-    /**
-     * 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;
-            }
-
-            // is this package a plugin?
-            $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
-
-            // 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);
-                });
-
-                // 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);
-                }
-
-                unset($operations[$idx]);
-            }
-        }
-
-        return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations);
-    }
-
-    /**
-     * 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)
-    {
-        $uninstOps = array();
-        foreach ($operations as $idx => $op) {
-            if ($op instanceof UninstallOperation) {
-                $uninstOps[] = $op;
-                unset($operations[$idx]);
-            }
-        }
-
-        return array_merge($uninstOps, $operations);
+        parent::__construct(
+            $localRepository->getPackages(),
+            $lockedRepository->getPackages()
+        );
     }
     }
 }
 }

+ 10 - 131
src/Composer/DependencyResolver/LockTransaction.php

@@ -23,12 +23,8 @@ use Composer\Test\Repository\ArrayRepositoryTest;
 /**
 /**
  * @author Nils Adermann <naderman@naderman.de>
  * @author Nils Adermann <naderman@naderman.de>
  */
  */
-class LockTransaction
+class LockTransaction extends Transaction
 {
 {
-    protected $policy;
-    /** @var Pool */
-    protected $pool;
-
     /**
     /**
      * packages in current lock file, platform repo or otherwise present
      * packages in current lock file, platform repo or otherwise present
      * @var array
      * @var array
@@ -40,107 +36,32 @@ class LockTransaction
      * @var array
      * @var array
      */
      */
     protected $unlockableMap;
     protected $unlockableMap;
-    protected $decisions;
-    protected $resultPackages;
 
 
     /**
     /**
      * @var array
      * @var array
      */
      */
-    protected $operations;
+    protected $resultPackages;
 
 
-    public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions)
+    public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions)
     {
     {
-        $this->policy = $policy;
-        $this->pool = $pool;
         $this->presentMap = $presentMap;
         $this->presentMap = $presentMap;
         $this->unlockableMap = $unlockableMap;
         $this->unlockableMap = $unlockableMap;
-        $this->decisions = $decisions;
-
-        $this->operations = $this->calculateOperations();
-    }
-
-    /**
-     * @return OperationInterface[]
-     */
-    public function getOperations()
-    {
-        return $this->operations;
-    }
-
-    protected function calculateOperations()
-    {
-        $operations = array();
-        $ignoreRemove = array();
-        $lockMeansUpdateMap = $this->findPotentialUpdates();
-
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $reason = $decision[Decisions::DECISION_REASON];
-
-            $package = $this->pool->literalToPackage($literal);
-
-            // wanted & !present
-            if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) {
-                if (isset($lockMeansUpdateMap[spl_object_hash($package)]) && !$package instanceof AliasPackage) {
-                    // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata
-                    // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update?
-                    $target = $lockMeansUpdateMap[spl_object_hash($package)];
-                    if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) {
-                        $operations[] = new Operation\UpdateOperation($target, $package, $reason);
-                    }
-
-                    // avoid updates to one package from multiple origins
-                    $ignoreRemove[spl_object_hash($lockMeansUpdateMap[spl_object_hash($package)])] = true;
-                    unset($lockMeansUpdateMap[spl_object_hash($package)]);
-                } else {
-                    if ($package instanceof AliasPackage) {
-                        $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason);
-                    } else {
-                        $operations[] = new Operation\InstallOperation($package, $reason);
-                    }
-                }
-            }
-        }
-
-        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->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[spl_object_hash($package)])) {
-                if ($package instanceof AliasPackage) {
-                    $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason);
-                } else {
-                    $operations[] = new Operation\UninstallOperation($package, $reason);
-                }
-            }
-        }
-
-        foreach ($this->presentMap as $package) {
-            if ($package->id === -1 && !isset($ignoreRemove[spl_object_hash($package)])) {
-                // TODO pass reason parameter to these two operations?
-                if ($package instanceof AliasPackage) {
-                    $operations[] = new Operation\MarkAliasUninstalledOperation($package);
-                } else {
-                    $operations[] = new Operation\UninstallOperation($package);
-                }
-            }
-        }
 
 
-        $this->setResultPackages();
+        $this->setResultPackages($pool, $decisions);
+        parent::__construct($this->presentMap, $this->resultPackages['all']);
 
 
-        return $operations;
     }
     }
 
 
     // TODO make this a bit prettier instead of the two text indexes?
     // TODO make this a bit prettier instead of the two text indexes?
-    public function setResultPackages()
+    public function setResultPackages(Pool $pool, Decisions $decisions)
     {
     {
-        $this->resultPackages = array('non-dev' => array(), 'dev' => array());
-        foreach ($this->decisions as $i => $decision) {
+        $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array());
+        foreach ($decisions as $i => $decision) {
             $literal = $decision[Decisions::DECISION_LITERAL];
             $literal = $decision[Decisions::DECISION_LITERAL];
 
 
             if ($literal > 0) {
             if ($literal > 0) {
-                $package = $this->pool->literalToPackage($literal);
+                $package = $pool->literalToPackage($literal);
+                $this->resultPackages['all'][] = $package;
                 if (!isset($this->unlockableMap[$package->id])) {
                 if (!isset($this->unlockableMap[$package->id])) {
                     $this->resultPackages['non-dev'][] = $package;
                     $this->resultPackages['non-dev'][] = $package;
                 }
                 }
@@ -191,46 +112,4 @@ class LockTransaction
 
 
         return $packages;
         return $packages;
     }
     }
-
-    protected function findPotentialUpdates()
-    {
-        $lockMeansUpdateMap = array();
-
-        $packages = array();
-
-        foreach ($this->decisions as $i => $decision) {
-            $literal = $decision[Decisions::DECISION_LITERAL];
-            $package = $this->pool->literalToPackage($literal);
-
-            if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)])) {
-                $packages[spl_object_hash($package)] = $package;
-            }
-        }
-
-        // some locked packages are not in the pool and thus, were not decided at all
-        foreach ($this->presentMap as $package) {
-            if ($package->id === -1) {
-                $packages[spl_object_hash($package)] = $package;
-            }
-        }
-
-        foreach ($packages as $package) {
-            if ($package instanceof AliasPackage) {
-                continue;
-            }
-
-            // TODO can't we just look at existing rules?
-            $updates = $this->policy->findUpdatePackages($this->pool, $package);
-
-            $updatesAndPackage = array_merge(array($package), $updates);
-
-            foreach ($updatesAndPackage as $update) {
-                if (!isset($lockMeansUpdateMap[spl_object_hash($update)])) {
-                    $lockMeansUpdateMap[spl_object_hash($update)] = $package;
-                }
-            }
-        }
-
-        return $lockMeansUpdateMap;
-    }
 }
 }

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

@@ -221,7 +221,7 @@ class Solver
             throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool);
             throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool);
         }
         }
 
 
-        return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
+        return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
     }
     }
 
 
     /**
     /**

+ 310 - 0
src/Composer/DependencyResolver/Transaction.php

@@ -0,0 +1,310 @@
+<?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>
+ */
+abstract class Transaction
+{
+    /**
+     * @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->presentPackages = $presentPackages;
+        $this->setResultPackageMaps($resultPackages);
+        $this->operations = $this->calculateOperations();
+
+    }
+
+    public function getOperations()
+    {
+        return $this->operations;
+    }
+
+    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;
+            }
+            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;
+            }
+        }
+
+        uasort($this->resultPackageMap, $packageSort);
+        foreach ($this->resultPackagesByName as $name => $packages) {
+            uasort($this->resultPackagesByName[$name], $packageSort);
+        }
+    }
+
+    protected function calculateOperations()
+    {
+        $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);
+
+            if (isset($processed[spl_object_hash($package)])) {
+                continue;
+            }
+
+            if (!isset($visited[spl_object_hash($package)])) {
+                $visited[spl_object_hash($package)] = true;
+
+                $stack[] = $package;
+                if ($package instanceof AliasPackage) {
+                    $stack[] = $package->getAliasOf();
+                } else {
+                    foreach ($package->getRequires() as $link) {
+                        $possibleRequires = $this->getProvidersInResult($link);
+
+                        foreach ($possibleRequires as $require) {
+                            $stack[] = $require;
+                        }
+                    }
+                }
+            } elseif (!isset($processed[spl_object_hash($package)])) {
+                $processed[spl_object_hash($package)] = true;
+
+                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 ($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;
+    }
+
+    /**
+     * 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()
+    {
+        $roots = $this->resultPackageMap;
+
+        foreach ($this->resultPackageMap as $packageHash => $package) {
+            if (!isset($roots[$packageHash])) {
+                continue;
+            }
+
+            foreach ($package->getRequires() as $link) {
+                $possibleRequires = $this->getProvidersInResult($link);
+
+                foreach ($possibleRequires as $require) {
+                    if ($require !== $package) {
+                        unset($roots[spl_object_hash($require)]);
+                    }
+                }
+            }
+        }
+
+        return $roots;
+    }
+
+    protected function getProvidersInResult(Link $link)
+    {
+        if (!isset($this->resultPackagesByName[$link->getTarget()])) {
+            return array();
+        }
+        return $this->resultPackagesByName[$link->getTarget()];
+    }
+
+    /**
+     * 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;
+            }
+
+            // is this package a plugin?
+            $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
+
+            // 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);
+                });
+
+                // 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);
+                }
+
+                unset($operations[$idx]);
+            }
+        }
+
+        return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations);
+    }
+
+    /**
+     * 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)
+    {
+        $uninstOps = array();
+        foreach ($operations as $idx => $op) {
+            if ($op instanceof UninstallOperation) {
+                $uninstOps[] = $op;
+                unset($operations[$idx]);
+            }
+        }
+
+        return array_merge($uninstOps, $operations);
+    }
+}

+ 0 - 17
src/Composer/Installer.php

@@ -486,23 +486,6 @@ class Installer
             if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) {
             if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) {
                 $this->io->writeError('  - ' . $operation);
                 $this->io->writeError('  - ' . $operation);
             }
             }
-
-            // output reasons why the operation was run, only for install/update operations
-            if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) {
-                $reason = $operation->getReason();
-                if ($reason instanceof Rule) {
-                    switch ($reason->getReason()) {
-                        case Rule::RULE_JOB_INSTALL:
-                            $this->io->writeError('    REASON: Required by the root package: '.$reason->getPrettyString($pool));
-                            $this->io->writeError('');
-                            break;
-                        case Rule::RULE_PACKAGE_REQUIRES:
-                            $this->io->writeError('    REASON: '.$reason->getPrettyString($pool));
-                            $this->io->writeError('');
-                            break;
-                    }
-                }
-            }
         }
         }
 
 
         $updatedLock = $this->locker->setLockData(
         $updatedLock = $this->locker->setLockData(

+ 15 - 14
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -168,9 +168,9 @@ class SolverTest extends TestCase
         $this->request->install('C');
         $this->request->install('C');
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
+            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageC),
             array('job' => 'install', 'package' => $packageC),
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'install', 'package' => $packageA),
         ));
         ));
     }
     }
 
 
@@ -338,15 +338,15 @@ class SolverTest extends TestCase
         $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0'));
         $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0'));
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
+            array(
+                'job' => 'remove',
+                'package' => $packageB,
+            ),
             array(
             array(
                 'job' => 'update',
                 'job' => 'update',
                 'from' => $packageA,
                 'from' => $packageA,
                 'to' => $newPackageA,
                 'to' => $newPackageA,
             ),
             ),
-            array(
-                'job' => 'remove',
-                'package' => $packageB,
-            ),
         ));
         ));
     }
     }
 
 
@@ -369,10 +369,10 @@ class SolverTest extends TestCase
         $this->request->remove('D');
         $this->request->remove('D');
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
+            array('job' => 'remove',  'package' => $packageD),
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'update',  'from' => $oldPackageC, 'to' => $packageC),
             array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageA),
-            array('job' => 'remove',  'package' => $packageD),
+            array('job' => 'update',  'from' => $oldPackageC, 'to' => $packageC),
         ));
         ));
     }
     }
 
 
@@ -406,7 +406,8 @@ class SolverTest extends TestCase
         $this->request->install('B');
         $this->request->install('B');
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
-            array('job' => 'update', 'from' => $packageA, 'to' => $packageB),
+            array('job' => 'remove', 'package' => $packageA),
+            array('job' => 'install', 'package' => $packageB),
         ));
         ));
     }
     }
 
 
@@ -526,8 +527,8 @@ class SolverTest extends TestCase
         $this->request->install('X');
         $this->request->install('X');
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $newPackageB),
             array('job' => 'install', 'package' => $newPackageB),
+            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageX),
             array('job' => 'install', 'package' => $packageX),
         ));
         ));
     }
     }
@@ -571,8 +572,8 @@ class SolverTest extends TestCase
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'install', 'package' => $packageC),
             array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageA),
+            array('job' => 'install', 'package' => $packageC),
         ));
         ));
     }
     }
 
 
@@ -829,9 +830,9 @@ class SolverTest extends TestCase
         $this->request->install('B');
         $this->request->install('B');
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
+            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageAAlias),
             array('job' => 'install', 'package' => $packageAAlias),
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'install', 'package' => $packageA),
         ));
         ));
     }
     }
 
 
@@ -894,12 +895,12 @@ class SolverTest extends TestCase
         $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral);
         $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral);
 
 
         $this->checkSolverResult(array(
         $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $packageC2),
-            array('job' => 'install', 'package' => $packageG2),
             array('job' => 'install', 'package' => $packageF1),
             array('job' => 'install', 'package' => $packageF1),
+            array('job' => 'install', 'package' => $packageD),
+            array('job' => 'install', 'package' => $packageG2),
+            array('job' => 'install', 'package' => $packageC2),
             array('job' => 'install', 'package' => $packageE),
             array('job' => 'install', 'package' => $packageE),
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'install', 'package' => $packageD),
             array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageA),
         ));
         ));
 
 

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

@@ -27,8 +27,8 @@ update
 Loading composer repositories with package information
 Loading composer repositories with package information
 Updating dependencies
 Updating dependencies
 Lock file operations: 2 installs, 0 updates, 0 removals
 Lock file operations: 2 installs, 0 updates, 0 removals
-  - Installing c/c (1.0.0)
   - Installing a/a (1.0.0)
   - Installing a/a (1.0.0)
+  - Installing c/c (1.0.0)
 Writing lock file
 Writing lock file
 Installing dependencies from lock file (including require-dev)
 Installing dependencies from lock file (including require-dev)
 Package operations: 2 installs, 0 updates, 0 removals
 Package operations: 2 installs, 0 updates, 0 removals

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

@@ -55,8 +55,8 @@ update a/a b/b --with-dependencies
 Loading composer repositories with package information
 Loading composer repositories with package information
 Updating dependencies
 Updating dependencies
 Lock file operations: 0 installs, 2 updates, 0 removals
 Lock file operations: 0 installs, 2 updates, 0 removals
-  - Updating b/b (1.0.0) to b/b (1.1.0)
   - Updating a/a (1.0.0) to a/a (1.1.0)
   - 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
 Writing lock file
 Installing dependencies from lock file (including require-dev)
 Installing dependencies from lock file (including require-dev)
 Package operations: 0 installs, 2 updates, 0 removals
 Package operations: 0 installs, 2 updates, 0 removals

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

@@ -22,8 +22,8 @@ update
 Loading composer repositories with package information
 Loading composer repositories with package information
 Updating dependencies
 Updating dependencies
 Lock file operations: 2 installs, 0 updates, 0 removals
 Lock file operations: 2 installs, 0 updates, 0 removals
-  - Installing b/b (1.0.0)
   - Installing a/a (1.0.0)
   - Installing a/a (1.0.0)
+  - Installing b/b (1.0.0)
 Writing lock file
 Writing lock file
 Installing dependencies from lock file (including require-dev)
 Installing dependencies from lock file (including require-dev)
 Package operations: 2 installs, 0 updates, 0 removals
 Package operations: 2 installs, 0 updates, 0 removals

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

@@ -50,8 +50,8 @@ update b/b --with-all-dependencies
 Loading composer repositories with package information
 Loading composer repositories with package information
 Updating dependencies
 Updating dependencies
 Lock file operations: 0 installs, 2 updates, 0 removals
 Lock file operations: 0 installs, 2 updates, 0 removals
-  - Updating b/b (1.0.0) to b/b (1.1.0)
   - Updating a/a (1.0.0) to a/a (1.1.0)
   - 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
 Writing lock file
 Installing dependencies from lock file (including require-dev)
 Installing dependencies from lock file (including require-dev)
 Package operations: 0 installs, 2 updates, 0 removals
 Package operations: 0 installs, 2 updates, 0 removals