diff --git a/README.md b/README.md index 9dae242e9..d0ca984a1 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,10 @@ merged. This is to ensure proper review of all the code. Fork the project, create a feature branch, and send us a pull request. +To ensure a consistent code base, you should make sure the code follows +the [Coding Standards](http://symfony.com/doc/2.0/contributing/code/standards.html) +which we borrowed from Symfony. + If you would like to help take a look at the [list of issues](http://github.com/composer/composer/issues). Community diff --git a/composer.json b/composer.json index 3531ba4ec..7b89586b2 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,9 @@ ], "require": { "php": ">=5.3.0", - "symfony/console": "2.1.0-dev", - "symfony/finder": ">2.0,<2.2-dev", - "symfony/process": ">2.0,<2.2-dev" + "symfony/console": "dev-master", + "symfony/finder": "dev-master", + "symfony/process": "dev-master" }, "recommend": { "ext-zip": "*" diff --git a/composer.lock b/composer.lock index da880d14a..4761b3466 100644 --- a/composer.lock +++ b/composer.lock @@ -1,17 +1,20 @@ { - "hash": "9c243b2c15fdc7c3e35c5200d704ba53", + "hash": "4ba2fad397e186b6bc453b4417c2ab00", "packages": [ { - "package": "symfony\/process", - "version": "2.1.0-dev" + "package": "symfony/console", + "version": "dev-master", + "source-reference": "75ca31776bd98ad427f759cbe8a62400e40c73a1" }, { - "package": "symfony\/finder", - "version": "2.1.0-dev" + "package": "symfony/finder", + "version": "dev-master", + "source-reference": "dd56fc9f1f0baa006d7491d5c17eb3e2dd8a066c" }, { - "package": "symfony\/console", - "version": "2.1.0-dev" + "package": "symfony/process", + "version": "dev-master", + "source-reference": "f381eeee3733ca0fd374491fab56dce0f3ca8e34" } ] -} \ No newline at end of file +} diff --git a/doc/00-intro.md b/doc/00-intro.md new file mode 100644 index 000000000..4d35b6924 --- /dev/null +++ b/doc/00-intro.md @@ -0,0 +1,65 @@ +# Introduction + +Composer is a tool for dependency management in PHP. It allows you to declare +the dependencies of your project and will install these dependencies for you. + +## Dependency management + +One important distinction to make is that composer is not a package manager. It +deals with packages, but it manages them on a per-project basis. By default it +will never install anything globally. Thus, it is a dependency manager. + +This idea is not new by any means. Composer is strongly inspired by +node's [npm](http://npmjs.org/) and ruby's [bundler](http://gembundler.com/). +But there has not been such a tool for PHP so far. + +The problem that composer solves is the following. You have a project that +depends on a number of libraries. Some of those libraries have dependencies of +their own. You declare the things you depend on. Composer will then go ahead +and find out which versions of which packages need to be installed, and +install them. + +## Declaring dependencies + +Let's say you are creating a project, and you need a library that does logging. +You decide to use [monolog](https://github.com/Seldaek/monolog). In order to +add it to your project, all you need to do is create a `composer.json` file +which describes the project's dependencies. + +```json +{ + "require": { + "monolog/monolog": "1.0.*" + } +} +``` + +We are simply stating that our project requires the `monolog/monolog` package, +any version beginning with `1.0`. + +## Installation + +To actually get it, we need to do two things. The first one is installing +composer: + + $ curl -s http://getcomposer.org/installer | php + +This will just check a few PHP settings and then download `composer.phar` to +your working directory. This is the composer binary. + +After that we run the command for installing all dependencies: + + $ php composer.phar install + +This will download monolog and dump it into `vendor/monolog/monolog`. + +## Autoloading + +After this you can just add the following line to your bootstrap code to get +autoloading: + +```php +require 'vendor/.composer/autoload.php'; +``` + +That's all it takes to have a basic setup. diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md new file mode 100644 index 000000000..5c17048e9 --- /dev/null +++ b/doc/01-basic-usage.md @@ -0,0 +1,189 @@ +# Basic usage + +## Installation + +To install composer, simply run this command on the command line: + + $ curl -s http://getcomposer.org/installer | php + +This will perform some checks on your environment to make sure you can +actually run it. + +This will download `composer.phar` and place it in your working directory. +`composer.phar` is the composer binary. It is a PHAR (PHP archive), which +is an archive format for PHP which can be run on the command line, amongst +other things. + +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`. + +To check if composer is working, just run the PHAR through `php`: + + $ php composer.phar + +This should give you a list of available commands. + +> **Note:** You can also perform the checks only without downloading composer +> by using the `--check` option. For more information, just use `--help`. +> +> $ curl -s http://getcomposer.org/installer | php -- --help + +## Project setup + +To start using composer in your project, all you need is a `composer.json` +file. This file describes the dependencies of your project and may contain +other metadata as well. + +The [JSON format](http://json.org/) is quite easy to write. It allows you to +define nested structures. + +The first (and often only) thing you specify in `composer.json` is the +`require` key. You're simply telling composer which packages your project +depends on. + +```json +{ + "require": { + "monolog/monolog": "1.0.*" + } +} +``` + +As you can see, `require` takes an object that maps package names to versions. + +## Package names + +The package name consists of a vendor name and the project's name. Often these +will be identical. The vendor name exists to prevent naming clashes. It allows +two different people to create a library named `json`, which would then just be +named `igorw/json` and `seldaek/json`. + +Here we are requiring `monolog/monolog`, so the vendor name is the same as the +project's name. For projects with a unique name this is recommended. It also +allows adding more related projects under the same namespace later on. If you +are maintaining a library, this would make it really easy to split it up into +smaller decoupled parts. + +## Package versions + +We are also requiring the version `1.0.*` of monolog. This means any version +in the `1.0` development branch. It would match `1.0.0`, `1.0.2` and `1.0.20`. + +Version constraints can be specified in a few different ways. + +* **Exact version:** You can specify the exact version of a package, for + example `1.0.2`. This is not used very often, but can be useful. + +* **Range:** By using comparison operators you can specify ranges of valid + versions. Valid operators are `>`, `>=`, `<`, `<=`. An example range would be + `>=1.0`. You can define multiple of these, separated by comma: `>=1.0,<2.0`. + +* **Wildcard:** You can specify a pattern with a `*` wildcard. `1.0.*` is the + equivalent of `>=1.0,<1.1-dev`. + +## Installing dependencies + +To fetch the defined dependencies into the local project, you simply run the +`install` command of `composer.phar`. + + $ php composer.phar install + +This will find the latest version of `monolog/monolog` that matches the +supplied version constraint and download it into the the `vendor` directory. +It's a convention to put third party code into a directory named `vendor`. +In case of monolog it will put it into `vendor/monolog/monolog`. + +**Tip:** If you are using git for your project, you probably want to add +`vendor` into your `.gitignore`. You really don't want to add all of that +code to your repository. + +Another thing that the `install` command does is it adds a `composer.lock` +file into your project root. + +## Lock file + +After installing the dependencies, composer writes the list of the exact +versions it installed into a `composer.lock` file. This locks the project +to those specific versions. + +**Commit your project's `composer.lock` into version control.** + +The reason is that anyone who sets up the project should get the same version. +The `install` command will check if a lock file is present. If it is, it will +use the versions specified there. If not, it will resolve the dependencies and +create a lock file. + +If any of the dependencies gets a new version, you can update to that version +by using the `update` command. This will fetch the latest matching versions and +also update the lock file. + + $ php composer.phar update + +## Packagist + +[Packagist](http://packagist.org/) is the main composer repository. A composer +repository is basically a package source. A place where you can get packages +from. Packagist aims to be the central repository that everybody uses. This +means that you can automatically `require` any package that is available +there. + +If you go to the [packagist website](http://packagist.org/) (packagist.org), +you can browse and search for packages. + +Any open source project using composer should publish their packages on +packagist. + +## Autoloading + +For libraries that follow the [PSR-0](https://github.com/php-fig/fig- +standards/blob/master/accepted/PSR-0.md) naming standard, composer generates a +`vendor/.composer/autoload.php` file for autoloading. You can simply include +this file and you will get autoloading for free. + +```php +require 'vendor/.composer/autoload.php'; +``` + +This makes it really easy to use third party code, because you really just +have to add one line to `composer.json` and run `install`. For monolog, it +means that we can just start using classes from it, and they will be +autoloaded. + +```php +$log = new Monolog\Logger('name'); +$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Logger::WARNING)); + +$log->addWarning('Foo'); +``` + +You can even add your own code to the autoloader by adding an `autoload` key +to `composer.json`. + +```json +{ + "autoload": { + "psr-0": {"Acme": "src/"} + } +} +``` + +This is a mapping from namespaces to directories. The `src` directory would be +in your project root. An example filename would be `src/Acme/Foo.php` +containing a `Acme\Foo` class. + +After adding the `autoload` key, you have to re-run `install` to re-generate +the `vendor/.composer/autoload.php` file. + +Including that file will also return the autoloader instance, so you can add +retrieve it and add more namespaces. This can be useful for autoloading +classes in a test suite, for example. + +```php +$loader = require 'vendor/.composer/autoload.php'; +$loader->add('Acme\Test', __DIR__); +``` + +> **Note:** Composer provides its own autoloader. If you don't want to use +that one, you can just include `vendor/.composer/autoload_namespaces.php`, +which returns an associative array mapping namespaces to directories. diff --git a/doc/02-libraries.md b/doc/02-libraries.md new file mode 100644 index 000000000..530c17c02 --- /dev/null +++ b/doc/02-libraries.md @@ -0,0 +1,168 @@ +# Libraries + +This chapter will tell you how to make your library installable through composer. + +## Every project is a package + +As soon as you have a `composer.json` in a directory, that directory is a +package. When you add a `require` to a project, you are making a package that +depends on other packages. The only difference between your project and +libraries is that your project is a package without a name. + +In order to make that package installable you need to give it a name. You do +this by adding a `name` to `composer.json`: + +```json +{ + "name": "acme/hello-world", + "require": { + "monolog/monolog": "1.0.*" + } +} +``` + +In this case the project name is `acme/hello-world`, where `acme` is the +vendor name. Supplying a vendor name is mandatory. + +> **Note:** If you don't know what to use as a vendor name, your GitHub +username is usually a good bet. While package names are case insensitive, the +convention is all lowercase and dashes for word separation. + +## Specifying the version + +You need to specify the version some way. Depending on the type of repository +you are using, it might be possible to omit it from `composer.json`, because +the repository is able to infer the version from elsewhere. + +If you do want to specify it explicitly, you can just add a `version` field: + +```json +{ + "version": "1.0.0" +} +``` + +However if you are using git, svn or hg, you don't have to specify it. +Composer will detect versions as follows: + +### Tags + +For every tag that looks like a version, a package version of that tag will be +created. It should match 'X.Y.Z' or 'vX.Y.Z', with an optional suffix for RC, +beta, alpha or patch. + +Here are a few examples of valid tag names: + + 1.0.0 + v1.0.0 + 1.10.5-RC1 + v4.4.4beta2 + v2.0.0-alpha + v2.0.4-p1 + +> **Note:** If you specify an explicit version in `composer.json`, the tag name must match the specified version. + +### Branches + +For every branch, a package development version will be created. If the branch +name looks like a version, the version will be `{branchname}-dev`. For example +a branch `2.0` will get a version `2.0-dev`. If the branch does not look like +a version, it will be `dev-{branchname}`. `master` results in a `dev-master` +version. + +Here are some examples of version branch names: + + 1.0 + 1.* + 1.1.x + 1.1.* + +> **Note:** When you install a dev version, it will install it from source. +See [Repositories] for more information. + +## Lock file + +For projects it is recommended to commit the `composer.lock` file into version +control. For libraries this is not the case. You do not want your library to +be tied to exact versions of the dependencies. It should work with any +compatible version, so make sure you specify your version constraints so that +they include all compatible versions. + +**Do not commit your library's `composer.lock` into version control.** + +If you are using git, add it to the `.gitignore`. + +## Publishing to a VCS + +Once you have a vcs repository (version control system, e.g. git) containing a +`composer.json` file, your library is already composer-installable. In this +example we will publish the `acme/hello-world` library on GitHub under +`github.com/composer/hello-world`. + +Now, To test installing the `acme/hello-world` package, we create a new +project locally. We will call it `acme/blog`. This blog will depend on `acme +/hello-world`, which in turn depends on `monolog/monolog`. We can accomplish +this by creating a new `blog` directory somewhere, containing a +`composer.json`: + +```json +{ + "name": "acme/blog", + "require": { + "acme/hello-world": "dev-master" + } +} +``` + +The name is not needed in this case, since we don't want to publish the blog +as a library. It is added here to clarify which `composer.json` is being +described. + +Now we need to tell the blog app where to find the `hello-world` dependency. +We do this by adding a package repository specification to the blog's +`composer.json`: + +```json +{ + "name": "acme/blog", + "repositories": { + "acme/hello-world": { + "vcs": { "url": "https://github.com/composer/hello-world" } + } + }, + "require": { + "acme/hello-world": "dev-master" + } +} +``` + +For more details on how package repositories work and what other types are +available, see [Repositories]. + +That's all. You can now install the dependencies by running composer's +`install` command! + +**Recap:** Any git/svn/hg repository containing a `composer.json` can be added +to your project by specifying the package repository and declaring the +dependency in the `require` field. + +## Publishing to packagist + +Alright, so now you can publish packages. But specifying the vcs repository +every time is cumbersome. You don't want to force all your users to do that. + +The other thing that you may have noticed is that we did not specify a package +repository for `monolog/monolog`. How did that work? The answer is packagist. + +[Packagist](http://packagist.org/) is the main package repository for +composer, and it is enabled by default. Anything that is published on +packagist is available automatically through composer. Since monolog +[is on packagist](http://packagist.org/packages/monolog/monolog), we can depend +on it without having to specify any additional repositories. + +Assuming we want to share `hello-world` with the world, we would want to +publish it on packagist as well. And this is really easy. + +You simply hit the big "Submit Package" button and sign up. Then you submit +the URL to your VCS repository, at which point packagist will start crawling +it. Once it is done, your package will be available to anyone. diff --git a/doc/03-cli.md b/doc/03-cli.md new file mode 100644 index 000000000..99b507deb --- /dev/null +++ b/doc/03-cli.md @@ -0,0 +1,139 @@ +# Command-line interface + +You've already learned how to use the command-line interface to do some +things. This chapter documents all the available commands. + +## init + +In the [Libraries] chapter we looked at how to create a `composer.json` by +hand. There is also an `init` command available that makes it a bit easier to +do this. + +When you run the command it will interactively ask you to fill in the fields, +while using some smart defaults. + + $ php composer.phar init + +## install + +The `install` command reads the `composer.json` file from the current +directory, resolves the dependencies, and installs them into `vendor`. + + $ php composer.phar install + +If there is a `composer.lock` file in the current directory, it will use the +exact versions from there instead of resolving them. This ensures that +everyone using the library will get the same versions of the dependencies. + +If there is no `composer.lock` file, composer will create one after dependency +resolution. + +### Options + +* **--prefer-source:** There are two ways of downloading a package: `source` and `dist`. For stable versions composer will use the `dist` by default. The `source` is a version control repository. If `--prefer-source` is enabled, composer will install from `source` if there is one. This is useful if you want to make a bugfix to a project and get a local git clone of the dependency directly. +* **--dry-run:** If you want to run through an installation without actually installing a package, you can use `--dry-run`. This will simulate the installation and show you what would happen. +* **--no-install-recommends:** By default composer will install all packages that are referenced by `recommend`. By passing this option you can disable that. +* **--install-suggests:** The packages referenced by `suggest` will not be installed by default. By passing this option, you can install them. + +## update + +In order to get the latest versions of the dependencies and to update the +`composer.lock` file, you should use the `update` command. + + $ php composer.phar update + +This will resolve all dependencies of the project and write the exact versions +into `composer.lock`. + +### Options + +* **--prefer-source:** Install packages from `source` when available. +* **--dry-run:** Simulate the command without actually doing anything. +* **--no-install-recommends:** Do not install packages referenced by `recommend`. +* **--install-suggests:** Install packages referenced by `suggest`. + +## search + +The search command allows you to search through the current project's package +repositories. Usually this will be just packagist. You simply pass it the +terms you want to search for. + + $ php composer.phar search monolog + +You can also search for more than one term by passing multiple arguments. + +## show + +To list all of the available packages, you can use the `show` command. + + $ php composer.phar show + +If you want to see the details of a certain package, you can pass the package +name. + + $ php composer.phar show monolog/monolog + + name : monolog/monolog + versions : master-dev, 1.0.2, 1.0.1, 1.0.0, 1.0.0-RC1 + type : library + names : monolog/monolog + source : [git] http://github.com/Seldaek/monolog.git 3d4e60d0cbc4b888fe5ad223d77964428b1978da + dist : [zip] http://github.com/Seldaek/monolog/zipball/3d4e60d0cbc4b888fe5ad223d77964428b1978da 3d4e60d0cbc4b888fe5ad223d77964428b1978da + license : MIT + + autoload + psr-0 + Monolog : src/ + + requires + php >=5.3.0 + +You can even pass the package version, which will tell you the details of that +specific version. + + $ php composer.phar show monolog/monolog 1.0.2 + +### Options + +* **--installed:** Will list the packages that are installed. +* **--platform:** Will list only [Platform packages]. + +## depends + +The `depends` command tells you which other packages depend on a certain +package. You can specify which link types (`require`, `recommend`, `suggest`) +should be included in the listing. + + $ php composer.phar depends --link-type=require monolog/monolog + + nrk/monolog-fluent + poc/poc + propel/propel + symfony/monolog-bridge + symfony/symfony + +### Options + +* **--link-type:** The link types to match on, can be specified multiple +times. + +## validate + +You should always run the `validate` command before you commit your +`composer.json` file, and before you tag a release. It will check if your +`composer.json` is valid. + + $ php composer.phar validate + +## self-update + +To update composer itself to the latest version, just run the `self-update` +command. It will replace your `composer.phar` with the latest version. + + $ php composer.phar self-update + +## help + +To get more information about a certain command, just use `help`. + + $ php composer.phar help install diff --git a/doc/04-schema.md b/doc/04-schema.md new file mode 100644 index 000000000..2e1c3d2f6 --- /dev/null +++ b/doc/04-schema.md @@ -0,0 +1,417 @@ +# composer.json + +This chapter will explain all of the options available in `composer.json`. + +## JSON schema + +We have a [JSON schema](http://json-schema.org) that documents the format and +can also be used to validate your `composer.json`. In fact, it is used by the +`validate` command. You can find it at: [`Resources/composer- +schema.json`](https://github.com/composer/composer/blob/master/res +/composer-schema.json). + +## Package root + +The root of the package definition is a JSON object. + +## name + +The name of the package. It consists of vendor name and project name, +separated by `/`. + +Examples: + +* monolog/monolog +* igorw/event-source + +Required for published packages (libraries). + +## description + +A short description of the package. Usually this is just one line long. + +Optional but recommended. + +## version + +The version of the package. + +This must follow the format of `X.Y.Z` with an optional suffix of `-dev`, +`alphaN`, `-betaN` or `-RCN`. + +Examples: + + 1.0.0 + 1.0.2 + 1.1.0 + 0.2.5 + 1.0.0-dev + 1.0.0-beta2 + 1.0.0-RC5 + +Optional if the package repository can infer the version from somewhere, such +as the VCS tag name in the VCS repository. In that case it is also recommended +to omit it. + +## type + +The type of the package. It defaults to `library`. + +Package types are used for custom installation logic. If you have a package +that needs some special logic, you can define a custom type. This could be a +`symfony-bundle`, a `wordpress-plugin` or a `typo3-module`. These will all be +specific to certain projects, and they will need to provide an installer +capable of installing packages of that type. + +Out of the box, composer supports two types: + +* **library:** This is the default. It will simply copy the files to `vendor`. +* **composer-installer:** A package of type `composer-installer` provides an +installer for other packages that have a custom type. Symfony could supply a +`symfony/bundle-installer` package, which every bundle would depend on. +Whenever you install a bundle, it will fetch the installer and register it, in +order to be able to install the bundle. + +Only use a custom type if you need custom logic during installation. It is +recommended to omit this field and have it just default to `library`. + +## keywords + +An array of keywords that the package is related to. These can be used for +searching and filtering. + +Examples: + + logging + events + database + redis + templating + +Optional. + +## homepage + +An URL to the website of the project. + +Optional. + +## time + +Release date of the version. + +Must be in `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS` format. + +Optional. + +## license + +The license of the package. This can be either a string or an array of strings. + +The recommended notation for the most common licenses is: + + MIT + BSD-2 + BSD-3 + BSD-4 + GPLv2 + GPLv3 + LGPLv2 + LGPLv3 + Apache2 + WTFPL + +Optional, but it is highly recommended to supply this. + +## authors + +The authors of the package. This is an array of objects. + +Each author object can have following properties: + +* **name:** The author's name. Usually his real name. +* **email:** The author's email address. +* **homepage:** An URL to the author's website. + +An example: + +```json +{ + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ] +} +``` + +Optional, but highly recommended. + +## Link types + +Each of these takes an object which maps package names to version constraints. + +* **require:** Packages required by this package. +* **recommend:** Recommended packages, installed by default. +* **suggest:** Suggested packages. These are displayed after installation, + but not installed by default. +* **conflict:** Mark this version of this package as conflicting with other + packages. +* **replace:** Packages that can be replaced by this package. This is useful + for large repositories with subtree splits. It allows the main package to + replace all of it's child packages. +* **provide:** List of other packages that are provided by this package. This + is mostly useful for common interfaces. A package could depend on some virtual + `logger` package, any library that provides this logger, would simply list it + in `provide`. + +Example: + +```json +{ + "require": { + "monolog/monolog": "1.0.*" + } +} +``` + +Optional. + +## autoload + +Autoload mapping for a PHP autoloader. + +Currently only [PSR-0](https://github.com/php-fig/fig- +standards/blob/master/accepted/PSR-0.md) autoloading is supported. Under the +`psr-0` key you define a mapping from namespaces to paths, relative to the +package root. + +Example: + +```json +{ + "autoload": { + "psr-0": { "Monolog": "src/" } + } +} +``` + +Optional, but it is highly recommended that you follow PSR-0 and use this. + +## target-dir + +Defines the installation target. + +In case the package root is below the namespace declaration you cannot +autoload properly. `target-dir` solves this problem. + +An example is Symfony. There are individual packages for the components. The +Yaml component is under `Symfony\Component\Yaml`. The package root is that +`Yaml` directory. To make autoloading possible, we need to make sure that it +is not installed into `vendor/symfony/yaml`, but instead into +`vendor/symfony/yaml/Symfony/Component/Yaml`, so that the autoloader can load +it from `vendor/symfony/yaml`. + +To do that, `autoload` and `target-dir` are defined as follows: + +```json +{ + "autoload": { + "psr-0": { "Symfony\\Component\\Yaml": "" } + }, + "target-dir": "Symfony/Component/Yaml" +} +``` + +Optional. + +## repositories + +Custom package repositories to use. + +By default composer just uses the packagist repository. By specifying +repositories you can get packages from elsewhere. + +Repositories are not resolved recursively. You can only add them to your main +`composer.json`. Repository declarations of dependencies' `composer.json`s are +ignored. + +Following repository types are supported: + +* **composer:** A composer repository is simply a `packages.json` file served + via HTTP that contains a list of `composer.json` objects with additional + `dist` and/or `source` information. +* **vcs:** The version control system repository can fetch packages from git, + svn and hg repositories. Note the distinction between package repository and + version control repository. +* **pear:** With this you can import any pear repository into your composer + project. +* **package:** If you depend on a project that does not have any support for + composer whatsoever you can define the package inline using a `package` + repository. You basically just inline the `composer.json` object. + +For more information on any of these, see [Repositories]. + +Example: + +```json +{ + "repositories": [ + { + "type": "composer", + "url": "http://packages.example.com" + }, + { + "type": "vcs", + "url": "https://github.com/Seldaek/monolog" + }, + { + "type": "pear", + "url": "http://pear2.php.net" + }, + { + "type": "package", + "package": { + "name": "smarty/smarty", + "version": "3.1.7", + "dist": { + "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", + "type": "zip" + }, + "source": { + "url": "http://smarty-php.googlecode.com/svn/", + "type": "svn", + "reference": "tags/Smarty_3_1_7/distribution/" + } + } + } + ] +} +``` + +> **Note:** Order is significant here. Repositories added later will take +precedence. This also means that custom repositories can override packages +that exist on packagist. + +You can also disable the packagist repository by setting `packagist` to +`false`. + +```json +{ + "repositories": [ + { + "packagist": false + } + ] +} +``` + +## config + +A set of configuration options. It is only used for projects. + +The following options are supported: + +* **vendor-dir:** Defaults to `vendor`. You can install dependencies into a + different directory if you want to. +* **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they + will be symlinked into this directory. + +Example: + +```json +{ + "config": { + "bin-dir": "bin" + } +} +``` + +## scripts + +Composer allows you to hook into various parts of the installation process through the use of scripts. + +These events are supported: + +* **pre-install-cmd:** Occurs before the install command is executed, contains + one or more Class::method callables. +* **post-install-cmd:** Occurs after the install command is executed, contains + one or more Class::method callables. +* **pre-update-cmd:** Occurs before the update command is executed, contains + one or more Class::method callables. +* **post-update-cmd:** Occurs after the update command is executed, contains + one or more Class::method callables. +* **pre-package-install:** Occurs before a package is installed, contains one + or more Class::method callables. +* **post-package-install:** Occurs after a package is installed, contains one + or more Class::method callables. +* **pre-package-update:** Occurs before a package is updated, contains one or + more Class::method callables. +* **post-package-update:** Occurs after a package is updated, contains one or + more Class::method callables. +* **pre-package-uninstall:** Occurs before a package has been uninstalled, + contains one or more Class::method callables. +* **post-package-uninstall:** Occurs after a package has been uninstalled, + contains one or more Class::method callables. + +For each of these events you can provide a static method on a class that will +handle it. + +Example: + +```json +{ + "scripts": { + "post-install-cmd": [ + "Acme\\ScriptHandler::doSomething" + ] + } +} +``` + +The event handler receives a `Composer\Script\Event` object as an argument, +which gives you access to the `Composer\Composer` instance through the +`getComposer` method. + +```php +namespace Acme; + +use Composer\Script\Event; + +class ScriptHandler +{ + static public function doSomething(Event $event) + { + // custom logic + } +} +``` + +## extra + +Arbitrary extra data for consumption by `scripts`. + +This can be virtually anything. To access it from within a script event +handler, you can do: + +```php +$extra = $event->getComposer()->getPackage()->getExtra(); +``` + +Optional. + +## bin + +A set of files that should be treated as binaries and symlinked into the `bin- +dir` (from config). + +See [articles/bin.md] for more details. + +Optional. diff --git a/doc/05-repositories.md b/doc/05-repositories.md new file mode 100644 index 000000000..4cdcf866a --- /dev/null +++ b/doc/05-repositories.md @@ -0,0 +1,263 @@ +# Repositories + +This chapter will explain the concept of packages and repositories, what kinds +of repositories are available, and how they work. + +## Concepts + +Before we look at the different types of repositories that we can have, we +need to understand some of the basic concepts that composer is built on. + +### Package + +Composer is a dependency manager. It installs packages. A package is +essentially just a directory containing something. In this case it is PHP +code, but in theory it could be anything. And it contains a package +description which has a name and a version. The name and the version are used +to identify the package. + +In fact, internally composer sees every version as a separate package. While +this distinction does not matter when you are using composer, it's quite +important when you want to change it. + +In addition to the name and the version, there is useful data. The only really +important piece of information is the package source, that describes where to +get the package contents. The package data points to the contents of the +package. And there are two options here: dist and source. + +**Dist:** The dist is a packaged version of the package data. Usually a +released version, usually a stable release. + +**Source:** The source is used for development. This will usually originate +from a source code repository, such as git. You can fetch this when you want +to modify the downloaded package. + +Packages can supply either of these, or even both. Depending on certain +factors, such as user-supplied options and stability of the package, one will +be preferred. + +### Repository + +A repository is a package source. It's a list of packages, of which you can +pick some to install. + +You can also add more repositories to your project by declaring them in +`composer.json`. + +## Types + +### Composer + +The main repository type is the `composer` repository. It uses a single +`packages.json` file that contains all of the package metadata. The JSON +format is as follows: + +```json +{ + "vendor/packageName": { + "name": "vendor/packageName", + "description": "Package description", + "versions": { + "master-dev": { @composer.json }, + "1.0.0": { @composer.json } + } + } +} +``` + +The `@composer.json` marker would be the contents of the `composer.json` from +that package version including as a minimum: + +* name +* version +* dist or source + +Here is a minimal package definition: + +```json +{ + "name": "smarty/smarty", + "version": "3.1.7", + "dist": { + "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", + "type": "zip" + } +} +``` + +It may include any of the other fields specified in the [schema]. + +The `composer` repository is also what packagist uses. To reference a +`composer` repository, just supply the path before the `packages.json` file. +In case of packagist, that file is located at `/packages.json`, so the URL of +the repository would be `http://packagist.org`. For +`http://example.org/packages.org` the repository URL would be +`http://example.org`. + +### VCS + +VCS stands for version control system. This includes versioning systems like +git, svn or hg. Composer has a repository type for installing packages from +these systems. + +There are a few use cases for this. The most common one is maintaining your +own fork of a third party library. If you are using a certain library for your +project and you decide to change something in the library, you will want your +project to use the patched version. If the library is on GitHub (this is the +case most of the time), you can simply fork it there and push your changes to +your fork. After that you update the project's `composer.json`. All you have +to do is add your fork as a repository and update the version constraint to +point to your custom branch. + +Example assuming you patched monolog to fix a bug in the `bugfix` branch: + +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "http://github.com/igorw/monolog" + } + ], + "require": { + "monolog/monolog": "dev-bugfix" + } +} +``` + +When you run `php composer.phar update`, you should get your modified version +of `monolog/monolog` instead of the one from packagist. + +Git is not the only version control system supported by the VCS repository. +The following are supported: + +* **Git:** [git-scm.com](http://git-scm.com) +* **Subversion:** [subversion.apache.org](http://subversion.apache.org) +* **Mercurial:** [mercurial.selenic.com](http://mercurial.selenic.com) + +To use these systems you need to have them installed. That can be +invonvenient. And for this reason there is special support for GitHub and +BitBucket that use the APIs provided by these sites, to fetch the packages +without having to install the version control system. The VCS repository +provides `dist`s for them that fetch the packages as zips. + +* **GitHub:** [github.com](https://github.com) (Git) +* **BitBucket:** [bitbucket.org](https://bitbucket.org) (Git and Mercurial) + +The VCS driver to be used is detected automatically based on the URL. + +### PEAR + +It is possible to install packages from any PEAR channel by using the `pear` +repository. Composer will prefix all package names with `pear-{channelName}/` to +avoid conflicts. + +Example using `pear2.php.net`: + +```json +{ + "repositories": [ + { + "type": "pear", + "url": "http://pear2.php.net" + } + ], + "require": { + "pear-pear2/PEAR2_HTTP_Request": "*" + } +} +``` + +In this case the short name of the channel is `pear2`, so the +`PEAR2_HTTP_Request` package name becomes `pear-pear2/PEAR2_HTTP_Request`. + +> **Note:** The `pear` repository requires doing quite a few requests per +> package, so this may considerably slow down the installation process. + +### Package + +If you want to use a project that does not support composer through any of the +means above, you still can define the package yourself using a `package` +repository. + +Basically, you define the same information that is included in the `composer` +repository's `packages.json`, but only for a single package. Again, the +minimally required fields are `name`, `version`, and either of `dist` or +`source`. + +Here is an example for the smarty template engine: + +```json +{ + "repositories": [ + { + "type": "package", + "package": { + "name": "smarty/smarty", + "version": "3.1.7", + "dist": { + "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", + "type": "zip" + }, + "source": { + "url": "http://smarty-php.googlecode.com/svn/", + "type": "svn", + "reference": "tags/Smarty_3_1_7/distribution/" + } + } + } + ], + "require": { + "smarty/smarty": "3.1.*" + } +} +``` + +Typically you would leave the source part off, as you don't really need it. + +## Hosting your own + +While you will probably want to put your packages on packagist most of the time, +there are some use cases for hosting your own repository. + +* **Private company packages:** If you are part of a company that uses composer + for their packages internally, you might want to keep those packages private. + +* **Separate ecosystem:** If you have a project which has its own ecosystem, + and the packages aren't really reusable by the greater PHP community, you + might want to keep them separate to packagist. An example of this would be + wordpress plugins. + +When hosting your own package repository it is recommended to use a `composer` +one. This is type that is native to composer and yields the best performance. + +There are a few different tools that can help you create a `composer` +repository. + +### Packagist + +The underlying application used by packagist is open source. This means that you +can just install your own copy of packagist, re-brand, and use it. It's really +quite straight-forward to do. + +Packagist is a Symfony2 application, and it is [available on +GitHub](https://github.com/composer/packagist). It uses composer internally and +acts as a proxy between VCS repositories and the composer users. It holds a list +of all VCS packages, periodically re-crawls them, and exposes them as a composer +repository. + +To set your own copy, simply follow the instructions from the [packagist +github repository](https://github.com/composer/packagist). + +### Satis + +Satis is a static `composer` repository generator. It is a bit like a ultra- +lightweight, file-based version of packagist. + +You give it a `composer.json` containing repositories, typically VCS and package +repository definitions. It will fetch all the packages that are `require`d from +these repositories and dump a `packages.json` that is your `composer` +repository. + +Check [the satis GitHub repository](https://github.com/composer/satis) for more +information. diff --git a/doc/06-community.md b/doc/06-community.md new file mode 100644 index 000000000..35a92092a --- /dev/null +++ b/doc/06-community.md @@ -0,0 +1,28 @@ +# Community + +We have a lot of people using composer, and also many contributors to the +project. + +## Contributing + +If you would like to contribute to composer, please read the +[README](https://github.com/composer/composer). + +The most important guidelines are described as follows: + +> All code contributions - including those of people having commit access - must +> go through a pull request and approved by a core developer before being +> merged. This is to ensure proper review of all the code. +> +> Fork the project, create a feature branch, and send us a pull request. +> +> To ensure a consistent code base, you should make sure the code follows +> the [Coding Standards](http://symfony.com/doc/2.0/contributing/code/standards.html) +> which we borrowed from Symfony. + +## IRC / mailing list + +The developer mailing list is on [google groups](http://groups.google.com/group +/composer-dev) IRC channels are available for discussion as well, on +irc.freenode.org [#composer](irc://irc.freenode.org/composer) for users and +[#composer-dev](irc://irc.freenode.org/composer-dev) for development. diff --git a/doc/faqs/packagist-update-schedule.md b/doc/articles/packagist-update-schedule.md similarity index 100% rename from doc/faqs/packagist-update-schedule.md rename to doc/articles/packagist-update-schedule.md diff --git a/doc/faqs/scripts.md b/doc/articles/scripts.md similarity index 100% rename from doc/faqs/scripts.md rename to doc/articles/scripts.md diff --git a/doc/faqs/vendor-bins.md b/doc/articles/vendor-bins.md similarity index 100% rename from doc/faqs/vendor-bins.md rename to doc/articles/vendor-bins.md diff --git a/doc/DefaultPolicy.md b/doc/dev/DefaultPolicy.md similarity index 100% rename from doc/DefaultPolicy.md rename to doc/dev/DefaultPolicy.md diff --git a/doc/composer-schema.json b/res/composer-schema.json similarity index 61% rename from doc/composer-schema.json rename to res/composer-schema.json index 2545a8d4b..62d6be0ed 100644 --- a/doc/composer-schema.json +++ b/res/composer-schema.json @@ -101,6 +101,20 @@ "description": "This is a hash of package name (keys) and version constraints (values) that this package suggests work well with it (typically this will only be suggested to the user).", "additionalProperties": true }, + "config": { + "type": ["object"], + "description": "Composer options.", + "properties": { + "vendor-dir": { + "type": "string", + "description": "The location where all packages are installed, defaults to \"vendor\"." + }, + "bin-dir": { + "type": "string", + "description": "The location where all binaries are linked, defaults to \"vendor/bin\"." + } + } + }, "extra": { "type": ["object", "array"], "description": "Arbitrary extra data that can be used by custom installers, for example, package of type composer-installer must have a 'class' key defining the installer class name.", @@ -121,6 +135,59 @@ "type": ["object", "array"], "description": "A set of additional repositories where packages can be found.", "additionalProperties": true + }, + "bin": { + "type": ["array"], + "description": "A set of files that should be treated as binaries and symlinked into bin-dir (from config).", + "items": { + "type": "string" + } + }, + "scripts": { + "type": ["object"], + "description": "Scripts listeners that will be executed before/after some events.", + "properties": { + "pre-install-cmd": { + "type": ["array", "string"], + "description": "Occurs before the install command is executed, contains one or more Class::method callables." + }, + "post-install-cmd": { + "type": ["array", "string"], + "description": "Occurs after the install command is executed, contains one or more Class::method callables." + }, + "pre-update-cmd": { + "type": ["array", "string"], + "description": "Occurs before the update command is executed, contains one or more Class::method callables." + }, + "post-update-cmd": { + "type": ["array", "string"], + "description": "Occurs after the update command is executed, contains one or more Class::method callables." + }, + "pre-package-install": { + "type": ["array", "string"], + "description": "Occurs before a package is installed, contains one or more Class::method callables." + }, + "post-package-install": { + "type": ["array", "string"], + "description": "Occurs after a package is installed, contains one or more Class::method callables." + }, + "pre-package-update": { + "type": ["array", "string"], + "description": "Occurs before a package is updated, contains one or more Class::method callables." + }, + "post-package-update": { + "type": ["array", "string"], + "description": "Occurs after a package is updated, contains one or more Class::method callables." + }, + "pre-package-uninstall": { + "type": ["array", "string"], + "description": "Occurs before a package has been uninstalled, contains one or more Class::method callables." + }, + "post-package-uninstall": { + "type": ["array", "string"], + "description": "Occurs after a package has been uninstalled, contains one or more Class::method callables." + } + } } } } diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index f695487ff..82ff5e02d 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -25,6 +25,8 @@ use Symfony\Component\Console\Output\OutputInterface; */ class DependsCommand extends Command { + protected $linkTypes = array('require', 'recommend', 'suggest'); + protected function configure() { $this @@ -32,7 +34,7 @@ class DependsCommand extends Command ->setDescription('Shows which packages depend on the given package') ->setDefinition(array( new InputArgument('package', InputArgument::REQUIRED, 'Package to inspect'), - new InputOption('link-type', '', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Link types to show', array('requires', 'recommends', 'suggests')) + new InputOption('link-type', '', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Link types to show', $this->linkTypes) )) ->setHelp(<<getRepositoryManager()->getRepositories(); $types = $input->getOption('link-type'); + foreach ($repos as $repository) { foreach ($repository->getPackages() as $package) { foreach ($types as $type) { - foreach ($package->{'get'.$type}() as $link) { + $type = rtrim($type, 's'); + if (!in_array($type, $this->linkTypes)) { + throw new \InvalidArgumentException('Unexpected link type: '.$type.', valid types: '.implode(', ', $this->linkTypes)); + } + foreach ($package->{'get'.$type.'s'}() as $link) { if ($link->getTarget() === $needle) { if ($verbose) { $references[] = array($type, $package, $link); diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index d656ad5d1..5885b033d 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -30,6 +30,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Solver; use Composer\IO\IOInterface; @@ -113,23 +114,20 @@ EOT } // creating requirements request + $installFromLock = false; $request = new Request($pool); if ($update) { $io->write('Updating dependencies'); $installedPackages = $installedRepo->getPackages(); $links = $this->collectLinks($composer->getPackage(), $noInstallRecommends, $installSuggests); - foreach ($links as $link) { - foreach ($installedPackages as $package) { - if ($package->getName() === $link->getTarget()) { - $request->update($package->getName(), new VersionConstraint('=', $package->getVersion())); - break; - } - } + $request->updateAll(); + foreach ($links as $link) { $request->install($link->getTarget(), $link->getConstraint()); } } elseif ($composer->getLocker()->isLocked()) { + $installFromLock = true; $io->write('Installing from lock file'); if (!$composer->getLocker()->isFresh()) { @@ -158,45 +156,63 @@ EOT // solve dependencies $operations = $solver->solve($request); - // check for missing deps - // TODO this belongs in the solver, but this will do for now to report top-level deps missing at least - foreach ($request->getJobs() as $job) { - if ('install' === $job['cmd']) { - foreach ($installedRepo->getPackages() as $package ) { - if ($installedRepo->hasPackage($package) && !$package->isPlatform() && !$installationManager->isPackageInstalled($package)) { - $operations[$job['packageName']] = new InstallOperation($package, Solver::RULE_PACKAGE_NOT_EXIST); - } - if (in_array($job['packageName'], $package->getNames())) { - continue 2; - } - } - foreach ($operations as $operation) { - if ('install' === $operation->getJobType() && in_array($job['packageName'], $operation->getPackage()->getNames())) { - continue 2; - } - if ('update' === $operation->getJobType() && in_array($job['packageName'], $operation->getTargetPackage()->getNames())) { - continue 2; - } - } - - if ($pool->whatProvides($job['packageName'])) { - throw new \UnexpectedValueException('Package '.$job['packageName'].' can not be installed, either because its version constraint is incorrect, or because one of its dependencies was not found.'); - } - throw new \UnexpectedValueException('Package '.$job['packageName'].' was not found in the package pool, check the name for typos.'); - } - } - // execute operations if (!$operations) { $io->write('Nothing to install/update'); } + + // force dev packages to be updated to latest reference on update + if ($update) { + foreach ($localRepo->getPackages() as $package) { + // skip non-dev packages + if (!$package->isDev()) { + continue; + } + + // skip packages that will be updated/uninstalled + foreach ($operations as $operation) { + if (('update' === $operation->getJobType() && $package === $operation->getInitialPackage()) + || ('uninstall' === $operation->getJobType() && $package === $operation->getPackage()) + ) { + continue 2; + } + } + + // force update + $newPackage = $composer->getRepositoryManager()->findPackage($package->getName(), $package->getVersion()); + if ($newPackage && $newPackage->getSourceReference() !== $package->getSourceReference()) { + $operations[] = new UpdateOperation($package, $newPackage); + } + } + } + foreach ($operations as $operation) { if ($verbose) { $io->write((string) $operation); } if (!$dryRun) { $eventDispatcher->dispatchPackageEvent(constant('Composer\Script\ScriptEvents::PRE_PACKAGE_'.strtoupper($operation->getJobType())), $operation); + + // if installing from lock, restore dev packages' references to their locked state + if ($installFromLock) { + $package = null; + if ('update' === $operation->getJobType()) { + $package = $operation->getTargetPackage(); + } elseif ('install' === $operation->getJobType()) { + $package = $operation->getPackage(); + } + if ($package && $package->isDev()) { + $lockData = $composer->getLocker()->getLockData(); + foreach ($lockData['packages'] as $lockedPackage) { + if (!empty($lockedPackage['source-reference']) && strtolower($lockedPackage['package']) === $package->getName()) { + $package->setSourceReference($lockedPackage['source-reference']); + break; + } + } + } + } $installationManager->execute($operation); + $eventDispatcher->dispatchPackageEvent(constant('Composer\Script\ScriptEvents::POST_PACKAGE_'.strtoupper($operation->getJobType())), $operation); } } diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index f06b4104b..63699d883 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -21,11 +21,6 @@ use Composer\Package\LinkConstraint\VersionConstraint; */ class DefaultPolicy implements PolicyInterface { - public function allowUninstall() - { - return true; - } - public function versionCompare(PackageInterface $a, PackageInterface $b, $operator) { $constraint = new VersionConstraint($operator, $b->getVersion()); diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index 45309a081..7c26ab63f 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -20,7 +20,6 @@ use Composer\Package\PackageInterface; */ interface PolicyInterface { - function allowUninstall(); function versionCompare(PackageInterface $a, PackageInterface $b, $operator); function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package); function installable(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package); diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 201caa1d3..3d1b28448 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -55,6 +55,11 @@ class Request ); } + public function updateAll() + { + $this->jobs[] = array('cmd' => 'update-all', 'packages' => array()); + } + public function getJobs() { return $this->jobs; diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index 6d79a9678..1f444c788 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -20,7 +20,6 @@ class RuleSet implements \IteratorAggregate, \Countable // highest priority => lowest number const TYPE_PACKAGE = 0; const TYPE_JOB = 1; - const TYPE_UPDATE = 2; const TYPE_FEATURE = 3; const TYPE_CHOICE = 4; const TYPE_LEARNED = 5; @@ -29,7 +28,6 @@ class RuleSet implements \IteratorAggregate, \Countable -1 => 'UNKNOWN', self::TYPE_PACKAGE => 'PACKAGE', self::TYPE_FEATURE => 'FEATURE', - self::TYPE_UPDATE => 'UPDATE', self::TYPE_JOB => 'JOB', self::TYPE_CHOICE => 'CHOICE', self::TYPE_LEARNED => 'LEARNED', diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 8f12e6e0a..ab85c78c2 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -52,7 +52,6 @@ class Solver protected $decisionMap; protected $installedMap; - protected $packageToUpdateRule = array(); protected $packageToFeatureRule = array(); public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed) @@ -148,10 +147,6 @@ class Solver */ protected function createInstallOneOfRule(array $packages, $reason, $reasonData = null) { - if (empty($packages)) { - return $this->createImpossibleRule($reason, $reasonData); - } - $literals = array(); foreach ($packages as $package) { $literals[] = new Literal($package, true); @@ -201,22 +196,6 @@ class Solver return new Rule(array(new Literal($issuer, false), new Literal($provider, false)), $reason, $reasonData); } - /** - * Intentionally creates a rule impossible to solve - * - * The rule is an empty one so it can never be satisfied. - * - * @param int $reason A RULE_* constant describing the reason for - * generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that goes with - * the reason - * @return Rule An empty rule - */ - protected function createImpossibleRule($reason, $reasonData = null) - { - return new Rule(array(), $reason, $reasonData); - } - /** * Adds a rule unless it duplicates an existing one of any type * @@ -305,7 +284,7 @@ class Solver // if ignoreinstalledsobsoletes is not set, we're also checking // obsoletes of installed packages (like newer rpm versions) // - /** @TODO: if ($this->noInstalledObsoletes) */ + /** TODO if ($this->noInstalledObsoletes) */ if (true) { $noObsoletes = isset($this->noObsoletes[$package->getId()]); $isInstalled = (isset($this->installedMap[$package->getId()])); @@ -508,7 +487,7 @@ class Solver // push all of our rules (can only be feature or job rules) // asserting this literal on the problem stack - foreach ($this->rules->getIteratorFor(array(RuleSet::TYPE_JOB, RuleSet::TYPE_UPDATE, RuleSet::TYPE_FEATURE)) as $assertRule) { + foreach ($this->rules->getIteratorFor(array(RuleSet::TYPE_JOB, RuleSet::TYPE_FEATURE)) as $assertRule) { if ($assertRule->isDisabled() || !$assertRule->isAssertion() || $assertRule->isWeak()) { continue; } @@ -882,11 +861,6 @@ class Solver protected function disableUpdateRule($package) { - // find update & feature rule and disable - if (isset($this->packageToUpdateRule[$package->getId()])) { - $this->packageToUpdateRule[$package->getId()]->disable(); - } - if (isset($this->packageToFeatureRule[$package->getId()])) { $this->packageToFeatureRule[$package->getId()]->disable(); } @@ -958,6 +932,14 @@ class Solver break; } } + + switch ($job['cmd']) { + case 'update-all': + foreach ($installedPackages as $package) { + $this->updateMap[$package->getId()] = true; + } + break; + } } foreach ($installedPackages as $package) { @@ -986,22 +968,21 @@ class Solver $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package); $rule = $this->createUpdateRule($package, $updates, self::RULE_INTERNAL_ALLOW_UPDATE, (string) $package); - if ($this->policy->allowUninstall()) { - $rule->setWeak(true); - $this->addRule(RuleSet::TYPE_FEATURE, $featureRule); - $this->packageToFeatureRule[$package->getId()] = $rule; - } else { - $this->addRule(RuleSet::TYPE_UPDATE, $rule); - $this->packageToUpdateRule[$package->getId()] = $rule; - } + $rule->setWeak(true); + $this->addRule(RuleSet::TYPE_FEATURE, $rule); + $this->packageToFeatureRule[$package->getId()] = $rule; } foreach ($this->jobs as $job) { switch ($job['cmd']) { case 'install': - $rule = $this->createInstallOneOfRule($job['packages'], self::RULE_JOB_INSTALL, $job['packageName']); - $this->addRule(RuleSet::TYPE_JOB, $rule); - $this->ruleToJob[$rule->getId()] = $job; + if (empty($job['packages'])) { + $this->problems[] = array($job); + } else { + $rule = $this->createInstallOneOfRule($job['packages'], self::RULE_JOB_INSTALL, $job['packageName']); + $this->addRule(RuleSet::TYPE_JOB, $rule); + $this->ruleToJob[$rule->getId()] = $job; + } break; case 'remove': // remove all packages with this name including uninstalled @@ -1046,6 +1027,10 @@ class Solver //findrecommendedsuggested(solv); //solver_prepare_solutions(solv); + if ($this->problems) { + throw new SolverProblemsException($this->problems, $this->learnedPool); + } + return $this->createTransaction(); } @@ -1061,9 +1046,6 @@ class Solver if (!$literal->isWanted() && isset($this->installedMap[$package->getId()])) { $literals = array(); - if (isset($this->packageToUpdateRule[$package->getId()])) { - $literals = array_merge($literals, $this->packageToUpdateRule[$package->getId()]->getLiterals()); - } if (isset($this->packageToFeatureRule[$package->getId()])) { $literals = array_merge($literals, $this->packageToFeatureRule[$package->getId()]->getLiterals()); } @@ -1127,6 +1109,8 @@ class Solver protected function addDecision(Literal $l, $level) { + assert($this->decisionMap[$l->getPackageId()] == 0); + if ($l->isWanted()) { $this->decisionMap[$l->getPackageId()] = $level; } else { @@ -1137,6 +1121,9 @@ class Solver protected function addDecisionId($literalId, $level) { $packageId = abs($literalId); + + assert($this->decisionMap[$packageId] == 0); + if ($literalId > 0) { $this->decisionMap[$packageId] = $level; } else { @@ -1179,8 +1166,8 @@ class Solver { $packageId = abs($literalId); return ( - $this->decisionMap[$packageId] > 0 && !($literalId < 0) || - $this->decisionMap[$packageId] < 0 && $literalId > 0 + ($this->decisionMap[$packageId] > 0 && $literalId < 0) || + ($this->decisionMap[$packageId] < 0 && $literalId > 0) ); } @@ -1227,7 +1214,8 @@ class Solver continue; } - for ($rule = $this->watches[$literal->getId()]; $rule !== null; $rule = $nextRule) { + $prevRule = null; + for ($rule = $this->watches[$literal->getId()]; $rule !== null; $prevRule = $rule, $rule = $nextRule) { $nextRule = $rule->getNext($literal); if ($rule->isDisabled()) { @@ -1247,16 +1235,27 @@ class Solver if ($otherWatch !== $ruleLiteral->getId() && !$this->decisionsConflict($ruleLiteral)) { - if ($literal->getId() === $rule->watch1) { $rule->watch1 = $ruleLiteral->getId(); - $rule->next1 = (isset($this->watches[$ruleLiteral->getId()])) ? $this->watches[$ruleLiteral->getId()] : null ; + $rule->next1 = (isset($this->watches[$ruleLiteral->getId()])) ? $this->watches[$ruleLiteral->getId()] : null; } else { $rule->watch2 = $ruleLiteral->getId(); - $rule->next2 = (isset($this->watches[$ruleLiteral->getId()])) ? $this->watches[$ruleLiteral->getId()] : null ; + $rule->next2 = (isset($this->watches[$ruleLiteral->getId()])) ? $this->watches[$ruleLiteral->getId()] : null; + } + + if ($prevRule) { + if ($prevRule->next1 == $rule) { + $prevRule->next1 = $nextRule; + } else { + $prevRule->next2 = $nextRule; + } + } else { + $this->watches[$literal->getId()] = $nextRule; } $this->watches[$ruleLiteral->getId()] = $rule; + + $rule = $prevRule; continue 2; } } @@ -1487,7 +1486,7 @@ class Solver } $why = count($this->learnedPool) - 1; - + assert($learnedLiterals[0] !== null); $newRule = new Rule($learnedLiterals, self::RULE_LEARNED, $why); return array($ruleLevel, $newRule, $why); @@ -1813,11 +1812,7 @@ class Solver $rule = null; - if (isset($this->packageToUpdateRule[$literal->getPackageId()])) { - $rule = $this->packageToUpdateRule[$literal->getPackageId()]; - } - - if ((!$rule || $rule->isDisabled()) && isset($this->packageToFeatureRule[$literal->getPackageId()])) { + if (isset($this->packageToFeatureRule[$literal->getPackageId()])) { $rule = $this->packageToFeatureRule[$literal->getPackageId()]; } @@ -2027,8 +2022,10 @@ class Solver } if ($level > 0) { echo ' +' . $this->pool->packageById($packageId)."\n"; - } else { + } elseif ($level < 0) { echo ' -' . $this->pool->packageById($packageId)."\n"; + } else { + echo ' ?' . $this->pool->packageById($packageId)."\n"; } } echo "\n"; @@ -2042,4 +2039,41 @@ class Solver } echo "\n"; } + + private function printWatches() + { + echo "\nWatches:\n"; + foreach ($this->watches as $literalId => $watch) { + echo ' '.$this->literalFromId($literalId)."\n"; + $queue = array(array(' ', $watch)); + + while (!empty($queue)) { + list($indent, $watch) = array_pop($queue); + + echo $indent.$watch; + + if ($watch) { + echo ' [id='.$watch->getId().',watch1='.$this->literalFromId($watch->watch1).',watch2='.$this->literalFromId($watch->watch2)."]"; + } + + echo "\n"; + + if ($watch && ($watch->next1 == $watch || $watch->next2 == $watch)) { + if ($watch->next1 == $watch) { + echo $indent." 1 *RECURSION*"; + } + if ($watch->next2 == $watch) { + echo $indent." 2 *RECURSION*"; + } + } elseif ($watch && ($watch->next1 || $watch->next2)) { + $indent = str_replace(array('1', '2'), ' ', $indent); + + array_push($queue, array($indent.' 2 ', $watch->next2)); + array_push($queue, array($indent.' 1 ', $watch->next1)); + } + } + + echo "\n"; + } + } } diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php new file mode 100644 index 000000000..cbc4fd571 --- /dev/null +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -0,0 +1,65 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +/** + * @author Nils Adermann + */ +class SolverProblemsException extends \RuntimeException +{ + protected $problems; + + public function __construct(array $problems, array $learnedPool) + { + $message = ''; + foreach ($problems as $i => $problem) { + $message .= '['; + foreach ($problem as $why) { + + if (is_int($why) && isset($learnedPool[$why])) { + $rules = $learnedPool[$why]; + } else { + $rules = $why; + } + + 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"; + } + + parent::__construct($message); + } + + public function jobToText($job) + { + //$output = serialize($job); + $output = 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.implode(', ', $job['packages']).'])'; + return $output; + } +} diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index e466bbb09..e1441f182 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -125,14 +125,14 @@ class DownloadManager $sourceType = $package->getSourceType(); $distType = $package->getDistType(); - if (!($preferSource && $sourceType) && $distType) { + if (!$package->isDev() && !($preferSource && $sourceType) && $distType) { $package->setInstallationSource('dist'); } elseif ($sourceType) { $package->setInstallationSource('source'); + } elseif ($package->isDev()) { + throw new \InvalidArgumentException('Dev package '.$package.' must have a source specified'); } else { - throw new \InvalidArgumentException( - 'Package '.$package.' should have source or dist specified' - ); + throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); } $fs = new Filesystem(); diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index e4c81e824..22bae3667 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -71,7 +71,15 @@ class Factory $rm = $this->createRepositoryManager($io); // load default repository unless it's explicitly disabled - if (!isset($packageConfig['repositories']['packagist']) || $packageConfig['repositories']['packagist'] !== false) { + $loadPackagist = true; + if (isset($packageConfig['repositories'])) { + foreach ($packageConfig['repositories'] as $repo) { + if (isset($repo['packagist']) && $repo['packagist'] === false) { + $loadPackagist = false; + } + } + } + if ($loadPackagist) { $this->addPackagistRepository($rm); } diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 1f94f4071..8eef0d7ed 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -78,11 +78,9 @@ class LibraryInstaller implements InstallerInterface */ public function install(PackageInterface $package) { + $this->initializeDirs(); $downloadPath = $this->getInstallPath($package); - $this->filesystem->ensureDirectoryExists($this->vendorDir); - $this->filesystem->ensureDirectoryExists($this->binDir); - // remove the binaries if it appears the package files are missing if (!is_readable($downloadPath) && $this->repository->hasPackage($package)) { $this->removeBinaries($package); @@ -104,16 +102,16 @@ class LibraryInstaller implements InstallerInterface throw new \InvalidArgumentException('Package is not installed: '.$initial); } + $this->initializeDirs(); $downloadPath = $this->getInstallPath($initial); - $this->filesystem->ensureDirectoryExists($this->vendorDir); - $this->filesystem->ensureDirectoryExists($this->binDir); - $this->removeBinaries($initial); $this->downloadManager->update($initial, $target, $downloadPath); $this->installBinaries($target); $this->repository->removePackage($initial); - $this->repository->addPackage(clone $target); + if (!$this->repository->hasPackage($target)) { + $this->repository->addPackage(clone $target); + } } /** @@ -191,6 +189,14 @@ class LibraryInstaller implements InstallerInterface } } + protected function initializeDirs() + { + $this->filesystem->ensureDirectoryExists($this->vendorDir); + $this->filesystem->ensureDirectoryExists($this->binDir); + $this->vendorDir = realpath($this->vendorDir); + $this->binDir = realpath($this->binDir); + } + private function generateWindowsProxyCode($bin, $link) { $binPath = $this->filesystem->findShortestPath($link, $bin); diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 6d6f4f15d..7c992392c 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -122,6 +122,8 @@ class ArrayLoader $package->setSourceType($config['source']['type']); $package->setSourceUrl($config['source']['url']); $package->setSourceReference($config['source']['reference']); + } elseif ($package->isDev()) { + throw new \UnexpectedValueException('Dev package '.$package.' must have a source specified'); } if (isset($config['dist'])) { diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 294dd3f42..26765c78a 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -38,20 +38,23 @@ class RootPackageLoader extends ArrayLoader $config['name'] = '__root__'; } if (!isset($config['version'])) { - $config['version'] = '1.0.0-dev'; + $config['version'] = '1.0.0'; } $package = parent::load($config); if (isset($config['repositories'])) { - foreach ($config['repositories'] as $repoName => $repo) { - if (false === $repo && 'packagist' === $repoName) { + foreach ($config['repositories'] as $index => $repo) { + if (isset($repo['packagist']) && $repo['packagist'] === false) { continue; } if (!is_array($repo)) { - throw new \UnexpectedValueException('Repository '.$repoName.' in '.$package->getPrettyName().' '.$package->getVersion().' should be an array, '.gettype($repo).' given'); + throw new \UnexpectedValueException('Repository '.$index.' should be an array, '.gettype($repo).' given'); } - $repository = $this->manager->createRepository(key($repo), current($repo)); + if (!isset($repo['type'])) { + throw new \UnexpectedValueException('Repository '.$index.' must have a type defined'); + } + $repository = $this->manager->createRepository($repo['type'], $repo); $this->manager->addRepository($repository); } $package->setRepositories($config['repositories']); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 21e09eaf3..c55c0e467 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -69,11 +69,7 @@ class Locker */ public function getLockedPackages() { - if (!$this->isLocked()) { - throw new \LogicException('No lockfile found. Unable to read locked packages'); - } - - $lockList = $this->lockFile->read(); + $lockList = $this->getLockData(); $packages = array(); foreach ($lockList['packages'] as $info) { $package = $this->repositoryManager->getLocalRepository()->findPackage($info['package'], $info['version']); @@ -95,6 +91,15 @@ class Locker return $packages; } + public function getLockData() + { + if (!$this->isLocked()) { + throw new \LogicException('No lockfile found. Unable to read locked packages'); + } + + return $this->lockFile->read(); + } + /** * Locks provided packages into lockfile. * @@ -116,8 +121,17 @@ class Locker )); } - $lock['packages'][] = array('package' => $name, 'version' => $version); + $spec = array('package' => $name, 'version' => $version); + + if ($package->isDev()) { + $spec['source-reference'] = $package->getSourceReference(); + } + + $lock['packages'][] = $spec; } + usort($lock['packages'], function ($a, $b) { + return strcmp($a['package'], $b['package']); + }); $this->lockFile->write($lock); } diff --git a/src/Composer/Package/MemoryPackage.php b/src/Composer/Package/MemoryPackage.php index d1e63e102..a57f2b6f6 100644 --- a/src/Composer/Package/MemoryPackage.php +++ b/src/Composer/Package/MemoryPackage.php @@ -41,6 +41,7 @@ class MemoryPackage extends BasePackage protected $extra = array(); protected $binaries = array(); protected $scripts = array(); + protected $dev; protected $requires = array(); protected $conflicts = array(); @@ -63,6 +64,16 @@ class MemoryPackage extends BasePackage $this->version = $version; $this->prettyVersion = $prettyVersion; + + $this->dev = 'dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4); + } + + /** + * {@inheritDoc} + */ + public function isDev() + { + return $this->dev; } /** diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index c8f92b581..1f19a835f 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -68,6 +68,13 @@ interface PackageInterface */ function matches($name, LinkConstraintInterface $constraint); + /** + * Returns whether the package is a development virtual package or a concrete one + * + * @return Boolean + */ + function isDev(); + /** * Returns the package type, e.g. library * diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 7bd941d93..716c24731 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -34,10 +34,15 @@ class VersionParser { $version = trim($version); - if (preg_match('{^(?:master|trunk|default)(?:[.-]?dev)?$}i', $version)) { + // match master-like branches + if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { return '9999999-dev'; } + if ('dev-' === strtolower(substr($version, 0, 4))) { + return strtolower($version); + } + // match classical versioning if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?'.$this->modifierRegex.'$}i', $version, $matches)) { $version = $matches[1] @@ -53,7 +58,7 @@ class VersionParser // add version modifiers if a version was matched if (isset($index)) { if (!empty($matches[$index])) { - $mod = array('{^pl?$}', '{^rc$}'); + $mod = array('{^pl?$}i', '{^rc$}i'); $modNormalized = array('patch', 'RC'); $version .= '-'.preg_replace($mod, $modNormalized, strtolower($matches[$index])) . (!empty($matches[$index+1]) ? $matches[$index+1] : ''); @@ -97,7 +102,7 @@ class VersionParser return str_replace('x', '9999999', $version).'-dev'; } - throw new \UnexpectedValueException('Invalid branch name '.$name); + return 'dev-'.$name; } /** diff --git a/src/Composer/Repository/PackageRepository.php b/src/Composer/Repository/PackageRepository.php index 01b1b24c2..fe2e83187 100644 --- a/src/Composer/Repository/PackageRepository.php +++ b/src/Composer/Repository/PackageRepository.php @@ -33,7 +33,7 @@ class PackageRepository extends ArrayRepository */ public function __construct(array $config) { - $this->config = $config; + $this->config = $config['package']; } /** diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index edf5b9e3b..ed7bd8d53 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -22,6 +22,7 @@ use Composer\Util\StreamContextFactory; class PearRepository extends ArrayRepository { private $url; + private $channel; private $streamContext; public function __construct(array $config) @@ -29,11 +30,14 @@ class PearRepository extends ArrayRepository if (!preg_match('{^https?://}', $config['url'])) { $config['url'] = 'http://'.$config['url']; } + if (!filter_var($config['url'], FILTER_VALIDATE_URL)) { throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$config['url']); } $this->url = rtrim($config['url'], '/'); + + $this->channel = !empty($config['channel']) ? $config['channel'] : null; } protected function initialize() @@ -50,6 +54,12 @@ class PearRepository extends ArrayRepository protected function fetchFromServer() { + if (!$this->channel) { + $channelXML = $this->requestXml($this->url . "/channel.xml"); + $this->channel = $channelXML->getElementsByTagName("suggestedalias")->item(0)->nodeValue + ?: $channelXML->getElementsByTagName("name")->item(0)->nodeValue; + } + $categoryXML = $this->requestXml($this->url . "/rest/c/categories.xml"); $categories = $categoryXML->getElementsByTagName("c"); @@ -81,6 +91,7 @@ class PearRepository extends ArrayRepository $loader = new ArrayLoader(); foreach ($packages as $package) { $packageName = $package->nodeValue; + $fullName = 'pear-'.$this->channel.'/'.$packageName; $packageLink = $package->getAttribute('xlink:href'); $releaseLink = $this->url . str_replace("/rest/p/", "/rest/r/", $packageLink); @@ -102,7 +113,7 @@ class PearRepository extends ArrayRepository $pearVersion = $release->getElementsByTagName('v')->item(0)->nodeValue; $packageData = array( - 'name' => $packageName, + 'name' => $fullName, 'type' => 'library', 'dist' => array('type' => 'pear', 'url' => $this->url.'/get/'.$packageName.'-'.$pearVersion.".tgz"), 'version' => $pearVersion, @@ -220,8 +231,9 @@ class PearRepository extends ArrayRepository $package = $information->getElementsByTagName('p')->item(0); $packageName = $package->getElementsByTagName('n')->item(0)->nodeValue; + $fullName = 'pear-'.$this->channel.'/'.$packageName; $packageData = array( - 'name' => $packageName, + 'name' => $fullName, 'type' => 'library' ); $packageKeys = array('l' => 'license', 'd' => 'description'); diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index 8a8d1f8c6..e99a0e5f8 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -15,6 +15,7 @@ class GitDriver extends VcsDriver implements VcsDriverInterface protected $branches; protected $rootIdentifier; protected $infoCache = array(); + protected $isLocal = false; public function __construct($url, IOInterface $io, ProcessExecutor $process = null) { @@ -30,10 +31,15 @@ class GitDriver extends VcsDriver implements VcsDriverInterface { $url = escapeshellarg($this->url); $tmpDir = escapeshellarg($this->tmpDir); - if (is_dir($this->tmpDir)) { - $this->process->execute(sprintf('cd %s && git fetch origin', $tmpDir), $output); + + if (static::isLocalUrl($url)) { + $this->isLocal = true; } else { - $this->process->execute(sprintf('git clone %s %s', $url, $tmpDir), $output); + if (is_dir($this->tmpDir)) { + $this->process->execute(sprintf('cd %s && git fetch origin', $tmpDir), $output); + } else { + $this->process->execute(sprintf('git clone %s %s', $url, $tmpDir), $output); + } } $this->getTags(); @@ -47,11 +53,27 @@ class GitDriver extends VcsDriver implements VcsDriverInterface { if (null === $this->rootIdentifier) { $this->rootIdentifier = 'master'; - $this->process->execute(sprintf('cd %s && git branch --no-color -r', escapeshellarg($this->tmpDir)), $output); - foreach ($this->process->splitLines($output) as $branch) { - if ($branch && preg_match('{/HEAD +-> +[^/]+/(\S+)}', $branch, $match)) { - $this->rootIdentifier = $match[1]; - break; + + 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->tmpDir)), $output); + $branches = $this->process->splitLines($output); + if (!in_array('* master', $branches)) { + foreach ($branches as $branch) { + if ($branch && preg_match('{^\* +(\S+)}', $branch, $match)) { + $this->rootIdentifier = $match[1]; + break; + } + } + } + } else { + // try to find a non-master remote HEAD branch + $this->process->execute(sprintf('cd %s && git branch --no-color -r', escapeshellarg($this->tmpDir)), $output); + foreach ($this->process->splitLines($output) as $branch) { + if ($branch && preg_match('{/HEAD +-> +[^/]+/(\S+)}', $branch, $match)) { + $this->rootIdentifier = $match[1]; + break; + } } } } @@ -132,7 +154,11 @@ class GitDriver extends VcsDriver implements VcsDriverInterface if (null === $this->branches) { $branches = array(); - $this->process->execute(sprintf('cd %s && git branch --no-color -rv', escapeshellarg($this->tmpDir)), $output); + $this->process->execute(sprintf( + 'cd %s && git branch --no-color --no-abbrev -v %s', + escapeshellarg($this->tmpDir), + $this->isLocal ? '' : '-r' + ), $output); foreach ($this->process->splitLines($output) as $branch) { if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) { preg_match('{^ *[^/]+/(\S+) *([a-f0-9]+) .*$}', $branch, $match); @@ -170,7 +196,7 @@ class GitDriver extends VcsDriver implements VcsDriverInterface } // local filesystem - if (preg_match('{^(file://|/|[a-z]:[\\\\/])}i', $url)) { + if (static::isLocalUrl($url)) { $process = new ProcessExecutor(); // check whether there is a git repo in that path if ($process->execute(sprintf('cd %s && git show', escapeshellarg($url)), $output) === 0) { diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index 7008f41be..75b631a42 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -68,4 +68,9 @@ abstract class VcsDriver $rfs = new RemoteFilesystem($this->io); return $rfs->getContents($this->url, $url, false); } + + protected static function isLocalUrl($url) + { + return (Boolean) preg_match('{^(file://|/|[a-z]:[\\\\/])}i', $url); + } } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index a2506d2df..2c4175fa0 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -76,20 +76,22 @@ class VcsRepository extends ArrayRepository } foreach ($driver->getTags() as $tag => $identifier) { - $this->io->overwrite('Get composer of ' . $this->packageName . ' (' . $tag . ')', false); + $msg = 'Get composer info for ' . $this->packageName . ' (' . $tag . ')'; + if ($debug) { + $this->io->write($msg); + } else { + $this->io->overwrite($msg, false); + } + $parsedTag = $this->validateTag($versionParser, $tag); if ($parsedTag && $driver->hasComposerFile($identifier)) { try { $data = $driver->getComposerInformation($identifier); } catch (\Exception $e) { - if (strpos($e->getMessage(), 'JSON Parse Error') !== false) { - if ($debug) { - $this->io->write('Skipped tag '.$tag.', '.$e->getMessage()); - } - continue; - } else { - throw $e; + if ($debug) { + $this->io->write('Skipped tag '.$tag.', '.$e->getMessage()); } + continue; } // manually versioned package @@ -103,7 +105,7 @@ class VcsRepository extends ArrayRepository // make sure tag packages have no -dev flag $data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']); - $data['version_normalized'] = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']); + $data['version_normalized'] = preg_replace('{(^dev-|[.-]?dev$)}i', '', $data['version_normalized']); // broken package, version doesn't match tag if ($data['version_normalized'] !== $parsedTag) { @@ -126,39 +128,33 @@ class VcsRepository extends ArrayRepository $this->io->overwrite('', false); foreach ($driver->getBranches() as $branch => $identifier) { - $this->io->overwrite('Get composer of ' . $this->packageName . ' (' . $branch . ')', false); + $msg = 'Get composer info for ' . $this->packageName . ' (' . $branch . ')'; + if ($debug) { + $this->io->write($msg); + } else { + $this->io->overwrite($msg, false); + } + $parsedBranch = $this->validateBranch($versionParser, $branch); if ($driver->hasComposerFile($identifier)) { $data = $driver->getComposerInformation($identifier); - // manually versioned package - if (isset($data['version'])) { - $data['version_normalized'] = $versionParser->normalize($data['version']); - } elseif ($parsedBranch) { - // auto-versionned package, read value from branch name - $data['version'] = $branch; - $data['version_normalized'] = $parsedBranch; - } else { + if (!$parsedBranch) { if ($debug) { $this->io->write('Skipped branch '.$branch.', invalid name and no composer file was found'); } continue; } - // make sure branch packages have a -dev flag - $normalizedStableVersion = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']); - $data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']) . '-dev'; - $data['version_normalized'] = $normalizedStableVersion . '-dev'; + // branches are always auto-versionned, read value from branch name + $data['version'] = $branch; + $data['version_normalized'] = $parsedBranch; - // Skip branches that contain a version that has been tagged already - foreach ($this->getPackages() as $package) { - if ($normalizedStableVersion === $package->getVersion()) { - if ($debug) { - $this->io->write('Skipped branch '.$branch.', already tagged'); - } - - continue 2; - } + // make sure branch packages have a dev flag + if ('dev-' === substr($parsedBranch, 0, 4) || '9999999-dev' === $parsedBranch) { + $data['version'] = 'dev-' . $data['version']; + } else { + $data['version'] = $data['version'] . '-dev'; } if ($debug) { diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index da186edb2..e5010e0e4 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -46,4 +46,16 @@ class RequestTest extends TestCase ), $request->getJobs()); } + + public function testUpdateAll() + { + $pool = new Pool; + $request = new Request($pool); + + $request->updateAll(); + + $this->assertEquals( + array(array('cmd' => 'update-all', 'packages' => array())), + $request->getJobs()); + } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 6340a7f68..28db18131 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -27,7 +27,7 @@ class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase new Rule(array(), 'job1', null), new Rule(array(), 'job2', null), ), - RuleSet::TYPE_UPDATE => array( + RuleSet::TYPE_FEATURE => array( new Rule(array(), 'update1', null), ), RuleSet::TYPE_PACKAGE => array(), @@ -46,7 +46,7 @@ class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase $expected = array( $this->rules[RuleSet::TYPE_JOB][0], $this->rules[RuleSet::TYPE_JOB][1], - $this->rules[RuleSet::TYPE_UPDATE][0], + $this->rules[RuleSet::TYPE_FEATURE][0], ); $this->assertEquals($expected, $result); @@ -64,7 +64,7 @@ class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase $expected = array( RuleSet::TYPE_JOB, RuleSet::TYPE_JOB, - RuleSet::TYPE_UPDATE, + RuleSet::TYPE_FEATURE, ); $this->assertEquals($expected, $result); diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 7a5b018b4..54ad88a58 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -27,10 +27,9 @@ class RuleSetTest extends TestCase new Rule(array(), 'job1', null), new Rule(array(), 'job2', null), ), - RuleSet::TYPE_UPDATE => array( + RuleSet::TYPE_FEATURE => array( new Rule(array(), 'update1', null), ), - RuleSet::TYPE_FEATURE => array(), RuleSet::TYPE_LEARNED => array(), RuleSet::TYPE_CHOICE => array(), ); @@ -38,7 +37,7 @@ class RuleSetTest extends TestCase $ruleSet = new RuleSet; $ruleSet->add($rules[RuleSet::TYPE_JOB][0], RuleSet::TYPE_JOB); - $ruleSet->add($rules[RuleSet::TYPE_UPDATE][0], RuleSet::TYPE_UPDATE); + $ruleSet->add($rules[RuleSet::TYPE_FEATURE][0], RuleSet::TYPE_FEATURE); $ruleSet->add($rules[RuleSet::TYPE_JOB][1], RuleSet::TYPE_JOB); $this->assertEquals($rules, $ruleSet->getRules()); @@ -81,7 +80,7 @@ class RuleSetTest extends TestCase $rule1 = new Rule(array(), 'job1', null); $rule2 = new Rule(array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); - $ruleSet->add($rule2, RuleSet::TYPE_UPDATE); + $ruleSet->add($rule2, RuleSet::TYPE_FEATURE); $iterator = $ruleSet->getIterator(); @@ -97,9 +96,9 @@ class RuleSetTest extends TestCase $rule2 = new Rule(array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); - $ruleSet->add($rule2, RuleSet::TYPE_UPDATE); + $ruleSet->add($rule2, RuleSet::TYPE_FEATURE); - $iterator = $ruleSet->getIteratorFor(RuleSet::TYPE_UPDATE); + $iterator = $ruleSet->getIteratorFor(RuleSet::TYPE_FEATURE); $this->assertSame($rule2, $iterator->current()); } @@ -111,7 +110,7 @@ class RuleSetTest extends TestCase $rule2 = new Rule(array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); - $ruleSet->add($rule2, RuleSet::TYPE_UPDATE); + $ruleSet->add($rule2, RuleSet::TYPE_FEATURE); $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_JOB); @@ -143,7 +142,7 @@ class RuleSetTest extends TestCase ->method('equal') ->will($this->returnValue(false)); - $ruleSet->add($rule, RuleSet::TYPE_UPDATE); + $ruleSet->add($rule, RuleSet::TYPE_FEATURE); $this->assertTrue($ruleSet->containsEqual($rule)); $this->assertFalse($ruleSet->containsEqual($rule2)); @@ -156,9 +155,9 @@ class RuleSetTest extends TestCase $literal = new Literal($this->getPackage('foo', '2.1'), true); $rule = new Rule(array($literal), 'job1', null); - $ruleSet->add($rule, RuleSet::TYPE_UPDATE); + $ruleSet->add($rule, RuleSet::TYPE_FEATURE); - $this->assertContains('UPDATE : (+foo-2.1.0.0)', $ruleSet->__toString()); + $this->assertContains('FEATURE : (+foo-2.1.0.0)', $ruleSet->__toString()); } private function getRuleMock() diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index a71feafce..fe6782177 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -19,6 +19,7 @@ use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Solver; +use Composer\DependencyResolver\SolverProblemsException; use Composer\Package\Link; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Test\TestCase; @@ -54,13 +55,28 @@ class SolverTest extends TestCase )); } + public function testInstallNonExistingPackageFails() + { + $this->repo->addPackage($this->getPackage('A', '1.0')); + $this->reposComplete(); + + $this->request->install('B'); + + try { + $transaction = $this->solver->solve($this->request); + $this->fail('Unsolvable conflict did not resolve in exception.'); + } catch (SolverProblemsException $e) { + // TODO assert problem properties + } + } + public function testSolverInstallWithDeps() { $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('<', '1.1'), 'requires'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires'))); $this->reposComplete(); @@ -122,12 +138,12 @@ class SolverTest extends TestCase $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); $this->reposComplete(); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0.0.0'), 'requires'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires'))); - $this->request->install('A', new VersionConstraint('=', '1.0.0.0')); - $this->request->install('B', new VersionConstraint('=', '1.1.0.0')); - $this->request->update('A', new VersionConstraint('=', '1.0.0.0')); - $this->request->update('B', new VersionConstraint('=', '1.0.0.0')); + $this->request->install('A', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0')); + $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->update('B', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -147,6 +163,26 @@ class SolverTest extends TestCase )); } + public function testSolverUpdateAll() + { + $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); + $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); + + $packageA->setRequires(array(new Link('A', 'B', null, 'requires'))); + + $this->reposComplete(); + + $this->request->install('A'); + $this->request->updateAll(); + + $this->checkSolverResult(array( + array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), + array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA), + )); + } + public function testSolverUpdateCurrent() { $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); @@ -181,7 +217,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', new VersionConstraint('<', '2.0.0.0')); + $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->request->update('A'); $this->checkSolverResult(array(array( @@ -198,8 +234,8 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', new VersionConstraint('<', '2.0.0.0')); - $this->request->update('A', new VersionConstraint('=', '1.0.0.0')); + $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -216,8 +252,8 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', new VersionConstraint('<', '2.0.0.0')); - $this->request->update('A', new VersionConstraint('=', '1.0.0.0')); + $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -236,7 +272,7 @@ class SolverTest extends TestCase $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); $this->repo->addPackage($packageC = $this->getPackage('C', '1.1')); $this->repo->addPackage($this->getPackage('D', '1.0')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('<', '1.1'), 'requires'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires'))); $this->reposComplete(); @@ -258,8 +294,8 @@ class SolverTest extends TestCase $this->repo->addPackage($middlePackageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); $this->repo->addPackage($oldPackageB = $this->getPackage('B', '0.9')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('<', '1.1'), 'requires'))); - $packageA->setConflicts(array(new Link('A', 'B', new VersionConstraint('<', '1.0'), 'conflicts'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires'))); + $packageA->setConflicts(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.0'), 'conflicts'))); $this->reposComplete(); @@ -305,8 +341,8 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '0.8')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageQ->setProvides(array(new Link('Q', 'B', new VersionConstraint('=', '1.0'), 'provides'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageQ->setProvides(array(new Link('Q', 'B', $this->getVersionConstraint('=', '1.0'), 'provides'))); $this->reposComplete(); @@ -323,8 +359,8 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageQ->setReplaces(array(new Link('Q', 'B', new VersionConstraint('>=', '1.0'), 'replaces'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageQ->setReplaces(array(new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'))); $this->reposComplete(); @@ -340,8 +376,8 @@ class SolverTest extends TestCase { $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageQ->setReplaces(array(new Link('Q', 'B', new VersionConstraint('>=', '1.0'), 'replaces'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageQ->setReplaces(array(new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'))); $this->reposComplete(); @@ -358,8 +394,8 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageQ->setReplaces(array(new Link('Q', 'B', new VersionConstraint('>=', '1.0'), 'replaces'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageQ->setReplaces(array(new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'))); $this->reposComplete(); @@ -376,24 +412,24 @@ class SolverTest extends TestCase { $this->repo->addPackage($packageX = $this->getPackage('X', '1.0')); $packageX->setRequires(array( - new Link('X', 'A', new VersionConstraint('>=', '2.0.0.0'), 'requires'), - new Link('X', 'B', new VersionConstraint('>=', '2.0.0.0'), 'requires'))); + new Link('X', 'A', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires'), + new Link('X', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires'))); $this->repo->addPackage($packageA = $this->getPackage('A', '2.0.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '2.1.0')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '2.1.0')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '2.0.0.0'), 'requires'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires'))); // new package A depends on version of package B that does not exist // => new package A is not installable - $newPackageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '2.2.0.0'), 'requires'))); + $newPackageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '2.2.0.0'), 'requires'))); // add a package S replacing both A and B, so that S and B or S and A cannot be simultaneously installed // but an alternative option for A and B both exists // this creates a more difficult so solve conflict $this->repo->addPackage($packageS = $this->getPackage('S', '2.0.0')); - $packageS->setReplaces(array(new Link('S', 'A', new VersionConstraint('>=', '2.0.0.0'), 'replaces'), new Link('S', 'B', new VersionConstraint('>=', '2.0.0.0'), 'replaces'))); + $packageS->setReplaces(array(new Link('S', 'A', $this->getVersionConstraint('>=', '2.0.0.0'), 'replaces'), new Link('S', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'replaces'))); $this->reposComplete(); @@ -411,8 +447,8 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB1 = $this->getPackage('B', '0.9')); $this->repo->addPackage($packageB2 = $this->getPackage('B', '1.1')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageB2->setRequires(array(new Link('B', 'A', new VersionConstraint('>=', '1.0'), 'requires'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageB2->setRequires(array(new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'))); $this->reposComplete(); @@ -426,16 +462,17 @@ class SolverTest extends TestCase public function testInstallAlternativeWithCircularRequire() { - $this->markTestIncomplete(); - $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($packageC = $this->getPackage('C', '1.0')); $this->repo->addPackage($packageD = $this->getPackage('D', '1.0')); - $packageA->setRequires(array(new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageB->setRequires(array(new Link('B', 'Virtual', new VersionConstraint('>=', '1.0'), 'requires'))); - $packageC->setRequires(array(new Link('C', 'Virtual', new VersionConstraint('==', '1.0'), 'provides'))); - $packageD->setRequires(array(new Link('D', 'Virtual', new VersionConstraint('==', '1.0'), 'provides'))); + $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageB->setRequires(array(new Link('B', 'Virtual', $this->getVersionConstraint('>=', '1.0'), 'requires'))); + $packageC->setProvides(array(new Link('C', 'Virtual', $this->getVersionConstraint('==', '1.0'), 'provides'))); + $packageD->setProvides(array(new Link('D', 'Virtual', $this->getVersionConstraint('==', '1.0'), 'provides'))); + + $packageC->setRequires(array(new Link('C', 'A', $this->getVersionConstraint('==', '1.0'), 'requires'))); + $packageD->setRequires(array(new Link('D', 'A', $this->getVersionConstraint('==', '1.0'), 'requires'))); $this->reposComplete(); @@ -460,18 +497,18 @@ class SolverTest extends TestCase $this->repo->addPackage($packageD2 = $this->getPackage('D', '1.1')); $packageA->setRequires(array( - new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'), - new Link('A', 'C', new VersionConstraint('>=', '1.0'), 'requires'), + new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'), + new Link('A', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'), )); $packageD->setReplaces(array( - new Link('D', 'B', new VersionConstraint('>=', '1.0'), 'replaces'), - new Link('D', 'C', new VersionConstraint('>=', '1.0'), 'replaces'), + new Link('D', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'), + new Link('D', 'C', $this->getVersionConstraint('>=', '1.0'), 'replaces'), )); $packageD2->setReplaces(array( - new Link('D', 'B', new VersionConstraint('>=', '1.0'), 'replaces'), - new Link('D', 'C', new VersionConstraint('>=', '1.0'), 'replaces'), + new Link('D', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'), + new Link('D', 'C', $this->getVersionConstraint('>=', '1.0'), 'replaces'), )); $this->reposComplete(); @@ -484,6 +521,83 @@ class SolverTest extends TestCase )); } + public function testIssue265() + { + $this->repo->addPackage($packageA1 = $this->getPackage('A', '2.0.999999-dev')); + $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.1-dev')); + $this->repo->addPackage($packageA3 = $this->getPackage('A', '2.2-dev')); + $this->repo->addPackage($packageB1 = $this->getPackage('B', '2.0.10')); + $this->repo->addPackage($packageB2 = $this->getPackage('B', '2.0.9')); + $this->repo->addPackage($packageC = $this->getPackage('C', '2.0-dev')); + $this->repo->addPackage($packageD = $this->getPackage('D', '2.0.9')); + + $packageC->setRequires(array( + new Link('C', 'A', $this->getVersionConstraint('>=', '2.0'), 'requires'), + new Link('C', 'D', $this->getVersionConstraint('>=', '2.0'), 'requires'), + )); + + $packageD->setRequires(array( + new Link('D', 'A', $this->getVersionConstraint('>=', '2.1'), 'requires'), + new Link('D', 'B', $this->getVersionConstraint('>=', '2.0-dev'), 'requires'), + )); + + $packageB1->setRequires(array(new Link('B', 'A', $this->getVersionConstraint('==', '2.1.0.0-dev'), 'requires'))); + $packageB2->setRequires(array(new Link('B', 'A', $this->getVersionConstraint('==', '2.1.0.0-dev'), 'requires'))); + + $packageB2->setReplaces(array(new Link('B', 'D', $this->getVersionConstraint('==', '2.0.9.0'), 'replaces'))); + + $this->reposComplete(); + + $this->request->install('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); + + $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + + $this->solver->solve($this->request); + } + + public function testConflictResultEmpty() + { + $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));; + + $packageA->setConflicts(array( + new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'conflicts'), + )); + + $this->reposComplete(); + + $this->request->install('A'); + $this->request->install('B'); + + try { + $transaction = $this->solver->solve($this->request); + $this->fail('Unsolvable conflict did not resolve in exception.'); + } catch (SolverProblemsException $e) { + // TODO assert problem properties + } + } + + public function testUnsatisfiableRequires() + { + $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); + + $packageA->setRequires(array( + new Link('A', 'B', $this->getVersionConstraint('>=', '2.0'), 'requires'), + )); + + $this->reposComplete(); + + $this->request->install('A'); + + try { + $transaction = $this->solver->solve($this->request); + $this->fail('Unsolvable conflict did not resolve in exception.'); + } catch (SolverProblemsException $e) { + // TODO assert problem properties + } + } + protected function reposComplete() { $this->pool->addRepository($this->repoInstalled); @@ -513,5 +627,4 @@ class SolverTest extends TestCase $this->assertEquals($expected, $result); } - } diff --git a/tests/Composer/Test/Installer/InstallerInstallerTest.php b/tests/Composer/Test/Installer/InstallerInstallerTest.php index 4e2f8c732..eab2d948a 100644 --- a/tests/Composer/Test/Installer/InstallerInstallerTest.php +++ b/tests/Composer/Test/Installer/InstallerInstallerTest.php @@ -67,9 +67,9 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase ->method('getPackages') ->will($this->returnValue(array($this->packages[0]))); $this->repository - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('hasPackage') - ->will($this->returnValue(true)); + ->will($this->onConsecutiveCalls(true, false)); $installer = new InstallerInstallerMock(__DIR__.'/Fixtures/', __DIR__.'/Fixtures/bin', $this->dm, $this->repository, $this->io, $this->im); $test = $this; @@ -90,9 +90,9 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase ->method('getPackages') ->will($this->returnValue(array($this->packages[1]))); $this->repository - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('hasPackage') - ->will($this->returnValue(true)); + ->will($this->onConsecutiveCalls(true, false)); $installer = new InstallerInstallerMock(__DIR__.'/Fixtures/', __DIR__.'/Fixtures/bin', $this->dm, $this->repository, $this->io, $this->im); $test = $this; diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index 3399345f0..ba86954e3 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -128,10 +128,9 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('package1')); $this->repository - ->expects($this->exactly(2)) + ->expects($this->exactly(3)) ->method('hasPackage') - ->with($initial) - ->will($this->onConsecutiveCalls(true, false)); + ->will($this->onConsecutiveCalls(true, false, false)); $this->dm ->expects($this->once()) diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php index faca00dc9..64a3e79f4 100644 --- a/tests/Composer/Test/Package/Version/VersionParserTest.php +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -49,9 +49,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'parses datetime' => array('20100102-203040', '20100102-203040'), 'parses dt+number' => array('20100102203040-10', '20100102203040-10'), 'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'), - 'parses master' => array('master', '9999999-dev'), - 'parses trunk' => array('trunk', '9999999-dev'), - 'parses trunk/2' => array('trunk-dev', '9999999-dev'), + 'parses master' => array('dev-master', '9999999-dev'), + 'parses trunk' => array('dev-trunk', '9999999-dev'), + 'parses arbitrary' => array('dev-feature-foo', 'dev-feature-foo'), + 'parses arbitrary2' => array('DEV-FOOBAR', 'dev-foobar'), ); } @@ -72,6 +73,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'invalid chars' => array('a'), 'invalid type' => array('1.0.0-meh'), 'too many bits' => array('1.0.0.0.0'), + 'non-dev arbitrary' => array('feature-foo'), ); } @@ -97,6 +99,8 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'parses long digits/2' => array('2.4.4', '2.4.4.9999999-dev'), 'parses master' => array('master', '9999999-dev'), 'parses trunk' => array('trunk', '9999999-dev'), + 'parses arbitrary' => array('feature-a', 'dev-feature-a'), + 'parses arbitrary/2' => array('foobar', 'dev-foobar'), ); } @@ -121,8 +125,9 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'no op means eq' => array('1.2.3', new VersionConstraint('=', '1.2.3.0')), 'completes version' => array('=1.0', new VersionConstraint('=', '1.0.0.0')), 'accepts spaces' => array('>= 1.2.3', new VersionConstraint('>=', '1.2.3.0')), - 'accepts master' => array('>=master-dev', new VersionConstraint('>=', '9999999-dev')), - 'accepts master/2' => array('master-dev', new VersionConstraint('=', '9999999-dev')), + 'accepts master' => array('>=dev-master', new VersionConstraint('>=', '9999999-dev')), + 'accepts master/2' => array('dev-master', new VersionConstraint('=', '9999999-dev')), + 'accepts arbitrary' => array('dev-feature-a', new VersionConstraint('=', 'dev-feature-a')), ); } diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php new file mode 100644 index 000000000..4ea24725b --- /dev/null +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -0,0 +1,140 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Json; + +use Symfony\Component\Process\ExecutableFinder; +use Composer\Package\Dumper\ArrayDumper; +use Composer\Repository\VcsRepository; +use Composer\Repository\Vcs\GitDriver; +use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; +use Composer\IO\NullIO; + +class VcsRepositoryTest extends \PHPUnit_Framework_TestCase +{ + private static $gitRepo; + private static $skipped; + + public static function setUpBeforeClass() + { + $oldCwd = getcwd(); + self::$gitRepo = sys_get_temp_dir() . '/composer-git-'.rand().'/'; + + $locator = new ExecutableFinder(); + if (!$locator->find('git')) { + self::$skipped = 'This test needs a git binary in the PATH to be able to run'; + return; + } + if (!mkdir(self::$gitRepo) || !chdir(self::$gitRepo)) { + self::$skipped = 'Could not create and move into the temp git repo '.self::$gitRepo; + return; + } + + // init + $process = new ProcessExecutor; + $process->execute('git init', $null); + touch('foo'); + $process->execute('git add foo', $null); + $process->execute('git commit -m init', $null); + + // non-composed tag & branch + $process->execute('git tag 0.5.0', $null); + $process->execute('git branch oldbranch', $null); + + // add composed tag & master branch + $composer = array('name' => 'a/b'); + file_put_contents('composer.json', json_encode($composer)); + $process->execute('git add composer.json', $null); + $process->execute('git commit -m addcomposer', $null); + $process->execute('git tag 0.6.0', $null); + + // add feature-a branch + $process->execute('git checkout -b feature-a', $null); + file_put_contents('foo', 'bar feature'); + $process->execute('git add foo', $null); + $process->execute('git commit -m change-a', $null); + + // add version to composer.json + $process->execute('git checkout master', $null); + $composer['version'] = '1.0.0'; + file_put_contents('composer.json', json_encode($composer)); + $process->execute('git add composer.json', $null); + $process->execute('git commit -m addversion', $null); + + // create tag with wrong version in it + $process->execute('git tag 0.9.0', $null); + // create tag with correct version in it + $process->execute('git tag 1.0.0', $null); + + // add feature-b branch + $process->execute('git checkout -b feature-b', $null); + file_put_contents('foo', 'baz feature'); + $process->execute('git add foo', $null); + $process->execute('git commit -m change-b', $null); + + // add 1.0 branch + $process->execute('git checkout master', $null); + $process->execute('git branch 1.0', $null); + + // add 1.0.x branch + $process->execute('git branch 1.0.x', $null); + + // update master to 2.0 + $composer['version'] = '2.0.0'; + file_put_contents('composer.json', json_encode($composer)); + $process->execute('git add composer.json', $null); + $process->execute('git commit -m bump-version', $null); + + chdir($oldCwd); + } + + public function setUp() + { + if (self::$skipped) { + $this->markTestSkipped(self::$skipped); + } + } + + public static function tearDownAfterClass() + { + $fs = new Filesystem; + $fs->removeDirectory(self::$gitRepo); + } + + public function testLoadVersions() + { + $expected = array( + '0.6.0' => true, + '1.0.0' => true, + '1.0-dev' => true, + '1.0.x-dev' => true, + 'dev-feature-b' => true, + 'dev-feature-a' => true, + 'dev-master' => true, + ); + + $repo = new VcsRepository(array('url' => self::$gitRepo), new NullIO); + $packages = $repo->getPackages(); + $dumper = new ArrayDumper(); + + foreach ($packages as $package) { + if (isset($expected[$package->getPrettyVersion()])) { + unset($expected[$package->getPrettyVersion()]); + } else { + $this->fail('Unexpected version '.$package->getPrettyVersion().' in '.json_encode($dumper->dump($package))); + } + } + + $this->assertEmpty($expected, 'Missing versions: '.implode(', ', array_keys($expected))); + } +} diff --git a/tests/Composer/Test/TestCase.php b/tests/Composer/Test/TestCase.php index 17255ab2e..1e3ae257e 100644 --- a/tests/Composer/Test/TestCase.php +++ b/tests/Composer/Test/TestCase.php @@ -1,34 +1,43 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Test; - -use Composer\Package\Version\VersionParser; -use Composer\Package\MemoryPackage; - -abstract class TestCase extends \PHPUnit_Framework_TestCase -{ - private static $versionParser; - - public static function setUpBeforeClass() - { - if (!self::$versionParser) { - self::$versionParser = new VersionParser(); - } - } - - protected function getPackage($name, $version) - { - $normVersion = self::$versionParser->normalize($version); - return new MemoryPackage($name, $normVersion, $version); - } -} \ No newline at end of file + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test; + +use Composer\Package\Version\VersionParser; +use Composer\Package\MemoryPackage; +use Composer\Package\LinkConstraint\VersionConstraint; + +abstract class TestCase extends \PHPUnit_Framework_TestCase +{ + private static $versionParser; + + public static function setUpBeforeClass() + { + if (!self::$versionParser) { + self::$versionParser = new VersionParser(); + } + } + + protected function getVersionConstraint($operator, $version) + { + return new VersionConstraint( + $operator, + self::$versionParser->normalize($version) + ); + } + + protected function getPackage($name, $version) + { + $normVersion = self::$versionParser->normalize($version); + return new MemoryPackage($name, $normVersion, $version); + } +}