diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 41cc73f..801d284 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,6 +3,7 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" + displayDetailsOnTestsThatTriggerWarnings="true" displayDetailsOnPhpunitDeprecations="true"> diff --git a/src/Composer/PatchAddCommand.php b/src/Composer/PatchAddCommand.php index 53d1dee..fc08434 100644 --- a/src/Composer/PatchAddCommand.php +++ b/src/Composer/PatchAddCommand.php @@ -146,32 +146,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('The patch was successfully added.'); if (!$input->getOption('no-update')) { - $application = $this->getApplication(); - $application->setAutoExit(FALSE); - if ($this->isComposerPatches1()) { // Trigger install command after adding a patch. - $install = Installer::create($this->getIO(), $this->requireComposer()); - $install->setUpdate(TRUE) - // Forward the option - ->setVerbose($input->getOption('verbose')) - // Only update the current package - ->setUpdateAllowList([$package]) - // Don't update the dependencies of the patched package. - ->setUpdateAllowTransitiveDependencies(Request::UPDATE_ONLY_LISTED) - // Patches are always considered to be applied in "dev mode". - // This is also required to prevent composer from removing all installed - // dev dependencies. - ->setDevMode($updateDevMode) - ->run(); + $this->runReinstall($package, $updateDevMode); } else { $output->writeln('Relocking patches...'); - $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + $this->runPatchesRelock(); $output->writeln('Repatching dependencies...'); - $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); - $output->writeln('Reinstalling package...'); - $application->run(new ArrayInput(['command' => 'reinstall', 'packages' => [$package]]), $output); + $this->runRepatch(); } } diff --git a/src/Composer/PatchBaseCommand.php b/src/Composer/PatchBaseCommand.php index 1e89caf..02c97d4 100644 --- a/src/Composer/PatchBaseCommand.php +++ b/src/Composer/PatchBaseCommand.php @@ -2,9 +2,14 @@ namespace szeidler\ComposerPatchesCLI\Composer; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Factory; use Composer\Command\BaseCommand; +use Composer\Json\JsonFile; use Composer\Semver\Comparator; +use Composer\Installer; +use Composer\DependencyResolver\Request; +use cweagans\Composer\Plugin\Patches; class PatchBaseCommand extends BaseCommand { @@ -87,6 +92,93 @@ protected function isComposerPatches1() { return $version && version_compare($version, '2.0.0', '<'); } + /** + * Get the Patches plugin instance. + */ + protected function getPatchesPluginInstance() { + foreach ($this->requireComposer()->getPluginManager()->getPlugins() as $plugin) { + $className = get_class($plugin); + if (str_starts_with($className, 'cweagans\Composer\Plugin\Patches')) { + return $plugin; + } + } + return NULL; + } + + /** + * Run the patches-relock command. + */ + protected function runPatchesRelock(): void { + $plugin = $this->getPatchesPluginInstance(); + if ($plugin) { + if (file_exists($plugin->getLockFile()->getPath())) { + unlink($plugin->getLockFile()->getPath()); + } + $plugin->createNewPatchesLock(); + } + } + + /** + * Run the patches-repatch command. + */ + protected function runRepatch(): void { + $plugin = $this->getPatchesPluginInstance(); + if ($plugin) { + $plugin->loadLockedPatches(); + $patchCollection = $plugin->getPatchCollection(); + if ($patchCollection) { + $localRepository = $this->requireComposer() + ->getRepositoryManager() + ->getLocalRepository(); + + $patched_packages = $patchCollection->getPatchedPackages(); + $packages = array_filter($localRepository->getPackages(), function ($val) use ($patched_packages) { + return in_array($val->getName(), $patched_packages); + }); + + $promises = []; + foreach ($packages as $package) { + $uninstallOperation = new UninstallOperation($package); + $promises[] = $this->requireComposer() + ->getInstallationManager() + ->uninstall($localRepository, $uninstallOperation); + } + + $promises = array_filter($promises); + if (!empty($promises)) { + $this->requireComposer()->getLoop()->wait($promises); + } + + $install = Installer::create($this->getIO(), $this->requireComposer()); + $install->run(); + } + } + } + + /** + * Run the reinstall command for a package. + * + * @param string $package + * @param bool $devMode + */ + protected function runReinstall(string $package, bool $devMode = TRUE): void { + $install = Installer::create($this->getIO(), $this->requireComposer()); + $install->setUpdate(TRUE) + ->setUpdateAllowList([$package]) + ->setUpdateAllowTransitiveDependencies(Request::UPDATE_ONLY_LISTED) + ->setDevMode($devMode) + ->run(); + } + + /** + * Updates the lock file hash. + */ + protected function updateLockFile(): void { + $composerJsonPath = Factory::getComposerFile(); + $composerJson = new JsonFile($composerJsonPath); + $this->requireComposer()->getLocker()->updateHash($composerJson); + } + /** * Get the patches from root composer or external file * diff --git a/src/Composer/PatchEnableCommand.php b/src/Composer/PatchEnableCommand.php index 3c0d124..f7012f2 100644 --- a/src/Composer/PatchEnableCommand.php +++ b/src/Composer/PatchEnableCommand.php @@ -43,6 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $patches_file = new JsonFile($patches_filename); if (!$patches_file->exists()) { if (copy(dirname(__FILE__) . '/../Fixtures/composer.patches.json', $patches_filename)) { + print_r(dirname(__FILE__)); $output->writeln('The composer patches file was created.'); } else { diff --git a/src/Composer/PatchMigrateCommand.php b/src/Composer/PatchMigrateCommand.php index c067614..5d6b62e 100644 --- a/src/Composer/PatchMigrateCommand.php +++ b/src/Composer/PatchMigrateCommand.php @@ -39,57 +39,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($patchType === self::PATCHTYPE_ROOT_CP1) { $output->writeln('Migrating patches from root composer.json...'); - + // Move patches to the new location. $composer_manipulator->removeSubNode('extra', 'patches'); $composer_manipulator->addSubNode('extra', 'composer-patches.patches', $patches); - - // Handle patches-ignore -> ignore-dependency-patches - if (isset($extra['patches-ignore'])) { - $ignored = []; - foreach ($extra['patches-ignore'] as $package => $package_patches) { - // CP1 patches-ignore format is slightly different, but often it was just a list of patches. - // "patches-ignore": { "source/package": { "target/package": { "description": "url" } } } - // CP2 ignore-dependency-patches is just a list of packages whose patches should be ignored. - // "ignore-dependency-patches": ["some/package"] - $ignored[] = $package; - } - $composer_manipulator->removeSubNode('extra', 'patches-ignore'); - $composer_manipulator->addSubNode('extra', 'composer-patches.ignore-dependency-patches', array_unique($ignored)); - } - - // Handle patchLevel -> package-depths - if (isset($extra['patchLevel'])) { - $depths = []; - foreach ($extra['patchLevel'] as $package => $level) { - // Convert -p1 to 1 - $depths[$package] = (int) str_replace('-p', '', $level); - } - $composer_manipulator->removeSubNode('extra', 'patchLevel'); - $composer_manipulator->addSubNode('extra', 'composer-patches.package-depths', $depths); - } - - // Handle composer-exit-on-patch-failure -> exit-on-patch-failure - if (isset($extra['composer-exit-on-patch-failure'])) { - $composer_manipulator->removeSubNode('extra', 'composer-exit-on-patch-failure'); - $composer_manipulator->addSubNode('extra', 'composer-patches.exit-on-patch-failure', $extra['composer-exit-on-patch-failure']); - } - - // Handle composer-patches-skip-reporting -> skip-reporting - if (isset($extra['composer-patches-skip-reporting'])) { - $composer_manipulator->removeSubNode('extra', 'composer-patches-skip-reporting'); - $composer_manipulator->addSubNode('extra', 'composer-patches.skip-reporting', $extra['composer-patches-skip-reporting']); - } - - // Handle enable-patching (cleanup) - if (isset($extra['enable-patching'])) { - $composer_manipulator->removeSubNode('extra', 'enable-patching'); - } - - // Store the manipulated JSON file. - if (!file_put_contents($composer_filename, $composer_manipulator->getContents())) { - throw new \Exception('Composer file could not be saved.'); - } } elseif ($patchType === self::PATCHTYPE_FILE_CP1) { $patches_filename = $extra['patches-file']; @@ -98,28 +51,63 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Update composer.json to use the new patches-file location. $composer_manipulator->removeSubNode('extra', 'patches-file'); $composer_manipulator->addSubNode('extra', 'composer-patches.patches-file', $patches_filename); - - if (!file_put_contents($composer_filename, $composer_manipulator->getContents())) { - throw new \Exception('Composer file could not be saved.'); + } + + // Handle patches-ignore -> ignore-dependency-patches + if (isset($extra['patches-ignore'])) { + $ignored = []; + foreach ($extra['patches-ignore'] as $package => $package_patches) { + // CP1 patches-ignore format is slightly different, but often it was just a list of patches. + // "patches-ignore": { "source/package": { "target/package": { "description": "url" } } } + // CP2 ignore-dependency-patches is just a list of packages whose patches should be ignored. + // "ignore-dependency-patches": ["some/package"] + $ignored[] = $package; + } + $composer_manipulator->removeSubNode('extra', 'patches-ignore'); + $composer_manipulator->addSubNode('extra', 'composer-patches.ignore-dependency-patches', array_unique($ignored)); + } + + // Handle patchLevel -> package-depths + if (isset($extra['patchLevel'])) { + $depths = []; + foreach ($extra['patchLevel'] as $package => $level) { + // Convert -p1 to 1 + $depths[$package] = (int) str_replace('-p', '', $level); } + $composer_manipulator->removeSubNode('extra', 'patchLevel'); + $composer_manipulator->addSubNode('extra', 'composer-patches.package-depths', $depths); + } + + // Handle enable-patching, composer-patches-skip-reporting and composer-exit-on-patch-failure (cleanup) + $cleanup_keys = [ + 'enable-patching', + 'composer-patches-skip-reporting', + 'composer-exit-on-patch-failure', + ]; + foreach ($cleanup_keys as $cleanup_key) { + if (isset($extra[$cleanup_key])) { + $composer_manipulator->removeSubNode('extra', $cleanup_key); + } + } - // Update the patches file itself. - $patches_file = new JsonFile($patches_filename); - $patches_manipulator = new JsonManipulator(file_get_contents($patches_file->getPath())); - // CP2 expects patches to be in the root "patches" key of the patches-file, which is the same as CP1. - // So no changes might be needed to the content of the file itself if it only contains "patches". - // However, we should check if there's anything else in there. + // Store the manipulated JSON file. + if (!file_put_contents($composer_filename, $composer_manipulator->getContents())) { + throw new \Exception('Composer file could not be saved.'); } $output->writeln('Migration completed successfully.'); - - $application = $this->getApplication(); - $application->setAutoExit(FALSE); + + $output->writeln( + 'Running composer update nothing to refresh lock file...', + ); + $this->updateLockFile(); + $this->resetComposer(); + $output->writeln('Relocking patches...'); - $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + $this->runPatchesRelock(); $output->writeln('Repatching dependencies...'); - $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); + $this->runRepatch(); return 0; } diff --git a/src/Composer/PatchMoveToLocalCommand.php b/src/Composer/PatchMoveToLocalCommand.php index 783ff3d..1403b8c 100644 --- a/src/Composer/PatchMoveToLocalCommand.php +++ b/src/Composer/PatchMoveToLocalCommand.php @@ -147,14 +147,11 @@ protected function execute( 'Remote Composer patches got successfully moved to local files and got updated in the composer.json or composer.patches.json.' ); - $application = $this->getApplication(); - $application->setAutoExit(FALSE); - if (!$this->isComposerPatches1()) { $output->writeln('Relocking patches...'); - $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + $this->runPatchesRelock(); $output->writeln('Repatching dependencies...'); - $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); + $this->runRepatch(); } } else { throw new \Exception( diff --git a/src/Composer/PatchRemoveCommand.php b/src/Composer/PatchRemoveCommand.php index a043641..05994f5 100644 --- a/src/Composer/PatchRemoveCommand.php +++ b/src/Composer/PatchRemoveCommand.php @@ -108,17 +108,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('The patch was successfully removed.'); if (!$input->getOption('no-update')) { - $application = $this->getApplication(); - $application->setAutoExit(FALSE); - + $updateDevMode = !$input->hasOption('no-dev') || !$input->getOption('no-dev'); if (!$this->isComposerPatches1()) { $output->writeln('Relocking patches...'); - $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + $this->runPatchesRelock(); $output->writeln('Repatching dependencies...'); - $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); + $this->runRepatch(); } $output->writeln('Reinstalling package...'); - $application->run(new ArrayInput(['command' => 'reinstall', 'packages' => [$package]]), $output); + $this->runReinstall($package, $updateDevMode); } return 0; diff --git a/src/Fixtures/composer.patches.json b/src/Fixtures/composer.patches.json index 5fdab15..4b3fa6f 100644 --- a/src/Fixtures/composer.patches.json +++ b/src/Fixtures/composer.patches.json @@ -1,3 +1,3 @@ { "patches": {} -} \ No newline at end of file +} diff --git a/tests/Fixtures/composer-migrate-root.json b/tests/Fixtures/composer-migrate-root.json new file mode 100644 index 0000000..8490698 --- /dev/null +++ b/tests/Fixtures/composer-migrate-root.json @@ -0,0 +1,24 @@ +{ + "name": "szeidler/composer-patches-cli-test", + "description": "Test package", + "require": { + "cweagans/composer-patches": "^2.0" + }, + "extra": { + "composer-exit-on-patch-failure": true, + "patchLevel": { + "drupal/core": "-p2" + }, + "enable-patching": true, + "patches": { + "vendor/package": { + "test patch": "test.patch" + } + } + }, + "config": { + "allow-plugins": { + "cweagans/composer-patches": true + } + } +} diff --git a/tests/Fixtures/composer-migrate-root.lock b/tests/Fixtures/composer-migrate-root.lock new file mode 100644 index 0000000..37d4d6c --- /dev/null +++ b/tests/Fixtures/composer-migrate-root.lock @@ -0,0 +1,142 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "de578f16f9dc9bdcc0a1b6b9f5d6fdd9", + "packages": [ + { + "name": "cweagans/composer-configurable-plugin", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-configurable-plugin.git", + "reference": "15433906511a108a1806710e988629fd24b89974" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-configurable-plugin/zipball/15433906511a108a1806710e988629fd24b89974", + "reference": "15433906511a108a1806710e988629fd24b89974", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "require-dev": { + "codeception/codeception": "~4.0", + "codeception/module-asserts": "^2.0", + "composer/composer": "~2.0", + "php-coveralls/php-coveralls": "~2.0", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpro/grumphp": "^1.8.0", + "sebastian/phpcpd": "^6.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a lightweight configuration system for Composer plugins.", + "support": { + "issues": "https://github.com/cweagans/composer-configurable-plugin/issues", + "source": "https://github.com/cweagans/composer-configurable-plugin/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/cweagans", + "type": "github" + } + ], + "time": "2023-02-12T04:58:58+00:00" + }, + { + "name": "cweagans/composer-patches", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "bfa6018a5f864653d9ed899b902ea72f858a2cf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/bfa6018a5f864653d9ed899b902ea72f858a2cf7", + "reference": "bfa6018a5f864653d9ed899b902ea72f858a2cf7", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "cweagans/composer-configurable-plugin": "^2.0", + "ext-json": "*", + "php": ">=8.0.0" + }, + "require-dev": { + "codeception/codeception": "~4.0", + "codeception/module-asserts": "^2.0", + "codeception/module-cli": "^2.0", + "codeception/module-filesystem": "^2.0", + "composer/composer": "~2.0", + "php-coveralls/php-coveralls": "~2.0", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpro/grumphp": "^1.8.0", + "sebastian/phpcpd": "^6.0", + "squizlabs/php_codesniffer": "^4.0" + }, + "type": "composer-plugin", + "extra": { + "_": "The following two lines ensure that composer-patches is loaded as early as possible.", + "class": "cweagans\\Composer\\Plugin\\Patches", + "plugin-modifies-downloads": true, + "plugin-modifies-install-path": true + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/cweagans", + "type": "github" + } + ], + "time": "2025-10-30T23:44:22+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/tests/Fixtures/composer-pre-enable.json b/tests/Fixtures/composer-pre-enable.json new file mode 100644 index 0000000..c16d82f --- /dev/null +++ b/tests/Fixtures/composer-pre-enable.json @@ -0,0 +1,11 @@ +{ + "name": "szeidler/composer-patches-cli-test-project", + "description": "Project for testing the extension.", + "type": "project", + "license": "MIT", + "config": { + "allow-plugins": { + "cweagans/composer-patches": true + } + } +} diff --git a/tests/PatchCommandTestBase.php b/tests/PatchCommandTestBase.php index d7e5616..5f65449 100644 --- a/tests/PatchCommandTestBase.php +++ b/tests/PatchCommandTestBase.php @@ -4,6 +4,7 @@ use Composer\Console\Application; use Composer\Factory; +use Composer\IO\BufferIO; use Composer\IO\NullIO; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -41,7 +42,9 @@ protected function setUp(): void { $this->tempDir = sys_get_temp_dir() . '/composer-test-' . uniqid(); mkdir($this->tempDir); $this->composerJsonPath = $this->tempDir . '/composer.json'; - copy('tests/Fixtures/composer.json', $this->composerJsonPath); + if (file_exists('tests/Fixtures/composer.json')) { + copy('tests/Fixtures/composer.json', $this->composerJsonPath); + } chdir($this->tempDir); } @@ -69,13 +72,57 @@ protected function recursiveRmdir(string $dir): void { rmdir($dir); } + /** + * Helper to create a dummy composer.lock file. + */ + protected function createLockFile(array $packages = []) { + $packagesData = []; + foreach ($packages as $name => $version) { + if (is_int($name)) { + $name = $version; + $version = '1.0.0'; + } + $packagesData[] = [ + 'name' => $name, + 'version' => $version, + 'source' => [ + 'type' => 'git', + 'url' => 'https://example.com/' . $name . '.git', + 'reference' => 'dummy', + ], + 'dist' => [ + 'type' => 'zip', + 'url' => 'https://example.com/' . $name . '.zip', + 'reference' => 'dummy', + 'shasum' => '', + ], + 'type' => 'library', + ]; + } + + $lockData = [ + '_readme' => ['This file is dummy.'], + 'content-hash' => 'dummy', + 'packages' => $packagesData, + 'packages-dev' => [], + 'aliases' => [], + 'minimum-stability' => 'stable', + 'stability-flags' => [], + 'prefer-stable' => false, + 'prefer-lowest' => false, + 'platform' => [], + 'platform-dev' => [], + ]; + file_put_contents($this->tempDir . '/composer.lock', json_encode($lockData, JSON_PRETTY_PRINT)); + } + /** * Helper to create a CommandTester for a given command class. * * @return \Symfony\Component\Console\Tester\CommandTester */ protected function getCommandTester(string $commandClass): CommandTester { - $io = new \Composer\IO\BufferIO(); + $io = new BufferIO(); $this->composer = Factory::create($io, $this->composerJsonPath, TRUE, TRUE); $application = new Application(); $application->setAutoExit(FALSE); diff --git a/tests/PatchEnableCommandTest.php b/tests/PatchEnableCommandTest.php index 91b9fc0..0744110 100644 --- a/tests/PatchEnableCommandTest.php +++ b/tests/PatchEnableCommandTest.php @@ -14,6 +14,7 @@ class PatchEnableCommandTest extends PatchCommandTestBase { * Tests that patching is enabled in composer.json (Composer Patches 2 style). */ public function testEnableComposerPatches2() { + copy(__DIR__ . '/Fixtures/composer-pre-enable.json', $this->composerJsonPath); // Ensure we start without patches $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); unset($json['extra']['composer-patches']['patches']); @@ -39,6 +40,7 @@ public function testEnableComposerPatches2() { * Tests that patching is enabled in composer.json (Composer Patches 1 style). */ public function testEnableComposerPatches1() { + copy(__DIR__ . '/Fixtures/composer-pre-enable.json', $this->composerJsonPath); // Mock Composer Patches 1 in requires $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); $json['require']['cweagans/composer-patches'] = '1.7.3'; @@ -62,6 +64,7 @@ public function testEnableComposerPatches1() { * Tests that patching is enabled with an external file. */ public function testEnableWithExternalFile() { + copy(__DIR__ . '/Fixtures/composer-pre-enable.json', $this->composerJsonPath); $tester = $this->getCommandTester(PatchEnableCommand::class); $patchesFilename = 'patches.json'; @@ -85,6 +88,7 @@ public function testEnableWithExternalFile() { * Tests that patching is enabled with an external file (Composer Patches 1 style). */ public function testEnableWithExternalFileComposerPatches1() { + copy(__DIR__ . '/Fixtures/composer-pre-enable.json', $this->composerJsonPath); // Mock Composer Patches 1 in requires $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); $json['require']['cweagans/composer-patches'] = '1.7.3'; @@ -113,6 +117,8 @@ public function testEnableWithExternalFileComposerPatches1() { * Tests that it fails if patches-file is already defined. */ public function testEnableAlreadyDefined() { + copy(__DIR__ . '/Fixtures/composer-pre-enable.json', $this->composerJsonPath); + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); $json['extra']['composer-patches']['patches-file'] = 'existing.json'; file_put_contents($this->composerJsonPath, json_encode($json)); diff --git a/tests/PatchMigrateCommandTest.php b/tests/PatchMigrateCommandTest.php index df2e111..274a702 100644 --- a/tests/PatchMigrateCommandTest.php +++ b/tests/PatchMigrateCommandTest.php @@ -2,6 +2,9 @@ namespace szeidler\ComposerPatchesCLI\Tests; +use Composer\Factory; +use Composer\IO\BufferIO; +use cweagans\Composer\Plugin\Patches; use szeidler\ComposerPatchesCLI\Composer\PatchMigrateCommand; use szeidler\ComposerPatchesCLI\Exception\PatchMigrateConfigurationExistsException; use szeidler\ComposerPatchesCLI\Exception\PatchMigrateNoConfigurationFoundException; @@ -11,35 +14,60 @@ */ class PatchMigrateCommandTest extends PatchCommandTestBase { + /** + * Helper to get a CommandTester with real CP2 commands. + */ + protected function getMigrateCommandTester() { + $io = new BufferIO(); + $this->composer = Factory::create($io, $this->composerJsonPath); + $application = new \Composer\Console\Application(); + $application->setAutoExit(FALSE); + + // Register real CP2 commands and plugin if using Composer Patches 2. + if (class_exists('cweagans\Composer\Command\RelockCommand')) { + if (method_exists($application, 'addCommand')) { + $application->addCommand(new \cweagans\Composer\Command\RelockCommand()); + $application->addCommand(new \cweagans\Composer\Command\RepatchCommand()); + } + else { + $application->add(new \cweagans\Composer\Command\RelockCommand()); + $application->add(new \cweagans\Composer\Command\RepatchCommand()); + } + + // Register the Patches plugin with the composer instance. + $plugin = new Patches(); + $this->composer->getPluginManager()->addPlugin($plugin); + } + + $command = new class extends PatchMigrateCommand { + protected function runRepatch(): void { + // Mock repatch to avoid full Installer run which requires real packages. + $this->getIO()->write('Repatching dependencies...'); + } + }; + $command->setComposer($this->composer); + $command->setIO($io); + $command->setApplication($application); + + return new \Symfony\Component\Console\Tester\CommandTester($command); + } + + /** + * Helper to create a dummy patch file. + */ + protected function createDummyPatch(string $filename) { + file_put_contents($this->tempDir . '/' . $filename, "--- a/file\n+++ b/file\n@@ -1,1 +1,1 @@\n-old\n+new\n"); + } + /** * Tests migrating patches from the root composer.json. */ public function testMigrateRoot() { - $composer_json = [ - 'name' => 'test/project', - 'extra' => [ - 'patches' => [ - 'vendor/package' => [ - 'description' => 'https://example.com/patch.patch', - ], - ], - 'patches-ignore' => [ - 'dependency/package' => [ - 'vendor/package' => [ - 'ignored patch' => 'https://example.com/ignored.patch' - ] - ], - ], - 'patchLevel' => [ - 'vendor/package' => '-p2', - ], - 'composer-exit-on-patch-failure' => true, - 'enable-patching' => true, - ], - ]; - file_put_contents($this->composerJsonPath, json_encode($composer_json, JSON_PRETTY_PRINT)); + $this->createDummyPatch('test.patch'); + copy(__DIR__ . '/Fixtures/composer-migrate-root.json', $this->composerJsonPath); + copy(__DIR__ . '/Fixtures/composer-migrate-root.lock', $this->tempDir . '/composer.lock'); - $commandTester = $this->getCommandTester(PatchMigrateCommand::class); + $commandTester = $this->getMigrateCommandTester(); $commandTester->execute([]); $this->assertStringContainsString('Migrating patches from root composer.json...', $commandTester->getDisplay()); @@ -48,32 +76,48 @@ public function testMigrateRoot() { $this->assertStringContainsString('Repatching dependencies...', $commandTester->getDisplay()); $updated_composer_json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - - $this->assertArrayNotHasKey('patches', $updated_composer_json['extra']); + $this->assertArrayHasKey('composer-patches', $updated_composer_json['extra']); - $this->assertEquals($composer_json['extra']['patches'], $updated_composer_json['extra']['composer-patches']['patches']); - - $this->assertArrayNotHasKey('patches-ignore', $updated_composer_json['extra']); - $this->assertEquals(['dependency/package'], $updated_composer_json['extra']['composer-patches']['ignore-dependency-patches']); - + $expected_patches = [ + 'vendor/package' => [ + 'test patch' => 'test.patch', + ], + ]; + $this->assertEquals($expected_patches, $updated_composer_json['extra']['composer-patches']['patches']); + $this->assertArrayNotHasKey('patches', $updated_composer_json['extra']); + $this->assertArrayNotHasKey('patchLevel', $updated_composer_json['extra']); - $this->assertEquals(['vendor/package' => 2], $updated_composer_json['extra']['composer-patches']['package-depths']); - - $this->assertArrayNotHasKey('composer-exit-on-patch-failure', $updated_composer_json['extra']); - $this->assertTrue($updated_composer_json['extra']['composer-patches']['exit-on-patch-failure']); + $this->assertEquals(['drupal/core' => 2], $updated_composer_json['extra']['composer-patches']['package-depths']); $this->assertArrayNotHasKey('enable-patching', $updated_composer_json['extra']); + $this->assertArrayNotHasKey('composer-exit-on-patch-failure', $updated_composer_json['extra']); } /** * Tests migrating patches from an external patches file. */ public function testMigrateFile() { + $this->createDummyPatch('test.patch'); $patches_file = 'patches.json'; $composer_json = [ 'name' => 'test/project', + 'require' => [ + 'cweagans/composer-patches' => '^2.0', + ], 'extra' => [ 'patches-file' => $patches_file, + 'patches-ignore' => [ + 'dependency/package' => [ + 'vendor/package' => [ + 'ignored patch' => 'https://example.com/ignored.patch' + ] + ], + ], + 'patchLevel' => [ + 'vendor/package' => '-p2', + ], + 'composer-patches-skip-reporting' => true, + 'composer-exit-on-patch-failure' => true, ], ]; file_put_contents($this->composerJsonPath, json_encode($composer_json, JSON_PRETTY_PRINT)); @@ -81,13 +125,14 @@ public function testMigrateFile() { $patches_json = [ 'patches' => [ 'vendor/package' => [ - 'description' => 'https://example.com/patch.patch', + 'description' => 'test.patch', ], ], ]; file_put_contents($this->tempDir . '/' . $patches_file, json_encode($patches_json, JSON_PRETTY_PRINT)); + $this->createLockFile(['vendor/package' => '1.0.0']); - $commandTester = $this->getCommandTester(PatchMigrateCommand::class); + $commandTester = $this->getMigrateCommandTester(); $commandTester->execute([]); $this->assertStringContainsString("Migrating patches from $patches_file...", $commandTester->getDisplay()); @@ -98,6 +143,15 @@ public function testMigrateFile() { $updated_composer_json = json_decode(file_get_contents($this->composerJsonPath), TRUE); $this->assertArrayNotHasKey('patches-file', $updated_composer_json['extra']); $this->assertEquals($patches_file, $updated_composer_json['extra']['composer-patches']['patches-file']); + + $this->assertArrayNotHasKey('patches-ignore', $updated_composer_json['extra']); + $this->assertEquals(['dependency/package'], $updated_composer_json['extra']['composer-patches']['ignore-dependency-patches']); + + $this->assertArrayNotHasKey('patchLevel', $updated_composer_json['extra']); + $this->assertEquals(['vendor/package' => 2], $updated_composer_json['extra']['composer-patches']['package-depths']); + + $this->assertArrayNotHasKey('composer-patches-skip-reporting', $updated_composer_json['extra']); + $this->assertArrayNotHasKey('composer-exit-on-patch-failure', $updated_composer_json['extra']); } /**