1
0
Fork 0

Merge pull request #736 from Seldaek/require-update

Require command & update <package>
pull/739/head
Nils Adermann 2012-05-27 17:11:48 -07:00
commit 242323cba4
20 changed files with 629 additions and 34 deletions

View File

@ -1,6 +1,8 @@
* 1.0.0-alpha4 * 1.0.0-alpha4
* Schema: Added references for dev versions, requiring `dev-master#abcdef` for example will force the abcdef commit * Schema: Added references for dev versions, requiring `dev-master#abcdef` for example will force the abcdef commit
* Added `require` command to add a package to your requirements and install it
* Added a whitelist to `update`. Calling `composer update foo/bar foo/baz` allows you to update only those packages
* Added caching of GitHub metadata (faster startup time with custom GitHub VCS repos) * Added caching of GitHub metadata (faster startup time with custom GitHub VCS repos)
* Added support for file:// URLs to GitDriver * Added support for file:// URLs to GitDriver
* Added --dev flag to `create-project` command * Added --dev flag to `create-project` command

View File

@ -53,12 +53,36 @@ In order to get the latest versions of the dependencies and to update the
This will resolve all dependencies of the project and write the exact versions This will resolve all dependencies of the project and write the exact versions
into `composer.lock`. into `composer.lock`.
If you just want to update a few packages and not all, you can list them as such:
$ php composer.phar update vendor/package vendor/package2
### Options ### Options
* **--prefer-source:** Install packages from `source` when available. * **--prefer-source:** Install packages from `source` when available.
* **--dry-run:** Simulate the command without actually doing anything. * **--dry-run:** Simulate the command without actually doing anything.
* **--dev:** Install packages listed in `require-dev`. * **--dev:** Install packages listed in `require-dev`.
## require
The `require` command adds new packages to the `composer.json` file from
the current directory.
$ php composer.phar require
After adding/changing the requirements, the modified requirements will be
installed or updated.
If you do not want to choose requirements interactively, you can just pass them
to the command.
$ php composer.phar require vendor/package:2.* vendor/package2:dev-master
### Options
* **--prefer-source:** Install packages from `source` when available.
* **--dev:** Add packages to `require-dev`.
## search ## search
The search command allows you to search through the current project's package The search command allows you to search through the current project's package

View File

@ -60,7 +60,8 @@ class InitCommand extends Command
new InputOption('author', null, InputOption::VALUE_NONE, 'Author name of package'), new InputOption('author', null, InputOption::VALUE_NONE, 'Author name of package'),
// new InputOption('version', null, InputOption::VALUE_NONE, 'Version of package'), // new InputOption('version', null, InputOption::VALUE_NONE, 'Version of package'),
new InputOption('homepage', null, InputOption::VALUE_NONE, 'Homepage of package'), new InputOption('homepage', null, InputOption::VALUE_NONE, 'Homepage of package'),
new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'An array required packages'), new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
)) ))
->setHelp(<<<EOT ->setHelp(<<<EOT
The <info>init</info> command creates a basic composer.json file The <info>init</info> command creates a basic composer.json file
@ -216,10 +217,15 @@ EOT
)); ));
$requirements = array(); $requirements = array();
if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dependencies interactively', 'yes', '?'), true)) { if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dependencies (require) interactively', 'yes', '?'), true)) {
$requirements = $this->determineRequirements($input, $output); $requirements = $this->determineRequirements($input, $output, $input->getOption('require'));
} }
$input->setOption('require', $requirements); $input->setOption('require', $requirements);
$devRequirements = array();
if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dev dependencies (require-dev) interactively', 'yes', '?'), true)) {
$devRequirements = $this->determineRequirements($input, $output, $input->getOption('require-dev'));
}
$input->setOption('require-dev', $devRequirements);
} }
protected function findPackages($name) protected function findPackages($name)
@ -246,12 +252,27 @@ EOT
return $packages; return $packages;
} }
protected function determineRequirements(InputInterface $input, OutputInterface $output) protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array())
{ {
$dialog = $this->getHelperSet()->get('dialog'); $dialog = $this->getHelperSet()->get('dialog');
$prompt = $dialog->getQuestion('Search for a package', false, ':'); $prompt = $dialog->getQuestion('Search for a package', false, ':');
$requires = $input->getOption('require') ?: array(); if ($requires) {
foreach ($requires as $key => $requirement) {
$requires[$key] = preg_replace('{^([^=: ]+)[=: ](.*)$}', '$1 $2', $requirement);
if (false === strpos($requires[$key], ' ') && $input->isInteractive()) {
$question = $dialog->getQuestion('Please provide a version constraint for the '.$requirement.' requirement');
if ($constraint = $dialog->ask($output, $question)) {
$requires[$key] .= ' ' . $constraint;
}
}
if (false === strpos($requires[$key], ' ')) {
throw new \InvalidArgumentException('The requirement '.$requirement.' must contain a version constraint');
}
}
return $requires;
}
while (null !== $package = $dialog->ask($output, $prompt)) { while (null !== $package = $dialog->ask($output, $prompt)) {
$matches = $this->findPackages($package); $matches = $this->findPackages($package);
@ -287,7 +308,7 @@ EOT
return sprintf('%s %s', $package->getName(), $package->getPrettyVersion()); return sprintf('%s %s', $package->getName(), $package->getPrettyVersion());
}; };
$package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a <package> <version> couple if it is not listed', false, ':'), $validator, 3); $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a "[package] [version]" couple if it is not listed', false, ':'), $validator, 3);
if (false !== $package) { if (false !== $package) {
$requires[] = $package; $requires[] = $package;

View File

@ -0,0 +1,123 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Json\JsonValidationException;
use Composer\Util\RemoteFilesystem;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class RequireCommand extends InitCommand
{
protected function configure()
{
$this
->setName('require')
->setDescription('Adds required packages to your composer.json and installs them')
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Required package with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
))
->setHelp(<<<EOT
The require command adds required packages to your composer.json and installs them
EOT
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$factory = new Factory;
$file = $factory->getComposerFile();
if (!file_exists($file)) {
$output->writeln('<error>'.$file.' not found.</error>');
return 1;
}
if (!is_readable($file)) {
$output->writeln('<error>'.$file.' is not readable.</error>');
return 1;
}
$dialog = $this->getHelperSet()->get('dialog');
$json = new JsonFile($file);
$composer = $json->read();
$requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'));
$requireKey = $input->getOption('dev') ? 'require-dev' : 'require';
$baseRequirements = array_key_exists($requireKey, $composer) ? $composer[$requireKey] : array();
$requirements = $this->formatRequirements($requirements);
if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey)) {
foreach ($requirements as $package => $version) {
$baseRequirements[$package] = $version;
}
$composer[$requireKey] = $baseRequirements;
$json->write($composer);
}
$output->writeln('<info>'.$file.' has been updated</info>');
// Update packages
$composer = $this->getComposer();
$io = $this->getIO();
$install = Installer::create($io, $composer);
$install
->setVerbose($input->getOption('verbose'))
->setPreferSource($input->getOption('prefer-source'))
->setDevMode($input->getOption('dev'))
->setUpdate(true)
->setUpdateWhitelist($requirements);
;
return $install->run() ? 0 : 1;
}
private function updateFileCleanly($json, array $base, array $new, $requireKey)
{
$contents = file_get_contents($json->getPath());
$manipulator = new JsonManipulator($contents);
foreach ($new as $package => $constraint) {
if (!$manipulator->addLink($requireKey, $package, $constraint)) {
return false;
}
}
file_put_contents($json->getPath(), $manipulator->getContents());
return true;
}
protected function interact(InputInterface $input, OutputInterface $output)
{
return;
}
}

View File

@ -15,6 +15,7 @@ namespace Composer\Command;
use Composer\Installer; use Composer\Installer;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
@ -28,6 +29,7 @@ class UpdateCommand extends Command
->setName('update') ->setName('update')
->setDescription('Updates your dependencies to the latest version, and updates the composer.lock file.') ->setDescription('Updates your dependencies to the latest version, and updates the composer.lock file.')
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of dev-require packages.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of dev-require packages.'),
@ -58,6 +60,7 @@ EOT
->setDevMode($input->getOption('dev')) ->setDevMode($input->getOption('dev'))
->setRunScripts(!$input->getOption('no-scripts')) ->setRunScripts(!$input->getOption('no-scripts'))
->setUpdate(true) ->setUpdate(true)
->setUpdateWhitelist($input->getArgument('packages'))
; ;
return $install->run() ? 0 : 1; return $install->run() ? 0 : 1;

View File

@ -64,7 +64,6 @@ class Application extends BaseApplication
*/ */
public function doRun(InputInterface $input, OutputInterface $output) public function doRun(InputInterface $input, OutputInterface $output)
{ {
$this->registerCommands();
$this->io = new ConsoleIO($input, $output, $this->getHelperSet()); $this->io = new ConsoleIO($input, $output, $this->getHelperSet());
if (version_compare(PHP_VERSION, '5.3.2', '<')) { if (version_compare(PHP_VERSION, '5.3.2', '<')) {
@ -106,21 +105,25 @@ class Application extends BaseApplication
/** /**
* Initializes all the composer commands * Initializes all the composer commands
*/ */
protected function registerCommands() protected function getDefaultCommands()
{ {
$this->add(new Command\AboutCommand()); $commands = parent::getDefaultCommands();
$this->add(new Command\DependsCommand()); $commands[] = new Command\AboutCommand();
$this->add(new Command\InitCommand()); $commands[] = new Command\DependsCommand();
$this->add(new Command\InstallCommand()); $commands[] = new Command\InitCommand();
$this->add(new Command\CreateProjectCommand()); $commands[] = new Command\InstallCommand();
$this->add(new Command\UpdateCommand()); $commands[] = new Command\CreateProjectCommand();
$this->add(new Command\SearchCommand()); $commands[] = new Command\UpdateCommand();
$this->add(new Command\ValidateCommand()); $commands[] = new Command\SearchCommand();
$this->add(new Command\ShowCommand()); $commands[] = new Command\ValidateCommand();
$commands[] = new Command\ShowCommand();
$commands[] = new Command\RequireCommand();
if ('phar:' === substr(__FILE__, 0, 5)) { if ('phar:' === substr(__FILE__, 0, 5)) {
$this->add(new Command\SelfUpdateCommand()); $commands[] = new Command\SelfUpdateCommand();
} }
return $commands;
} }
/** /**

View File

@ -62,6 +62,11 @@ class Factory
return $config; return $config;
} }
public function getComposerFile()
{
return getenv('COMPOSER') ?: 'composer.json';
}
/** /**
* Creates a Composer instance * Creates a Composer instance
* *
@ -73,7 +78,7 @@ class Factory
{ {
// load Composer configuration // load Composer configuration
if (null === $localConfig) { if (null === $localConfig) {
$localConfig = getenv('COMPOSER') ?: 'composer.json'; $localConfig = $this->getComposerFile();
} }
if (is_string($localConfig)) { if (is_string($localConfig)) {

View File

@ -89,6 +89,7 @@ class Installer
protected $verbose = false; protected $verbose = false;
protected $update = false; protected $update = false;
protected $runScripts = true; protected $runScripts = true;
protected $updateWhitelist = null;
/** /**
* @var array * @var array
@ -219,6 +220,8 @@ class Installer
$stabilityFlags = $this->locker->getStabilityFlags(); $stabilityFlags = $this->locker->getStabilityFlags();
} }
$this->whitelistUpdateDependencies($localRepo, $devMode);
// creating repository pool // creating repository pool
$pool = new Pool($minimumStability, $stabilityFlags); $pool = new Pool($minimumStability, $stabilityFlags);
$pool->addRepository($installedRepo); $pool->addRepository($installedRepo);
@ -275,8 +278,11 @@ class Installer
// fix the version of all installed packages (+ platform) that are not // fix the version of all installed packages (+ platform) that are not
// in the current local repo to prevent rogue updates (e.g. non-dev // in the current local repo to prevent rogue updates (e.g. non-dev
// updating when in dev) // updating when in dev)
//
// if the updateWhitelist is enabled, packages not in it are also fixed
// to their currently installed version
foreach ($installedRepo->getPackages() as $package) { foreach ($installedRepo->getPackages() as $package) {
if ($package->getRepository() === $localRepo) { if ($package->getRepository() === $localRepo && (!$this->updateWhitelist || $this->isUpdateable($package))) {
continue; continue;
} }
@ -331,6 +337,11 @@ class Installer
} else { } else {
// force update to latest on update // force update to latest on update
if ($this->update) { if ($this->update) {
// skip package if the whitelist is enabled and it is not in it
if ($this->updateWhitelist && !$this->isUpdateable($package)) {
continue;
}
$newPackage = $this->repositoryManager->findPackage($package->getName(), $package->getVersion()); $newPackage = $this->repositoryManager->findPackage($package->getName(), $package->getVersion());
if ($newPackage && $newPackage->getSourceReference() !== $package->getSourceReference()) { if ($newPackage && $newPackage->getSourceReference() !== $package->getSourceReference()) {
$operations[] = new UpdateOperation($package, $newPackage); $operations[] = new UpdateOperation($package, $newPackage);
@ -441,6 +452,64 @@ class Installer
return $aliases; return $aliases;
} }
private function isUpdateable(PackageInterface $package)
{
if (!$this->updateWhitelist) {
throw new \LogicException('isUpdateable should only be called when a whitelist is present');
}
return isset($this->updateWhitelist[$package->getName()]);
}
/**
* Adds all dependencies of the update whitelist to the whitelist, too.
*
* @param RepositoryInterface $localRepo
* @param boolean $devMode
*/
private function whitelistUpdateDependencies($localRepo, $devMode)
{
if (!$this->updateWhitelist) {
return;
}
$pool = new Pool;
$pool->addRepository($localRepo);
$seen = array();
foreach ($this->updateWhitelist as $packageName => $void) {
$packageQueue = new \SplQueue;
foreach ($pool->whatProvides($packageName) as $depPackage) {
$packageQueue->enqueue($depPackage);
}
while (!$packageQueue->isEmpty()) {
$package = $packageQueue->dequeue();
if (isset($seen[$package->getId()])) {
continue;
}
$seen[$package->getId()] = true;
$this->updateWhitelist[$package->getName()] = true;
$requires = $package->getRequires();
if ($devMode) {
$requires = array_merge($requires, $package->getDevRequires());
}
foreach ($requires as $require) {
$requirePackages = $pool->whatProvides($require->getTarget());
foreach ($requirePackages as $requirePackage) {
$packageQueue->enqueue($requirePackage);
}
}
}
}
}
/** /**
* Create Installer * Create Installer
* *
@ -551,4 +620,18 @@ class Installer
return $this; return $this;
} }
/**
* restrict the update operation to a few packages, all other packages
* that are already installed will be kept at their current version
*
* @param array $packages
* @return Installer
*/
public function setUpdateWhitelist(array $packages)
{
$this->updateWhitelist = array_flip(array_map('strtolower', $packages));
return $this;
}
} }

View File

@ -0,0 +1,121 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Json;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class JsonManipulator
{
private $contents;
private $newline;
private $indent;
public function __construct($contents)
{
$contents = trim($contents);
if (!preg_match('#^\{(.*)\}$#s', $contents)) {
throw new \InvalidArgumentException('The json file must be an object ({})');
}
$this->newline = false !== strpos("\r\n", $contents) ? "\r\n": "\n";
$this->contents = $contents;
$this->detectIndenting();
}
public function getContents()
{
return $this->contents . $this->newline;
}
public function addLink($type, $package, $constraint)
{
// no link of that type yet
if (!preg_match('#"'.$type.'":\s*\{#', $this->contents)) {
$this->addMainKey($type, $this->format(array($package => $constraint)));
return true;
}
$linksRegex = '#("'.$type.'":\s*\{)([^}]+)(\})#s';
if (!preg_match($linksRegex, $this->contents, $match)) {
return false;
}
$links = $match[2];
$packageRegex = str_replace('/', '\\\\?/', preg_quote($package));
// link exists already
if (preg_match('{"'.$packageRegex.'"\s*:}i', $links)) {
$links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)"[^"]+"}i', JsonFile::encode($package).'$1"'.$constraint.'"', $links);
} elseif (preg_match('#[^\s](\s*)$#', $links, $match)) {
// link missing but non empty links
$links = preg_replace(
'#'.$match[1].'$#',
',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1],
$links
);
} else {
// links empty
$links = $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $links;
}
$this->contents = preg_replace($linksRegex, '$1'.$links.'$3', $this->contents);
return true;
}
public function addMainKey($key, $content)
{
if (preg_match('#[^{\s](\s*)\}$#', $this->contents, $match)) {
$this->contents = preg_replace(
'#'.$match[1].'\}$#',
',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}',
$this->contents
);
} else {
$this->contents = preg_replace(
'#\}$#',
$this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}',
$this->contents
);
}
}
protected function format($data)
{
if (is_array($data)) {
reset($data);
if (is_numeric(key($data))) {
return '['.implode(', ', $data).']';
}
$out = '{' . $this->newline;
foreach ($data as $key => $val) {
$elems[] = $this->indent . $this->indent . JsonFile::encode($key). ': '.$this->format($val);
}
return $out . implode(','.$this->newline, $elems) . $this->newline . $this->indent . '}';
}
return JsonFile::encode($data);
}
protected function detectIndenting()
{
if (preg_match('{^(\s+)"}', $this->contents, $match)) {
$this->indent = $match[1];
} else {
$this->indent = ' ';
}
}
}

View File

@ -10,5 +10,7 @@
<installed.json file definition> <installed.json file definition>
--INSTALLED:DEV-- --INSTALLED:DEV--
<installed_dev.json file definition> <installed_dev.json file definition>
--EXPECT-- or --EXPECT:UPDATE-- or --EXPECT:DEV-- or --EXPECT:UPDATE:DEV-- --RUN--
install
--EXPECT--
<output (stringified operations)> <output (stringified operations)>

View File

@ -42,6 +42,8 @@ Aliases take precedence over default package even if default is selected
"a/req": "dev-feature-foo as dev-master" "a/req": "dev-feature-foo as dev-master"
} }
} }
--RUN--
install
--EXPECT-- --EXPECT--
Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f) Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f)
Installing a/req (dev-feature-foo feat.f) Installing a/req (dev-feature-foo feat.f)

View File

@ -44,6 +44,8 @@ Aliases take precedence over default package
"a/c": "dev-feature-foo as dev-master" "a/c": "dev-feature-foo as dev-master"
} }
} }
--RUN--
install
--EXPECT-- --EXPECT--
Installing a/b (dev-master forked) Installing a/b (dev-master forked)
Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)

View File

@ -18,6 +18,8 @@ Installs a package in dev env
"a/b": "1.0.0" "a/b": "1.0.0"
} }
} }
--EXPECT:DEV-- --RUN--
install --dev
--EXPECT--
Installing a/a (1.0.0) Installing a/a (1.0.0)
Installing a/b (1.0.0) Installing a/b (1.0.0)

View File

@ -17,5 +17,7 @@ Installs a dev package forcing it's reference
"a/a": "dev-master#def000" "a/a": "dev-master#def000"
} }
} }
--RUN--
install
--EXPECT-- --EXPECT--
Installing a/a (dev-master def000) Installing a/a (dev-master def000)

View File

@ -14,5 +14,7 @@ Installs a simple package with exact match requirement
"a/a": "1.0.0" "a/a": "1.0.0"
} }
} }
--RUN--
install
--EXPECT-- --EXPECT--
Installing a/a (1.0.0) Installing a/a (1.0.0)

View File

@ -36,6 +36,8 @@ Updates updateable packages
[ [
{ "name": "a/b", "version": "1.0.0" } { "name": "a/b", "version": "1.0.0" }
] ]
--EXPECT:UPDATE:DEV-- --RUN--
update --dev
--EXPECT--
Updating a/a (1.0.0) to a/a (1.0.1) Updating a/a (1.0.0) to a/a (1.0.1)
Updating a/b (1.0.0) to a/b (2.0.0) Updating a/b (1.0.0) to a/b (2.0.0)

View File

@ -24,5 +24,7 @@ Updates a dev package forcing it's reference
"source": { "reference": "abc123", "url": "", "type": "git" } "source": { "reference": "abc123", "url": "", "type": "git" }
} }
] ]
--RUN--
install
--EXPECT-- --EXPECT--
Updating a/a (dev-master abc123) to a/a (dev-master def000) Updating a/a (dev-master abc123) to a/a (dev-master def000)

View File

@ -0,0 +1,40 @@
--TEST--
Update with a package whitelist only updates those packages and their dependencies if they are not present in composer.json
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{ "name": "fixed", "version": "1.1.0" },
{ "name": "fixed", "version": "1.0.0" },
{ "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } },
{ "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
{ "name": "dependency", "version": "1.1.0" },
{ "name": "dependency", "version": "1.0.0" },
{ "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } },
{ "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
{ "name": "unrelated-dependency", "version": "1.1.0" },
{ "name": "unrelated-dependency", "version": "1.0.0" }
]
}
],
"require": {
"fixed": "1.*",
"whitelisted": "1.*",
"unrelated": "1.*"
}
}
--INSTALLED--
[
{ "name": "fixed", "version": "1.0.0" },
{ "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } },
{ "name": "dependency", "version": "1.0.0" },
{ "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } },
{ "name": "unrelated-dependency", "version": "1.0.0" }
]
--RUN--
update whitelisted
--EXPECT--
Updating dependency (1.0.0) to dependency (1.1.0)
Updating whitelisted (1.0.0) to whitelisted (1.1.0)

View File

@ -12,6 +12,7 @@
namespace Composer\Test; namespace Composer\Test;
use Composer\Installer; use Composer\Installer;
use Composer\Console\Application;
use Composer\Config; use Composer\Config;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Repository\ArrayRepository; use Composer\Repository\ArrayRepository;
@ -24,6 +25,8 @@ use Composer\Test\Mock\FactoryMock;
use Composer\Test\Mock\InstalledFilesystemRepositoryMock; use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
use Composer\Test\Mock\InstallationManagerMock; use Composer\Test\Mock\InstallationManagerMock;
use Composer\Test\Mock\WritableRepositoryMock; use Composer\Test\Mock\WritableRepositoryMock;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput;
class InstallerTest extends TestCase class InstallerTest extends TestCase
{ {
@ -121,7 +124,7 @@ class InstallerTest extends TestCase
/** /**
* @dataProvider getIntegrationTests * @dataProvider getIntegrationTests
*/ */
public function testIntegration($file, $message, $condition, $composer, $lock, $installed, $installedDev, $update, $dev, $expect) public function testIntegration($file, $message, $condition, $composer, $lock, $installed, $installedDev, $run, $expect)
{ {
if ($condition) { if ($condition) {
eval('$res = '.$condition.';'); eval('$res = '.$condition.';');
@ -177,14 +180,31 @@ class InstallerTest extends TestCase
$autoloadGenerator $autoloadGenerator
); );
$installer->setDevMode($dev)->setUpdate($update); $application = new Application;
$application->get('install')->setCode(function ($input, $output) use ($installer) {
$installer->setDevMode($input->getOption('dev'));
$result = $installer->run(); return $installer->run();
$this->assertTrue($result, $output); });
$expectedInstalled = isset($options['install']) ? $options['install'] : array(); $application->get('update')->setCode(function ($input, $output) use ($installer) {
$expectedUpdated = isset($options['update']) ? $options['update'] : array(); $installer
$expectedUninstalled = isset($options['uninstall']) ? $options['uninstall'] : array(); ->setDevMode($input->getOption('dev'))
->setUpdate(true)
->setUpdateWhitelist($input->getArgument('packages'));
return $installer->run();
});
if (!preg_match('{^(install|update)\b}', $run)) {
throw new \UnexpectedValueException('The run command only supports install and update');
}
$application->setAutoExit(false);
$appOutput = fopen('php://memory', 'w+');
$result = $application->run(new StringInput($run), new StreamOutput($appOutput));
fseek($appOutput, 0);
$this->assertEquals(0, $result, $output . stream_get_contents($appOutput));
$installationManager = $composer->getInstallationManager(); $installationManager = $composer->getInstallationManager();
$this->assertSame($expect, implode("\n", $installationManager->getTrace())); $this->assertSame($expect, implode("\n", $installationManager->getTrace()));
@ -210,7 +230,8 @@ class InstallerTest extends TestCase
(?:--LOCK--\s*(?P<lock>'.$content.'))?\s* (?:--LOCK--\s*(?P<lock>'.$content.'))?\s*
(?:--INSTALLED--\s*(?P<installed>'.$content.'))?\s* (?:--INSTALLED--\s*(?P<installed>'.$content.'))?\s*
(?:--INSTALLED:DEV--\s*(?P<installedDev>'.$content.'))?\s* (?:--INSTALLED:DEV--\s*(?P<installedDev>'.$content.'))?\s*
--EXPECT(?P<update>:UPDATE)?(?P<dev>:DEV)?--\s*(?P<expect>.*?)\s* --RUN--\s*(?P<run>.*?)\s*
--EXPECT--\s*(?P<expect>.*?)\s*
$}xs'; $}xs';
$installed = array(); $installed = array();
@ -231,8 +252,7 @@ class InstallerTest extends TestCase
if (!empty($match['installedDev'])) { if (!empty($match['installedDev'])) {
$installedDev = JsonFile::parseJson($match['installedDev']); $installedDev = JsonFile::parseJson($match['installedDev']);
} }
$update = !empty($match['update']); $run = $match['run'];
$dev = !empty($match['dev']);
$expect = $match['expect']; $expect = $match['expect'];
} catch (\Exception $e) { } catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
@ -241,7 +261,7 @@ class InstallerTest extends TestCase
die(sprintf('Test "%s" is not valid, did not match the expected format.', str_replace($fixturesDir.'/', '', $file))); die(sprintf('Test "%s" is not valid, did not match the expected format.', str_replace($fixturesDir.'/', '', $file)));
} }
$tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $installedDev, $update, $dev, $expect); $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $installedDev, $run, $expect);
} }
return $tests; return $tests;

View File

@ -0,0 +1,134 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Json;
use Composer\Json\JsonManipulator;
class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider linkProvider
*/
public function testAddLink($json, $type, $package, $constraint, $expected)
{
$manipulator = new JsonManipulator($json);
$this->assertTrue($manipulator->addLink($type, $package, $constraint));
$this->assertEquals($expected, $manipulator->getContents());
}
public function linkProvider()
{
return array(
array(
'{
}',
'require',
'vendor/baz',
'qux',
'{
"require": {
"vendor/baz": "qux"
}
}
'
),
array(
'{
"foo": "bar"
}',
'require',
'vendor/baz',
'qux',
'{
"foo": "bar",
"require": {
"vendor/baz": "qux"
}
}
'
),
array(
'{
"require": {
}
}',
'require',
'vendor/baz',
'qux',
'{
"require": {
"vendor/baz": "qux"
}
}
'
),
array(
'{
"require": {
"foo": "bar"
}
}',
'require',
'vendor/baz',
'qux',
'{
"require": {
"foo": "bar",
"vendor/baz": "qux"
}
}
'
),
array(
'{
"require":
{
"foo": "bar",
"vendor/baz": "baz"
}
}',
'require',
'vendor/baz',
'qux',
'{
"require":
{
"foo": "bar",
"vendor/baz": "qux"
}
}
'
),
array(
'{
"require":
{
"foo": "bar",
"vendor\/baz": "baz"
}
}',
'require',
'vendor/baz',
'qux',
'{
"require":
{
"foo": "bar",
"vendor/baz": "qux"
}
}
'
),
);
}
}