소스 검색

Merge remote-tracking branch 'upstream/master' into svn-auth-reloaded

till 13 년 전
부모
커밋
c6566825ad
37개의 변경된 파일586개의 추가작업 그리고 208개의 파일을 삭제
  1. 4 3
      CHANGELOG.md
  2. 6 4
      composer.lock
  3. 2 2
      doc/00-intro.md
  4. 1 1
      doc/01-basic-usage.md
  5. 7 2
      doc/03-cli.md
  6. 2 2
      src/Composer/Autoload/ClassLoader.php
  7. 8 0
      src/Composer/Command/Command.php
  8. 4 3
      src/Composer/Command/CreateProjectCommand.php
  9. 1 1
      src/Composer/Command/InitCommand.php
  10. 1 1
      src/Composer/Command/InstallCommand.php
  11. 2 1
      src/Composer/Command/SearchCommand.php
  12. 4 5
      src/Composer/Command/SelfUpdateCommand.php
  13. 2 1
      src/Composer/Command/ShowCommand.php
  14. 2 1
      src/Composer/Command/ValidateCommand.php
  15. 2 0
      src/Composer/Console/Application.php
  16. 150 0
      src/Composer/DependencyResolver/Problem.php
  17. 1 0
      src/Composer/DependencyResolver/Request.php
  18. 75 0
      src/Composer/DependencyResolver/Rule.php
  19. 59 65
      src/Composer/DependencyResolver/Solver.php
  20. 13 34
      src/Composer/DependencyResolver/SolverProblemsException.php
  21. 0 1
      src/Composer/Downloader/FileDownloader.php
  22. 3 2
      src/Composer/Factory.php
  23. 14 4
      src/Composer/Installer.php
  24. 17 10
      src/Composer/Json/JsonFile.php
  25. 6 2
      src/Composer/Repository/ComposerRepository.php
  26. 16 15
      src/Composer/Repository/PearRepository.php
  27. 10 12
      src/Composer/Repository/Vcs/GitDriver.php
  28. 2 2
      src/Composer/Repository/VcsRepository.php
  29. 52 0
      src/Composer/Util/ErrorHandler.php
  30. 5 4
      src/Composer/Util/ProcessExecutor.php
  31. 42 13
      src/Composer/Util/RemoteFilesystem.php
  32. 3 3
      src/Composer/Util/StreamContextFactory.php
  33. 5 5
      tests/Composer/Test/DependencyResolver/RequestTest.php
  34. 11 5
      tests/Composer/Test/DependencyResolver/SolverTest.php
  35. 49 0
      tests/Composer/Test/Util/ErrorHandlerTest.php
  36. 2 1
      tests/Composer/Test/Util/RemoteFilesystemTest.php
  37. 3 3
      tests/Composer/Test/Util/StreamContextFactoryTest.php

+ 4 - 3
CHANGELOG.md

@@ -2,13 +2,14 @@
 
   * Added `create-project` command to install a project from scratch with composer
   * Added automated `classmap` autoloading support for non-PSR-0 compliant projects
-  * Improved clones from GitHub which now automatically select between git/https/http protocols
+  * Added human readable error reporting when deps can not be solved
   * Added support for private GitHub repositories (use --no-interaction for CI)
-  * Improved `validate` command to give more feedback
   * Added "file" downloader type to download plain files
   * Added support for authentication with svn repositories
-  * Removed dependency on filter_var
+  * Improved clones from GitHub which now automatically select between git/https/http protocols
+  * Improved `validate` command to give more feedback
   * Improved the `search` & `show` commands output
+  * Removed dependency on filter_var
   * Various robustness & error handling improvements, docs fixes and more
 
 * 1.0.0-alpha1 (2012-03-01)

+ 6 - 4
composer.lock

@@ -14,18 +14,20 @@
         {
             "package": "symfony/console",
             "version": "dev-master",
-            "source-reference": "2ee50c7c845ef7f8bce9c540709ecfd64cbcda87"
+            "source-reference": "88a283f6c18d1e644ebca1e3df4c870eac8108dd"
         },
         {
             "package": "symfony/finder",
             "version": "dev-master",
-            "source-reference": "b3adc8d5c29593db93c0abc4711a1e25fd3a6fa0"
+            "source-reference": "be30ecc95281d729ee51b9e89644d442bcf60451"
         },
         {
             "package": "symfony/process",
             "version": "dev-master",
-            "source-reference": "6aceac404d8574cf7da57e7e29b00a665b7bd559"
+            "source-reference": "0aad81ae9f884cf7df6387cb52a11b5b4f07b3d6"
         }
     ],
-    "aliases": []
+    "aliases": [
+
+    ]
 }

+ 2 - 2
doc/00-intro.md

@@ -44,7 +44,7 @@ any version beginning with `1.0`.
 
 ## Installation
 
-### 1) Downloading the Composer Executable
+### Downloading the Composer Executable
 
 To actually get Composer, we need to do two things. The first one is installing
 Composer (again, this mean downloading it into your project):
@@ -65,7 +65,7 @@ You can place this file anywhere you wish. If you put it in your `PATH`,
 you can access it globally. On unixy systems you can even make it
 executable and invoke it without `php`.
 
-### 2) Using Composer
+### Using Composer
 
 Next, run the command the `install` command to resolve and download dependencies:
 

+ 1 - 1
doc/01-basic-usage.md

@@ -144,7 +144,7 @@ means that we can just start using classes from it, and they will be
 autoloaded.
 
     $log = new Monolog\Logger('name');
-    $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Logger::WARNING));
+    $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
 
     $log->addWarning('Foo');
 

+ 7 - 2
doc/03-cli.md

@@ -179,10 +179,15 @@ directory to something other than `vendor/bin`.
 This env var controls the time composer waits for commands (such as git
 commands) to finish executing. The default value is 60 seconds.
 
-### HTTP_PROXY
+### http_proxy or HTTP_PROXY
 
 If you are using composer from behind an HTTP proxy, you can use the standard
-`HTTP_PROXY` or `http_proxy` env vars. Simply set it to the URL of your proxy.
+`http_proxy` or `HTTP_PROXY` env vars. Simply set it to the URL of your proxy.
 Many operating systems already set this variable for you.
 
+Using `http_proxy` (lowercased) or even defining both might be preferrable since
+some tools like git or curl will only use the lower-cased `http_proxy` version.
+Alternatively you can also define the git proxy using
+`git config --global http.proxy <proxy url>`.
+
 &larr; [Libraries](02-libraries.md)  |  [Schema](04-schema.md) &rarr;

+ 2 - 2
src/Composer/Autoload/ClassLoader.php

@@ -181,8 +181,8 @@ class ClassLoader
         $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
 
         foreach ($this->prefixes as $prefix => $dirs) {
-            foreach ($dirs as $dir) {
-                if (0 === strpos($class, $prefix)) {
+            if (0 === strpos($class, $prefix)) {
+                foreach ($dirs as $dir) {
                     if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
                         return $dir . DIRECTORY_SEPARATOR . $classPath;
                     }

+ 8 - 0
src/Composer/Command/Command.php

@@ -29,4 +29,12 @@ abstract class Command extends BaseCommand
     {
         return $this->getApplication()->getComposer($required);
     }
+
+    /**
+     * @return \Composer\IO\ConsoleIO
+     */
+    protected function getIO()
+    {
+        return $this->getApplication()->getIO();
+    }
 }

+ 4 - 3
src/Composer/Command/CreateProjectCommand.php

@@ -23,6 +23,7 @@ use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Composer\Json\JsonFile;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * Install a package as new project into new directory.
@@ -84,11 +85,11 @@ EOT
         }
 
         if (null === $repositoryUrl) {
-            $sourceRepo = new ComposerRepository(array('url' => 'http://packagist.org'));
+            $sourceRepo = new ComposerRepository(array('url' => 'http://packagist.org'), $this->getIO());
         } elseif (".json" === substr($repositoryUrl, -5)) {
-            $sourceRepo = new FilesystemRepository(new JsonFile($repositoryUrl));
+            $sourceRepo = new FilesystemRepository(new JsonFile($repositoryUrl, new RemoteFilesystem($io)));
         } elseif (0 === strpos($repositoryUrl, 'http')) {
-            $sourceRepo = new ComposerRepository(array('url' => $repositoryUrl));
+            $sourceRepo = new ComposerRepository(array('url' => $repositoryUrl), $this->getIO());
         } else {
             throw new \InvalidArgumentException("Invalid repository url given. Has to be a .json file or an http url.");
         }

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

@@ -229,7 +229,7 @@ EOT
         if (!$this->repos) {
             $this->repos = new CompositeRepository(array(
                 new PlatformRepository,
-                new ComposerRepository(array('url' => 'http://packagist.org'))
+                new ComposerRepository(array('url' => 'http://packagist.org'), $this->getIO())
             ));
         }
 

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

@@ -61,6 +61,6 @@ EOT
             ->setInstallSuggests($input->getOption('install-suggests'))
         ;
 
-        return $install->run();
+        return $install->run() ? 0 : 1;
     }
 }

+ 2 - 1
src/Composer/Command/SearchCommand.php

@@ -54,7 +54,8 @@ EOT
         } else {
             $output->writeln('No composer.json found in the current directory, showing packages from packagist.org');
             $installedRepo = $platformRepo;
-            $repos = new CompositeRepository(array($installedRepo, new ComposerRepository(array('url' => 'http://packagist.org'))));
+            $packagist = new ComposerRepository(array('url' => 'http://packagist.org'), $this->getIO());
+            $repos = new CompositeRepository(array($installedRepo, $packagist));
         }
 
         $tokens = $input->getArgument('tokens');

+ 4 - 5
src/Composer/Command/SelfUpdateCommand.php

@@ -13,7 +13,7 @@
 namespace Composer\Command;
 
 use Composer\Composer;
-use Composer\Util\StreamContextFactory;
+use Composer\Util\RemoteFilesystem;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
@@ -40,9 +40,8 @@ EOT
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $ctx = StreamContextFactory::getContext();
-
-        $latest = trim(file_get_contents('http://getcomposer.org/version', false, $ctx));
+        $rfs = new RemoteFilesystem($this->getIO());
+        $latest = trim($rfs->getContents('getcomposer.org', 'http://getcomposer.org/version', false));
 
         if (Composer::VERSION !== $latest) {
             $output->writeln(sprintf("Updating to version <info>%s</info>.", $latest));
@@ -50,7 +49,7 @@ EOT
             $remoteFilename = 'http://getcomposer.org/composer.phar';
             $localFilename = $_SERVER['argv'][0];
 
-            copy($remoteFilename, $localFilename, $ctx);
+            $rfs->copy('getcomposer.org', $remoteFilename, $localFilename);
         } else {
             $output->writeln("<info>You are using the latest composer version.</info>");
         }

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

@@ -65,7 +65,8 @@ EOT
         } else {
             $output->writeln('No composer.json found in the current directory, showing packages from packagist.org');
             $installedRepo = $platformRepo;
-            $repos = new CompositeRepository(array($installedRepo, new ComposerRepository(array('url' => 'http://packagist.org'))));
+            $packagist = new ComposerRepository(array('url' => 'http://packagist.org'), $this->getIO());
+            $repos = new CompositeRepository(array($installedRepo, $packagist));
         }
 
         // show single package or single version

+ 2 - 1
src/Composer/Command/ValidateCommand.php

@@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Output\OutputInterface;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonValidationException;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * @author Robert Schönthal <seroscho@googlemail.com>
@@ -55,7 +56,7 @@ EOT
 
         $laxValid = false;
         try {
-            $json = new JsonFile($file);
+            $json = new JsonFile($file, new RemoteFilesystem($this->getIO()));
             $json->read();
 
             $json->validateSchema(JsonFile::LAX_SCHEMA);

+ 2 - 0
src/Composer/Console/Application.php

@@ -25,6 +25,7 @@ use Composer\Composer;
 use Composer\Factory;
 use Composer\IO\IOInterface;
 use Composer\IO\ConsoleIO;
+use Composer\Util\ErrorHandler;
 
 /**
  * The console application that handles the commands
@@ -40,6 +41,7 @@ class Application extends BaseApplication
 
     public function __construct()
     {
+        ErrorHandler::register();
         parent::__construct('Composer', Composer::VERSION);
     }
 

+ 150 - 0
src/Composer/DependencyResolver/Problem.php

@@ -0,0 +1,150 @@
+<?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;
+
+/**
+ * Represents a problem detected while solving dependencies
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class Problem
+{
+    /**
+     * A set of reasons for the problem, each is a rule or a job and a rule
+     * @var array
+     */
+    protected $reasons;
+
+    /**
+     * Add a job as a reason
+     *
+     * @param   array   $job    A job descriptor which is a reason for this problem
+     * @param   Rule    $rule   An optional rule associated with the job
+     */
+    public function addJobRule($job, Rule $rule = null)
+    {
+        $this->addReason(serialize($job), array(
+            'rule' => $rule,
+            'job' => $job,
+        ));
+    }
+
+    /**
+     * Add a rule as a reason
+     *
+     * @param   Rule    $rule   A rule which is a reason for this problem
+     */
+    public function addRule(Rule $rule)
+    {
+        $this->addReason($rule->getId(), array(
+            'rule' => $rule,
+            'job' => null,
+        ));
+    }
+
+    /**
+     * Retrieve all reasons for this problem
+     *
+     * @return  array   The problem's reasons
+     */
+    public function getReasons()
+    {
+        return $this->reasons;
+    }
+
+    /**
+     * A human readable textual representation of the problem's reasons
+     */
+    public function __toString()
+    {
+        if (count($this->reasons) === 1) {
+            reset($this->reasons);
+            $reason = current($this->reasons);
+
+            $rule = $reason['rule'];
+            $job = $reason['job'];
+
+            if ($job && $job['cmd'] === 'install' && empty($job['packages'])) {
+                // handle php extensions
+                if (0 === stripos($job['packageName'], 'ext-')) {
+                    $ext = substr($job['packageName'], 4);
+                    $error = extension_loaded($ext) ? 'has the wrong version ('.phpversion($ext).') installed' : 'is missing from your system';
+                    return 'The requested PHP extension "'.$job['packageName'].'" '.$this->constraintToText($job['constraint']).$error.'.';
+                }
+                return 'The requested package "'.$job['packageName'].'" '.$this->constraintToText($job['constraint']).'could not be found.';
+            }
+        }
+
+        $messages = array("Problem caused by:");
+
+        foreach ($this->reasons as $reason) {
+
+            $rule = $reason['rule'];
+            $job = $reason['job'];
+
+            if ($job) {
+                $messages[] = $this->jobToText($job);
+            } elseif ($rule) {
+                if ($rule instanceof Rule) {
+                    $messages[] = $rule->toHumanReadableString();
+                }
+            }
+        }
+
+        return implode("\n\t\t\t- ", $messages);
+    }
+
+    /**
+     * Store a reason descriptor but ignore duplicates
+     *
+     * @param   string  $id         A canonical identifier for the reason
+     * @param   string  $reason     The reason descriptor
+     */
+    protected function addReason($id, $reason)
+    {
+        if (!isset($this->reasons[$id])) {
+            $this->reasons[$id] = $reason;
+        }
+    }
+
+    /**
+     * Turns a job into a human readable description
+     *
+     * @param   array   $job
+     * @return  string
+     */
+    protected function jobToText($job)
+    {
+        switch ($job['cmd']) {
+            case 'install':
+                return 'Installation of package "'.$job['packageName'].'" '.$this->constraintToText($job['constraint']).'was requested. Satisfiable by packages ['.implode(', ', $job['packages']).'].';
+            case 'update':
+                return 'Update of package "'.$job['packageName'].'" '.$this->constraintToText($job['constraint']).'was requested.';
+            case 'remove':
+                return 'Removal of package "'.$job['packageName'].'" '.$this->constraintToText($job['constraint']).'was requested.';
+        }
+
+        return 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.implode(', ', $job['packages']).'])';
+    }
+
+    /**
+     * Turns a constraint into text usable in a sentence describing a job
+     *
+     * @param   LinkConstraint  $constraint
+     * @return  string
+     */
+    protected function constraintToText($constraint)
+    {
+        return ($constraint) ? 'with constraint '.$constraint.' ' : '';
+    }
+}

+ 1 - 0
src/Composer/DependencyResolver/Request.php

@@ -52,6 +52,7 @@ class Request
             'packages' => $packages,
             'cmd' => $cmd,
             'packageName' => $packageName,
+            'constraint' => $constraint,
         );
     }
 

+ 75 - 0
src/Composer/DependencyResolver/Rule.php

@@ -17,6 +17,19 @@ namespace Composer\DependencyResolver;
  */
 class Rule
 {
+    const RULE_INTERNAL_ALLOW_UPDATE = 1;
+    const RULE_JOB_INSTALL = 2;
+    const RULE_JOB_REMOVE = 3;
+    const RULE_JOB_LOCK = 4;
+    const RULE_NOT_INSTALLABLE = 5;
+    const RULE_PACKAGE_CONFLICT = 6;
+    const RULE_PACKAGE_REQUIRES = 7;
+    const RULE_PACKAGE_OBSOLETES = 8;
+    const RULE_INSTALLED_PACKAGE_OBSOLETES = 9;
+    const RULE_PACKAGE_SAME_NAME = 10;
+    const RULE_PACKAGE_IMPLICIT_OBSOLETES = 11;
+    const RULE_LEARNED = 12;
+
     protected $disabled;
     protected $literals;
     protected $type;
@@ -163,6 +176,68 @@ class Rule
         }
     }
 
+    public function toHumanReadableString()
+    {
+        $ruleText = '';
+        foreach ($this->literals as $i => $literal) {
+            if ($i != 0) {
+                $ruleText .= '|';
+            }
+            $ruleText .= $literal;
+        }
+
+        switch ($this->reason) {
+            case self::RULE_INTERNAL_ALLOW_UPDATE:
+                return $ruleText;
+
+            case self::RULE_JOB_INSTALL:
+                return "Install command rule ($ruleText)";
+
+            case self::RULE_JOB_REMOVE:
+                return "Remove command rule ($ruleText)";
+
+            case self::RULE_JOB_LOCK:
+                return "Lock command rule ($ruleText)";
+
+            case self::RULE_NOT_INSTALLABLE:
+                return $ruleText;
+
+            case self::RULE_PACKAGE_CONFLICT:
+                $package1 = $this->literals[0]->getPackage();
+                $package2 = $this->literals[1]->getPackage();
+                return 'Package "'.$package1.'" conflicts with "'.$package2.'"';
+
+            case self::RULE_PACKAGE_REQUIRES:
+                $literals = $this->literals;
+                $sourceLiteral = array_shift($literals);
+                $sourcePackage = $sourceLiteral->getPackage();
+
+                $requires = array();
+                foreach ($literals as $literal) {
+                    $requires[] = $literal->getPackage();
+                }
+
+                $text = 'Package "'.$sourcePackage.'" contains the rule '.$this->reasonData.'. ';
+                if ($requires) {
+                    $text .= 'Any of these packages satisfy the dependency: '.implode(', ', $requires).'.';
+                } else {
+                    $text .= 'No package satisfies this dependency.';
+                }
+                return $text;
+
+            case self::RULE_PACKAGE_OBSOLETES:
+                return $ruleText;
+            case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
+                return $ruleText;
+            case self::RULE_PACKAGE_SAME_NAME:
+                return $ruleText;
+            case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
+                return $ruleText;
+            case self::RULE_LEARNED:
+                return 'learned: '.$ruleText;
+        }
+    }
+
     /**
      * Formats a rule as a string of the format (Literal1|Literal2|...)
      *

+ 59 - 65
src/Composer/DependencyResolver/Solver.php

@@ -21,21 +21,6 @@ use Composer\DependencyResolver\Operation;
  */
 class Solver
 {
-    const RULE_INTERNAL_ALLOW_UPDATE = 1;
-    const RULE_JOB_INSTALL = 2;
-    const RULE_JOB_REMOVE = 3;
-    const RULE_JOB_LOCK = 4;
-    const RULE_NOT_INSTALLABLE = 5;
-    const RULE_NOTHING_PROVIDES_DEP = 6;
-    const RULE_PACKAGE_CONFLICT = 7;
-    const RULE_PACKAGE_NOT_EXIST = 8;
-    const RULE_PACKAGE_REQUIRES = 9;
-    const RULE_PACKAGE_OBSOLETES = 10;
-    const RULE_INSTALLED_PACKAGE_OBSOLETES = 11;
-    const RULE_PACKAGE_SAME_NAME = 12;
-    const RULE_PACKAGE_IMPLICIT_OBSOLETES = 13;
-    const RULE_LEARNED = 14;
-
     protected $policy;
     protected $pool;
     protected $installed;
@@ -235,7 +220,7 @@ class Solver
             }
 
             if (!$dontFix && !$this->policy->installable($this, $this->pool, $this->installedMap, $package)) {
-                $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRemoveRule($package, self::RULE_NOT_INSTALLABLE, (string) $package));
+                $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRemoveRule($package, Rule::RULE_NOT_INSTALLABLE, (string) $package));
                 continue;
             }
 
@@ -261,7 +246,7 @@ class Solver
                     }
                 }
 
-                $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, self::RULE_PACKAGE_REQUIRES, (string) $link));
+                $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, (string) $link));
 
                 foreach ($possibleRequires as $require) {
                     $workQueue->enqueue($require);
@@ -276,7 +261,7 @@ class Solver
                         continue;
                     }
 
-                    $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, self::RULE_PACKAGE_CONFLICT, (string) $link));
+                    $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, (string) $link));
                 }
             }
 
@@ -301,7 +286,7 @@ class Solver
                             continue; // don't repair installed/installed problems
                         }
 
-                        $reason = ($isInstalled) ? self::RULE_INSTALLED_PACKAGE_OBSOLETES : self::RULE_PACKAGE_OBSOLETES;
+                        $reason = ($isInstalled) ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
                         $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $provider, $reason, (string) $link));
                     }
                 }
@@ -327,7 +312,7 @@ class Solver
                             continue;
                         }
 
-                        $reason = ($package->getName() == $provider->getName()) ? self::RULE_PACKAGE_SAME_NAME : self::RULE_PACKAGE_IMPLICIT_OBSOLETES;
+                        $reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
                         $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package));
                     }
                 }
@@ -466,24 +451,29 @@ class Solver
             $conflict = $this->findDecisionRule($literal->getPackage());
             /** TODO: handle conflict with systemsolvable? */
 
-            $this->learnedPool[] = array($rule, $conflict);
-
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
 
+                $problem = new Problem;
+
                 if ($rule->getType() == RuleSet::TYPE_JOB) {
-                    $why = $this->ruleToJob[$rule->getId()];
+                    $job = $this->ruleToJob[$rule->getId()];
+
+                    $problem->addJobRule($job, $rule);
+                    $problem->addRule($conflict);
+                    $this->disableProblem($job);
                 } else {
-                    $why = $rule;
+                    $problem->addRule($rule);
+                    $problem->addRule($conflict);
+                    $this->disableProblem($rule);
                 }
-                $this->problems[] = array($why);
-
-                $this->disableProblem($why);
+                $this->problems[] = $problem;
                 continue;
             }
 
             // conflict with another job or update/feature rule
-
-            $this->problems[] = array();
+            $problem = new Problem;
+            $problem->addRule($rule);
+            $problem->addRule($conflict);
 
             // push all of our rules (can only be feature or job rules)
             // asserting this literal on the problem stack
@@ -500,14 +490,16 @@ class Solver
                 }
 
                 if ($assertRule->getType() === RuleSet::TYPE_JOB) {
-                    $why = $this->ruleToJob[$assertRule->getId()];
+                    $job = $this->ruleToJob[$assertRule->getId()];
+
+                    $problem->addJobRule($job, $assertRule);
+                    $this->disableProblem($job);
                 } else {
-                    $why = $assertRule;
+                    $problem->addRule($assertRule);
+                    $this->disableProblem($assertRule);
                 }
-                $this->problems[count($this->problems) - 1][] = $why;
-
-                $this->disableProblem($why);
             }
+            $this->problems[] = $problem;
 
             // start over
             while (count($this->decisionQueue) > $decisionStart) {
@@ -966,7 +958,7 @@ class Solver
 
         foreach ($installedPackages as $package) {
             $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package);
-            $rule = $this->createUpdateRule($package, $updates, self::RULE_INTERNAL_ALLOW_UPDATE, (string) $package);
+            $rule = $this->createUpdateRule($package, $updates, Rule::RULE_INTERNAL_ALLOW_UPDATE, (string) $package);
 
             $rule->setWeak(true);
             $this->addRule(RuleSet::TYPE_FEATURE, $rule);
@@ -977,9 +969,11 @@ class Solver
             switch ($job['cmd']) {
                 case 'install':
                     if (empty($job['packages'])) {
-                        $this->problems[] = array($job);
+                        $problem = new Problem();
+                        $problem->addJobRule($job);
+                        $this->problems[] = $problem;
                     } else {
-                        $rule = $this->createInstallOneOfRule($job['packages'], self::RULE_JOB_INSTALL, $job['packageName']);
+                        $rule = $this->createInstallOneOfRule($job['packages'], Rule::RULE_JOB_INSTALL, $job['packageName']);
                         $this->addRule(RuleSet::TYPE_JOB, $rule);
                         $this->ruleToJob[$rule->getId()] = $job;
                     }
@@ -990,7 +984,7 @@ class Solver
 
                     // todo: cleandeps
                     foreach ($job['packages'] as $package) {
-                        $rule = $this->createRemoveRule($package, self::RULE_JOB_REMOVE);
+                        $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE);
                         $this->addRule(RuleSet::TYPE_JOB, $rule);
                         $this->ruleToJob[$rule->getId()] = $job;
                     }
@@ -998,9 +992,9 @@ class Solver
                 case 'lock':
                     foreach ($job['packages'] as $package) {
                         if (isset($this->installedMap[$package->getId()])) {
-                            $rule = $this->createInstallRule($package, self::RULE_JOB_LOCK);
+                            $rule = $this->createInstallRule($package, Rule::RULE_JOB_LOCK);
                         } else {
-                            $rule = $this->createRemoveRule($package, self::RULE_JOB_LOCK);
+                            $rule = $this->createRemoveRule($package, Rule::RULE_JOB_LOCK);
                         }
                         $this->addRule(RuleSet::TYPE_JOB, $rule);
                         $this->ruleToJob[$rule->getId()] = $job;
@@ -1028,7 +1022,7 @@ class Solver
         //solver_prepare_solutions(solv);
 
         if ($this->problems) {
-            throw new SolverProblemsException($this->problems, $this->learnedPool);
+            throw new SolverProblemsException($this->problems);
         }
 
         return $this->createTransaction();
@@ -1487,22 +1481,21 @@ class Solver
 
         $why = count($this->learnedPool) - 1;
         assert($learnedLiterals[0] !== null);
-        $newRule = new Rule($learnedLiterals, self::RULE_LEARNED, $why);
+        $newRule = new Rule($learnedLiterals, Rule::RULE_LEARNED, $why);
 
         return array($ruleLevel, $newRule, $why);
     }
 
-    private function analyzeUnsolvableRule($conflictRule, &$lastWeakWhy)
+    private function analyzeUnsolvableRule($problem, $conflictRule, &$lastWeakWhy)
     {
         $why = $conflictRule->getId();
 
         if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) {
-
             $learnedWhy = $this->learnedWhy[$why];
-            $problem = $this->learnedPool[$learnedWhy];
+            $problemRules = $this->learnedPool[$learnedWhy];
 
-            foreach ($problem as $problemRule) {
-                $this->analyzeUnsolvableRule($problemRule, $lastWeakWhy);
+            foreach ($problemRules as $problemRule) {
+                $this->analyzeUnsolvableRule($problem, $problemRule, $lastWeakWhy);
             }
             return;
         }
@@ -1520,24 +1513,22 @@ class Solver
         }
 
         if ($conflictRule->getType() == RuleSet::TYPE_JOB) {
-            $why = $this->ruleToJob[$conflictRule->getId()];
-        }
-
-        // if this problem was already found skip it
-        if (in_array($why, $this->problems[count($this->problems) - 1], true)) {
-            return;
+            $job = $this->ruleToJob[$conflictRule->getId()];
+            $problem->addJobRule($job, $conflictRule);
+        } else {
+            $problem->addRule($conflictRule);
         }
-
-        $this->problems[count($this->problems) - 1][] = $why;
     }
 
     private function analyzeUnsolvable($conflictRule, $disableRules)
     {
         $lastWeakWhy = null;
-        $this->problems[] = array();
-        $this->learnedPool[] = array($conflictRule);
+        $problem = new Problem;
+        $problem->addRule($conflictRule);
+
+        $this->analyzeUnsolvableRule($problem, $conflictRule, $lastWeakWhy);
 
-        $this->analyzeUnsolvableRule($conflictRule, $lastWeakWhy);
+        $this->problems[] = $problem;
 
         $seen = array();
         $literals = $conflictRule->getLiterals();
@@ -1569,9 +1560,9 @@ class Solver
             }
 
             $why = $this->decisionQueueWhy[$decisionId];
-            $this->learnedPool[count($this->learnedPool) - 1][] = $why;
+            $problem->addRule($why);
 
-            $this->analyzeUnsolvableRule($why, $lastWeakWhy);
+            $this->analyzeUnsolvableRule($problem, $why, $lastWeakWhy);
 
             $literals = $why->getLiterals();
 /* unnecessary because unlike rule.d, watch2 == 2nd literal, unless watch2 changed
@@ -1591,7 +1582,6 @@ class Solver
 
         if ($lastWeakWhy) {
             array_pop($this->problems);
-            array_pop($this->learnedPool);
 
             if ($lastWeakWhy->getType() === RuleSet::TYPE_JOB) {
                 $why = $this->ruleToJob[$lastWeakWhy];
@@ -1616,8 +1606,12 @@ class Solver
         }
 
         if ($disableRules) {
-            foreach ($this->problems[count($this->problems) - 1] as $why) {
-                $this->disableProblem($why);
+            foreach ($this->problems[count($this->problems) - 1] as $reason) {
+                if ($reason['job']) {
+                    $this->disableProblem($reason['job']);
+                } else {
+                    $this->disableProblem($reason['rule']);
+                }
             }
 
             $this->resetSolver();
@@ -1670,10 +1664,10 @@ class Solver
     {
         foreach ($this->rules->getIteratorFor(RuleSet::TYPE_LEARNED) as $rule) {
             $why = $this->learnedWhy[$rule->getId()];
-            $problem = $this->learnedPool[$why];
+            $problemRules = $this->learnedPool[$why];
 
             $foundDisabled = false;
-            foreach ($problem as $problemRule) {
+            foreach ($problemRules as $problemRule) {
                 if ($problemRule->disabled()) {
                     $foundDisabled = true;
                     break;

+ 13 - 34
src/Composer/DependencyResolver/SolverProblemsException.php

@@ -19,47 +19,26 @@ class SolverProblemsException extends \RuntimeException
 {
     protected $problems;
 
-    public function __construct(array $problems, array $learnedPool)
+    public function __construct(array $problems)
     {
-        $message = '';
-        foreach ($problems as $i => $problem) {
-            $message .= '[';
-            foreach ($problem as $why) {
+        $this->problems = $problems;
 
-                if (is_int($why) && isset($learnedPool[$why])) {
-                    $rules = $learnedPool[$why];
-                } else {
-                    $rules = $why;
-                }
+        parent::__construct($this->createMessage());
+    }
+
+    protected function createMessage()
+    {
+        $messages = array();
 
-                if (isset($rules['packages'])) {
-                    $message .= $this->jobToText($rules);
-                } else {
-                    $message .= '(';
-                    foreach ($rules as $rule) {
-                        if ($rule instanceof Rule) {
-                            if ($rule->getType() == RuleSet::TYPE_LEARNED) {
-                                $message .= 'learned: ';
-                            }
-                            $message .= $rule . ', ';
-                        } else {
-                            $message .= 'String(' . $rule . '), ';
-                        }
-                    }
-                    $message .= ')';
-                }
-                $message .= ', ';
-            }
-            $message .= "]\n";
+        foreach ($this->problems as $problem) {
+            $messages[] = (string) $problem;
         }
 
-        parent::__construct($message);
+        return "\n\tProblems:\n\t\t- ".implode("\n\t\t- ", $messages);
     }
 
-    public function jobToText($job)
+    public function getProblems()
     {
-        //$output = serialize($job);
-        $output = 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.implode(', ', $job['packages']).'])';
-        return $output;
+        return $this->problems;
     }
 }

+ 0 - 1
src/Composer/Downloader/FileDownloader.php

@@ -74,7 +74,6 @@ class FileDownloader implements DownloaderInterface
         $url = $this->processUrl($url);
 
         $this->rfs->copy($package->getSourceUrl(), $url, $fileName);
-        $this->io->write('');
 
         if (!file_exists($fileName)) {
             throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'

+ 3 - 2
src/Composer/Factory.php

@@ -16,6 +16,7 @@ use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
 use Composer\Repository\RepositoryManager;
 use Composer\Util\ProcessExecutor;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * Creates an configured instance of composer.
@@ -38,7 +39,7 @@ class Factory
             $composerFile = getenv('COMPOSER') ?: 'composer.json';
         }
 
-        $file = new JsonFile($composerFile);
+        $file = new JsonFile($composerFile, new RemoteFilesystem($io));
         if (!$file->exists()) {
             if ($composerFile === 'composer.json') {
                 $message = 'Composer could not find a composer.json file in '.getcwd();
@@ -98,7 +99,7 @@ class Factory
 
         // init locker
         $lockFile = substr($composerFile, -5) === '.json' ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock';
-        $locker = new Package\Locker(new JsonFile($lockFile), $rm, md5_file($composerFile));
+        $locker = new Package\Locker(new JsonFile($lockFile, new RemoteFilesystem($io)), $rm, md5_file($composerFile));
 
         // initialize composer
         $composer = new Composer();

+ 14 - 4
src/Composer/Installer.php

@@ -18,6 +18,7 @@ use Composer\DependencyResolver\Operation\UpdateOperation;
 use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Request;
 use Composer\DependencyResolver\Solver;
+use Composer\DependencyResolver\SolverProblemsException;
 use Composer\Downloader\DownloadManager;
 use Composer\Installer\InstallationManager;
 use Composer\IO\IOInterface;
@@ -163,7 +164,7 @@ class Installer
         $installFromLock = false;
         $request = new Request($pool);
         if ($this->update) {
-            $this->io->write('<info>Updating dependencies</info>');
+            $this->io->write('Updating dependencies');
 
             $request->updateAll();
 
@@ -174,7 +175,7 @@ class Installer
             }
         } elseif ($this->locker->isLocked()) {
             $installFromLock = true;
-            $this->io->write('<info>Installing from lock file</info>');
+            $this->io->write('Installing from lock file');
 
             if (!$this->locker->isFresh()) {
                 $this->io->write('<warning>Your lock file is out of sync with your composer.json, run "composer.phar update" to update dependencies</warning>');
@@ -192,7 +193,7 @@ class Installer
                 $request->install($package->getName(), $constraint);
             }
         } else {
-            $this->io->write('<info>Installing dependencies</info>');
+            $this->io->write('Installing dependencies');
 
             $links = $this->collectLinks();
 
@@ -206,7 +207,14 @@ class Installer
         $solver = new Solver($policy, $pool, $installedRepo);
 
         // solve dependencies
-        $operations = $solver->solve($request);
+        try {
+            $operations = $solver->solve($request);
+        } catch (SolverProblemsException $e) {
+            $this->io->write('<error>Your requirements could not be solved to an installable set of packages.</error>');
+            $this->io->write($e->getMessage());
+
+            return false;
+        }
 
         // force dev packages to be updated to latest reference on update
         if ($this->update) {
@@ -299,6 +307,8 @@ class Installer
             $eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD;
             $this->eventDispatcher->dispatchCommandEvent($eventName);
         }
+
+        return true;
     }
 
     private function collectLinks()

+ 17 - 10
src/Composer/Json/JsonFile.php

@@ -17,6 +17,7 @@ use Composer\Composer;
 use JsonSchema\Validator;
 use Seld\JsonLint\JsonParser;
 use Composer\Util\StreamContextFactory;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * Reads/writes json files.
@@ -34,15 +35,22 @@ class JsonFile
     const JSON_UNESCAPED_UNICODE = 256;
 
     private $path;
+    private $rfs;
 
     /**
      * Initializes json file reader/parser.
      *
      * @param   string  $lockFile   path to a lockfile
+     * @param   RemoteFilesystem  $rfs   required for loading http/https json files
      */
-    public function __construct($path)
+    public function __construct($path, RemoteFilesystem $rfs = null)
     {
         $this->path = $path;
+
+        if (null === $rfs && preg_match('{^https?://}i', $path)) {
+            throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
+        }
+        $this->rfs = $rfs;
     }
 
     public function getPath()
@@ -67,15 +75,14 @@ class JsonFile
      */
     public function read()
     {
-        $ctx = StreamContextFactory::getContext(array(
-            'http' => array(
-                'header' => 'User-Agent: Composer/'.Composer::VERSION."\r\n"
-            )
-        ));
-
-        $json = file_get_contents($this->path, false, $ctx);
-        if (!$json) {
-            throw new \RuntimeException('Could not read '.$this->path.', you are probably offline');
+        try {
+            if ($this->rfs) {
+                $json = $this->rfs->getContents($this->path, $this->path, false);
+            } else {
+                $json = file_get_contents($this->path);
+            }
+        } catch (\Exception $e) {
+            throw new \RuntimeException('Could not read '.$this->path.', you are probably offline ('.$e->getMessage().')');
         }
 
         return static::parseJson($json);

+ 6 - 2
src/Composer/Repository/ComposerRepository.php

@@ -15,6 +15,8 @@ namespace Composer\Repository;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\LinkConstraint\VersionConstraint;
 use Composer\Json\JsonFile;
+use Composer\IO\IOInterface;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -22,9 +24,10 @@ use Composer\Json\JsonFile;
 class ComposerRepository extends ArrayRepository
 {
     protected $url;
+    protected $io;
     protected $packages;
 
-    public function __construct(array $config)
+    public function __construct(array $config, IOInterface $io)
     {
         if (!preg_match('{^\w+://}', $config['url'])) {
             // assume http as the default protocol
@@ -36,12 +39,13 @@ class ComposerRepository extends ArrayRepository
         }
 
         $this->url = $config['url'];
+        $this->io = $io;
     }
 
     protected function initialize()
     {
         parent::initialize();
-        $json     = new JsonFile($this->url.'/packages.json');
+        $json     = new JsonFile($this->url.'/packages.json', new RemoteFilesystem($this->io));
         $packages = $json->read();
         if (!$packages) {
             throw new \UnexpectedValueException('Could not parse package list from the '.$this->url.' repository');

+ 16 - 15
src/Composer/Repository/PearRepository.php

@@ -12,8 +12,10 @@
 
 namespace Composer\Repository;
 
+use Composer\IO\IOInterface;
 use Composer\Package\Loader\ArrayLoader;
-use Composer\Util\StreamContextFactory;
+use Composer\Util\RemoteFilesystem;
+use Composer\Downloader\TransportException;
 
 /**
  * @author Benjamin Eberlei <kontakt@beberlei.de>
@@ -23,9 +25,10 @@ class PearRepository extends ArrayRepository
 {
     private $url;
     private $channel;
-    private $streamContext;
+    private $io;
+    private $rfs;
 
-    public function __construct(array $config)
+    public function __construct(array $config, IOInterface $io, RemoteFilesystem $rfs = null)
     {
         if (!preg_match('{^https?://}', $config['url'])) {
             $config['url'] = 'http://'.$config['url'];
@@ -36,20 +39,17 @@ class PearRepository extends ArrayRepository
         }
 
         $this->url = rtrim($config['url'], '/');
-
         $this->channel = !empty($config['channel']) ? $config['channel'] : null;
+        $this->io = $io;
+        $this->rfs = $rfs ?: new RemoteFilesystem($this->io);
     }
 
     protected function initialize()
     {
         parent::initialize();
 
-        set_error_handler(function($severity, $message, $file, $line) {
-            throw new \ErrorException($message, $severity, $severity, $file, $line);
-        });
-        $this->streamContext = StreamContextFactory::getContext();
+        $this->io->write('Initializing PEAR repository '.$this->url);
         $this->fetchFromServer();
-        restore_error_handler();
     }
 
     protected function fetchFromServer()
@@ -68,7 +68,7 @@ class PearRepository extends ArrayRepository
             try {
                 $packagesLink = str_replace("info.xml", "packagesinfo.xml", $link);
                 $this->fetchPear2Packages($this->url . $packagesLink);
-            } catch (\ErrorException $e) {
+            } catch (TransportException $e) {
                 if (false === strpos($e->getMessage(), '404')) {
                     throw $e;
                 }
@@ -81,7 +81,7 @@ class PearRepository extends ArrayRepository
 
     /**
      * @param   string $categoryLink
-     * @throws  ErrorException
+     * @throws  TransportException
      * @throws  InvalidArgumentException
      */
     private function fetchPearPackages($categoryLink)
@@ -99,7 +99,7 @@ class PearRepository extends ArrayRepository
 
             try {
                 $releasesXML = $this->requestXml($allReleasesLink);
-            } catch (\ErrorException $e) {
+            } catch (TransportException $e) {
                 if (strpos($e->getMessage(), '404')) {
                     continue;
                 }
@@ -120,8 +120,8 @@ class PearRepository extends ArrayRepository
                 );
 
                 try {
-                    $deps = file_get_contents($releaseLink . "/deps.".$pearVersion.".txt", false, $this->streamContext);
-                } catch (\ErrorException $e) {
+                    $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false);
+                } catch (TransportException $e) {
                     if (strpos($e->getMessage(), '404')) {
                         continue;
                     }
@@ -226,6 +226,7 @@ class PearRepository extends ArrayRepository
     {
         $loader = new ArrayLoader();
         $packagesXml = $this->requestXml($packagesLink);
+
         $informations = $packagesXml->getElementsByTagName('pi');
         foreach ($informations as $information) {
             $package = $information->getElementsByTagName('p')->item(0);
@@ -289,7 +290,7 @@ class PearRepository extends ArrayRepository
      */
     private function requestXml($url)
     {
-        $content = file_get_contents($url, false, $this->streamContext);
+        $content = $this->rfs->getContents($this->url, $url, false);
         if (!$content) {
             throw new \UnexpectedValueException('The PEAR channel at '.$url.' did not respond.');
         }

+ 10 - 12
src/Composer/Repository/Vcs/GitDriver.php

@@ -35,11 +35,10 @@ class GitDriver extends VcsDriver
             $this->repoDir = $this->url;
         } else {
             $this->repoDir = sys_get_temp_dir() . '/composer-' . preg_replace('{[^a-z0-9]}i', '-', $url) . '/';
-            $repoDir = escapeshellarg($this->repoDir);
             if (is_dir($this->repoDir)) {
-                $this->process->execute(sprintf('cd %s && git fetch origin', $repoDir), $output);
+                $this->process->execute('git fetch origin', $output, $this->repoDir);
             } else {
-                $this->process->execute(sprintf('git clone %s %s', $url, $repoDir), $output);
+                $this->process->execute(sprintf('git clone %s %s', $url, escapeshellarg($this->repoDir)), $output);
             }
         }
 
@@ -57,7 +56,7 @@ class GitDriver extends VcsDriver
 
             if ($this->isLocal) {
                 // select currently checked out branch if master is not available
-                $this->process->execute(sprintf('cd %s && git branch --no-color', escapeshellarg($this->repoDir)), $output);
+                $this->process->execute('git branch --no-color', $output, $this->repoDir);
                 $branches = $this->process->splitLines($output);
                 if (!in_array('* master', $branches)) {
                     foreach ($branches as $branch) {
@@ -69,7 +68,7 @@ class GitDriver extends VcsDriver
                 }
             } else {
                 // try to find a non-master remote HEAD branch
-                $this->process->execute(sprintf('cd %s && git branch --no-color -r', escapeshellarg($this->repoDir)), $output);
+                $this->process->execute('git branch --no-color -r', $output, $this->repoDir);
                 foreach ($this->process->splitLines($output) as $branch) {
                     if ($branch && preg_match('{/HEAD +-> +[^/]+/(\S+)}', $branch, $match)) {
                         $this->rootIdentifier = $match[1];
@@ -114,7 +113,7 @@ class GitDriver extends VcsDriver
     public function getComposerInformation($identifier)
     {
         if (!isset($this->infoCache[$identifier])) {
-            $this->process->execute(sprintf('cd %s && git show %s:composer.json', escapeshellarg($this->repoDir), escapeshellarg($identifier)), $composer);
+            $this->process->execute(sprintf('git show %s:composer.json', escapeshellarg($identifier)), $composer, $this->repoDir);
 
             if (!trim($composer)) {
                 return;
@@ -123,7 +122,7 @@ class GitDriver extends VcsDriver
             $composer = JsonFile::parseJson($composer);
 
             if (!isset($composer['time'])) {
-                $this->process->execute(sprintf('cd %s && git log -1 --format=%%at %s', escapeshellarg($this->repoDir), escapeshellarg($identifier)), $output);
+                $this->process->execute(sprintf('git log -1 --format=%%at %s', escapeshellarg($identifier)), $output, $this->repoDir);
                 $date = new \DateTime('@'.trim($output));
                 $composer['time'] = $date->format('Y-m-d H:i:s');
             }
@@ -139,7 +138,7 @@ class GitDriver extends VcsDriver
     public function getTags()
     {
         if (null === $this->tags) {
-            $this->process->execute(sprintf('cd %s && git tag', escapeshellarg($this->repoDir)), $output);
+            $this->process->execute('git tag', $output, $this->repoDir);
             $output = $this->process->splitLines($output);
             $this->tags = $output ? array_combine($output, $output) : array();
         }
@@ -156,10 +155,9 @@ class GitDriver extends VcsDriver
             $branches = array();
 
             $this->process->execute(sprintf(
-                'cd %s && git branch --no-color --no-abbrev -v %s',
-                escapeshellarg($this->repoDir),
+                'git branch --no-color --no-abbrev -v %s',
                 $this->isLocal ? '' : '-r'
-            ), $output);
+            ), $output, $this->repoDir);
             foreach ($this->process->splitLines($output) as $branch) {
                 if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
                     preg_match('{^(?:\* )? *(?:[^/]+/)?(\S+) *([a-f0-9]+) .*$}', $branch, $match);
@@ -186,7 +184,7 @@ class GitDriver extends VcsDriver
         if (static::isLocalUrl($url)) {
             $process = new ProcessExecutor();
             // check whether there is a git repo in that path
-            if ($process->execute(sprintf('cd %s && git tag', escapeshellarg($url)), $output) === 0) {
+            if ($process->execute('git tag', $output, $url) === 0) {
                 return true;
             }
         }

+ 2 - 2
src/Composer/Repository/VcsRepository.php

@@ -35,7 +35,7 @@ class VcsRepository extends ArrayRepository
 
         $this->url = $config['url'];
         $this->io = $io;
-        $this->type = $config['type'];
+        $this->type = isset($config['type']) ? $config['type'] : 'vcs';
     }
 
     public function setDebug($debug)
@@ -118,7 +118,7 @@ class VcsRepository extends ArrayRepository
                 }
             } catch (\Exception $e) {
                 if ($debug) {
-                    $this->io->write('Skipped tag '.$tag.', '.$e->getMessage());
+                    $this->io->write('Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found' : $e->getMessage()));
                 }
                 continue;
             }

+ 52 - 0
src/Composer/Util/ErrorHandler.php

@@ -0,0 +1,52 @@
+<?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\Util;
+
+/**
+ * Convert PHP errors into exceptions
+ *
+ * @author Artem Lopata <biozshock@gmail.com>
+ */
+class ErrorHandler
+{
+    /**
+     * Error handler
+     *
+     * @param int    $level   Level of the error raised
+     * @param string $message Error message
+     * @param string $file    Filename that the error was raised in
+     * @param int    $line    Line number the error was raised at
+     *
+     * @static
+     * @throws \ErrorException
+     */
+    public static function handle($level, $message, $file, $line)
+    {
+        // respect error_reporting being disabled
+        if (!error_reporting()) {
+            return;
+        }
+
+        throw new \ErrorException($message, 0, $level, $file, $line);
+    }
+
+    /**
+     * Register error handler
+     *
+     * @static
+     */
+    public static function register()
+    {
+        set_error_handler(array(__CLASS__, 'handle'));
+    }
+}

+ 5 - 4
src/Composer/Util/ProcessExecutor.php

@@ -26,15 +26,16 @@ class ProcessExecutor
     /**
      * runs a process on the commandline
      *
-     * @param $command the command to execute
-     * @param null $output the output will be written into this var if passed
+     * @param string $command the command to execute
+     * @param null   $output  the output will be written into this var if passed
+     * @param string $cwd     the working directory
      * @return int statuscode
      */
-    public function execute($command, &$output = null)
+    public function execute($command, &$output = null, $cwd = null)
     {
         $captureOutput = count(func_get_args()) > 1;
         $this->errorOutput = null;
-        $process = new Process($command, null, null, null, static::getTimeout());
+        $process = new Process($command, $cwd, null, null, static::getTimeout());
         $process->run(function($type, $buffer) use ($captureOutput) {
             if ($captureOutput) {
                 return;

+ 42 - 13
src/Composer/Util/RemoteFilesystem.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Util;
 
+use Composer\Composer;
 use Composer\IO\IOInterface;
 use Composer\Downloader\TransportException;
 
@@ -101,26 +102,50 @@ class RemoteFilesystem
         }
 
         $result = @file_get_contents($fileUrl, false, $ctx);
-        if (null !== $fileName) {
-            $result = @file_put_contents($fileName, $result) ? true : false;
-        }
 
         // fix for 5.4.0 https://bugs.php.net/bug.php?id=61336
         if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ 404}i', $http_response_header[0])) {
             $result = false;
         }
 
+        // decode gzip
+        if (false !== $result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http') {
+            $decode = false;
+            foreach ($http_response_header as $header) {
+                if (preg_match('{^content-encoding: *gzip *$}i', $header)) {
+                    $decode = true;
+                    continue;
+                } elseif (preg_match('{^HTTP/}i', $header)) {
+                    $decode = false;
+                }
+            }
+
+            if ($decode) {
+                if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+                    $result = zlib_decode($result);
+                } else {
+                    // work around issue with gzuncompress & co that do not work with all gzip checksums
+                    $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result));
+                }
+            }
+        }
+
+        // handle copy command if download was successful
+        if (false !== $result && null !== $fileName) {
+            $result = (Boolean) @file_put_contents($fileName, $result);
+        }
+
         // avoid overriding if content was loaded by a sub-call to get()
         if (null === $this->result) {
             $this->result = $result;
         }
 
         if ($this->progress) {
-            $this->io->overwrite("    Downloading", false);
+            $this->io->overwrite("    Downloading: <comment>100%</comment>");
         }
 
         if (false === $this->result) {
-            throw new TransportException("The '$fileUrl' file could not be downloaded");
+            throw new TransportException('The "'.$fileUrl.'" file could not be downloaded');
         }
     }
 
@@ -138,7 +163,7 @@ class RemoteFilesystem
     {
         switch ($notificationCode) {
             case STREAM_NOTIFY_FAILURE:
-                throw new TransportException(trim($message), $messageCode);
+                throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', $messageCode);
                 break;
 
             case STREAM_NOTIFY_AUTH_REQUIRED:
@@ -184,17 +209,21 @@ class RemoteFilesystem
         }
     }
 
-    protected function getOptionsForUrl($url)
+    protected function getOptionsForUrl($originUrl)
     {
-        $options = array();
-        if ($this->io->hasAuthorization($url)) {
-            $auth = $this->io->getAuthorization($url);
+        $options['http']['header'] = 'User-Agent: Composer/'.Composer::VERSION."\r\n";
+        if (extension_loaded('zlib')) {
+            $options['http']['header'] .= 'Accept-Encoding: gzip'."\r\n";
+        }
+
+        if ($this->io->hasAuthorization($originUrl)) {
+            $auth = $this->io->getAuthorization($originUrl);
             $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
-            $options['http'] = array('header' => "Authorization: Basic $authStr\r\n");
+            $options['http']['header'] .= "Authorization: Basic $authStr\r\n";
         } elseif (null !== $this->io->getLastUsername()) {
             $authStr = base64_encode($this->io->getLastUsername() . ':' . $this->io->getLastPassword());
-            $options['http'] = array('header' => "Authorization: Basic $authStr\r\n");
-            $this->io->setAuthorization($url, $this->io->getLastUsername(), $this->io->getLastPassword());
+            $options['http']['header'] .= "Authorization: Basic $authStr\r\n";
+            $this->io->setAuthorization($originUrl, $this->io->getLastUsername(), $this->io->getLastPassword());
         }
 
         return $options;

+ 3 - 3
src/Composer/Util/StreamContextFactory.php

@@ -34,15 +34,15 @@ final class StreamContextFactory
         // Handle system proxy
         if (isset($_SERVER['HTTP_PROXY']) || isset($_SERVER['http_proxy'])) {
             // Some systems seem to rely on a lowercased version instead...
-            $proxy = isset($_SERVER['HTTP_PROXY']) ? $_SERVER['HTTP_PROXY'] : $_SERVER['http_proxy'];
-            
+            $proxy = isset($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY'];
+
             // http(s):// is not supported in proxy
             $proxy = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxy);
 
             if (0 === strpos($proxy, 'ssl:') && !extension_loaded('openssl')) {
                 throw new \RuntimeException('You must enable the openssl extension to use a proxy over https');
             }
-            
+
             $options['http'] = array(
                 'proxy'           => $proxy,
                 'request_fulluri' => true,

+ 5 - 5
tests/Composer/Test/DependencyResolver/RequestTest.php

@@ -40,9 +40,9 @@ class RequestTest extends TestCase
 
         $this->assertEquals(
             array(
-                array('packages' => array($foo), 'cmd' => 'install', 'packageName' => 'foo'),
-                array('packages' => array($bar), 'cmd' => 'install', 'packageName' => 'bar'),
-                array('packages' => array($foobar), 'cmd' => 'remove', 'packageName' => 'foobar'),
+                array('packages' => array($foo), 'cmd' => 'install', 'packageName' => 'foo', 'constraint' => null),
+                array('packages' => array($bar), 'cmd' => 'install', 'packageName' => 'bar', 'constraint' => null),
+                array('packages' => array($foobar), 'cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null),
             ),
             $request->getJobs());
     }
@@ -63,11 +63,11 @@ class RequestTest extends TestCase
         $pool->addRepository($repo2);
 
         $request = new Request($pool);
-        $request->install('foo', $this->getVersionConstraint('=', '1'));
+        $request->install('foo', $constraint = $this->getVersionConstraint('=', '1'));
 
         $this->assertEquals(
             array(
-                    array('packages' => array($foo1, $foo2), 'cmd' => 'install', 'packageName' => 'foo'),
+                    array('packages' => array($foo1, $foo2), 'cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint),
             ),
             $request->getJobs()
         );

+ 11 - 5
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -59,13 +59,15 @@ class SolverTest extends TestCase
         $this->repo->addPackage($this->getPackage('A', '1.0'));
         $this->reposComplete();
 
-        $this->request->install('B');
+        $this->request->install('B', $this->getVersionConstraint('=', '1'));
 
         try {
             $transaction = $this->solver->solve($this->request);
-            $this->fail('Unsolvable conflict did not resolve in exception.');
+            $this->fail('Unsolvable conflict did not result in exception.');
         } catch (SolverProblemsException $e) {
-            // TODO assert problem properties
+            $problems = $e->getProblems();
+            $this->assertEquals(1, count($problems));
+            $this->assertEquals('The requested package "b" with constraint == 1.0.0.0 could not be found.', (string) $problems[0]);
         }
     }
 
@@ -589,8 +591,10 @@ class SolverTest extends TestCase
 
         try {
             $transaction = $this->solver->solve($this->request);
-            $this->fail('Unsolvable conflict did not resolve in exception.');
+            $this->fail('Unsolvable conflict did not result in exception.');
         } catch (SolverProblemsException $e) {
+            $problems = $e->getProblems();
+            $this->assertEquals(1, count($problems));
             // TODO assert problem properties
         }
     }
@@ -610,8 +614,10 @@ class SolverTest extends TestCase
 
         try {
             $transaction = $this->solver->solve($this->request);
-            $this->fail('Unsolvable conflict did not resolve in exception.');
+            $this->fail('Unsolvable conflict did not result in exception.');
         } catch (SolverProblemsException $e) {
+            $problems = $e->getProblems();
+            $this->assertEquals(1, count($problems));
             // TODO assert problem properties
         }
     }

+ 49 - 0
tests/Composer/Test/Util/ErrorHandlerTest.php

@@ -0,0 +1,49 @@
+<?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\Util;
+
+use Composer\Util\ErrorHandler;
+use Composer\Test\TestCase;
+
+/**
+ * ErrorHandler test case
+ *
+ * @author Artem Lopata <biozshock@gmail.com>
+ */
+class ErrorHandlerTest extends TestCase
+{
+    /**
+     * Test ErrorHandler handles notices
+     */
+    public function testErrorHandlerCaptureNotice()
+    {
+        $this->setExpectedException('\ErrorException', 'Undefined index: baz');
+
+        ErrorHandler::register();
+
+        $array = array('foo' => 'bar');
+        $array['baz'];
+    }
+
+    /**
+     * Test ErrorHandler handles warnings
+     */
+    public function testErrorHandlerCaptureWarning()
+    {
+        $this->setExpectedException('\ErrorException', 'array_merge(): Argument #2 is not an array');
+
+        ErrorHandler::register();
+
+        array_merge(array(), 'string');
+    }
+}

+ 2 - 1
tests/Composer/Test/Util/RemoteFilesystemTest.php

@@ -31,7 +31,8 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase
             ->will($this->returnValue(null))
         ;
 
-        $this->assertEquals(array(), $this->callGetOptionsForUrl($io, array('http://example.org')));
+        $res = $this->callGetOptionsForUrl($io, array('http://example.org'));
+        $this->assertTrue(isset($res['http']['header']) && false !== strpos($res['http']['header'], 'User-Agent'), 'getOptions must return an array with a header containing a User-Agent');
     }
 
     public function testGetOptionsForUrlWithAuthorization()

+ 3 - 3
tests/Composer/Test/Util/StreamContextFactoryTest.php

@@ -57,13 +57,13 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase
 
     public function testHttpProxy()
     {
-        $_SERVER['HTTP_PROXY'] = 'http://username:password@proxyserver.net:port/';
-        $_SERVER['http_proxy'] = 'http://proxyserver/';
+        $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:port/';
+        $_SERVER['HTTP_PROXY'] = 'http://proxyserver/';
 
         $context = StreamContextFactory::getContext(array('http' => array('method' => 'GET')));
         $options = stream_context_get_options($context);
 
-        $this->assertSame('http://proxyserver/', $_SERVER['http_proxy']);
+        $this->assertSame('http://proxyserver/', $_SERVER['HTTP_PROXY']);
 
         $this->assertEquals(array('http' => array(
             'proxy' => 'tcp://username:password@proxyserver.net:port/',