From 260d7d434f994abcb144d5b2f9a479826eb52a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81my=20Romey?= Date: Sun, 1 Apr 2012 00:39:43 +0200 Subject: [PATCH 01/15] Add a command for adding package to composer.json Updated doc for require command --- doc/03-cli.md | 14 ++- src/Composer/Command/RequireCommand.php | 110 ++++++++++++++++++++++++ src/Composer/Console/Application.php | 1 + src/Composer/Json/JsonFile.php | 10 +++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/Composer/Command/RequireCommand.php diff --git a/doc/03-cli.md b/doc/03-cli.md index 45344adea..cff1be360 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -62,6 +62,18 @@ into `composer.lock`. * **--no-install-recommends:** Do not install packages referenced by `recommend`. * **--install-suggests:** Install packages referenced by `suggest`. +## require + +The `require` command adds new packages to the `composer.json` file from the current +directory. + + $ php composer.phar require + +This will update your `composer.json` file keeping previous required packages. +If a package is already in the `composer.json` file you will be asked to choose which version you want. + +Like the `init` command, `require` allows you to search for packages. + ## search The search command allows you to search through the current project's package @@ -190,4 +202,4 @@ some tools like git or curl will only use the lower-cased `http_proxy` version. Alternatively you can also define the git proxy using `git config --global http.proxy `. -← [Libraries](02-libraries.md) | [Schema](04-schema.md) → \ No newline at end of file +← [Libraries](02-libraries.md) | [Schema](04-schema.md) → diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php new file mode 100644 index 000000000..80cc68e02 --- /dev/null +++ b/src/Composer/Command/RequireCommand.php @@ -0,0 +1,110 @@ + + * Jordi Boggiano + * + * 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\Json\JsonFile; +use Composer\Json\JsonValidationException; +use Composer\Util\RemoteFilesystem; + +/** + * @author Jérémy Romey + */ +class RequireCommand extends InitCommand +{ + protected function configure() + { + $this + ->setName('require') + ->setDescription('Adds a required package to a composer.json') + ->setDefinition(array( + new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json'), + new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'An array of required packages'), + )) + ->setHelp(<<getArgument('file'); + + if (!file_exists($file)) { + $output->writeln(''.$file.' not found.'); + return 1; + } + if (!is_readable($file)) { + $output->writeln(''.$file.' is not readable.'); + return 1; + } + + $laxValid = false; + try { + $json = new JsonFile($file, new RemoteFilesystem($this->getIO())); + $json->read(); + + $json->validateSchema(JsonFile::LAX_SCHEMA); + $laxValid = true; + $json->validateSchema(); + + } catch (\Exception $e) { + $output->writeln(''.$file.' has an error. Run the validate command for more info'); + return 1; + } + + $output->writeln(array( + '', + 'Updating your dependencies.', + '' + )); + + $dialog = $this->getHelperSet()->get('dialog'); + + $options = json_decode($json->getResult(), true); + + $requirements = array(); + $requirements = $this->determineRequirements($input, $output); + + $baseRequirements = array_key_exists('require', $options) ? $options['require'] : array(); + $requirements = $this->formatRequirements($requirements); + + foreach ($requirements as $package => $version) { + if (array_key_exists($package, $baseRequirements)) { + if ($dialog->askConfirmation($output, $dialog->getQuestion('The package '.$package.' is already in requirements. Would you like to update the version required from '.$baseRequirements[$package].' to '.$version, 'yes', '?'), true)) { + $baseRequirements[$package] = $version; + } + } else { + $baseRequirements[$package] = $version; + } + } + + $options['require'] = $baseRequirements; + + $json->encode($options); + $json->write($options); + + $output->writeln(''.$file.' has been updated'); + } + + protected function interact(InputInterface $input, OutputInterface $output) + { + return; + } +} diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index bcdd1817d..7aae3869d 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -113,6 +113,7 @@ class Application extends BaseApplication $this->add(new Command\SearchCommand()); $this->add(new Command\ValidateCommand()); $this->add(new Command\ShowCommand()); + $this->add(new Command\RequireCommand()); if ('phar:' === substr(__FILE__, 0, 5)) { $this->add(new Command\SelfUpdateCommand()); diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 5c891814e..e90479d1c 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -293,4 +293,14 @@ class JsonFile throw $result; } + + /** + * Returns the content of the file + * + * @return string result + */ + public function getResult() + { + return $this->rfs->getContents($this->path, $this->path, false); + } } From 7cd5f4c142494bb2c8b8137d686fa40496f14e88 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 14:43:44 +0200 Subject: [PATCH 02/15] Add Factory::getComposerFile --- src/Composer/Factory.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index fd71b6a1c..21a6c9a69 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -62,6 +62,11 @@ class Factory return $config; } + public function getComposerFile() + { + return getenv('COMPOSER') ?: 'composer.json'; + } + /** * Creates a Composer instance * @@ -73,7 +78,7 @@ class Factory { // load Composer configuration if (null === $localConfig) { - $localConfig = getenv('COMPOSER') ?: 'composer.json'; + $localConfig = $this->getComposerFile(); } if (is_string($localConfig)) { From f5e091810480544104be119b03e5580737433a1e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 14:44:24 +0200 Subject: [PATCH 03/15] Add JsonManipulator class for doing raw manipulation of json strings --- src/Composer/Json/JsonManipulator.php | 120 ++++++++++++++++ .../Test/Json/JsonManipulatorTest.php | 134 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 src/Composer/Json/JsonManipulator.php create mode 100644 tests/Composer/Test/Json/JsonManipulatorTest.php diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php new file mode 100644 index 000000000..f070fe792 --- /dev/null +++ b/src/Composer/Json/JsonManipulator.php @@ -0,0 +1,120 @@ + + * Jordi Boggiano + * + * 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 + */ +class JsonManipulator +{ + private $contents; + private $newline; + private $indent; + + public function __construct($contents) + { + if (!preg_match('#^\{(.*)\}$#s', trim($contents), $match)) { + throw new \InvalidArgumentException('The json file must be an object ({})'); + } + $this->newline = false !== strpos("\r\n", $contents) ? "\r\n": "\n"; + $this->contents = '{' . $match[1] . '}'; + $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 = ' '; + } + } +} diff --git a/tests/Composer/Test/Json/JsonManipulatorTest.php b/tests/Composer/Test/Json/JsonManipulatorTest.php new file mode 100644 index 000000000..e6718b56a --- /dev/null +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -0,0 +1,134 @@ + + * 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 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" + } +} +' + ), + ); + } +} From 27f8019dbdfc6c93673edfce4b7dc1ee9351aeb9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 14:45:19 +0200 Subject: [PATCH 04/15] Refactor init & require commands --- src/Composer/Command/InitCommand.php | 24 ++++++-- src/Composer/Command/RequireCommand.php | 74 ++++++++++++------------- src/Composer/Json/JsonFile.php | 10 ---- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index f363ddb1b..e405df919 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -60,7 +60,8 @@ class InitCommand extends Command new InputOption('author', null, InputOption::VALUE_NONE, 'Author name of package'), // new InputOption('version', null, InputOption::VALUE_NONE, 'Version 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(<<init command creates a basic composer.json file @@ -216,10 +217,15 @@ EOT )); $requirements = array(); - if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dependencies interactively', 'yes', '?'), true)) { - $requirements = $this->determineRequirements($input, $output); + if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dependencies (require) interactively', 'yes', '?'), true)) { + $requirements = $this->determineRequirements($input, $output, $input->getOption('require')); } $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) @@ -246,12 +252,18 @@ EOT return $packages; } - protected function determineRequirements(InputInterface $input, OutputInterface $output) + protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array()) { $dialog = $this->getHelperSet()->get('dialog'); $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); + } + + return $requires; + } while (null !== $package = $dialog->ask($output, $prompt)) { $matches = $this->findPackages($package); @@ -287,7 +299,7 @@ EOT return sprintf('%s %s', $package->getName(), $package->getPrettyVersion()); }; - $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a 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) { $requires[] = $package; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 80cc68e02..26840e7f3 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -16,12 +16,15 @@ 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\Json\JsonFile; +use Composer\Json\JsonManipulator; use Composer\Json\JsonValidationException; use Composer\Util\RemoteFilesystem; /** * @author Jérémy Romey + * @author Jordi Boggiano */ class RequireCommand extends InitCommand { @@ -31,11 +34,11 @@ class RequireCommand extends InitCommand ->setName('require') ->setDescription('Adds a required package to a composer.json') ->setDefinition(array( - new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json'), - new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'An array of required packages'), + 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.'), )) ->setHelp(<<getArgument('file'); + $factory = new Factory; + $file = $factory->getComposerFile(); if (!file_exists($file)) { $output->writeln(''.$file.' not found.'); @@ -55,52 +59,44 @@ EOT return 1; } - $laxValid = false; - try { - $json = new JsonFile($file, new RemoteFilesystem($this->getIO())); - $json->read(); - - $json->validateSchema(JsonFile::LAX_SCHEMA); - $laxValid = true; - $json->validateSchema(); - - } catch (\Exception $e) { - $output->writeln(''.$file.' has an error. Run the validate command for more info'); - return 1; - } - - $output->writeln(array( - '', - 'Updating your dependencies.', - '' - )); - $dialog = $this->getHelperSet()->get('dialog'); - $options = json_decode($json->getResult(), true); + $json = new JsonFile($file); + $composer = $json->read(); - $requirements = array(); - $requirements = $this->determineRequirements($input, $output); + $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages')); - $baseRequirements = array_key_exists('require', $options) ? $options['require'] : array(); + $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; + $baseRequirements = array_key_exists($requireKey, $composer) ? $composer[$requireKey] : array(); $requirements = $this->formatRequirements($requirements); - foreach ($requirements as $package => $version) { - if (array_key_exists($package, $baseRequirements)) { - if ($dialog->askConfirmation($output, $dialog->getQuestion('The package '.$package.' is already in requirements. Would you like to update the version required from '.$baseRequirements[$package].' to '.$version, 'yes', '?'), true)) { - $baseRequirements[$package] = $version; - } - } else { + if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey)) { + foreach ($requirements as $package => $version) { $baseRequirements[$package] = $version; } + + $composer[$requireKey] = $baseRequirements; + $json->write($composer); + } + + $output->writeln(''.$file.' has been updated'); + } + + 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; + } } - $options['require'] = $baseRequirements; + file_put_contents($json->getPath(), $manipulator->getContents()); - $json->encode($options); - $json->write($options); - - $output->writeln(''.$file.' has been updated'); + return true; } protected function interact(InputInterface $input, OutputInterface $output) diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 09ae3eb0f..f463c777e 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -299,14 +299,4 @@ class JsonFile throw $result; } - - /** - * Returns the content of the file - * - * @return string result - */ - public function getResult() - { - return $this->rfs->getContents($this->path, $this->path, false); - } } From 1443ea25f9d50d0ed208d5f27cee91ebe24b0e35 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 14:51:06 +0200 Subject: [PATCH 05/15] Make sure a constraint is provided with input requirements --- src/Composer/Command/InitCommand.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index e405df919..988dd0220 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -260,6 +260,15 @@ EOT 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; From 42c501aaa44d4a46367ba78236fcb4a6218ba483 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 15:17:52 +0200 Subject: [PATCH 06/15] Force the installation of the newly required packages in require command --- src/Composer/Command/RequireCommand.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 26840e7f3..92d21fef2 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -17,6 +17,7 @@ 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; @@ -32,13 +33,14 @@ class RequireCommand extends InitCommand { $this ->setName('require') - ->setDescription('Adds a required package to a composer.json') + ->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(<<writeln(''.$file.' has been updated'); + + // 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) From c580cb8ba855f41b6d7eb98c3d7fb95339b37e8e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 15:20:27 +0200 Subject: [PATCH 07/15] Add package argument to the update command, fixes #470, fixes #450 --- src/Composer/Command/UpdateCommand.php | 3 +++ src/Composer/Installer.php | 27 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index e832b1fd0..f45a4b3b3 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -15,6 +15,7 @@ namespace Composer\Command; use Composer\Installer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** @@ -28,6 +29,7 @@ class UpdateCommand extends Command ->setName('update') ->setDescription('Updates your dependencies to the latest version, and updates the composer.lock file.') ->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('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.'), @@ -58,6 +60,7 @@ EOT ->setDevMode($input->getOption('dev')) ->setRunScripts(!$input->getOption('no-scripts')) ->setUpdate(true) + ->setUpdateWhitelist($input->getArgument('packages')) ; return $install->run() ? 0 : 1; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index f4f6777b3..77542284e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -89,6 +89,7 @@ class Installer protected $verbose = false; protected $update = false; protected $runScripts = true; + protected $updateWhitelist = null; /** * @var array @@ -275,8 +276,13 @@ class Installer // fix the version of all installed packages (+ platform) that are not // in the current local repo to prevent rogue updates (e.g. non-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) { - if ($package->getRepository() === $localRepo) { + if ($package->getRepository() === $localRepo + && (!$this->updateWhitelist || in_array($package->getName(), $this->updateWhitelist)) + ) { continue; } @@ -331,6 +337,11 @@ class Installer } else { // force update to latest on update if ($this->update) { + // skip package is the whitelist is enabled and it is not in it + if ($this->updateWhitelist && !in_array($package->getName(), $this->updateWhitelist)) { + continue; + } + $newPackage = $this->repositoryManager->findPackage($package->getName(), $package->getVersion()); if ($newPackage && $newPackage->getSourceReference() !== $package->getSourceReference()) { $operations[] = new UpdateOperation($package, $newPackage); @@ -551,4 +562,18 @@ class Installer 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_map('strtolower', $packages); + + return $this; + } } From 97c2fe966e0b28a46ade6200c3af3af68cd8bf5f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 15:20:37 +0200 Subject: [PATCH 08/15] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 727820765..e7c61d32e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ * 1.0.0-alpha4 * 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 support for file:// URLs to GitDriver * Added --dev flag to `create-project` command From 76f727b27376e2c52ad69ecd330d38a41b230ae9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 26 May 2012 15:27:35 +0200 Subject: [PATCH 09/15] Update docs --- doc/03-cli.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index 86977fd9d..f911005a7 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -53,6 +53,10 @@ 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 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 * **--prefer-source:** Install packages from `source` when available. @@ -61,15 +65,23 @@ into `composer.lock`. ## require -The `require` command adds new packages to the `composer.json` file from the current -directory. +The `require` command adds new packages to the `composer.json` file from +the current directory. $ php composer.phar require -This will update your `composer.json` file keeping previous required packages. -If a package is already in the `composer.json` file you will be asked to choose which version you want. +After adding/changing the requirements, the modified requirements will be +installed or updated. -Like the `init` command, `require` allows you to search for packages. +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 From 8ffe3c2e2638ea8ea3b1d2f06e9769ed8c9eb4bd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 27 May 2012 23:21:10 +0200 Subject: [PATCH 10/15] Cosmetic fixes --- CHANGELOG.md | 2 +- src/Composer/Command/RequireCommand.php | 2 +- src/Composer/Installer.php | 2 +- src/Composer/Json/JsonManipulator.php | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7c61d32e..793d311d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ * 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 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 support for file:// URLs to GitDriver * Added --dev flag to `create-project` command diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 92d21fef2..78d7f326f 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -70,7 +70,7 @@ EOT $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $baseRequirements = array_key_exists($requireKey, $composer) ? $composer[$requireKey] : array(); - $requirements = $this->formatRequirements($requirements); + $requirements = $this->formatRequirements($requirements); if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey)) { foreach ($requirements as $package => $version) { diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 77542284e..97a1bcbc6 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -337,7 +337,7 @@ class Installer } else { // force update to latest on update if ($this->update) { - // skip package is the whitelist is enabled and it is not in it + // skip package if the whitelist is enabled and it is not in it if ($this->updateWhitelist && !in_array($package->getName(), $this->updateWhitelist)) { continue; } diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index f070fe792..59413aaac 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -23,11 +23,12 @@ class JsonManipulator public function __construct($contents) { - if (!preg_match('#^\{(.*)\}$#s', trim($contents), $match)) { + $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 = '{' . $match[1] . '}'; + $this->contents = $contents; $this->detectIndenting(); } From 9841b6f36e16d2c809dd81bc1c1555e1f1e364b5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 28 May 2012 00:10:02 +0200 Subject: [PATCH 11/15] Extend proper method to register commands at startup --- src/Composer/Console/Application.php | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 1174c2c90..30f232acd 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -64,7 +64,6 @@ class Application extends BaseApplication */ public function doRun(InputInterface $input, OutputInterface $output) { - $this->registerCommands(); $this->io = new ConsoleIO($input, $output, $this->getHelperSet()); if (version_compare(PHP_VERSION, '5.3.2', '<')) { @@ -106,22 +105,25 @@ class Application extends BaseApplication /** * Initializes all the composer commands */ - protected function registerCommands() + protected function getDefaultCommands() { - $this->add(new Command\AboutCommand()); - $this->add(new Command\DependsCommand()); - $this->add(new Command\InitCommand()); - $this->add(new Command\InstallCommand()); - $this->add(new Command\CreateProjectCommand()); - $this->add(new Command\UpdateCommand()); - $this->add(new Command\SearchCommand()); - $this->add(new Command\ValidateCommand()); - $this->add(new Command\ShowCommand()); - $this->add(new Command\RequireCommand()); + $commands = parent::getDefaultCommands(); + $commands[] = new Command\AboutCommand(); + $commands[] = new Command\DependsCommand(); + $commands[] = new Command\InitCommand(); + $commands[] = new Command\InstallCommand(); + $commands[] = new Command\CreateProjectCommand(); + $commands[] = new Command\UpdateCommand(); + $commands[] = new Command\SearchCommand(); + $commands[] = new Command\ValidateCommand(); + $commands[] = new Command\ShowCommand(); + $commands[] = new Command\RequireCommand(); if ('phar:' === substr(__FILE__, 0, 5)) { - $this->add(new Command\SelfUpdateCommand()); + $commands[] = new Command\SelfUpdateCommand(); } + + return $commands; } /** From 43150b88a29a3dd85f0900f0924c286645ab8f90 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 28 May 2012 00:11:18 +0200 Subject: [PATCH 12/15] Refactor integration tests to accept any run command --- tests/Composer/Test/Fixtures/installer/SAMPLE | 4 +- .../aliased-priority-conflicting.test | 2 + .../Fixtures/installer/aliased-priority.test | 2 + .../Test/Fixtures/installer/install-dev.test | 4 +- .../Fixtures/installer/install-reference.test | 2 + .../Fixtures/installer/install-simple.test | 2 + .../Test/Fixtures/installer/update-all.test | 4 +- .../Fixtures/installer/update-reference.test | 2 + tests/Composer/Test/InstallerTest.php | 42 ++++++++++++++----- 9 files changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/SAMPLE b/tests/Composer/Test/Fixtures/installer/SAMPLE index 618f6edf2..90afe7058 100644 --- a/tests/Composer/Test/Fixtures/installer/SAMPLE +++ b/tests/Composer/Test/Fixtures/installer/SAMPLE @@ -10,5 +10,7 @@ --INSTALLED:DEV-- ---EXPECT-- or --EXPECT:UPDATE-- or --EXPECT:DEV-- or --EXPECT:UPDATE:DEV-- +--RUN-- +install +--EXPECT-- \ No newline at end of file diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index 9c015fb9b..1ffa936b4 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -42,6 +42,8 @@ Aliases take precedence over default package even if default is selected "a/req": "dev-feature-foo as dev-master" } } +--RUN-- +install --EXPECT-- 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) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority.test b/tests/Composer/Test/Fixtures/installer/aliased-priority.test index 048399584..35af86fcd 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority.test @@ -44,6 +44,8 @@ Aliases take precedence over default package "a/c": "dev-feature-foo as dev-master" } } +--RUN-- +install --EXPECT-- Installing a/b (dev-master forked) Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) diff --git a/tests/Composer/Test/Fixtures/installer/install-dev.test b/tests/Composer/Test/Fixtures/installer/install-dev.test index 0dfa1bec1..3b03675bb 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev.test @@ -18,6 +18,8 @@ Installs a package in dev env "a/b": "1.0.0" } } ---EXPECT:DEV-- +--RUN-- +install --dev +--EXPECT-- Installing a/a (1.0.0) Installing a/b (1.0.0) \ No newline at end of file diff --git a/tests/Composer/Test/Fixtures/installer/install-reference.test b/tests/Composer/Test/Fixtures/installer/install-reference.test index 433b6fcbd..f8e696f99 100644 --- a/tests/Composer/Test/Fixtures/installer/install-reference.test +++ b/tests/Composer/Test/Fixtures/installer/install-reference.test @@ -17,5 +17,7 @@ Installs a dev package forcing it's reference "a/a": "dev-master#def000" } } +--RUN-- +install --EXPECT-- Installing a/a (dev-master def000) diff --git a/tests/Composer/Test/Fixtures/installer/install-simple.test b/tests/Composer/Test/Fixtures/installer/install-simple.test index 6cc46d3aa..008bf76e7 100644 --- a/tests/Composer/Test/Fixtures/installer/install-simple.test +++ b/tests/Composer/Test/Fixtures/installer/install-simple.test @@ -14,5 +14,7 @@ Installs a simple package with exact match requirement "a/a": "1.0.0" } } +--RUN-- +install --EXPECT-- Installing a/a (1.0.0) \ No newline at end of file diff --git a/tests/Composer/Test/Fixtures/installer/update-all.test b/tests/Composer/Test/Fixtures/installer/update-all.test index e7641dbb9..b5806687e 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all.test +++ b/tests/Composer/Test/Fixtures/installer/update-all.test @@ -36,6 +36,8 @@ Updates updateable packages [ { "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/b (1.0.0) to a/b (2.0.0) \ No newline at end of file diff --git a/tests/Composer/Test/Fixtures/installer/update-reference.test b/tests/Composer/Test/Fixtures/installer/update-reference.test index c6418a8eb..9dca245ee 100644 --- a/tests/Composer/Test/Fixtures/installer/update-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-reference.test @@ -24,5 +24,7 @@ Updates a dev package forcing it's reference "source": { "reference": "abc123", "url": "", "type": "git" } } ] +--RUN-- +install --EXPECT-- Updating a/a (dev-master abc123) to a/a (dev-master def000) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 6ceaa069e..6de28f45f 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -12,6 +12,7 @@ namespace Composer\Test; use Composer\Installer; +use Composer\Console\Application; use Composer\Config; use Composer\Json\JsonFile; use Composer\Repository\ArrayRepository; @@ -24,6 +25,8 @@ use Composer\Test\Mock\FactoryMock; use Composer\Test\Mock\InstalledFilesystemRepositoryMock; use Composer\Test\Mock\InstallationManagerMock; use Composer\Test\Mock\WritableRepositoryMock; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\StreamOutput; class InstallerTest extends TestCase { @@ -121,7 +124,7 @@ class InstallerTest extends TestCase /** * @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) { eval('$res = '.$condition.';'); @@ -177,14 +180,31 @@ class InstallerTest extends TestCase $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(); - $this->assertTrue($result, $output); + return $installer->run(); + }); - $expectedInstalled = isset($options['install']) ? $options['install'] : array(); - $expectedUpdated = isset($options['update']) ? $options['update'] : array(); - $expectedUninstalled = isset($options['uninstall']) ? $options['uninstall'] : array(); + $application->get('update')->setCode(function ($input, $output) use ($installer) { + $installer + ->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(); $this->assertSame($expect, implode("\n", $installationManager->getTrace())); @@ -210,7 +230,8 @@ class InstallerTest extends TestCase (?:--LOCK--\s*(?P'.$content.'))?\s* (?:--INSTALLED--\s*(?P'.$content.'))?\s* (?:--INSTALLED:DEV--\s*(?P'.$content.'))?\s* - --EXPECT(?P:UPDATE)?(?P:DEV)?--\s*(?P.*?)\s* + --RUN--\s*(?P.*?)\s* + --EXPECT--\s*(?P.*?)\s* $}xs'; $installed = array(); @@ -231,8 +252,7 @@ class InstallerTest extends TestCase if (!empty($match['installedDev'])) { $installedDev = JsonFile::parseJson($match['installedDev']); } - $update = !empty($match['update']); - $dev = !empty($match['dev']); + $run = $match['run']; $expect = $match['expect']; } catch (\Exception $e) { 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))); } - $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; From 734317b812929f60d9c09d8f1d6c9ff67fd1e2cb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 28 May 2012 00:11:47 +0200 Subject: [PATCH 13/15] Add test and fix update whitelist feature --- src/Composer/Installer.php | 25 +++++++++++--- .../Fixtures/installer/update-whitelist.test | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-whitelist.test diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 97a1bcbc6..31299ceff 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -280,9 +280,7 @@ class Installer // if the updateWhitelist is enabled, packages not in it are also fixed // to their currently installed version foreach ($installedRepo->getPackages() as $package) { - if ($package->getRepository() === $localRepo - && (!$this->updateWhitelist || in_array($package->getName(), $this->updateWhitelist)) - ) { + if ($package->getRepository() === $localRepo && (!$this->updateWhitelist || $this->isUpdateable($package))) { continue; } @@ -338,7 +336,7 @@ class Installer // force update to latest on update if ($this->update) { // skip package if the whitelist is enabled and it is not in it - if ($this->updateWhitelist && !in_array($package->getName(), $this->updateWhitelist)) { + if ($this->updateWhitelist && !$this->isUpdateable($package)) { continue; } @@ -452,6 +450,25 @@ class Installer return $aliases; } + private function isUpdateable(PackageInterface $package) + { + if (!$this->updateWhitelist) { + throw new \LogicException('isUpdateable should only be called when a whitelist is present'); + } + + if (in_array($package->getName(), $this->updateWhitelist)) { + return true; + } + + foreach ($this->package->getRequires() as $link) { + if ($link->getTarget() === $package->getName()) { + return false; + } + } + + return true; + } + /** * Create Installer * diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test new file mode 100644 index 000000000..79d035c8f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -0,0 +1,33 @@ +--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" } + ] + } + ], + "require": { + "fixed": "1.*", + "whitelisted": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0" }, + { "name": "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) From 90c515522ac9713c1d1dfb6cb3aad5541530abe2 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 28 May 2012 01:25:34 +0200 Subject: [PATCH 14/15] Change whitelist integration test to check it doesn't update unrelated packages --- .../Test/Fixtures/installer/update-whitelist.test | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 79d035c8f..0edf18ea5 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -11,20 +11,27 @@ Update with a package whitelist only updates those packages and their dependenci { "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": "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.*" + "whitelisted": "1.*", + "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0" }, - { "name": "dependency", "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 From 2d9aa3d49d1c3f366c5ee150e8ebd7fe143a628a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 28 May 2012 01:58:54 +0200 Subject: [PATCH 15/15] Update dependencies of whitelisted packages, but not random dependencies --- src/Composer/Installer.php | 57 ++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 31299ceff..ba16eeee4 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -220,6 +220,8 @@ class Installer $stabilityFlags = $this->locker->getStabilityFlags(); } + $this->whitelistUpdateDependencies($localRepo, $devMode); + // creating repository pool $pool = new Pool($minimumStability, $stabilityFlags); $pool->addRepository($installedRepo); @@ -456,17 +458,56 @@ class Installer throw new \LogicException('isUpdateable should only be called when a whitelist is present'); } - if (in_array($package->getName(), $this->updateWhitelist)) { - return true; + 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; } - foreach ($this->package->getRequires() as $link) { - if ($link->getTarget() === $package->getName()) { - return false; + $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); + } + } } } - - return true; } /** @@ -589,7 +630,7 @@ class Installer */ public function setUpdateWhitelist(array $packages) { - $this->updateWhitelist = array_map('strtolower', $packages); + $this->updateWhitelist = array_flip(array_map('strtolower', $packages)); return $this; }