diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f6bcdd..6df48c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,11 +6,13 @@ on: jobs: test: + name: PHP ${{ matrix.php }} / Symfony ${{ matrix.symfony }} / Composer Patches ${{ matrix.composer-patches }} runs-on: ubuntu-latest strategy: matrix: php: ['8.2', '8.3', '8.4'] symfony: ['^7.0', '^8.0'] + composer-patches: ['^1.7', '^2.0'] exclude: - php: '8.2' symfony: '^8.0' @@ -32,7 +34,7 @@ jobs: # Install dependencies - name: Install dependencies run: | - composer require "symfony/console:${{ matrix.symfony }}" --dev --no-update --no-interaction + composer require "symfony/console:${{ matrix.symfony }}" "cweagans/composer-patches:${{ matrix.composer-patches }}" --dev --no-update --no-interaction composer update --prefer-dist --no-progress # Run PHPUnit tests diff --git a/composer.json b/composer.json index 211504e..7b04c85 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "license": "MIT", "require": { "composer-plugin-api": "^2.0", - "cweagans/composer-patches": "^1.7", + "cweagans/composer-patches": "^1.7 || ^2.0", "php": "^8.0" }, "require-dev": { diff --git a/src/Composer/PatchAddCommand.php b/src/Composer/PatchAddCommand.php index 59e3790..fc3b7d3 100644 --- a/src/Composer/PatchAddCommand.php +++ b/src/Composer/PatchAddCommand.php @@ -2,6 +2,7 @@ namespace szeidler\ComposerPatchesCLI\Composer; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -9,9 +10,6 @@ use Symfony\Component\Console\Question\Question; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; -use Composer\Installer; -use Composer\Plugin\PluginInterface; -use Composer\DependencyResolver\Request; class PatchAddCommand extends PatchBaseCommand { @@ -76,11 +74,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($this->getPatchType() === self::PATCHTYPE_ROOT) { + $manipulator_filename = 'composer.json'; + $json_node = 'extra'; + $json_name = 'composer-patches.patches'; + } + elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) { $manipulator_filename = 'composer.json'; $json_node = 'extra'; $json_name = 'patches'; } elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { + $manipulator_filename = $extra['composer-patches']['patches-file']; + $json_node = null; + $json_name = 'patches'; + } + elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) { $manipulator_filename = $extra['patches-file']; $json_node = null; $json_name = 'patches'; @@ -131,47 +139,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Store the manipulated JSON file. if (!file_put_contents($manipulator_filename, $manipulator->getContents())) { - throw new \Exception($extra['patches-file'] . ' file could not be saved. Please check the permissions.'); + throw new \Exception($manipulator_filename . ' file could not be saved. Please check the permissions.'); } $output->writeln('The patch was successfully added.'); if (!$input->getOption('no-update')) { - // Trigger install command after adding a patch. - $install = Installer::create($this->getIO(), $this->requireComposer()); - - // We run an update, because the patch will otherwise not end up in the - // composer.lock. Beware: This could update the package unwanted. - // Support Composer 1 and Composer 2 methods. - switch (PluginInterface::PLUGIN_API_VERSION) { - case '1.1.0': - $install->setUpdate(TRUE) - // Forward the option - ->setVerbose($input->getOption('verbose')) - // Only update the current package - ->setUpdateWhitelist([$package]) - // Don't update the dependencies of the patched package. - ->setWhitelistTransitiveDependencies(FALSE) - ->setWhitelistAllDependencies(FALSE) - // 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(); - break; - default: - $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_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE) - // 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(); - } + $application = $this->getApplication(); + $application->setAutoExit(FALSE); + $output->writeln('Relocking patches...'); + $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + $output->writeln('Repatching dependencies...'); + $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); } return 0; diff --git a/src/Composer/PatchBaseCommand.php b/src/Composer/PatchBaseCommand.php index b1baf26..1e89caf 100644 --- a/src/Composer/PatchBaseCommand.php +++ b/src/Composer/PatchBaseCommand.php @@ -4,11 +4,14 @@ use Composer\Factory; use Composer\Command\BaseCommand; +use Composer\Semver\Comparator; class PatchBaseCommand extends BaseCommand { const PATCHTYPE_ROOT = 1; const PATCHTYPE_FILE = 2; + const PATCHTYPE_ROOT_CP1 = 3; + const PATCHTYPE_FILE_CP1 = 4; protected function configure(): void { parent::configure(); @@ -25,22 +28,71 @@ protected function configure(): void { protected function getPatchType() { $extra = $this->requireComposer()->getPackage()->getExtra(); - if (isset($extra['patches'])) { + if (isset($extra['composer-patches']['patches'])) { return self::PATCHTYPE_ROOT; } - elseif (isset($extra['patches-file'])) { + elseif (isset($extra['patches'])) { + return self::PATCHTYPE_ROOT_CP1; + } + elseif (isset($extra['composer-patches']['patches-file'])) { return self::PATCHTYPE_FILE; } + elseif (isset($extra['patches-file'])) { + return self::PATCHTYPE_FILE_CP1; + } + + return NULL; + } + + /** + * Returns the version of cweagans/composer-patches if installed. + * + * @return string|null + */ + protected function getComposerPatchesVersion() { + $composer = $this->requireComposer(); + $repositoryManager = $composer->getRepositoryManager(); + $localRepository = $repositoryManager->getLocalRepository(); + $packages = $localRepository->getPackages(); + + foreach ($packages as $package) { + if ($package->getName() === 'cweagans/composer-patches') { + return $package->getVersion(); + } + } + + // Fallback: check require in composer.json if not in local repo (e.g. during tests or before install) + $configPath = Factory::getComposerFile(); + if (file_exists($configPath)) { + $config = json_decode(file_get_contents($configPath), true); + $allRequires = array_merge($config['require'] ?? [], $config['require-dev'] ?? []); + if (isset($allRequires['cweagans/composer-patches'])) { + $versionConstraint = $allRequires['cweagans/composer-patches']; + if (Comparator::lessThan($versionConstraint, '2.0.0') || strpos($versionConstraint, 'dev-') === 0) { + return '1.99.99'; // Simulated version for Composer Patches 1 + } + } + } return NULL; } + /** + * Checks if the installed version of Composer Patches is version 1. + * + * @return bool + */ + protected function isComposerPatches1() { + $version = $this->getComposerPatchesVersion(); + return $version && version_compare($version, '2.0.0', '<'); + } + /** * Get the patches from root composer or external file * * Currently directly extracted from the Composer Patches code base. * - * @return Patches + * @return array * @throws \Exception * @see https://github.com/cweagans/composer-patches/blob/1.x/src/Patches.php */ @@ -49,13 +101,55 @@ protected function grabPatches() { $extra = $this->requireComposer()->getPackage()->getExtra(); if ($this->getPatchType() === self::PATCHTYPE_ROOT) { $this->getIO()->write('Gathering patches from root composer.json.'); + $patches = $extra['composer-patches']['patches']; + return $patches; + } + elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) { + $this->getIO()->write('Gathering patches from root composer.json (extra.patches).'); $patches = $extra['patches']; return $patches; } // If it's not specified there, look for a patches-file definition. elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { $this->getIO()->write('Gathering patches from patch file.'); - $patches = file_get_contents($extra['patches-file']); + $patchesFile = $extra['composer-patches']['patches-file']; + $patches = file_get_contents($patchesFile); + $patches = json_decode($patches, TRUE); + $error = json_last_error(); + if ($error != 0) { + switch ($error) { + case JSON_ERROR_DEPTH: + $msg = ' - Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = ' - Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = ' - Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $msg = ' - Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $msg = ' - Unknown error'; + break; + } + throw new \Exception('There was an error in the supplied patches file:' . $msg); + } + if (isset($patches['patches'])) { + return $patches['patches']; + } + elseif (!$patches) { + throw new \Exception('There was an error in the supplied patch file'); + } + } + elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) { + $this->getIO()->write('Gathering patches from patch file (extra.patches-file).'); + $patchesFile = $extra['patches-file']; + $patches = file_get_contents($patchesFile); $patches = json_decode($patches, TRUE); $error = json_last_error(); if ($error != 0) { @@ -82,8 +176,7 @@ protected function grabPatches() { throw new \Exception('There was an error in the supplied patches file:' . $msg); } if (isset($patches['patches'])) { - $patches = $patches['patches']; - return $patches; + return $patches['patches']; } elseif (!$patches) { throw new \Exception('There was an error in the supplied patch file'); diff --git a/src/Composer/PatchEnableCommand.php b/src/Composer/PatchEnableCommand.php index e5969d5..3c0d124 100644 --- a/src/Composer/PatchEnableCommand.php +++ b/src/Composer/PatchEnableCommand.php @@ -27,7 +27,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extra = $this->requireComposer()->getPackage()->getExtra(); // Check, if patch file is already defined. - if (!empty($extra['patches-file'])) { + if (!empty($extra['patches-file']) || !empty($extra['composer-patches']['patches-file'])) { throw new \Exception('Patch file was already defined in your composer.json.'); } @@ -49,18 +49,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new \Exception('Patch could not be created.'); } } - $manipulator->addProperty('extra.patches-file', $patches_filename); + + if ($this->isComposerPatches1()) { + $manipulator->addProperty('extra.patches-file', $patches_filename); + } + else { + $manipulator->addProperty('extra.composer-patches.patches-file', $patches_filename); + } } else { // Create an empty patches definition in the root composer.json. - if (!isset($extra['patches'])) { - $manipulator->addProperty('extra.patches', []); + if (!isset($extra['patches']) && !isset($extra['composer-patches']['patches'])) { + if ($this->isComposerPatches1()) { + $manipulator->addProperty('extra.patches', []); + } + else { + $manipulator->addProperty('extra.composer-patches.patches', []); + } } } - // Enable patching. - $manipulator->addProperty('extra.enable-patching', TRUE); - // Store the manipulated JSON file. if (!file_put_contents($composer_filename, $manipulator->getContents())) { throw new \Exception('Composer file could not be saved. Please check the permissions.'); diff --git a/src/Composer/PatchMoveToLocalCommand.php b/src/Composer/PatchMoveToLocalCommand.php index 0e46dac..d459cce 100644 --- a/src/Composer/PatchMoveToLocalCommand.php +++ b/src/Composer/PatchMoveToLocalCommand.php @@ -2,6 +2,7 @@ namespace szeidler\ComposerPatchesCLI\Composer; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -33,10 +34,18 @@ protected function execute( $extra = $this->requireComposer()->getPackage()->getExtra(); if ($this->getPatchType() === self::PATCHTYPE_ROOT) { + $manipulator_filename = 'composer.json'; + $json_node = 'extra'; + $json_name = 'composer-patches.patches'; + } elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) { $manipulator_filename = 'composer.json'; $json_node = 'extra'; $json_name = 'patches'; } elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { + $manipulator_filename = $extra['composer-patches']['patches-file']; + $json_node = null; + $json_name = 'patches'; + } elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) { $manipulator_filename = $extra['patches-file']; $json_node = null; $json_name = 'patches'; @@ -137,6 +146,13 @@ protected function execute( $this->getIO()->write( '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); + $output->writeln('Relocking patches...'); + $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + $output->writeln('Repatching dependencies...'); + $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); } else { throw new \Exception( 'Composer patches file could not be saved. Please check the permissions.' diff --git a/src/Composer/PatchRemoveCommand.php b/src/Composer/PatchRemoveCommand.php index 0f75ab6..0ba3d80 100644 --- a/src/Composer/PatchRemoveCommand.php +++ b/src/Composer/PatchRemoveCommand.php @@ -2,8 +2,12 @@ namespace szeidler\ComposerPatchesCLI\Composer; +use Composer\DependencyResolver\Request; +use Composer\Installer; +use Symfony\Component\Console\Input\ArrayInput; 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 Symfony\Component\Console\Question\Question; use Composer\Json\JsonFile; @@ -16,7 +20,8 @@ protected function configure(): void { ->setDescription('Remove a from a composer patch file.') ->setDefinition([ new InputArgument('package', InputArgument::REQUIRED), - new InputArgument('description', InputArgument::REQUIRED) + new InputArgument('description', InputArgument::REQUIRED), + new InputOption('no-update', null, InputOption::VALUE_NONE, 'Do not run an update: as side effect patch will not be removed from the installed package.'), ]); parent::configure(); @@ -42,11 +47,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $description = $input->getArgument('description'); if ($this->getPatchType() === self::PATCHTYPE_ROOT) { + $manipulator_filename = 'composer.json'; + $json_node = 'extra'; + $json_name = 'composer-patches.patches'; + } + elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) { $manipulator_filename = 'composer.json'; $json_node = 'extra'; $json_name = 'patches'; } elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { + $manipulator_filename = $extra['composer-patches']['patches-file']; + $json_node = null; + $json_name = 'patches'; + } + elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) { $manipulator_filename = $extra['patches-file']; $json_node = null; $json_name = 'patches'; @@ -77,21 +92,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Merge in the updated packages into the JSON again. - if ($this->getPatchType() === self::PATCHTYPE_ROOT) { + if ($this->getPatchType() === self::PATCHTYPE_ROOT || $this->getPatchType() === self::PATCHTYPE_ROOT_CP1) { $manipulator->addSubNode($json_node, $json_name, $patches); } - elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { + elseif ($this->getPatchType() === self::PATCHTYPE_FILE || $this->getPatchType() === self::PATCHTYPE_FILE_CP1) { $manipulator->removeMainKey('patches'); $manipulator->addMainKey('patches', $patches); } // Store the manipulated JSON file. if (!file_put_contents($manipulator_filename, $manipulator->getContents())) { - throw new \Exception($extra['patches-file'] . ' file could not be saved. Please check the permissions.'); + throw new \Exception($manipulator_filename . ' file could not be saved. Please check the permissions.'); } $output->writeln('The patch was successfully removed.'); + if (!$input->getOption('no-update')) { + $application = $this->getApplication(); + $application->setAutoExit(FALSE); + $output->writeln('Relocking patches...'); + $application->run(new ArrayInput(['command' => 'patches-relock']), $output); + + $output->writeln('Reinstalling package...'); + $application->run(new ArrayInput(['command' => 'reinstall', 'packages' => [$package]]), $output); + + $output->writeln('Repatching dependencies...'); + $application->run(new ArrayInput(['command' => 'patches-repatch']), $output); + } + return 0; } } diff --git a/tests/Fixtures/composer.json b/tests/Fixtures/composer.json index abef229..8745069 100644 --- a/tests/Fixtures/composer.json +++ b/tests/Fixtures/composer.json @@ -9,6 +9,8 @@ } }, "extra": { - "patches": {} + "composer-patches": { + "patches": {} + } } } diff --git a/tests/PatchAddCommandTest.php b/tests/PatchAddCommandTest.php index 327dcab..cc50151 100644 --- a/tests/PatchAddCommandTest.php +++ b/tests/PatchAddCommandTest.php @@ -32,11 +32,51 @@ public function testPatchIsAddedToComposerJson() { $this->assertStringContainsString('The patch was successfully added.', $output); $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $this->assertArrayHasKey('vendor/package', $json['extra']['patches']); + $this->assertArrayHasKey('vendor/package', $json['extra']['composer-patches']['patches']); $this->assertArrayHasKey('Fix something', - $json['extra']['patches']['vendor/package']); + $json['extra']['composer-patches']['patches']['vendor/package']); $this->assertSame($patchFile, - $json['extra']['patches']['vendor/package']['Fix something']); + $json['extra']['composer-patches']['patches']['vendor/package']['Fix something']); + } + + /** + * Tests that a patch is added to composer.json using extra.patches (Composer Patches 1 style). + */ + public function testPatchIsAddedToExtraPatches() { + file_put_contents($this->composerJsonPath, json_encode([ + 'name' => 'test/project', + 'extra' => [ + 'patches' => [ + 'vendor/package' => [ + 'Existing Composer Patches 1 patch' => 'https://example.com/existing.patch', + ], + ], + ], + ])); + + $tester = $this->getCommandTester(PatchAddCommand::class); + + // Create a patch file. + $patchFile = $this->tempDir . '/fix.patch'; + file_put_contents($patchFile, 'dummy patch content'); + + // Execute patch-add command. + $tester->execute([ + 'package' => 'vendor/package', + 'url' => $patchFile, + 'description' => 'Fix something', + '--no-update' => TRUE, + ]); + + // Run assertions. + $output = $tester->getDisplay(); + + $this->assertStringContainsString('The patch was successfully added.', $output); + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $this->assertArrayHasKey('vendor/package', $json['extra']['patches']); + $this->assertArrayHasKey('Existing Composer Patches 1 patch', $json['extra']['patches']['vendor/package']); + $this->assertArrayHasKey('Fix something', $json['extra']['patches']['vendor/package']); + $this->assertSame($patchFile, $json['extra']['patches']['vendor/package']['Fix something']); } /** @@ -47,7 +87,9 @@ public function testPatchIsAddedToExternalFile() { file_put_contents($this->composerJsonPath, json_encode([ 'name' => 'test/project', 'extra' => [ - 'patches-file' => 'patches.json', + 'composer-patches' => [ + 'patches-file' => 'patches.json', + ], ], ])); file_put_contents($patchesFile, json_encode(['patches' => []])); @@ -80,9 +122,11 @@ public function testDuplicatePatchHandling() { file_put_contents($this->composerJsonPath, json_encode([ 'name' => 'test/project', 'extra' => [ - 'patches' => [ - 'vendor/package' => [ - 'Existing description' => $patchFile, + 'composer-patches' => [ + 'patches' => [ + 'vendor/package' => [ + 'Existing description' => $patchFile, + ], ], ], ], diff --git a/tests/PatchCommandTestBase.php b/tests/PatchCommandTestBase.php index e5f07ea..d7e5616 100644 --- a/tests/PatchCommandTestBase.php +++ b/tests/PatchCommandTestBase.php @@ -27,6 +27,13 @@ abstract class PatchCommandTestBase extends TestCase { */ protected $tempDir; + /** + * The Composer instance. + * + * @var \Composer\Composer + */ + protected $composer; + /** * {@inheritDoc} */ @@ -68,14 +75,14 @@ protected function recursiveRmdir(string $dir): void { * @return \Symfony\Component\Console\Tester\CommandTester */ protected function getCommandTester(string $commandClass): CommandTester { - $io = new NullIO(); - $composer = Factory::create($io, $this->composerJsonPath, TRUE, TRUE); + $io = new \Composer\IO\BufferIO(); + $this->composer = Factory::create($io, $this->composerJsonPath, TRUE, TRUE); $application = new Application(); $application->setAutoExit(FALSE); /** @var \szeidler\ComposerPatchesCLI\Composer\PatchBaseCommand $command */ $command = new $commandClass(); - $command->setComposer($composer); + $command->setComposer($this->composer); $command->setIO($io); $command->setApplication($application); diff --git a/tests/PatchEnableCommandTest.php b/tests/PatchEnableCommandTest.php index b2a1744..91b9fc0 100644 --- a/tests/PatchEnableCommandTest.php +++ b/tests/PatchEnableCommandTest.php @@ -3,6 +3,7 @@ namespace szeidler\ComposerPatchesCLI\Tests; use szeidler\ComposerPatchesCLI\Composer\PatchEnableCommand; +use Composer\Package\Package; /** * Tests the PatchEnableCommand. @@ -10,9 +11,41 @@ class PatchEnableCommandTest extends PatchCommandTestBase { /** - * Tests that patching is enabled in composer.json. + * Tests that patching is enabled in composer.json (Composer Patches 2 style). */ - public function testEnableBasic() { + public function testEnableComposerPatches2() { + // Ensure we start without patches + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + unset($json['extra']['composer-patches']['patches']); + file_put_contents($this->composerJsonPath, json_encode($json)); + + $tester = $this->getCommandTester(PatchEnableCommand::class); + + // Mock Composer Patches 2 version + $package = new Package('cweagans/composer-patches', '2.0.0.0', '2.0.0'); + $this->composer->getRepositoryManager()->getLocalRepository()->addPackage($package); + + $tester->execute([]); + + $output = $tester->getDisplay(); + $this->assertStringContainsString('The composer patches functionality was enabled successfully.', $output); + + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $this->assertArrayHasKey('patches', $json['extra']['composer-patches']); + $this->assertEquals([], $json['extra']['composer-patches']['patches']); + } + + /** + * Tests that patching is enabled in composer.json (Composer Patches 1 style). + */ + public function testEnableComposerPatches1() { + // Mock Composer Patches 1 in requires + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $json['require']['cweagans/composer-patches'] = '1.7.3'; + // Remove existing patch definition from fixture to allow enabling it again + unset($json['extra']['composer-patches']['patches']); + file_put_contents($this->composerJsonPath, json_encode($json)); + $tester = $this->getCommandTester(PatchEnableCommand::class); $tester->execute([]); @@ -21,7 +54,6 @@ public function testEnableBasic() { $this->assertStringContainsString('The composer patches functionality was enabled successfully.', $output); $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $this->assertTrue($json['extra']['enable-patching']); $this->assertArrayHasKey('patches', $json['extra']); $this->assertEquals([], $json['extra']['patches']); } @@ -42,7 +74,34 @@ public function testEnableWithExternalFile() { $this->assertStringContainsString('The composer patches functionality was enabled successfully.', $output); $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $this->assertTrue($json['extra']['enable-patching']); + $this->assertEquals($patchesFilename, $json['extra']['composer-patches']['patches-file']); + $this->assertFileExists($this->tempDir . '/' . $patchesFilename); + + $patchesJson = json_decode(file_get_contents($this->tempDir . '/' . $patchesFilename), TRUE); + $this->assertArrayHasKey('patches', $patchesJson); + } + + /** + * Tests that patching is enabled with an external file (Composer Patches 1 style). + */ + public function testEnableWithExternalFileComposerPatches1() { + // Mock Composer Patches 1 in requires + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $json['require']['cweagans/composer-patches'] = '1.7.3'; + file_put_contents($this->composerJsonPath, json_encode($json)); + + $tester = $this->getCommandTester(PatchEnableCommand::class); + $patchesFilename = 'patches.json'; + + $tester->execute([ + '--file' => $patchesFilename, + ]); + + $output = $tester->getDisplay(); + $this->assertStringContainsString('The composer patches file was created.', $output); + $this->assertStringContainsString('The composer patches functionality was enabled successfully.', $output); + + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); $this->assertEquals($patchesFilename, $json['extra']['patches-file']); $this->assertFileExists($this->tempDir . '/' . $patchesFilename); @@ -55,7 +114,7 @@ public function testEnableWithExternalFile() { */ public function testEnableAlreadyDefined() { $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $json['extra']['patches-file'] = 'existing.json'; + $json['extra']['composer-patches']['patches-file'] = 'existing.json'; file_put_contents($this->composerJsonPath, json_encode($json)); $tester = $this->getCommandTester(PatchEnableCommand::class); diff --git a/tests/PatchListCommandTest.php b/tests/PatchListCommandTest.php index 4cd94a4..649c468 100644 --- a/tests/PatchListCommandTest.php +++ b/tests/PatchListCommandTest.php @@ -14,7 +14,7 @@ class PatchListCommandTest extends PatchCommandTestBase { */ public function testListAll() { $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $json['extra']['patches'] = [ + $json['extra']['composer-patches']['patches'] = [ 'vendor/package1' => [ 'Fix 1' => 'https://example.com/fix1.patch', ], @@ -41,7 +41,7 @@ public function testListAll() { */ public function testListPackage() { $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $json['extra']['patches'] = [ + $json['extra']['composer-patches']['patches'] = [ 'vendor/package1' => [ 'Fix 1' => 'https://example.com/fix1.patch', ], diff --git a/tests/PatchMoveToLocalCommandTest.php b/tests/PatchMoveToLocalCommandTest.php index 16a8e29..6846f4e 100644 --- a/tests/PatchMoveToLocalCommandTest.php +++ b/tests/PatchMoveToLocalCommandTest.php @@ -26,7 +26,7 @@ public function testMoveToLocal() { // Set up composer.json with this remote patch. $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $json['extra']['patches'] = [ + $json['extra']['composer-patches']['patches'] = [ 'vendor/package' => [ 'Fix something' => $url, ], @@ -42,7 +42,7 @@ public function testMoveToLocal() { ]); $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $newPath = $json['extra']['patches']['vendor/package']['Fix something']; + $newPath = $json['extra']['composer-patches']['patches']['vendor/package']['Fix something']; $this->assertStringContainsString('patches/fix-something-1.diff', $newPath); $this->assertFileExists($this->tempDir . '/' . $newPath); @@ -55,8 +55,8 @@ public function testMoveToLocal() { public function testMoveToLocalExternalFile() { $patchesFile = 'patches.json'; $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $json['extra']['patches-file'] = $patchesFile; - unset($json['extra']['patches']); + $json['extra']['composer-patches']['patches-file'] = $patchesFile; + unset($json['extra']['composer-patches']['patches']); file_put_contents($this->composerJsonPath, json_encode($json)); $remoteDir = $this->tempDir . '/remote/-/merge_requests'; @@ -96,7 +96,7 @@ public function testNoPatchesModified() { $url = 'https://example.com/regular.patch'; // Does not contain 'merge_requests/' $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $json['extra']['patches'] = [ + $json['extra']['composer-patches']['patches'] = [ 'vendor/package' => [ 'Regular patch' => $url, ], @@ -110,6 +110,91 @@ public function testNoPatchesModified() { ]); $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $this->assertEquals($url, $json['extra']['patches']['vendor/package']['Regular patch']); + $this->assertEquals($url, $json['extra']['composer-patches']['patches']['vendor/package']['Regular patch']); + } + + /** + * Tests moving a remote patch to local using Composer Patches 1 style. + */ + public function testMoveToLocalComposerPatches1() { + // Create a "remote" patch file. + $remoteDir = $this->tempDir . '/remote/-/merge_requests'; + mkdir($remoteDir, 0777, TRUE); + $patchFile = $remoteDir . '/1.diff'; + file_put_contents($patchFile, 'patch content'); + + $url = 'file://' . $patchFile; + + // Set up composer.json with CP1 style patches. + $json = [ + 'name' => 'test/project', + 'extra' => [ + 'patches' => [ + 'vendor/package' => [ + 'Fix bug with Composer Patches 1' => $url, + ], + ], + ], + ]; + file_put_contents($this->composerJsonPath, json_encode($json)); + + $tester = $this->getCommandTester(PatchMoveToLocalCommand::class); + + $localDir = 'patches'; + $tester->execute([ + 'directory' => $localDir, + ]); + + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $newPath = $json['extra']['patches']['vendor/package']['Fix bug with Composer Patches 1']; + + $this->assertStringContainsString('patches/fix-bug-with-composer-patches-1-1.diff', $newPath); + $this->assertFileExists($this->tempDir . '/' . $newPath); + } + + /** + * Tests moving remote patches when using an external patches file with Composer Patches 1 style. + */ + public function testMoveToLocalExternalFileComposerPatches1() { + $patchesFile = 'patches.json'; + $json = [ + 'name' => 'test/project', + 'require' => [ + 'cweagans/composer-patches' => '1.7.3' + ], + 'extra' => [ + 'patches-file' => $patchesFile, + ] + ]; + file_put_contents($this->composerJsonPath, json_encode($json)); + + $remoteDir = $this->tempDir . '/remote/-/merge_requests'; + if (!is_dir($remoteDir)) { + mkdir($remoteDir, 0777, TRUE); + } + $patchFile = $remoteDir . '/2.diff'; + file_put_contents($patchFile, 'external patch content'); + $url = 'file://' . $patchFile; + + file_put_contents($this->tempDir . '/' . $patchesFile, json_encode([ + 'patches' => [ + 'vendor/package' => [ + 'External fix' => $url, + ], + ], + ])); + + $tester = $this->getCommandTester(PatchMoveToLocalCommand::class); + + $tester->execute([ + 'directory' => 'local_patches', + ]); + + $patchesJson = json_decode(file_get_contents($this->tempDir . '/' . $patchesFile), TRUE); + $newPath = $patchesJson['patches']['vendor/package']['External fix']; + + $this->assertStringContainsString('local_patches/external-fix-2.diff', $newPath); + $this->assertFileExists($this->tempDir . '/' . $newPath); + $this->assertEquals('external patch content', file_get_contents($this->tempDir . '/' . $newPath)); } } diff --git a/tests/PatchRemoveCommandTest.php b/tests/PatchRemoveCommandTest.php index 6ca1c84..2aeb3cb 100644 --- a/tests/PatchRemoveCommandTest.php +++ b/tests/PatchRemoveCommandTest.php @@ -13,12 +13,40 @@ class PatchRemoveCommandTest extends PatchCommandTestBase { * Tests that a patch is removed from composer.json. */ public function testPatchIsRemovedFromComposerJson() { + file_put_contents($this->composerJsonPath, json_encode([ + 'name' => 'test/project', + 'extra' => [ + 'composer-patches' => [ + 'patches' => [ + 'vendor/package' => [ + 'Fix bug' => 'path/to/fix.patch', + ], + ], + ], + ], + ])); + + $tester = $this->getCommandTester(PatchRemoveCommand::class); + $tester->execute([ + 'package' => 'vendor/package', + 'description' => 'Fix bug', + ]); + + $this->assertStringContainsString('The patch was successfully removed.', $tester->getDisplay()); + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $this->assertArrayNotHasKey('vendor/package', $json['extra']['composer-patches']['patches']); + } + + /** + * Tests that a patch is removed from composer.json using extra.patches (Composer Patches 1 style). + */ + public function testPatchIsRemovedFromExtraPatches() { file_put_contents($this->composerJsonPath, json_encode([ 'name' => 'test/project', 'extra' => [ 'patches' => [ 'vendor/package' => [ - 'Fix bug' => 'path/to/fix.patch', + 'Fix bug with Composer Patches 1' => 'path/to/fix.patch', ], ], ], @@ -27,7 +55,8 @@ public function testPatchIsRemovedFromComposerJson() { $tester = $this->getCommandTester(PatchRemoveCommand::class); $tester->execute([ 'package' => 'vendor/package', - 'description' => 'Fix bug', + 'description' => 'Fix bug with Composer Patches 1', + '--no-update' => TRUE, ]); $this->assertStringContainsString('The patch was successfully removed.', $tester->getDisplay()); @@ -42,7 +71,7 @@ public function testPatchIsRemovedFromExternalFile() { $patchesFile = $this->tempDir . '/patches.json'; file_put_contents($this->composerJsonPath, json_encode([ 'name' => 'test/project', - 'extra' => ['patches-file' => 'patches.json'], + 'extra' => ['composer-patches' => ['patches-file' => 'patches.json']], ])); file_put_contents($patchesFile, json_encode([ 'patches' => [ @@ -71,7 +100,7 @@ public function testPatchIsRemovedFromExternalFile() { public function testRemoveNonExistentPatchThrowsException() { file_put_contents($this->composerJsonPath, json_encode([ 'name' => 'test/project', - 'extra' => ['patches' => ['vendor/package' => ['Real patch' => 'path/to/patch']]], + 'extra' => ['composer-patches' => ['patches' => ['vendor/package' => ['Real patch' => 'path/to/patch']]]], ])); $tester = $this->getCommandTester(PatchRemoveCommand::class);