Bläddra i källkod

Merge branch 'refactoring'

Jordi Boggiano 13 år sedan
förälder
incheckning
c6d7760145
44 ändrade filer med 2613 tillägg och 408 borttagningar
  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);
+    }
+}