diff --git a/CHANGELOG.md b/CHANGELOG.md index 727820765..793d311d6 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 diff --git a/doc/03-cli.md b/doc/03-cli.md index 0c3343010..f911005a7 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -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 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. * **--dry-run:** Simulate the command without actually doing anything. * **--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 The search command allows you to search through the current project's package diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index f363ddb1b..988dd0220 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,27 @@ 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); + 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)) { $matches = $this->findPackages($package); @@ -287,7 +308,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 new file mode 100644 index 000000000..78d7f326f --- /dev/null +++ b/src/Composer/Command/RequireCommand.php @@ -0,0 +1,123 @@ + + * 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\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 + * @author Jordi Boggiano + */ +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(<<getComposerFile(); + + if (!file_exists($file)) { + $output->writeln(''.$file.' not found.'); + return 1; + } + if (!is_readable($file)) { + $output->writeln(''.$file.' is not readable.'); + 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(''.$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) + { + $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; + } +} 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/Console/Application.php b/src/Composer/Console/Application.php index ae7518618..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,21 +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()); + $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; } /** 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)) { diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 09ef70a88..a4156c38c 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 @@ -219,6 +220,8 @@ class Installer $stabilityFlags = $this->locker->getStabilityFlags(); } + $this->whitelistUpdateDependencies($localRepo, $devMode); + // creating repository pool $pool = new Pool($minimumStability, $stabilityFlags); $pool->addRepository($installedRepo); @@ -275,8 +278,11 @@ 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 || $this->isUpdateable($package))) { continue; } @@ -331,6 +337,11 @@ class Installer } else { // 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 && !$this->isUpdateable($package)) { + continue; + } + $newPackage = $this->repositoryManager->findPackage($package->getName(), $package->getVersion()); if ($newPackage && $newPackage->getSourceReference() !== $package->getSourceReference()) { $operations[] = new UpdateOperation($package, $newPackage); @@ -441,6 +452,64 @@ 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'); + } + + 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 * @@ -551,4 +620,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_flip(array_map('strtolower', $packages)); + + return $this; + } } diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php new file mode 100644 index 000000000..59413aaac --- /dev/null +++ b/src/Composer/Json/JsonManipulator.php @@ -0,0 +1,121 @@ + + * 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) + { + $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 = ' '; + } + } +} 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/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test new file mode 100644 index 000000000..0edf18ea5 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -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) 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; 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" + } +} +' + ), + ); + } +}