Browse Source

Merge branch 'refactoring'

Jordi Boggiano 13 năm trước cách đây
mục cha
commit
c6d7760145
44 tập tin đã thay đổi với 2613 bổ sung408 xóa
  1. 33 11
      bin/composer
  2. 1 1
      doc/composer-schema.json
  3. 7 1
      src/Composer/Command/Command.php
  4. 36 116
      src/Composer/Command/InstallCommand.php
  5. 36 87
      src/Composer/Composer.php
  6. 5 3
      src/Composer/Console/Application.php
  7. 58 0
      src/Composer/DependencyResolver/Operation/InstallOperation.php
  8. 37 0
      src/Composer/DependencyResolver/Operation/OperationInterface.php
  9. 45 0
      src/Composer/DependencyResolver/Operation/SolverOperation.php
  10. 58 0
      src/Composer/DependencyResolver/Operation/UninstallOperation.php
  11. 71 0
      src/Composer/DependencyResolver/Operation/UpdateOperation.php
  12. 24 25
      src/Composer/DependencyResolver/Solver.php
  13. 171 0
      src/Composer/Downloader/DownloadManager.php
  14. 30 12
      src/Composer/Downloader/DownloaderInterface.php
  15. 24 18
      src/Composer/Downloader/GitDownloader.php
  16. 1 1
      src/Composer/Downloader/PearDownloader.php
  17. 1 1
      src/Composer/Downloader/ZipDownloader.php
  18. 131 0
      src/Composer/Installer/InstallationManager.php
  19. 34 10
      src/Composer/Installer/InstallerInterface.php
  20. 97 12
      src/Composer/Installer/LibraryInstaller.php
  21. 6 17
      src/Composer/Package/BasePackage.php
  22. 58 0
      src/Composer/Package/Dumper/ArrayDumper.php
  23. 114 0
      src/Composer/Package/Loader/ArrayLoader.php
  24. 60 0
      src/Composer/Package/Loader/JsonLoader.php
  25. 17 0
      src/Composer/Package/MemoryPackage.php
  26. 21 0
      src/Composer/Package/PackageInterface.php
  27. 91 0
      src/Composer/Package/PackageLock.php
  28. 43 0
      src/Composer/Package/Version/VersionParser.php
  29. 38 0
      src/Composer/Repository/ArrayRepository.php
  30. 11 56
      src/Composer/Repository/ComposerRepository.php
  31. 82 0
      src/Composer/Repository/FilesystemRepository.php
  32. 5 3
      src/Composer/Repository/GitRepository.php
  33. 5 4
      src/Composer/Repository/PearRepository.php
  34. 7 6
      src/Composer/Repository/PlatformRepository.php
  35. 19 0
      src/Composer/Repository/RepositoryInterface.php
  36. 83 0
      src/Composer/Repository/RepositoryManager.php
  37. 64 0
      src/Composer/Repository/WrapperRepository.php
  38. 42 0
      src/Composer/Repository/WritableRepositoryInterface.php
  39. 17 23
      tests/Composer/Test/DependencyResolver/SolverTest.php
  40. 499 0
      tests/Composer/Test/Downloader/DownloadManagerTest.php
  41. 180 0
      tests/Composer/Test/Installer/InstallationManagerTest.php
  42. 169 0
      tests/Composer/Test/Installer/LibraryInstallerTest.php
  43. 27 1
      tests/Composer/Test/Repository/ArrayRepositoryTest.php
  44. 55 0
      tests/Composer/Test/Repository/FilesystemRepositoryTest.php

+ 33 - 11
bin/composer

@@ -4,21 +4,43 @@
 require __DIR__.'/../tests/bootstrap.php';
 
 use Composer\Composer;
-use Composer\Downloader\GitDownloader;
-use Composer\Downloader\PearDownloader;
-use Composer\Downloader\ZipDownloader;
-use Composer\Installer\LibraryInstaller;
-use Composer\Console\Application;
+use Composer\Installer;
+use Composer\Downloader;
+use Composer\Repository;
+use Composer\Package;
+use Composer\Console\Application as ComposerApplication;
 
-setlocale(LC_ALL, 'C');
+// initialize repository manager
+$rm = new Repository\RepositoryManager();
+$localRepository = new Repository\WrapperRepository(array(
+    new Repository\ArrayRepository('.composer/installed.json'),
+    new Repository\PlatformRepository(),
+));
+$rm->setLocalRepository($localRepository);
+$rm->setRepository('Packagist', new Repository\ComposerRepository('http://packagist.org'));
+
+// initialize download manager
+$dm = new Downloader\DownloadManager($preferSource = false);
+$dm->setDownloader('git',  new Downloader\GitDownloader());
+//$dm->setDownloader('pear', new Downloader\PearDownloader());
+//$dm->setDownloader('zip',  new Downloader\ZipDownloader());
+
+// initialize installation manager
+$im = new Installer\InstallationManager();
+$im->setInstaller('library', new Installer\LibraryInstaller('vendor', $dm, $rm->getLocalRepository()));
+
+// load package
+$loader  = new Package\Loader\JsonLoader();
+$package = $loader->load('composer.json');
 
 // initialize composer
 $composer = new Composer();
-$composer->addDownloader('git', new GitDownloader);
-$composer->addDownloader('pear', new PearDownloader);
-$composer->addDownloader('zip', new ZipDownloader);
-$composer->addInstaller('library', new LibraryInstaller);
+$composer->setPackage($package);
+$composer->setPackageLock(new Package\PackageLock('composer.lock'));
+$composer->setRepositoryManager($rm);
+$composer->setDownloadManager($dm);
+$composer->setInstallationManager($im);
 
 // run the command application
-$application = new Application($composer);
+$application = new ComposerApplication($composer);
 $application->run();

+ 1 - 1
doc/composer-schema.json

@@ -9,7 +9,7 @@
             "required": true
         },
         "type": {
-            "description": "Package type, either 'Library', or the parent project it applies to if it's a plugin for a framework or application (e.g. 'Symfony2', 'Typo3', 'Drupal', ..).",
+            "description": "Package type, either 'Library', or the parent project it applies to if it's a plugin for a framework or application (e.g. 'Symfony2', 'Typo3', 'Drupal', ..), note that this has to be defined and communicated by any project implementing a custom composer installer, those are just unreliable examples.",
             "type": "string",
             "optional": true
         },

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

@@ -13,11 +13,17 @@
 namespace Composer\Command;
 
 use Symfony\Component\Console\Command\Command as BaseCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Composer\DependencyResolver\Request;
+use Composer\DependencyResolver\Solver;
+use Composer\Installer\Operation;
 
 /**
  * Base class for Composer commands
  *
  * @author Ryan Weaver <ryan@knplabs.com>
+ * @authro Konstantin Kudryashov <ever.zet@gmail.com>
  */
 abstract class Command extends BaseCommand
 {
@@ -28,4 +34,4 @@ abstract class Command extends BaseCommand
     {
         return $this->getApplication()->getComposer();
     }
-}
+}

+ 36 - 116
src/Composer/Command/InstallCommand.php

@@ -12,20 +12,17 @@
 
 namespace Composer\Command;
 
+use Composer\DependencyResolver;
 use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
-use Composer\DependencyResolver\DefaultPolicy;
-use Composer\DependencyResolver\Solver;
-use Composer\Repository\PlatformRepository;
-use Composer\Package\MemoryPackage;
-use Composer\Package\LinkConstraint\VersionConstraint;
+use Composer\DependencyResolver\Operation;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
-
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Ryan Weaver <ryan@knplabs.com>
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
  */
 class InstallCommand extends Command
 {
@@ -48,130 +45,53 @@ EOT
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        // TODO this needs a parameter to enable installing from source (i.e. git clone, instead of downloading archives)
-        $sourceInstall = false;
-
-        $config = $this->loadConfig();
+        $composer = $this->getComposer();
 
-        $output->writeln('<info>Loading repositories</info>');
+        if ($composer->getPackageLock()->isLocked()) {
+            $output->writeln('<info>Found lockfile. Reading</info>');
 
-        if (isset($config['repositories'])) {
-            foreach ($config['repositories'] as $name => $spec) {
-                $this->getComposer()->addRepository($name, $spec);
+            $installationManager = $composer->getInstallationManager();
+            foreach ($composer->getPackageLock()->getLockedPackages() as $package) {
+                if (!$installationManager->isPackageInstalled($package)) {
+                    $operation = new Operation\InstallOperation($package, 'lock resolving');
+                    $installationManager->execute($operation);
+                }
             }
+
+            return 0;
         }
 
+        // creating repository pool
         $pool = new Pool;
-
-        $repoInstalled = new PlatformRepository;
-        $pool->addRepository($repoInstalled);
-
-        // TODO check the lock file to see what's currently installed
-        // $repoInstalled->addPackage(new MemoryPackage('$Package', '$Version'));
-
-        $output->writeln('Loading package list');
-
-        foreach ($this->getComposer()->getRepositories() as $repository) {
+        $pool->addRepository($composer->getRepositoryManager()->getLocalRepository());
+        foreach ($composer->getRepositoryManager()->getRepositories() as $repository) {
             $pool->addRepository($repository);
         }
 
+        // creating requirements request
         $request = new Request($pool);
-
-        $output->writeln('Building up request');
-
-        // TODO there should be an update flag or dedicated update command
-        // TODO check lock file to remove packages that disappeared from the requirements
-        foreach ($config['require'] as $name => $version) {
-            if ('latest' === $version) {
-                $request->install($name);
-            } else {
-                preg_match('#^([>=<~]*)([\d.]+.*)$#', $version, $match);
-                if (!$match[1]) {
-                    $match[1] = '=';
-                }
-                $constraint = new VersionConstraint($match[1], $match[2]);
-                $request->install($name, $constraint);
-            }
-        }
-
-        $output->writeln('Solving dependencies');
-
-        $policy = new DefaultPolicy;
-        $solver = new Solver($policy, $pool, $repoInstalled);
-        $transaction = $solver->solve($request);
-
-        $lock = array();
-
-        foreach ($transaction as $task) {
-            switch ($task['job']) {
-            case 'install':
-                $package = $task['package'];
-                $output->writeln('> Installing '.$package->getPrettyName());
-                if ($sourceInstall) {
-                    // TODO
-                } else {
-                    if ($package->getDistType()) {
-                        $downloaderType = $package->getDistType();
-                        $type = 'dist';
-                    } elseif ($package->getSourceType()) {
-                        $output->writeln('Package '.$package->getPrettyName().' has no dist url, installing from source instead.');
-                        $downloaderType = $package->getSourceType();
-                        $type = 'source';
-                    } else {
-                        throw new \UnexpectedValueException('Package '.$package->getPrettyName().' has no source or dist URL.');
-                    }
-                    $downloader = $this->getComposer()->getDownloader($downloaderType);
-                    $installer = $this->getComposer()->getInstaller($package->getType());
-                    if (!$installer->install($package, $downloader, $type)) {
-                        throw new \LogicException($package->getPrettyName().' could not be installed.');
-                    }
-                }
-                $lock[$package->getName()] = array('version' => $package->getVersion());
-                break;
-            default:
-                throw new \UnexpectedValueException('Unhandled job type : '.$task['job']);
-            }
+        foreach ($composer->getPackage()->getRequires() as $link) {
+            $request->install($link->getTarget(), $link->getConstraint());
         }
-        $output->writeln('> Done');
 
-        $this->storeLockFile($lock, $output);
-    }
+        // prepare solver
+        $installationManager = $composer->getInstallationManager();
+        $localRepo           = $composer->getRepositoryManager()->getLocalRepository();
+        $policy              = new DependencyResolver\DefaultPolicy();
+        $solver              = new DependencyResolver\Solver($policy, $pool, $localRepo);
 
-    protected function loadConfig()
-    {
-        if (!file_exists('composer.json')) {
-            throw new \UnexpectedValueException('composer.json config file not found in '.getcwd());
+        // solve dependencies and execute operations
+        foreach ($solver->solve($request) as $operation) {
+            $installationManager->execute($operation);
         }
-        $config = json_decode(file_get_contents('composer.json'), true);
-        if (!$config) {
-            switch (json_last_error()) {
-            case JSON_ERROR_NONE:
-                $msg = 'No error has occurred, is your composer.json file empty?';
-                break;
-            case JSON_ERROR_DEPTH:
-                $msg = 'The maximum stack depth has been exceeded';
-                break;
-            case JSON_ERROR_STATE_MISMATCH:
-                $msg = 'Invalid or malformed JSON';
-                break;
-            case JSON_ERROR_CTRL_CHAR:
-                $msg = 'Control character error, possibly incorrectly encoded';
-                break;
-            case JSON_ERROR_SYNTAX:
-                $msg = 'Syntax error';
-                break;
-            case JSON_ERROR_UTF8:
-                $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
-                break;
-            }
-            throw new \UnexpectedValueException('Incorrect composer.json file: '.$msg);
+
+        if (false) {
+            $composer->getPackageLock()->lock($localRepo->getPackages());
+            $output->writeln('> Locked');
         }
-        return $config;
-    }
 
-    protected function storeLockFile(array $content, OutputInterface $output)
-    {
-        file_put_contents('composer.lock', json_encode($content, JSON_FORCE_OBJECT)."\n");
-        $output->writeln('> composer.lock dumped');
+        $localRepo->write();
+
+        $output->writeln('> Done');
     }
-}
+}

+ 36 - 87
src/Composer/Composer.php

@@ -12,125 +12,74 @@
 
 namespace Composer;
 
-use Composer\Downloader\DownloaderInterface;
-use Composer\Installer\InstallerInterface;
-use Composer\Repository\ComposerRepository;
-use Composer\Repository\PlatformRepository;
-use Composer\Repository\GitRepository;
-use Composer\Repository\PearRepository;
+use Composer\Package\PackageInterface;
+use Composer\Package\PackageLock;
+use Composer\Repository\RepositoryManager;
+use Composer\Installer\InstallationManager;
+use Composer\Downloader\DownloadManager;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Konstantin Kudryashiv <ever.zet@gmail.com>
  */
 class Composer
 {
     const VERSION = '1.0.0-DEV';
 
-    protected $repositories = array();
-    protected $downloaders = array();
-    protected $installers = array();
+    private $package;
+    private $lock;
 
-    public function __construct()
+    private $rm;
+    private $dm;
+    private $im;
+
+    public function setPackage(PackageInterface $package)
     {
-        $this->addRepository('Packagist', array('composer' => 'http://packagist.org'));
+        $this->package = $package;
     }
 
-    /**
-     * Add downloader for type
-     *
-     * @param string              $type
-     * @param DownloaderInterface $downloader
-     */
-    public function addDownloader($type, DownloaderInterface $downloader)
+    public function getPackage()
     {
-        $type = strtolower($type);
-        $this->downloaders[$type] = $downloader;
+        return $this->package;
     }
 
-    /**
-     * Get type downloader
-     *
-     * @param string $type
-     *
-     * @return DownloaderInterface
-     */
-    public function getDownloader($type)
+    public function setPackageLock($lock)
     {
-        $type = strtolower($type);
-        if (!isset($this->downloaders[$type])) {
-            throw new \UnexpectedValueException('Unknown source type: '.$type);
-        }
-        return $this->downloaders[$type];
+        $this->lock = $lock;
     }
 
-    /**
-     * Add installer for type
-     *
-     * @param  string            $type
-     * @param InstallerInterface $installer
-     */
-    public function addInstaller($type, InstallerInterface $installer)
+    public function getPackageLock()
     {
-        $type = strtolower($type);
-        $this->installers[$type] = $installer;
+        return $this->lock;
     }
 
-    /**
-     * Get type installer
-     *
-     * @param string $type
-     *
-     * @return InstallerInterface
-     */
-    public function getInstaller($type)
+    public function setRepositoryManager(RepositoryManager $manager)
     {
-        $type = strtolower($type);
-        if (!isset($this->installers[$type])) {
-            throw new \UnexpectedValueException('Unknown dependency type: '.$type);
-        }
-        return $this->installers[$type];
+        $this->rm = $manager;
     }
 
-    public function addRepository($name, $spec)
+    public function getRepositoryManager()
     {
-        if (null === $spec) {
-            unset($this->repositories[$name]);
-        }
-        if (is_array($spec) && count($spec) === 1) {
-            return $this->repositories[$name] = $this->createRepository($name, key($spec), current($spec));
-        }
-        throw new \UnexpectedValueException('Invalid repositories specification '.json_encode($spec).', should be: {"type": "url"}');
+        return $this->rm;
     }
 
-    public function getRepositories()
+    public function setDownloadManager(DownloadManager $manager)
     {
-        return $this->repositories;
+        $this->dm = $manager;
     }
 
-    public function createRepository($name, $type, $spec)
+    public function getDownloadManager()
     {
-        if (is_string($spec)) {
-            $spec = array('url' => $spec);
-        }
-        $spec['url'] = rtrim($spec['url'], '/');
-
-        switch ($type) {
-        case 'git-bare':
-        case 'git-multi':
-            throw new \Exception($type.' repositories not supported yet');
-            break;
-
-        case 'git':
-            return new GitRepository($spec['url']);
-
-        case 'composer':
-            return new ComposerRepository($spec['url']);
+        return $this->dm;
+    }
 
-        case 'pear':
-            return new PearRepository($spec['url'], $name);
+    public function setInstallationManager(InstallationManager $manager)
+    {
+        $this->im = $manager;
+    }
 
-        default:
-            throw new \UnexpectedValueException('Unknown repository type: '.$type.', could not create repository '.$name);
-        }
+    public function getInstallationManager()
+    {
+        return $this->im;
     }
 }

+ 5 - 3
src/Composer/Console/Application.php

@@ -13,11 +13,13 @@
 namespace Composer\Console;
 
 use Symfony\Component\Console\Application as BaseApplication;
-use Composer\Composer;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Finder\Finder;
 use Composer\Command\InstallCommand;
+use Composer\Composer;
+use Composer\Package\PackageInterface;
+use Composer\Package\PackageLock;
 
 /**
  * The console application that handles the commands
@@ -59,10 +61,10 @@ class Application extends BaseApplication
     }
 
     /**
-     * Initializes all the composer commands
+     * Looks for all *Command files in Composer's Command directory
      */
     protected function registerCommands()
     {
         $this->add(new InstallCommand());
     }
-}
+}

+ 58 - 0
src/Composer/DependencyResolver/Operation/InstallOperation.php

@@ -0,0 +1,58 @@
+<?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\Operation;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * Solver install operation.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class InstallOperation extends SolverOperation
+{
+    protected $package;
+
+    /**
+     * Initializes operation.
+     *
+     * @param   PackageInterface    $package    package instance
+     * @param   string              $reason     operation reason
+     */
+    public function __construct(PackageInterface $package, $reason = null)
+    {
+        parent::__construct($reason);
+
+        $this->package = $package;
+    }
+
+    /**
+     * Returns package instance.
+     *
+     * @return  PackageInterface
+     */
+    public function getPackage()
+    {
+        return $this->package;
+    }
+
+    /**
+     * Returns job type.
+     *
+     * @return  string
+     */
+    public function getJobType()
+    {
+        return 'install';
+    }
+}

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

@@ -0,0 +1,37 @@
+<?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\Operation;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * Solver operation interface.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+interface OperationInterface
+{
+    /**
+     * Returns job type.
+     *
+     * @return  string
+     */
+    function getJobType();
+
+    /**
+     * Returns operation reason.
+     *
+     * @return  string
+     */
+    function getReason();
+}

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

@@ -0,0 +1,45 @@
+<?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\Operation;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * Abstract solver operation class.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+abstract class SolverOperation implements OperationInterface
+{
+    protected $reason;
+
+    /**
+     * Initializes operation.
+     *
+     * @param   string  $reason     operation reason
+     */
+    public function __construct($reason = null)
+    {
+        $this->reason = $reason;
+    }
+
+    /**
+     * Returns operation reason.
+     *
+     * @return  string
+     */
+    public function getReason()
+    {
+        return $this->reason;
+    }
+}

+ 58 - 0
src/Composer/DependencyResolver/Operation/UninstallOperation.php

@@ -0,0 +1,58 @@
+<?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\Operation;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * Solver uninstall operation.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class UninstallOperation extends SolverOperation
+{
+    protected $package;
+
+    /**
+     * Initializes operation.
+     *
+     * @param   PackageInterface    $package    package instance
+     * @param   string              $reason     operation reason
+     */
+    public function __construct(PackageInterface $package, $reason = null)
+    {
+        parent::__construct($reason);
+
+        $this->package = $package;
+    }
+
+    /**
+     * Returns package instance.
+     *
+     * @return  PackageInterface
+     */
+    public function getPackage()
+    {
+        return $this->package;
+    }
+
+    /**
+     * Returns job type.
+     *
+     * @return  string
+     */
+    public function getJobType()
+    {
+        return 'uninstall';
+    }
+}

+ 71 - 0
src/Composer/DependencyResolver/Operation/UpdateOperation.php

@@ -0,0 +1,71 @@
+<?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\Operation;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * Solver update operation.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class UpdateOperation extends SolverOperation
+{
+    protected $initialPackage;
+    protected $targetPackage;
+
+    /**
+     * Initializes update operation.
+     *
+     * @param   PackageInterface    $initial    initial package
+     * @param   PackageInterface    $target     target package (updated)
+     * @param   string              $reason     update reason
+     */
+    public function __construct(PackageInterface $initial, PackageInterface $target, $reason = null)
+    {
+        parent::__construct($reason);
+
+        $this->initialPackage = $initial;
+        $this->targetPackage  = $target;
+    }
+
+    /**
+     * Returns initial package.
+     *
+     * @return  PackageInterface
+     */
+    public function getInitialPackage()
+    {
+        return $this->initialPackage;
+    }
+
+    /**
+     * Returns target package.
+     *
+     * @return  PackageInterface
+     */
+    public function getTargetPackage()
+    {
+        return $this->targetPackage;
+    }
+
+    /**
+     * Returns job type.
+     *
+     * @return  string
+     */
+    public function getJobType()
+    {
+        return 'update';
+    }
+}

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

@@ -14,6 +14,7 @@ namespace Composer\DependencyResolver;
 
 use Composer\Repository\RepositoryInterface;
 use Composer\Package\PackageInterface;
+use Composer\DependencyResolver\Operation;
 
 /**
  * @author Nils Adermann <naderman@naderman.de>
@@ -49,6 +50,7 @@ class Solver
     protected $watches = array();
     protected $removeWatches = array();
     protected $decisionMap;
+    protected $installedPackageMap;
 
     protected $packageToUpdateRule = array();
     protected $packageToFeatureRule = array();
@@ -251,7 +253,7 @@ class Solver
             $this->addedMap[$package->getId()] = true;
 
             $dontFix = 0;
-            if ($this->installed === $package->getRepository() && !isset($this->fixMap[$package->getId()])) {
+            if (isset($this->installedPackageMap[$package->getId()]) && !isset($this->fixMap[$package->getId()])) {
                 $dontFix = 1;
             }
 
@@ -270,7 +272,7 @@ class Solver
                 if ($dontFix) {
                     $foundInstalled = false;
                     foreach ($possibleRequires as $require) {
-                        if ($this->installed === $require->getRepository()) {
+                        if (isset($this->installedPackageMap[$require->getId()])) {
                             $foundInstalled = true;
                             break;
                         }
@@ -293,7 +295,7 @@ class Solver
                 $possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
 
                 foreach ($possibleConflicts as $conflict) {
-                    if ($dontFix && $this->installed === $conflict->getRepository()) {
+                    if ($dontFix && isset($this->installedPackageMap[$conflict->getId()])) {
                         continue;
                     }
 
@@ -308,7 +310,7 @@ class Solver
             /** @TODO: if ($this->noInstalledObsoletes) */
             if (true) {
                 $noObsoletes = isset($this->noObsoletes[$package->getId()]);
-                $isInstalled = ($this->installed === $package->getRepository());
+                $isInstalled = (isset($this->installedPackageMap[$package->getId()]));
 
                 foreach ($package->getReplaces() as $link) {
                     $obsoleteProviders = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
@@ -757,7 +759,7 @@ class Solver
         switch ($job['cmd']) {
             case 'install':
                 foreach ($job['packages'] as $package) {
-                    if ($this->installed === $package->getRepository()) {
+                    if (isset($this->installedPackageMap[$package->getId()])) {
                         $disableQueue[] = array('type' => 'update', 'package' => $package);
                     }
 
@@ -870,7 +872,7 @@ class Solver
 
             case 'remove':
                 foreach ($job['packages'] as $package) {
-                    if ($this->installed === $package->getRepository()) {
+                    if (isset($this->installedPackageMap[$package->getId()])) {
                         $disableQueue[] = array('type' => 'update', 'package' => $package);
                     }
                 }
@@ -932,6 +934,10 @@ class Solver
     {
         $this->jobs = $request->getJobs();
         $installedPackages = $this->installed->getPackages();
+        $this->installedPackageMap = array();
+        foreach ($installedPackages as $package) {
+            $this->installedPackageMap[$package->getId()] = $package;
+        }
 
         $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1);
 
@@ -953,12 +959,12 @@ class Solver
             foreach ($job['packages'] as $package) {
                 switch ($job['cmd']) {
                     case 'fix':
-                        if ($this->installed === $package->getRepository()) {
+                        if (isset($this->installedPackageMap[$package->getId()])) {
                             $this->fixMap[$package->getId()] = true;
                         }
                         break;
                     case 'update':
-                        if ($this->installed === $package->getRepository()) {
+                        if (isset($this->installedPackageMap[$package->getId()])) {
                             $this->updateMap[$package->getId()] = true;
                         }
                         break;
@@ -1038,7 +1044,7 @@ class Solver
                     break;
                 case 'lock':
                     foreach ($job['packages'] as $package) {
-                        if ($this->installed === $package->getRepository()) {
+                        if (isset($this->installedPackageMap[$package->getId()])) {
                             $rule = $this->createInstallRule($package, self::RULE_JOB_LOCK);
                         } else {
                             $rule = $this->createRemoveRule($package, self::RULE_JOB_LOCK);
@@ -1082,7 +1088,7 @@ class Solver
             $package = $literal->getPackage();
 
             // !wanted & installed
-            if (!$literal->isWanted() && $this->installed === $package->getRepository()) {
+            if (!$literal->isWanted() && isset($this->installedPackageMap[$package->getId()])) {
                 $updateRule = $this->packageToUpdateRule[$package->getId()];
 
                 foreach ($updateRule->getLiterals() as $updateLiteral) {
@@ -1097,7 +1103,7 @@ class Solver
             $package = $literal->getPackage();
 
             // wanted & installed || !wanted & !installed
-            if ($literal->isWanted() == ($this->installed === $package->getRepository())) {
+            if ($literal->isWanted() == (isset($this->installedPackageMap[$package->getId()]))) {
                 continue;
             }
 
@@ -1105,28 +1111,21 @@ class Solver
                 if (isset($installMeansUpdateMap[$literal->getPackageId()])) {
                     $source = $installMeansUpdateMap[$literal->getPackageId()];
 
-                    $transaction[] = array(
-                        'job' => 'update',
-                        'from' => $source,
-                        'to' => $package,
-                        'why' => $this->decisionQueueWhy[$i],
+                    $transaction[] = new Operation\UpdateOperation(
+                        $source, $package, $this->decisionQueueWhy[$i]
                     );
 
                     // avoid updates to one package from multiple origins
                     unset($installMeansUpdateMap[$literal->getPackageId()]);
                     $ignoreRemove[$source->getId()] = true;
                 } else {
-                    $transaction[] = array(
-                        'job' => 'install',
-                        'package' => $package,
-                        'why' => $this->decisionQueueWhy[$i],
+                    $transaction[] = new Operation\InstallOperation(
+                        $package, $this->decisionQueueWhy[$i]
                     );
                 }
             } else if (!isset($ignoreRemove[$package->getId()])) {
-                $transaction[] = array(
-                    'job' => 'remove',
-                    'package' => $package,
-                    'why' => $this->decisionQueueWhy[$i],
+                $transaction[] = new Operation\UninstallOperation(
+                    $package, $this->decisionQueueWhy[$i]
                 );
             }
         }
@@ -2060,4 +2059,4 @@ class Solver
         }
         echo "\n";
     }
-}
+}

+ 171 - 0
src/Composer/Downloader/DownloadManager.php

@@ -0,0 +1,171 @@
+<?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\Downloader;
+
+use Composer\Package\PackageInterface;
+use Composer\Downloader\DownloaderInterface;
+
+/**
+ * Downloaders manager.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class DownloadManager
+{
+    private $preferSource = false;
+    private $downloaders  = array();
+
+    /**
+     * Initializes download manager.
+     *
+     * @param   Boolean $preferSource   prefer downloading from source
+     */
+    public function __construct($preferSource = false)
+    {
+        $this->preferSource = $preferSource;
+    }
+
+    /**
+     * Makes downloader prefer source installation over the dist.
+     *
+     * @param   Boolean $preferSource   prefer downloading from source
+     */
+    public function preferSource($preferSource = true)
+    {
+        $this->preferSource = $preferSource;
+    }
+
+    /**
+     * Sets installer downloader for a specific installation type.
+     *
+     * @param   string              $type       installation type
+     * @param   DownloaderInterface $downloader downloader instance
+     */
+    public function setDownloader($type, DownloaderInterface $downloader)
+    {
+        $this->downloaders[$type] = $downloader;
+    }
+
+    /**
+     * Returns downloader for a specific installation type.
+     *
+     * @param   string  $type   installation type
+     *
+     * @return  DownloaderInterface
+     *
+     * @throws  UnexpectedValueException    if downloader for provided type is not registeterd
+     */
+    public function getDownloader($type)
+    {
+        if (!isset($this->downloaders[$type])) {
+            throw new \UnexpectedValueException('Unknown source type: '.$type);
+        }
+
+        return $this->downloaders[$type];
+    }
+
+    /**
+     * Downloads package into target dir.
+     *
+     * @param   PackageInterface    $package        package instance
+     * @param   string              $targetDir      target dir
+     * @param   Boolean             $preferSource   prefer installation from source
+     *
+     * @return  string                              downloader type (source/dist)
+     *
+     * @throws  InvalidArgumentException            if package have no urls to download from
+     */
+    public function download(PackageInterface $package, $targetDir, $preferSource = null)
+    {
+        $preferSource = null !== $preferSource ? $preferSource : $this->preferSource;
+        $sourceType   = $package->getSourceType();
+        $distType     = $package->getDistType();
+
+        if (!($preferSource && $sourceType) && $distType) {
+            $downloader = $this->getDownloader($distType);
+            $downloader->download(
+                $package, $targetDir,
+                $package->getDistUrl(), $package->getDistSha1Checksum(),
+                $preferSource
+            );
+            $package->setInstallationSource('dist');
+        } elseif ($sourceType) {
+            $downloader = $this->getDownloader($sourceType);
+            $downloader->download($package, $targetDir, $package->getSourceUrl(), $preferSource);
+            $package->setInstallationSource('source');
+        } else {
+            throw new \InvalidArgumentException('Package should have dist or source specified');
+        }
+    }
+
+    /**
+     * Updates package from initial to target version.
+     *
+     * @param   PackageInterface    $initial    initial package version
+     * @param   PackageInterface    $target     target package version
+     * @param   string              $targetDir  target dir
+     *
+     * @throws  InvalidArgumentException        if initial package is not installed
+     */
+    public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
+    {
+        if (null === $installationType = $initial->getInstallationSource()) {
+            throw new \InvalidArgumentException(
+                'Package '.$initial.' was not been installed propertly and can not be updated'
+            );
+        }
+        $useSource = 'source' === $installationType;
+
+        if (!$useSource) {
+            $initialType = $initial->getDistType();
+            $targetType  = $target->getDistType();
+        } else {
+            $initialType = $initial->getSourceType();
+            $targetType  = $target->getSourceType();
+        }
+
+        $downloader = $this->getDownloader($initialType);
+
+        if ($initialType === $targetType) {
+            $downloader->update($initial, $target, $targetDir, $useSource);
+        } else {
+            $downloader->remove($initial, $targetDir, $useSource);
+            $this->download($target, $targetDir, $useSource);
+        }
+    }
+
+    /**
+     * Removes package from target dir.
+     *
+     * @param   PackageInterface    $package    package instance
+     * @param   string              $targetDir  target dir
+     */
+    public function remove(PackageInterface $package, $targetDir)
+    {
+        if (null === $installationType = $package->getInstallationSource()) {
+            throw new \InvalidArgumentException(
+                'Package '.$package.' was not been installed propertly and can not be removed'
+            );
+        }
+        $useSource = 'source' === $installationType;
+
+        // get proper downloader
+        if (!$useSource) {
+            $downloader = $this->getDownloader($package->getDistType());
+        } else {
+            $downloader = $this->getDownloader($package->getSourceType());
+        }
+
+        $downloader->remove($package, $targetDir, $useSource);
+    }
+}

+ 30 - 12
src/Composer/Downloader/DownloaderInterface.php

@@ -15,21 +15,39 @@ namespace Composer\Downloader;
 use Composer\Package\PackageInterface;
 
 /**
- * Package Downloader
- * 
- * @author Kirill chEbba Chebunin <iam@chebba.org>
- */ 
-interface DownloaderInterface 
+ * Downloader interface.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+interface DownloaderInterface
 {
     /**
-     * Download package
+     * Downloads specific package into specific folder.
      *
-     * @param PackageInterface $package Downloaded package
-     * @param string           $path Download to
-     * @param string           $url Download from
-     * @param string|null      $checksum Package checksum
+     * @param   PackageInterface    $package    package instance
+     * @param   string              $path       download path
+     * @param   string              $url        download url
+     * @param   string              $checksum   package checksum (for dists)
+     * @param   Boolean             $useSource  download as source
+     */
+    function download(PackageInterface $package, $path, $url, $checksum = null, $useSource = false);
+
+    /**
+     * Updates specific package in specific folder from initial to target version.
+     *
+     * @param   PackageInterface    $initial    initial package
+     * @param   PackageInterface    $target     updated package
+     * @param   string              $path       download path
+     * @param   Boolean             $useSource  download as source
+     */
+    function update(PackageInterface $initial, PackageInterface $target, $path, $useSource = false);
+
+    /**
+     * Removes specific package from specific folder.
      *
-     * @throws \UnexpectedValueException
+     * @param   PackageInterface    $package    package instance
+     * @param   string              $path       download path
+     * @param   Boolean             $useSource  download as source
      */
-    function download(PackageInterface $package, $path, $url, $checksum = null);
+    function remove(PackageInterface $package, $path, $useSource = false);
 }

+ 24 - 18
src/Composer/Downloader/GitDownloader.php

@@ -19,27 +19,33 @@ use Composer\Package\PackageInterface;
  */
 class GitDownloader implements DownloaderInterface
 {
-    protected $clone;
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, $path, $url, $checksum = null, $useSource = false)
+    {
+        system('git clone '.escapeshellarg($url).' -b master '.escapeshellarg($path));
+
+        // TODO non-source installs:
+        // system('git archive --format=tar --prefix='.escapeshellarg($package->getName()).' --remote='.escapeshellarg($url).' master | tar -xf -');
+    }
 
-    public function __construct($clone = true)
+    /**
+     * {@inheritDoc}
+     */
+    public function update(PackageInterface $initial, PackageInterface $target, $path, $useSource = false)
     {
-        $this->clone = $clone;
+        $cwd = getcwd();
+        chdir($path);
+        system('git pull');
+        chdir($cwd);
     }
 
-    public function download(PackageInterface $package, $path, $url, $checksum = null)
+    /**
+     * {@inheritDoc}
+     */
+    public function remove(PackageInterface $package, $path, $useSource = false)
     {
-        if (!is_dir($path)) {
-            if (file_exists($path)) {
-                throw new \UnexpectedValueException($path.' exists and is not a directory.');
-            }
-            if (!mkdir($path, 0777, true)) {
-                throw new \UnexpectedValueException($path.' does not exist and could not be created.');
-            }
-        }
-        if ($this->clone) {
-            system('git clone '.escapeshellarg($url).' -b master '.escapeshellarg($path.'/'.$package->getName()));
-        } else {
-            system('git archive --format=tar --prefix='.escapeshellarg($package->getName()).' --remote='.escapeshellarg($url).' master | tar -xf -');
-        }
+        echo 'rm -rf '.$path; // TODO
     }
-}
+}

+ 1 - 1
src/Composer/Downloader/PearDownloader.php

@@ -66,4 +66,4 @@ class PearDownloader implements DownloaderInterface
         }
         chdir($cwd);
     }
-}
+}

+ 1 - 1
src/Composer/Downloader/ZipDownloader.php

@@ -73,4 +73,4 @@ class ZipDownloader implements DownloaderInterface
             throw new \UnexpectedValueException($zipName.' is not a valid zip archive, got error code '.$retval);
         }
     }
-}
+}

+ 131 - 0
src/Composer/Installer/InstallationManager.php

@@ -0,0 +1,131 @@
+<?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\Installer;
+
+use Composer\Package\PackageInterface;
+use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\DependencyResolver\Operation\UpdateOperation;
+use Composer\DependencyResolver\Operation\UninstallOperation;
+
+/**
+ * Package operation manager.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class InstallationManager
+{
+    private $installers = array();
+
+    /**
+     * Sets installer for a specific package type.
+     *
+     * @param   string              $type       package type (library f.e.)
+     * @param   InstallerInterface  $installer  installer instance
+     */
+    public function setInstaller($type, InstallerInterface $installer)
+    {
+        $this->installers[$type] = $installer;
+    }
+
+    /**
+     * Returns installer for a specific package type.
+     *
+     * @param   string              $type       package type
+     *
+     * @return  InstallerInterface
+     *
+     * @throws  InvalidArgumentException        if installer for provided type is not registered
+     */
+    public function getInstaller($type)
+    {
+        if (!isset($this->installers[$type])) {
+            throw new \InvalidArgumentException('Unknown installer type: '.$type);
+        }
+
+        return $this->installers[$type];
+    }
+
+    /**
+     * Checks whether provided package is installed in one of the registered installers.
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @return  Boolean
+     */
+    public function isPackageInstalled(PackageInterface $package)
+    {
+        foreach ($this->installers as $installer) {
+            if ($installer->isInstalled($package)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes solver operation.
+     *
+     * @param   OperationInterface  $operation  operation instance
+     */
+    public function execute(OperationInterface $operation)
+    {
+        $method = $operation->getJobType();
+        $this->$method($operation);
+    }
+
+    /**
+     * Executes install operation.
+     *
+     * @param   InstallOperation    $operation  operation instance
+     */
+    public function install(InstallOperation $operation)
+    {
+        $installer = $this->getInstaller($operation->getPackage()->getType());
+        $installer->install($operation->getPackage());
+    }
+
+    /**
+     * Executes update operation.
+     *
+     * @param   InstallOperation    $operation  operation instance
+     */
+    public function update(UpdateOperation $operation)
+    {
+        $initial = $operation->getInitialPackage();
+        $target  = $operation->getTargetPackage();
+
+        $initialType = $initial->getType();
+        $targetType  = $target->getType();
+
+        if ($initialType === $targetType) {
+            $installer = $this->getInstaller($initialType);
+            $installer->update($initial, $target);
+        } else {
+            $this->getInstaller($initialType)->uninstall($initial);
+            $this->getInstaller($targetType)->install($target);
+        }
+    }
+
+    /**
+     * Uninstalls package.
+     *
+     * @param   UninstallOperation  $operation  operation instance
+     */
+    public function uninstall(UninstallOperation $operation)
+    {
+        $installer = $this->getInstaller($operation->getPackage()->getType());
+        $installer->uninstall($operation->getPackage());
+    }
+}

+ 34 - 10
src/Composer/Installer/InstallerInterface.php

@@ -12,22 +12,46 @@
 
 namespace Composer\Installer;
 
-use Composer\Downloader\DownloaderInterface;
+use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\Package\PackageInterface;
 
 /**
- * Package Installer
- * 
- * @author Kirill chEbba Chebunin <iam@chebba.org>
- */ 
+ * Interface for the package installation manager.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
 interface InstallerInterface
 {
     /**
-     * Install package
+     * Checks that provided package is installed.
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @return  Boolean
+     */
+    function isInstalled(PackageInterface $package);
+
+    /**
+     * Installs specific package.
+     *
+     * @param   PackageInterface    $package    package instance
+     */
+    function install(PackageInterface $package);
+
+    /**
+     * Updates specific package.
+     *
+     * @param   PackageInterface    $initial    already installed package version
+     * @param   PackageInterface    $target     updated version
+     *
+     * @throws  InvalidArgumentException        if $from package is not installed
+     */
+    function update(PackageInterface $initial, PackageInterface $target);
+
+    /**
+     * Uninstalls specific package.
      *
-     * @param PackageInterface    $package
-     * @param DownloaderInterface $downloader
-     * @param string              $type
+     * @param   PackageInterface    $package    package instance
      */
-    function install(PackageInterface $package, DownloaderInterface $downloader, $type);
+    function uninstall(PackageInterface $package);
 }

+ 97 - 12
src/Composer/Installer/LibraryInstaller.php

@@ -12,30 +12,115 @@
 
 namespace Composer\Installer;
 
-use Composer\Downloader\DownloaderInterface;
+use Composer\Downloader\DownloadManager;
+use Composer\Repository\WritableRepositoryInterface;
+use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\Package\PackageInterface;
 
 /**
+ * Package installation manager.
+ *
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
  */
 class LibraryInstaller implements InstallerInterface
 {
-    protected $dir;
+    private $dir;
+    private $dm;
+    private $repository;
 
-    public function __construct($dir = 'vendor')
+    /**
+     * Initializes library installer.
+     *
+     * @param   string                      $dir        relative path for packages home
+     * @param   DownloadManager             $dm         download manager
+     * @param   WritableRepositoryInterface $repository repository controller
+     */
+    public function __construct($dir, DownloadManager $dm, WritableRepositoryInterface $repository)
     {
         $this->dir = $dir;
+        $this->dm  = $dm;
+
+        if (!is_dir($this->dir)) {
+            if (file_exists($this->dir)) {
+                throw new \UnexpectedValueException(
+                    $this->dir.' exists and is not a directory.'
+                );
+            }
+            if (!mkdir($this->dir, 0777, true)) {
+                throw new \UnexpectedValueException(
+                    $this->dir.' does not exist and could not be created.'
+                );
+            }
+        }
+
+        $this->repository = $repository;
+    }
+
+    /**
+     * Checks that specific package is installed.
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @return  Boolean
+     */
+    public function isInstalled(PackageInterface $package)
+    {
+        return $this->repository->hasPackage($package);
     }
 
-    public function install(PackageInterface $package, DownloaderInterface $downloader, $type)
+    /**
+     * Installs specific package.
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @throws  InvalidArgumentException        if provided package have no urls to download from
+     */
+    public function install(PackageInterface $package)
     {
-        if ($type === 'dist') {
-            $downloader->download($package, $this->dir, $package->getDistUrl(), $package->getDistSha1Checksum());
-        } elseif ($type === 'source') {
-            $downloader->download($package, $this->dir, $package->getSourceUrl());
-        } else {
-            throw new \InvalidArgumentException('Type must be one of (dist, source), '.$type.' given.');
+        $downloadPath = $this->dir.DIRECTORY_SEPARATOR.$package->getName();
+
+        $this->dm->download($package, $downloadPath);
+        $this->repository->addPackage($package);
+    }
+
+    /**
+     * Updates specific package.
+     *
+     * @param   PackageInterface    $initial    already installed package version
+     * @param   PackageInterface    $target     updated version
+     *
+     * @throws  InvalidArgumentException        if $from package is not installed
+     */
+    public function update(PackageInterface $initial, PackageInterface $target)
+    {
+        if (!$this->repository->hasPackage($initial)) {
+            throw new \InvalidArgumentException('Package is not installed: '.$initial);
         }
-        return true;
+
+        $downloadPath = $this->dir.DIRECTORY_SEPARATOR.$initial->getName();
+
+        $this->dm->update($initial, $target, $downloadPath);
+        $this->repository->removePackage($initial);
+        $this->repository->addPackage($target);
+    }
+
+    /**
+     * Uninstalls specific package.
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @throws  InvalidArgumentException        if package is not installed
+     */
+    public function uninstall(PackageInterface $package)
+    {
+        if (!$this->repository->hasPackage($package)) {
+            throw new \InvalidArgumentException('Package is not installed: '.$package);
+        }
+
+        $downloadPath = $this->dir.DIRECTORY_SEPARATOR.$package->getName();
+
+        $this->dm->remove($package, $downloadPath);
+        $this->repository->removePackage($package);
     }
-}
+}

+ 6 - 17
src/Composer/Package/BasePackage.php

@@ -135,33 +135,22 @@ abstract class BasePackage implements PackageInterface
     }
 
     /**
-     * Converts the package into a readable and unique string
+     * Returns package unique name, constructed from name, version and release type.
      *
      * @return string
      */
-    public function __toString()
+    public function getUniqueName()
     {
         return $this->getName().'-'.$this->getVersion().'-'.$this->getReleaseType();
     }
 
     /**
-     * Parses a version string and returns an array with the version, its type (alpha, beta, RC, stable) and a dev flag (for development branches tracking)
+     * Converts the package into a readable and unique string
      *
-     * @param string $version
-     * @return array
+     * @return string
      */
-    public static function parseVersion($version)
+    public function __toString()
     {
-        if (!preg_match('#^v?(\d+)(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?-?(dev)?$#i', $version, $matches)) {
-            throw new \UnexpectedValueException('Invalid version string '.$version);
-        }
-
-        return array(
-            'version' => $matches[1]
-                .(!empty($matches[2]) ? $matches[2] : '.0')
-                .(!empty($matches[3]) ? $matches[3] : '.0'),
-            'type' => !empty($matches[4]) ? strtolower($matches[4]) : 'stable',
-            'dev' => !empty($matches[5]),
-        );
+        return $this->getUniqueName();
     }
 }

+ 58 - 0
src/Composer/Package/Dumper/ArrayDumper.php

@@ -0,0 +1,58 @@
+<?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\Package\Dumper;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * @author Konstantin Kudryashiv <ever.zet@gmail.com>
+ */
+class ArrayDumper
+{
+    public function dump(PackageInterface $package)
+    {
+        $keys = array(
+            'type',
+            'names',
+            'extra',
+            'installationSource',
+            'sourceType',
+            'sourceUrl',
+            'distType',
+            'distUrl',
+            'distSha1Checksum',
+            'releaseType',
+            'version',
+            'license',
+            'requires',
+            'conflicts',
+            'provides',
+            'replaces',
+            'recommends',
+            'suggests'
+        );
+
+        $data = array();
+        $data['name'] = $package->getPrettyName();
+        foreach ($keys as $key) {
+            $getter = 'get'.ucfirst($key);
+            $value  = $package->$getter();
+
+            if (null !== $value && !(is_array($value) && 0 === count($value))) {
+                $data[$key] = $value;
+            }
+        }
+
+        return $data;
+    }
+}

+ 114 - 0
src/Composer/Package/Loader/ArrayLoader.php

@@ -0,0 +1,114 @@
+<?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\Package\Loader;
+
+use Composer\Package;
+
+/**
+ * @author Konstantin Kudryashiv <ever.zet@gmail.com>
+ */
+class ArrayLoader
+{
+    protected $supportedLinkTypes = array(
+        'require'   => 'requires',
+        'conflict'  => 'conflicts',
+        'provide'   => 'provides',
+        'replace'   => 'replaces',
+        'recommend' => 'recommends',
+        'suggest'   => 'suggests',
+    );
+
+    public function load($config)
+    {
+        $this->validateConfig($config);
+
+        $versionParser = new Package\Version\VersionParser();
+        $version = $versionParser->parse($config['version']);
+        $package = new Package\MemoryPackage($config['name'], $version['version'], $version['type']);
+
+        $package->setType(isset($config['type']) ? $config['type'] : 'library');
+
+        if (isset($config['extra'])) {
+            $package->setExtra($config['extra']);
+        }
+
+        if (isset($config['license'])) {
+            $package->setLicense($config['license']);
+        }
+
+        if (isset($config['source'])) {
+            if (!isset($config['source']['type']) || !isset($config['source']['url'])) {
+                throw new \UnexpectedValueException(sprintf(
+                    "package source should be specified as {\"type\": ..., \"url\": ...},\n%s given",
+                    json_encode($config['source'])
+                ));
+            }
+            $package->setSourceType($config['source']['type']);
+            $package->setSourceUrl($config['source']['url']);
+        }
+
+        if (isset($config['dist'])) {
+            if (!isset($config['dist']['type'])
+             || !isset($config['dist']['url'])
+             || !isset($config['dist']['shasum'])) {
+                throw new \UnexpectedValueException(sprintf(
+                    "package dist should be specified as ".
+                    "{\"type\": ..., \"url\": ..., \"shasum\": ...},\n%s given",
+                    json_encode($config['source'])
+                ));
+            }
+            $package->setDistType($config['dist']['type']);
+            $package->setDistUrl($config['dist']['url']);
+            $package->setDistSha1Checksum($config['dist']['shasum']);
+        }
+
+        foreach ($this->supportedLinkTypes as $type => $description) {
+            if (isset($config[$type])) {
+                $method = 'set'.ucfirst($description);
+                $package->{$method}(
+                    $this->loadLinksFromConfig($package->getName(), $description, $config['require'])
+                );
+            }
+        }
+
+        return $package;
+    }
+
+    private function loadLinksFromConfig($srcPackageName, $description, array $linksSpecs)
+    {
+        $links = array();
+        foreach ($linksSpecs as $packageName => $version) {
+            $name = strtolower($packageName);
+
+            preg_match('#^([>=<~]*)([\d.]+.*)$#', $version, $match);
+            if (!$match[1]) {
+                $match[1] = '=';
+            }
+
+            $constraint = new Package\LinkConstraint\VersionConstraint($match[1], $match[2]);
+            $links[]    = new Package\Link($srcPackageName, $packageName, $constraint, $description);
+        }
+
+        return $links;
+    }
+
+    private function validateConfig(array $config)
+    {
+        if (!isset($config['name'])) {
+            throw new \UnexpectedValueException('name is required for package');
+        }
+        if (!isset($config['version'])) {
+            throw new \UnexpectedValueException('version is required for package');
+        }
+    }
+}

+ 60 - 0
src/Composer/Package/Loader/JsonLoader.php

@@ -0,0 +1,60 @@
+<?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\Package\Loader;
+
+/**
+ * @author Konstantin Kudryashiv <ever.zet@gmail.com>
+ */
+class JsonLoader extends ArrayLoader
+{
+    public function load($json)
+    {
+        $config = $this->loadJsonConfig($json);
+
+        return parent::load($config);
+    }
+
+    private function loadJsonConfig($json)
+    {
+        if (is_file($json)) {
+            $json = file_get_contents($json);
+        }
+
+        $config = json_decode($json, true);
+        if (!$config) {
+            switch (json_last_error()) {
+            case JSON_ERROR_NONE:
+                $msg = 'No error has occurred, is your composer.json file empty?';
+                break;
+            case JSON_ERROR_DEPTH:
+                $msg = 'The maximum stack depth has been exceeded';
+                break;
+            case JSON_ERROR_STATE_MISMATCH:
+                $msg = 'Invalid or malformed JSON';
+                break;
+            case JSON_ERROR_CTRL_CHAR:
+                $msg = 'Control character error, possibly incorrectly encoded';
+                break;
+            case JSON_ERROR_SYNTAX:
+                $msg = 'Syntax error';
+                break;
+            case JSON_ERROR_UTF8:
+                $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
+                break;
+            }
+            throw new \UnexpectedValueException('Incorrect composer.json file: '.$msg);
+        }
+
+        return $config;
+    }
+}

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

@@ -20,6 +20,7 @@ namespace Composer\Package;
 class MemoryPackage extends BasePackage
 {
     protected $type;
+    protected $installationSource;
     protected $sourceType;
     protected $sourceUrl;
     protected $distType;
@@ -84,6 +85,22 @@ class MemoryPackage extends BasePackage
         return $this->extra;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function setInstallationSource($type)
+    {
+        $this-> installationSource = $type;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getInstallationSource()
+    {
+        return $this->installationSource;
+    }
+
     /**
      * @param string $type
      */

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

@@ -82,6 +82,20 @@ interface PackageInterface
      */
     function getExtra();
 
+    /**
+     * Sets source from which this package was installed (source/dist).
+     *
+     * @param   string  $type   source/dist
+     */
+    function setInstallationSource($type);
+
+    /**
+     * Returns source from which this package was installed (source/dist).
+     *
+     * @param   string  $type   source/dist
+     */
+    function getInstallationSource();
+
     /**
      * Returns the repository type of this package, e.g. git, svn
      *
@@ -202,6 +216,13 @@ interface PackageInterface
      */
     function getRepository();
 
+    /**
+     * Returns package unique name, constructed from name, version and release type.
+     *
+     * @return string
+     */
+    function getUniqueName();
+
     /**
      * Converts the package into a readable and unique string
      *

+ 91 - 0
src/Composer/Package/PackageLock.php

@@ -0,0 +1,91 @@
+<?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\Package;
+
+use Composer\Package\MemoryPackage;
+use Composer\Package\Version\VersionParser;
+
+/**
+ * @author Konstantin Kudryashiv <ever.zet@gmail.com>
+ */
+class PackageLock
+{
+    private $file;
+    private $isLocked = false;
+
+    public function __construct($file = 'composer.lock')
+    {
+        if (file_exists($file)) {
+            $this->file     = $file;
+            $this->isLocked = true;
+        }
+    }
+
+    public function isLocked()
+    {
+        return $this->isLocked;
+    }
+
+    public function getLockedPackages()
+    {
+        $lockList = $this->loadJsonConfig($this->file);
+
+        $versionParser = new VersionParser();
+        $packages      = array();
+        foreach ($lockList as $info) {
+            $version    = $versionParser->parse($info['version']);
+            $packages[] = new MemoryPackage($info['package'], $version['version'], $version['type']);
+        }
+
+        return $packages;
+    }
+
+    public function lock(array $packages)
+    {
+        // TODO: write installed packages info into $this->file
+    }
+
+    private function loadJsonConfig($json)
+    {
+        if (is_file($json)) {
+            $json = file_get_contents($json);
+        }
+
+        $config = json_decode($json, true);
+        if (!$config) {
+            switch (json_last_error()) {
+            case JSON_ERROR_NONE:
+                $msg = 'No error has occurred, is your composer.json file empty?';
+                break;
+            case JSON_ERROR_DEPTH:
+                $msg = 'The maximum stack depth has been exceeded';
+                break;
+            case JSON_ERROR_STATE_MISMATCH:
+                $msg = 'Invalid or malformed JSON';
+                break;
+            case JSON_ERROR_CTRL_CHAR:
+                $msg = 'Control character error, possibly incorrectly encoded';
+                break;
+            case JSON_ERROR_SYNTAX:
+                $msg = 'Syntax error';
+                break;
+            case JSON_ERROR_UTF8:
+                $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
+                break;
+            }
+            throw new \UnexpectedValueException('Incorrect composer.json file: '.$msg);
+        }
+
+        return $config;
+    }
+}

+ 43 - 0
src/Composer/Package/Version/VersionParser.php

@@ -0,0 +1,43 @@
+<?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\Package\Version;
+
+/**
+ * Version parser
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class VersionParser
+{
+    /**
+     * Parses a version string and returns an array with the version, its type (alpha, beta, RC, stable) and a dev flag (for development branches tracking)
+     *
+     * @param string $version
+     * @return array
+     */
+    public function parse($version)
+    {
+        if (!preg_match('#^v?(\d+)(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?-?(dev)?$#i', $version, $matches)) {
+            throw new \UnexpectedValueException('Invalid version string '.$version);
+        }
+
+        return array(
+            'version' => $matches[1]
+                .(!empty($matches[2]) ? $matches[2] : '.0')
+                .(!empty($matches[3]) ? $matches[3] : '.0'),
+            'type' => strtolower(!empty($matches[4]) ? $matches[4] : 'stable'),
+            'dev' => !empty($matches[5]),
+        );
+    }
+}

+ 38 - 0
src/Composer/Repository/ArrayRepository.php

@@ -23,6 +23,26 @@ class ArrayRepository implements RepositoryInterface
 {
     protected $packages;
 
+    /**
+     * Checks if specified package in this repository.
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @return  Boolean
+     */
+    public function hasPackage(PackageInterface $package)
+    {
+        $packageId = $package->getUniqueName();
+
+        foreach ($this->getPackages() as $repoPackage) {
+            if ($packageId === $repoPackage->getUniqueName()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Adds a new package to the repository
      *
@@ -37,6 +57,24 @@ class ArrayRepository implements RepositoryInterface
         $this->packages[] = $package;
     }
 
+    /**
+     * Removes package from repository.
+     *
+     * @param   PackageInterface    $package    package instance
+     */
+    public function removePackage(PackageInterface $package)
+    {
+        $packageId = $package->getUniqueName();
+
+        foreach ($this->getPackages() as $key => $repoPackage) {
+            if ($packageId === $repoPackage->getUniqueName()) {
+                array_splice($this->packages, $key, 1);
+
+                return;
+            }
+        }
+    }
+
     /**
      * Returns all contained packages
      *

+ 11 - 56
src/Composer/Repository/ComposerRepository.php

@@ -12,9 +12,7 @@
 
 namespace Composer\Repository;
 
-use Composer\Package\MemoryPackage;
-use Composer\Package\BasePackage;
-use Composer\Package\Link;
+use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\LinkConstraint\VersionConstraint;
 
 /**
@@ -22,10 +20,16 @@ use Composer\Package\LinkConstraint\VersionConstraint;
  */
 class ComposerRepository extends ArrayRepository
 {
+    protected $url;
     protected $packages;
 
     public function __construct($url)
     {
+        $url = rtrim($url, '/');
+        if (!filter_var($url, FILTER_VALIDATE_URL)) {
+            throw new \UnexpectedValueException('Invalid url given for Composer repository: '.$url);
+        }
+
         $this->url = $url;
     }
 
@@ -42,60 +46,11 @@ class ComposerRepository extends ArrayRepository
         }
     }
 
-    protected function createPackages($data)
+    private function createPackages($data)
     {
         foreach ($data['versions'] as $rev) {
-            $version = BasePackage::parseVersion($rev['version']);
-
-            $package = new MemoryPackage($rev['name'], $version['version'], $version['type']);
-            $package->setSourceType($rev['source']['type']);
-            $package->setSourceUrl($rev['source']['url']);
-
-            $package->setDistType($rev['dist']['type']);
-            $package->setDistUrl($rev['dist']['url']);
-            $package->setDistSha1Checksum($rev['dist']['shasum']);
-
-            if (isset($rev['type'])) {
-                $package->setType($rev['type']);
-            }
-
-            if (isset($rev['extra'])) {
-                $package->setExtra($rev['extra']);
-            }
-
-            if (isset($rev['license'])) {
-                $package->setLicense($rev['license']);
-            }
-
-            $links = array(
-                'require',
-                'conflict',
-                'provide',
-                'replace',
-                'recommend',
-                'suggest',
-            );
-            foreach ($links as $link) {
-                if (isset($rev[$link])) {
-                    $method = 'set'.$link.'s';
-                    $package->{$method}($this->createLinks($rev['name'], $link.'s', $rev[$link]));
-                }
-            }
-            $this->addPackage($package);
-        }
-    }
-
-    protected function createLinks($name, $description, $linkSpecs)
-    {
-        $links = array();
-        foreach ($linkSpecs as $dep => $ver) {
-            preg_match('#^([>=<~]*)([\d.]+.*)$#', $ver, $match);
-            if (!$match[1]) {
-                $match[1] = '=';
-            }
-            $constraint = new VersionConstraint($match[1], $match[2]);
-            $links[] = new Link($name, $dep, $constraint, $description);
+            $loader = new ArrayLoader();
+            $this->addPackage($loader->load($rev));
         }
-        return $links;
     }
-}
+}

+ 82 - 0
src/Composer/Repository/FilesystemRepository.php

@@ -0,0 +1,82 @@
+<?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\Repository;
+
+use Composer\Package\PackageInterface;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\Dumper\ArrayDumper;
+
+/**
+ * Filesystem repository.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class FilesystemRepository extends ArrayRepository implements WritableRepositoryInterface
+{
+    private $file;
+
+    /**
+     * Initializes filesystem repository.
+     *
+     * @param   string  $group  registry (installer) group
+     */
+    public function __construct($repositoryFile)
+    {
+        $this->file = $repositoryFile;
+        $path       = dirname($this->file);
+
+        if (!is_dir($path)) {
+            if (file_exists($path)) {
+                throw new \UnexpectedValueException(
+                    $path.' exists and is not a directory.'
+                );
+            }
+            if (!mkdir($path, 0777, true)) {
+                throw new \UnexpectedValueException(
+                    $path.' does not exist and could not be created.'
+                );
+            }
+        }
+    }
+
+    /**
+     * Initializes repository (reads file, or remote address).
+     */
+    protected function initialize()
+    {
+        parent::initialize();
+
+        $packages = @json_decode(file_get_contents($this->file), true);
+
+        if (is_array($packages)) {
+            $loader = new ArrayLoader();
+            foreach ($packages as $package) {
+                $this->addPackage($loader->load($package));
+            }
+        }
+    }
+
+    /**
+     * Writes writable repository.
+     */
+    public function write()
+    {
+        $packages = array();
+        $dumper   = new ArrayDumper();
+        foreach ($this->getPackages() as $package) {
+            $packages[] = $dumper->dump($package);
+        }
+
+        file_put_contents($this->file, json_encode($packages));
+    }
+}

+ 5 - 3
src/Composer/Repository/GitRepository.php

@@ -24,11 +24,13 @@ use Composer\Package\LinkConstraint\VersionConstraint;
  */
 class GitRepository extends ArrayRepository
 {
-    protected $packages;
+    protected $url;
+    protected $cacheDir;
 
-    public function __construct($url)
+    public function __construct($url, $cacheDir)
     {
         $this->url = $url;
+        $this->cacheDir = $cacheDir;
     }
 
     protected function initialize()
@@ -98,4 +100,4 @@ class GitRepository extends ArrayRepository
         }
         return $links;
     }
-}
+}

+ 5 - 4
src/Composer/Repository/PearRepository.php

@@ -23,16 +23,17 @@ use Composer\Package\LinkConstraint\VersionConstraint;
  */
 class PearRepository extends ArrayRepository
 {
-    private $name;
-    private $url;
+    protected $url;
+    protected $cacheDir;
 
-    public function __construct($url, $name = '')
+    public function __construct($url, $cacheDir)
     {
         if (!filter_var($url, FILTER_VALIDATE_URL)) {
-            throw new \UnexpectedValueException('Invalid url given for PEAR repository "'.$name.'": '.$url);
+            throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$url);
         }
 
         $this->url = $url;
+        $this->cacheDir = $cacheDir;
     }
 
     protected function initialize()

+ 7 - 6
src/Composer/Repository/PlatformRepository.php

@@ -14,22 +14,23 @@ namespace Composer\Repository;
 
 use Composer\Package\MemoryPackage;
 use Composer\Package\BasePackage;
+use Composer\Package\Version\VersionParser;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
 class PlatformRepository extends ArrayRepository
 {
-    protected $packages;
-
     protected function initialize()
     {
         parent::initialize();
 
+        $versionParser = new VersionParser();
+
         try {
-            $version = BasePackage::parseVersion(PHP_VERSION);
+            $version = $versionParser->parse(PHP_VERSION);
         } catch (\UnexpectedValueException $e) {
-            $version = BasePackage::parseVersion(preg_replace('#^(.+?)(-.+)?$#', '$1', PHP_VERSION));
+            $version = $versionParser->parse(preg_replace('#^(.+?)(-.+)?$#', '$1', PHP_VERSION));
         }
 
         $php = new MemoryPackage('php', $version['version'], $version['type']);
@@ -42,7 +43,7 @@ class PlatformRepository extends ArrayRepository
 
             $reflExt = new \ReflectionExtension($ext);
             try {
-                $version = BasePackage::parseVersion($reflExt->getVersion());
+                $version = $versionParser->parse($reflExt->getVersion());
             } catch (\UnexpectedValueException $e) {
                 $version = array('version' => '0', 'type' => 'stable');
             }
@@ -51,4 +52,4 @@ class PlatformRepository extends ArrayRepository
             $this->addPackage($ext);
         }
     }
-}
+}

+ 19 - 0
src/Composer/Repository/RepositoryInterface.php

@@ -12,10 +12,29 @@
 
 namespace Composer\Repository;
 
+use Composer\Package\PackageInterface;
+
 /**
+ * Repository interface.
+ *
  * @author Nils Adermann <naderman@naderman.de>
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
  */
 interface RepositoryInterface extends \Countable
 {
+    /**
+     * Checks if specified package registered (installed).
+     *
+     * @param   PackageInterface    $package    package instance
+     *
+     * @return  Boolean
+     */
+    function hasPackage(PackageInterface $package);
+
+    /**
+     * Returns list of registered packages.
+     *
+     * @return  array
+     */
     function getPackages();
 }

+ 83 - 0
src/Composer/Repository/RepositoryManager.php

@@ -0,0 +1,83 @@
+<?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\Repository;
+
+/**
+ * Repositories manager.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class RepositoryManager
+{
+    private $localRepository;
+    private $repositories = array();
+
+    /**
+     * Sets repository with specific name.
+     *
+     * @param   string              $type       repository name
+     * @param   RepositoryInterface $repository repository instance
+     */
+    public function setRepository($type, RepositoryInterface $repository)
+    {
+        $this->repositories[$type] = $repository;
+    }
+
+    /**
+     * Returns repository for a specific installation type.
+     *
+     * @param   string  $type   installation type
+     *
+     * @return  RepositoryInterface
+     *
+     * @throws  InvalidArgumentException     if repository for provided type is not registeterd
+     */
+    public function getRepository($type)
+    {
+        if (!isset($this->repositories[$type])) {
+            throw new \InvalidArgumentException('Repository is not registered: '.$type);
+        }
+
+        return $this->repositories[$type];
+    }
+
+    /**
+     * Returns all repositories, except local one.
+     *
+     * @return  array
+     */
+    public function getRepositories()
+    {
+        return $this->repositories;
+    }
+
+    /**
+     * Sets local repository for the project.
+     *
+     * @param   RepositoryInterface $repository repository instance
+     */
+    public function setLocalRepository(RepositoryInterface $repository)
+    {
+        $this->localRepository = $repository;
+    }
+
+    /**
+     * Returns local repository for the project.
+     *
+     * @return  RepositoryInterface
+     */
+    public function getLocalRepository()
+    {
+        return $this->localRepository;
+    }
+}

+ 64 - 0
src/Composer/Repository/WrapperRepository.php

@@ -0,0 +1,64 @@
+<?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\Repository;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class WrapperRepository extends ArrayRepository implements WritableRepositoryInterface
+{
+    private $repositories;
+
+    public function __construct(array $repositories)
+    {
+        $this->repositories = $repositories;
+    }
+
+    protected function initialize()
+    {
+        parent::initialize();
+
+        foreach ($this->repositories as $repo) {
+            foreach ($repo->getPackages() as $package) {
+                $this->packages[] = $package;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function addPackage(PackageInterface $package)
+    {
+        throw new \LogicException('Can not add packages to a wrapper repository');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function removePackage(PackageInterface $package)
+    {
+        throw new \LogicException('Can not remove packages to a wrapper repository');
+    }
+
+    public function write()
+    {
+        foreach ($this->repositories as $repo) {
+            if ($repo instanceof WritableRepositoryInterface) {
+                $repo->write();
+            }
+        }
+    }
+}

+ 42 - 0
src/Composer/Repository/WritableRepositoryInterface.php

@@ -0,0 +1,42 @@
+<?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\Repository;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * Writable repository interface.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+interface WritableRepositoryInterface extends RepositoryInterface
+{
+    /**
+     * Writes repository (f.e. to the disc).
+     */
+    function write();
+
+    /**
+     * Adds package to the repository.
+     *
+     * @param   PackageInterface    $package    package instance
+     */
+    function addPackage(PackageInterface $package);
+
+    /**
+     * Removes package from the repository.
+     *
+     * @param   PackageInterface    $package    package instance
+     */
+    function removePackage(PackageInterface $package);
+}

+ 17 - 23
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -213,25 +213,6 @@ class SolverTest extends \PHPUnit_Framework_TestCase
         ));
     }
 
-    public function testSolverWithComposerRepo()
-    {
-        $this->repoInstalled = new PlatformRepository;
-
-        // overwrite solver with custom installed repo
-        $this->solver = new Solver($this->policy, $this->pool, $this->repoInstalled);
-
-        $this->repo = new ComposerRepository('http://packagist.org');
-        list($monolog) = $this->repo->getPackages();
-
-        $this->reposComplete();
-
-        $this->request->install('Monolog');
-
-        $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $monolog),
-        ));
-    }
-
     protected function reposComplete()
     {
         $this->pool->addRepository($this->repoInstalled);
@@ -240,10 +221,23 @@ class SolverTest extends \PHPUnit_Framework_TestCase
 
     protected function checkSolverResult(array $expected)
     {
-        $result = $this->solver->solve($this->request);
-
-        foreach ($result as &$step) {
-            unset($step['why']);
+        $transaction = $this->solver->solve($this->request);
+
+        $result = array();
+        foreach ($transaction as $operation) {
+            if ('update' === $operation->getJobType()) {
+                $result[] = array(
+                    'job'  => 'update',
+                    'from' => $operation->getInitialPackage(),
+                    'to'   => $operation->getTargetPackage()
+                );
+            } else {
+                $job = ('uninstall' === $operation->getJobType() ? 'remove' : 'install');
+                $result[] = array(
+                    'job'     => $job,
+                    'package' => $operation->getPackage()
+                );
+            }
         }
 
         $this->assertEquals($expected, $result);

+ 499 - 0
tests/Composer/Test/Downloader/DownloadManagerTest.php

@@ -0,0 +1,499 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Downloader;
+
+use Composer\Downloader\DownloadManager;
+
+class DownloadManagerTest extends \PHPUnit_Framework_TestCase
+{
+    public function testSetGetDownloader()
+    {
+        $downloader = $this->createDownloaderMock();
+        $manager    = new DownloadManager();
+
+        $manager->setDownloader('test', $downloader);
+        $this->assertSame($downloader, $manager->getDownloader('test'));
+
+        $this->setExpectedException('UnexpectedValueException');
+        $manager->getDownloader('unregistered');
+    }
+
+    public function testFullPackageDownload()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $package
+            ->expects($this->once())
+            ->method('getDistUrl')
+            ->will($this->returnValue('dist_url'));
+        $package
+            ->expects($this->once())
+            ->method('getDistSha1Checksum')
+            ->will($this->returnValue('sha1'));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $pearDownloader = $this->createDownloaderMock();
+        $pearDownloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir', 'dist_url', 'sha1', false);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('pear', $pearDownloader);
+
+        $manager->download($package, 'target_dir');
+    }
+
+    public function testBadPackageDownload()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue(null));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue(null));
+
+        $manager = new DownloadManager();
+
+        $this->setExpectedException('InvalidArgumentException');
+        $manager->download($package, 'target_dir');
+    }
+
+    public function testDistOnlyPackageDownload()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue(null));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $package
+            ->expects($this->once())
+            ->method('getDistUrl')
+            ->will($this->returnValue('dist_url'));
+        $package
+            ->expects($this->once())
+            ->method('getDistSha1Checksum')
+            ->will($this->returnValue('sha1'));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $pearDownloader = $this->createDownloaderMock();
+        $pearDownloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir', 'dist_url', 'sha1', false);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('pear', $pearDownloader);
+
+        $manager->download($package, 'target_dir');
+    }
+
+    public function testSourceOnlyPackageDownload()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue(null));
+
+        $package
+            ->expects($this->once())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('source_url'));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $gitDownloader = $this->createDownloaderMock();
+        $gitDownloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'vendor/pkg', 'source_url', false);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('git', $gitDownloader);
+
+        $manager->download($package, 'vendor/pkg');
+    }
+
+    public function testFullPackageDownloadWithSourcePreferred()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $package
+            ->expects($this->once())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('source_url'));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $gitDownloader = $this->createDownloaderMock();
+        $gitDownloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'vendor/pkg', 'source_url', true);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('git', $gitDownloader);
+        $manager->preferSource();
+
+        $manager->download($package, 'vendor/pkg');
+    }
+
+    public function testDistOnlyPackageDownloadWithSourcePreferred()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue(null));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $package
+            ->expects($this->once())
+            ->method('getDistUrl')
+            ->will($this->returnValue('dist_url'));
+        $package
+            ->expects($this->once())
+            ->method('getDistSha1Checksum')
+            ->will($this->returnValue('sha1'));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $pearDownloader = $this->createDownloaderMock();
+        $pearDownloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir', 'dist_url', 'sha1', true);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('pear', $pearDownloader);
+        $manager->preferSource();
+
+        $manager->download($package, 'target_dir');
+    }
+
+    public function testSourceOnlyPackageDownloadWithSourcePreferred()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue(null));
+
+        $package
+            ->expects($this->once())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('source_url'));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $gitDownloader = $this->createDownloaderMock();
+        $gitDownloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'vendor/pkg', 'source_url', true);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('git', $gitDownloader);
+        $manager->preferSource();
+
+        $manager->download($package, 'vendor/pkg');
+    }
+
+    public function testBadPackageDownloadWithSourcePreferred()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue(null));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue(null));
+
+        $manager = new DownloadManager();
+        $manager->preferSource();
+
+        $this->setExpectedException('InvalidArgumentException');
+        $manager->download($package, 'target_dir');
+    }
+
+    public function testUpdateDistWithEqualTypes()
+    {
+        $initial = $this->createPackageMock();
+        $initial
+            ->expects($this->once())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
+        $initial
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $target = $this->createPackageMock();
+        $target
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $pearDownloader = $this->createDownloaderMock();
+        $pearDownloader
+            ->expects($this->once())
+            ->method('update')
+            ->with($initial, $target, 'vendor/bundles/FOS/UserBundle', false);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('pear', $pearDownloader);
+
+        $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
+    }
+
+    public function testUpdateDistWithNotEqualTypes()
+    {
+        $initial = $this->createPackageMock();
+        $initial
+            ->expects($this->once())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
+        $initial
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $target = $this->createPackageMock();
+        $target
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('composer'));
+
+        $pearDownloader = $this->createDownloaderMock();
+        $pearDownloader
+            ->expects($this->once())
+            ->method('remove')
+            ->with($initial, 'vendor/bundles/FOS/UserBundle', false);
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setMethods(array('download'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('download')
+            ->with($target, 'vendor/bundles/FOS/UserBundle', false);
+
+        $manager->setDownloader('pear', $pearDownloader);
+        $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
+    }
+
+    public function testUpdateSourceWithEqualTypes()
+    {
+        $initial = $this->createPackageMock();
+        $initial
+            ->expects($this->once())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('source'));
+        $initial
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('svn'));
+
+        $target = $this->createPackageMock();
+        $target
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('svn'));
+
+        $svnDownloader = $this->createDownloaderMock();
+        $svnDownloader
+            ->expects($this->once())
+            ->method('update')
+            ->with($initial, $target, 'vendor/pkg', true);
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('svn', $svnDownloader);
+
+        $manager->update($initial, $target, 'vendor/pkg');
+    }
+
+    public function testUpdateSourceWithNotEqualTypes()
+    {
+        $initial = $this->createPackageMock();
+        $initial
+            ->expects($this->once())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('source'));
+        $initial
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('svn'));
+
+        $target = $this->createPackageMock();
+        $target
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+
+        $svnDownloader = $this->createDownloaderMock();
+        $svnDownloader
+            ->expects($this->once())
+            ->method('remove')
+            ->with($initial, 'vendor/pkg', true);
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setMethods(array('download'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('download')
+            ->with($target, 'vendor/pkg', true);
+        $manager->setDownloader('svn', $svnDownloader);
+
+        $manager->update($initial, $target, 'vendor/pkg');
+    }
+
+    public function testUpdateBadlyInstalledPackage()
+    {
+        $initial = $this->createPackageMock();
+        $target  = $this->createPackageMock();
+
+        $this->setExpectedException('InvalidArgumentException');
+
+        $manager = new DownloadManager();
+        $manager->update($initial, $target, 'vendor/pkg');
+    }
+
+    public function testRemoveDist()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+
+        $pearDownloader = $this->createDownloaderMock();
+        $pearDownloader
+            ->expects($this->once())
+            ->method('remove')
+            ->with($package, 'vendor/bundles/FOS/UserBundle');
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('pear', $pearDownloader);
+
+        $manager->remove($package, 'vendor/bundles/FOS/UserBundle');
+    }
+
+    public function testRemoveSource()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('source'));
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('svn'));
+
+        $svnDownloader = $this->createDownloaderMock();
+        $svnDownloader
+            ->expects($this->once())
+            ->method('remove')
+            ->with($package, 'vendor/pkg');
+
+        $manager = new DownloadManager();
+        $manager->setDownloader('svn', $svnDownloader);
+
+        $manager->remove($package, 'vendor/pkg');
+    }
+
+    public function testRemoveBadlyInstalledPackage()
+    {
+        $package = $this->createPackageMock();
+        $manager = new DownloadManager();
+
+        $this->setExpectedException('InvalidArgumentException');
+
+        $manager->remove($package, 'vendor/pkg');
+    }
+
+    private function createDownloaderMock()
+    {
+        return $this->getMockBuilder('Composer\Downloader\DownloaderInterface')
+            ->getMock();
+    }
+
+    private function createPackageMock()
+    {
+        return $this->getMockBuilder('Composer\Package\PackageInterface')
+            ->getMock();
+    }
+}

+ 180 - 0
tests/Composer/Test/Installer/InstallationManagerTest.php

@@ -0,0 +1,180 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Installer;
+
+use Composer\Installer\InstallationManager;
+use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\DependencyResolver\Operation\UpdateOperation;
+use Composer\DependencyResolver\Operation\UninstallOperation;
+
+class InstallationManagerTest extends \PHPUnit_Framework_TestCase
+{
+    public function testSetGetInstaller()
+    {
+        $installer = $this->createInstallerMock();
+        $manager   = new InstallationManager();
+
+        $manager->setInstaller('vendor', $installer);
+        $this->assertSame($installer, $manager->getInstaller('vendor'));
+
+        $this->setExpectedException('InvalidArgumentException');
+        $manager->getInstaller('unregistered');
+    }
+
+    public function testExecute()
+    {
+        $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')
+            ->setMethods(array('install', 'update', 'uninstall'))
+            ->getMock();
+
+        $installOperation = new InstallOperation($this->createPackageMock());
+        $removeOperation  = new UninstallOperation($this->createPackageMock());
+        $updateOperation  = new UpdateOperation(
+            $this->createPackageMock(), $this->createPackageMock()
+        );
+
+        $manager
+            ->expects($this->once())
+            ->method('install')
+            ->with($installOperation);
+        $manager
+            ->expects($this->once())
+            ->method('uninstall')
+            ->with($removeOperation);
+        $manager
+            ->expects($this->once())
+            ->method('update')
+            ->with($updateOperation);
+
+        $manager->execute($installOperation);
+        $manager->execute($removeOperation);
+        $manager->execute($updateOperation);
+    }
+
+    public function testInstall()
+    {
+        $installer = $this->createInstallerMock();
+        $manager   = new InstallationManager();
+        $manager->setInstaller('library', $installer);
+
+        $package   = $this->createPackageMock();
+        $operation = new InstallOperation($package, 'test');
+
+        $package
+            ->expects($this->once())
+            ->method('getType')
+            ->will($this->returnValue('library'));
+
+        $installer
+            ->expects($this->once())
+            ->method('install')
+            ->with($package);
+
+        $manager->install($operation);
+    }
+
+    public function testUpdateWithEqualTypes()
+    {
+        $installer = $this->createInstallerMock();
+        $manager   = new InstallationManager();
+        $manager->setInstaller('library', $installer);
+
+        $initial   = $this->createPackageMock();
+        $target    = $this->createPackageMock();
+        $operation = new UpdateOperation($initial, $target, 'test');
+
+        $initial
+            ->expects($this->once())
+            ->method('getType')
+            ->will($this->returnValue('library'));
+        $target
+            ->expects($this->once())
+            ->method('getType')
+            ->will($this->returnValue('library'));
+
+        $installer
+            ->expects($this->once())
+            ->method('update')
+            ->with($initial, $target);
+
+        $manager->update($operation);
+    }
+
+    public function testUpdateWithNotEqualTypes()
+    {
+        $installer1 = $this->createInstallerMock();
+        $installer2 = $this->createInstallerMock();
+        $manager    = new InstallationManager();
+        $manager->setInstaller('library', $installer1);
+        $manager->setInstaller('bundles', $installer2);
+
+        $initial   = $this->createPackageMock();
+        $target    = $this->createPackageMock();
+        $operation = new UpdateOperation($initial, $target, 'test');
+
+        $initial
+            ->expects($this->once())
+            ->method('getType')
+            ->will($this->returnValue('library'));
+        $target
+            ->expects($this->once())
+            ->method('getType')
+            ->will($this->returnValue('bundles'));
+
+        $installer1
+            ->expects($this->once())
+            ->method('uninstall')
+            ->with($initial);
+
+        $installer2
+            ->expects($this->once())
+            ->method('install')
+            ->with($target);
+
+        $manager->update($operation);
+    }
+
+    public function testUninstall()
+    {
+        $installer = $this->createInstallerMock();
+        $manager   = new InstallationManager();
+        $manager->setInstaller('library', $installer);
+
+        $package   = $this->createPackageMock();
+        $operation = new UninstallOperation($package, 'test');
+
+        $package
+            ->expects($this->once())
+            ->method('getType')
+            ->will($this->returnValue('library'));
+
+        $installer
+            ->expects($this->once())
+            ->method('uninstall')
+            ->with($package);
+
+        $manager->uninstall($operation);
+    }
+
+    private function createInstallerMock()
+    {
+        return $this->getMockBuilder('Composer\Installer\InstallerInterface')
+            ->getMock();
+    }
+
+    private function createPackageMock()
+    {
+        return $this->getMockBuilder('Composer\Package\PackageInterface')
+            ->getMock();
+    }
+}

+ 169 - 0
tests/Composer/Test/Installer/LibraryInstallerTest.php

@@ -0,0 +1,169 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Installer;
+
+use Composer\Installer\LibraryInstaller;
+use Composer\DependencyResolver\Operation;
+
+class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
+{
+    private $dir;
+    private $dm;
+    private $repository;
+    private $library;
+
+    protected function setUp()
+    {
+        $this->dir = sys_get_temp_dir().'/composer';
+        if (is_dir($this->dir)) {
+            rmdir($this->dir);
+        }
+
+        $this->dm = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->repository = $this->getMockBuilder('Composer\Repository\WritableRepositoryInterface')
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+
+    public function testInstallerCreation()
+    {
+        $library = new LibraryInstaller($this->dir, $this->dm, $this->repository);
+        $this->assertTrue(is_dir($this->dir));
+
+        $file = sys_get_temp_dir().'/file';
+        touch($file);
+
+        $this->setExpectedException('UnexpectedValueException');
+        $library = new LibraryInstaller($file, $this->dm, $this->repository);
+    }
+
+    public function testIsInstalled()
+    {
+        $library = new LibraryInstaller($this->dir, $this->dm, $this->repository);
+        $package = $this->createPackageMock();
+
+        $this->repository
+            ->expects($this->exactly(2))
+            ->method('hasPackage')
+            ->with($package)
+            ->will($this->onConsecutiveCalls(true, false));
+
+        $this->assertTrue($library->isInstalled($package));
+        $this->assertFalse($library->isInstalled($package));
+    }
+
+    public function testInstall()
+    {
+        $library = new LibraryInstaller($this->dir, $this->dm, $this->repository);
+        $package = $this->createPackageMock();
+
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('some/package'));
+
+        $this->dm
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, $this->dir.'/some/package');
+
+        $this->repository
+            ->expects($this->once())
+            ->method('addPackage')
+            ->with($package);
+
+        $library->install($package);
+    }
+
+    public function testUpdate()
+    {
+        $library = new LibraryInstaller($this->dir, $this->dm, $this->repository);
+        $initial = $this->createPackageMock();
+        $target  = $this->createPackageMock();
+
+        $initial
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('package1'));
+
+        $this->repository
+            ->expects($this->exactly(2))
+            ->method('hasPackage')
+            ->with($initial)
+            ->will($this->onConsecutiveCalls(true, false));
+
+        $this->dm
+            ->expects($this->once())
+            ->method('update')
+            ->with($initial, $target, $this->dir.'/package1');
+
+        $this->repository
+            ->expects($this->once())
+            ->method('removePackage')
+            ->with($initial);
+
+        $this->repository
+            ->expects($this->once())
+            ->method('addPackage')
+            ->with($target);
+
+        $library->update($initial, $target);
+
+        $this->setExpectedException('InvalidArgumentException');
+
+        $library->update($initial, $target);
+    }
+
+    public function testUninstall()
+    {
+        $library = new LibraryInstaller($this->dir, $this->dm, $this->repository);
+        $package = $this->createPackageMock();
+
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('pkg'));
+
+        $this->repository
+            ->expects($this->exactly(2))
+            ->method('hasPackage')
+            ->with($package)
+            ->will($this->onConsecutiveCalls(true, false));
+
+        $this->dm
+            ->expects($this->once())
+            ->method('remove')
+            ->with($package, $this->dir.'/pkg');
+
+        $this->repository
+            ->expects($this->once())
+            ->method('removePackage')
+            ->with($package);
+
+        $library->uninstall($package);
+
+        $this->setExpectedException('InvalidArgumentException');
+
+        $library->uninstall($package);
+    }
+
+    private function createPackageMock()
+    {
+        return $this->getMockBuilder('Composer\Package\MemoryPackage')
+            ->setConstructorArgs(array(md5(rand()), '1.0.0'))
+            ->getMock();
+    }
+}

+ 27 - 1
tests/Composer/Test/Repository/ArrayRepositoryTest.php

@@ -17,11 +17,37 @@ use Composer\Package\MemoryPackage;
 
 class ArrayRepositoryTest extends \PHPUnit_Framework_TestCase
 {
-    public function testAddLiteral()
+    public function testAddPackage()
     {
         $repo = new ArrayRepository;
         $repo->addPackage(new MemoryPackage('foo', '1'));
 
         $this->assertEquals(1, count($repo));
     }
+
+    public function testRemovePackage()
+    {
+        $package = new MemoryPackage('bar', '2');
+
+        $repo = new ArrayRepository;
+        $repo->addPackage(new MemoryPackage('foo', '1'));
+        $repo->addPackage($package);
+
+        $this->assertEquals(2, count($repo));
+
+        $repo->removePackage(new MemoryPackage('foo', '1'));
+
+        $this->assertEquals(1, count($repo));
+        $this->assertEquals(array($package), $repo->getPackages());
+    }
+
+    public function testHasPackage()
+    {
+        $repo = new ArrayRepository;
+        $repo->addPackage(new MemoryPackage('foo', '1'));
+        $repo->addPackage(new MemoryPackage('bar', '2'));
+
+        $this->assertTrue($repo->hasPackage(new MemoryPackage('foo', '1')));
+        $this->assertFalse($repo->hasPackage(new MemoryPackage('bar', '1')));
+    }
 }

+ 55 - 0
tests/Composer/Test/Repository/FilesystemRepositoryTest.php

@@ -0,0 +1,55 @@
+<?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\Repository;
+
+use Composer\Repository\FilesystemRepository;
+
+class FilesystemRepositoryTest extends \PHPUnit_Framework_TestCase
+{
+    private $dir;
+    private $repositoryFile;
+
+    protected function setUp()
+    {
+        $this->dir = sys_get_temp_dir().'/.composer';
+        $this->repositoryFile = $this->dir.'/some_registry-reg.json';
+
+        if (file_exists($this->repositoryFile)) {
+            unlink($this->repositoryFile);
+        }
+    }
+
+    public function testRepositoryReadWrite()
+    {
+        $this->assertFileNotExists($this->repositoryFile);
+        $repository = new FilesystemRepository($this->repositoryFile);
+
+        $repository->getPackages();
+        $repository->write();
+        $this->assertFileExists($this->repositoryFile);
+
+        file_put_contents($this->repositoryFile, json_encode(array(
+            array('name' => 'package1', 'version' => '1.0.0-beta', 'type' => 'vendor')
+        )));
+
+        $repository = new FilesystemRepository($this->repositoryFile);
+        $repository->getPackages();
+        $repository->write();
+        $this->assertFileExists($this->repositoryFile);
+
+        $data = json_decode(file_get_contents($this->repositoryFile), true);
+        $this->assertEquals(array(
+            array('name' => 'package1', 'type' => 'vendor', 'version' => '1.0.0', 'releaseType' => 'beta', 'names' => array('package1'))
+        ), $data);
+    }
+}