From e6f6b44708c0eb43b3dede1269528efb6a7b351b Mon Sep 17 00:00:00 2001 From: Stephan Zeidler Date: Sun, 28 Dec 2025 15:57:11 +0100 Subject: [PATCH 1/5] Issue #35: Add composer-patches 2 support --- composer.json | 2 +- src/Composer/PatchAddCommand.php | 50 +++++------------------- src/Composer/PatchBaseCommand.php | 14 +++---- src/Composer/PatchEnableCommand.php | 10 ++--- src/Composer/PatchMoveToLocalCommand.php | 12 +++++- src/Composer/PatchRemoveCommand.php | 24 ++++++++++-- tests/Fixtures/composer.json | 4 +- tests/PatchAddCommandTest.php | 18 +++++---- tests/PatchEnableCommandTest.php | 10 ++--- tests/PatchListCommandTest.php | 4 +- tests/PatchMoveToLocalCommandTest.php | 12 +++--- tests/PatchRemoveCommandTest.php | 14 ++++--- 12 files changed, 87 insertions(+), 87 deletions(-) diff --git a/composer.json b/composer.json index 211504e..8bf0155 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": "^2.0", "php": "^8.0" }, "require-dev": { diff --git a/src/Composer/PatchAddCommand.php b/src/Composer/PatchAddCommand.php index 59e3790..eb5854a 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 { @@ -78,10 +76,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($this->getPatchType() === self::PATCHTYPE_ROOT) { $manipulator_filename = 'composer.json'; $json_node = 'extra'; - $json_name = 'patches'; + $json_name = 'composer-patches.patches'; } elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { - $manipulator_filename = $extra['patches-file']; + $manipulator_filename = $extra['composer-patches']['patches-file']; $json_node = null; $json_name = 'patches'; } @@ -136,42 +134,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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..80a06dc 100644 --- a/src/Composer/PatchBaseCommand.php +++ b/src/Composer/PatchBaseCommand.php @@ -25,10 +25,10 @@ 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['composer-patches']['patches-file'])) { return self::PATCHTYPE_FILE; } @@ -40,7 +40,7 @@ protected function getPatchType() { * * 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 +49,14 @@ 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['patches']; + $patches = $extra['composer-patches']['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) { @@ -82,8 +83,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..7d1f8a8 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,17 +49,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new \Exception('Patch could not be created.'); } } - $manipulator->addProperty('extra.patches-file', $patches_filename); + $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'])) { + $manipulator->addProperty('extra.composer-patches.patches', []); } } // Enable patching. - $manipulator->addProperty('extra.enable-patching', TRUE); + $test = $manipulator->getContents(); // Store the manipulated JSON file. if (!file_put_contents($composer_filename, $manipulator->getContents())) { diff --git a/src/Composer/PatchMoveToLocalCommand.php b/src/Composer/PatchMoveToLocalCommand.php index 0e46dac..30b7cb7 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; @@ -35,9 +36,9 @@ protected function execute( if ($this->getPatchType() === self::PATCHTYPE_ROOT) { $manipulator_filename = 'composer.json'; $json_node = 'extra'; - $json_name = 'patches'; + $json_name = 'composer-patches.patches'; } elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { - $manipulator_filename = $extra['patches-file']; + $manipulator_filename = $extra['composer-patches']['patches-file']; $json_node = null; $json_name = 'patches'; } else { @@ -137,6 +138,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..dd42ff2 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(); @@ -44,10 +49,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($this->getPatchType() === self::PATCHTYPE_ROOT) { $manipulator_filename = 'composer.json'; $json_node = 'extra'; - $json_name = 'patches'; + $json_name = 'composer-patches.patches'; } elseif ($this->getPatchType() === self::PATCHTYPE_FILE) { - $manipulator_filename = $extra['patches-file']; + $manipulator_filename = $extra['composer-patches']['patches-file']; $json_node = null; $json_name = 'patches'; } @@ -92,6 +97,19 @@ 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); + $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..0ffcd60 100644 --- a/tests/PatchAddCommandTest.php +++ b/tests/PatchAddCommandTest.php @@ -32,11 +32,11 @@ 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']); } /** @@ -47,7 +47,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 +82,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/PatchEnableCommandTest.php b/tests/PatchEnableCommandTest.php index b2a1744..2fce096 100644 --- a/tests/PatchEnableCommandTest.php +++ b/tests/PatchEnableCommandTest.php @@ -21,9 +21,8 @@ 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']); + $this->assertArrayHasKey('patches', $json['extra']['composer-patches']); + $this->assertEquals([], $json['extra']['composer-patches']['patches']); } /** @@ -42,8 +41,7 @@ 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']['patches-file']); + $this->assertEquals($patchesFilename, $json['extra']['composer-patches']['patches-file']); $this->assertFileExists($this->tempDir . '/' . $patchesFilename); $patchesJson = json_decode(file_get_contents($this->tempDir . '/' . $patchesFilename), TRUE); @@ -55,7 +53,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..41f5168 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,6 @@ 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']); } } diff --git a/tests/PatchRemoveCommandTest.php b/tests/PatchRemoveCommandTest.php index 6ca1c84..403a5c8 100644 --- a/tests/PatchRemoveCommandTest.php +++ b/tests/PatchRemoveCommandTest.php @@ -16,9 +16,11 @@ public function testPatchIsRemovedFromComposerJson() { file_put_contents($this->composerJsonPath, json_encode([ 'name' => 'test/project', 'extra' => [ - 'patches' => [ - 'vendor/package' => [ - 'Fix bug' => 'path/to/fix.patch', + 'composer-patches' => [ + 'patches' => [ + 'vendor/package' => [ + 'Fix bug' => 'path/to/fix.patch', + ], ], ], ], @@ -32,7 +34,7 @@ public function testPatchIsRemovedFromComposerJson() { $this->assertStringContainsString('The patch was successfully removed.', $tester->getDisplay()); $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); - $this->assertArrayNotHasKey('vendor/package', $json['extra']['patches']); + $this->assertArrayNotHasKey('vendor/package', $json['extra']['composer-patches']['patches']); } /** @@ -42,7 +44,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 +73,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); From 15e9f6736384d22e6178ab4a3c8f1dd813f3f973 Mon Sep 17 00:00:00 2001 From: Stephan Zeidler Date: Sun, 28 Dec 2025 16:25:45 +0100 Subject: [PATCH 2/5] Try to support composer-patches 1 and 2 at the same time --- .github/workflows/ci.yml | 8 ++---- composer.json | 2 +- src/Composer/PatchAddCommand.php | 5 ++++ src/Composer/PatchBaseCommand.php | 43 +++++++++++++++++++++++++++++ src/Composer/PatchEnableCommand.php | 8 +++++- src/Composer/PatchRemoveCommand.php | 7 ++++- tests/PatchAddCommandTest.php | 40 +++++++++++++++++++++++++++ tests/PatchCommandTestBase.php | 13 +++++++-- tests/PatchEnableCommandTest.php | 37 +++++++++++++++++++++++-- tests/PatchRemoveCommandTest.php | 27 ++++++++++++++++++ 10 files changed, 176 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f6bcdd..6e4ff13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,7 @@ jobs: matrix: php: ['8.2', '8.3', '8.4'] symfony: ['^7.0', '^8.0'] - exclude: - - php: '8.2' - symfony: '^8.0' - - php: '8.3' - symfony: '^8.0' + composer-patches: ['^1.7', '^2.0'] steps: # Checkout the code @@ -32,7 +28,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 8bf0155..7b04c85 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "license": "MIT", "require": { "composer-plugin-api": "^2.0", - "cweagans/composer-patches": "^2.0", + "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 eb5854a..64fa94b 100644 --- a/src/Composer/PatchAddCommand.php +++ b/src/Composer/PatchAddCommand.php @@ -78,6 +78,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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; diff --git a/src/Composer/PatchBaseCommand.php b/src/Composer/PatchBaseCommand.php index 80a06dc..5cd61b0 100644 --- a/src/Composer/PatchBaseCommand.php +++ b/src/Composer/PatchBaseCommand.php @@ -4,11 +4,13 @@ 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; protected function configure(): void { parent::configure(); @@ -28,6 +30,9 @@ protected function getPatchType() { if (isset($extra['composer-patches']['patches'])) { return self::PATCHTYPE_ROOT; } + elseif (isset($extra['patches'])) { + return self::PATCHTYPE_ROOT_CP1; + } elseif (isset($extra['composer-patches']['patches-file'])) { return self::PATCHTYPE_FILE; } @@ -35,6 +40,39 @@ protected function getPatchType() { 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; + } + /** * Get the patches from root composer or external file * @@ -52,6 +90,11 @@ protected function grabPatches() { $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.'); diff --git a/src/Composer/PatchEnableCommand.php b/src/Composer/PatchEnableCommand.php index 7d1f8a8..05facbd 100644 --- a/src/Composer/PatchEnableCommand.php +++ b/src/Composer/PatchEnableCommand.php @@ -54,7 +54,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int else { // Create an empty patches definition in the root composer.json. if (!isset($extra['patches']) && !isset($extra['composer-patches']['patches'])) { - $manipulator->addProperty('extra.composer-patches.patches', []); + $version = $this->getComposerPatchesVersion(); + if ($version && version_compare($version, '2.0.0', '<')) { + $manipulator->addProperty('extra.patches', []); + } + else { + $manipulator->addProperty('extra.composer-patches.patches', []); + } } } diff --git a/src/Composer/PatchRemoveCommand.php b/src/Composer/PatchRemoveCommand.php index dd42ff2..f2256e1 100644 --- a/src/Composer/PatchRemoveCommand.php +++ b/src/Composer/PatchRemoveCommand.php @@ -51,6 +51,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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; @@ -82,7 +87,7 @@ 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) { diff --git a/tests/PatchAddCommandTest.php b/tests/PatchAddCommandTest.php index 0ffcd60..cc50151 100644 --- a/tests/PatchAddCommandTest.php +++ b/tests/PatchAddCommandTest.php @@ -39,6 +39,46 @@ public function testPatchIsAddedToComposerJson() { $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']); + } + /** * Tests that a patch is added to an external patches file. */ 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 2fce096..1da5fe3 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,11 +11,20 @@ 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(); @@ -25,6 +35,29 @@ public function testEnableBasic() { $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([]); + + $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']); + $this->assertEquals([], $json['extra']['patches']); + } + /** * Tests that patching is enabled with an external file. */ diff --git a/tests/PatchRemoveCommandTest.php b/tests/PatchRemoveCommandTest.php index 403a5c8..2aeb3cb 100644 --- a/tests/PatchRemoveCommandTest.php +++ b/tests/PatchRemoveCommandTest.php @@ -37,6 +37,33 @@ public function testPatchIsRemovedFromComposerJson() { $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 with Composer Patches 1' => 'path/to/fix.patch', + ], + ], + ], + ])); + + $tester = $this->getCommandTester(PatchRemoveCommand::class); + $tester->execute([ + 'package' => 'vendor/package', + 'description' => 'Fix bug with Composer Patches 1', + '--no-update' => TRUE, + ]); + + $this->assertStringContainsString('The patch was successfully removed.', $tester->getDisplay()); + $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); + $this->assertArrayNotHasKey('vendor/package', $json['extra']['patches']); + } + /** * Tests that a patch is removed from an external patches file. */ From 9343df8bb3ef1c9c79fda220036ce6a46ba363b9 Mon Sep 17 00:00:00 2001 From: Stephan Zeidler Date: Sun, 28 Dec 2025 16:29:06 +0100 Subject: [PATCH 3/5] Fix Symfony / PHP compatibility in testing jobs --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e4ff13..90f0d48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,11 @@ jobs: 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' + - php: '8.3' + symfony: '^8.0' steps: # Checkout the code From daef988fda5ee8db886e8471a8627c79ebad3354 Mon Sep 17 00:00:00 2001 From: Stephan Zeidler Date: Mon, 29 Dec 2025 15:08:22 +0100 Subject: [PATCH 4/5] Give the matrix a proper name so that we easily can see what is tested --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90f0d48..6df48c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: jobs: test: + name: PHP ${{ matrix.php }} / Symfony ${{ matrix.symfony }} / Composer Patches ${{ matrix.composer-patches }} runs-on: ubuntu-latest strategy: matrix: From 7af94f53f959cb20e52fbc11e32c0545c4da2214 Mon Sep 17 00:00:00 2001 From: Stephan Zeidler Date: Mon, 29 Dec 2025 15:22:13 +0100 Subject: [PATCH 5/5] Add a Composer Patches 1 patch file type to simplify logic --- src/Composer/PatchAddCommand.php | 7 +- src/Composer/PatchBaseCommand.php | 50 ++++++++++++++ src/Composer/PatchEnableCommand.php | 14 ++-- src/Composer/PatchMoveToLocalCommand.php | 8 +++ src/Composer/PatchRemoveCommand.php | 9 ++- tests/PatchEnableCommandTest.php | 28 ++++++++ tests/PatchMoveToLocalCommandTest.php | 85 ++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 9 deletions(-) diff --git a/src/Composer/PatchAddCommand.php b/src/Composer/PatchAddCommand.php index 64fa94b..fc3b7d3 100644 --- a/src/Composer/PatchAddCommand.php +++ b/src/Composer/PatchAddCommand.php @@ -88,6 +88,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $json_node = null; $json_name = 'patches'; } + elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) { + $manipulator_filename = $extra['patches-file']; + $json_node = null; + $json_name = 'patches'; + } else { throw new \Exception('Composer patches seems to be not enabled. Please enable composer patches first.'); } @@ -134,7 +139,7 @@ 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.'); diff --git a/src/Composer/PatchBaseCommand.php b/src/Composer/PatchBaseCommand.php index 5cd61b0..1e89caf 100644 --- a/src/Composer/PatchBaseCommand.php +++ b/src/Composer/PatchBaseCommand.php @@ -11,6 +11,7 @@ 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(); @@ -36,6 +37,9 @@ protected function getPatchType() { elseif (isset($extra['composer-patches']['patches-file'])) { return self::PATCHTYPE_FILE; } + elseif (isset($extra['patches-file'])) { + return self::PATCHTYPE_FILE_CP1; + } return NULL; } @@ -73,6 +77,16 @@ protected function getComposerPatchesVersion() { 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 * @@ -132,6 +146,42 @@ protected function grabPatches() { 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) { + 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'); + } + } else { return []; } diff --git a/src/Composer/PatchEnableCommand.php b/src/Composer/PatchEnableCommand.php index 05facbd..3c0d124 100644 --- a/src/Composer/PatchEnableCommand.php +++ b/src/Composer/PatchEnableCommand.php @@ -49,13 +49,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new \Exception('Patch could not be created.'); } } - $manipulator->addProperty('extra.composer-patches.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']) && !isset($extra['composer-patches']['patches'])) { - $version = $this->getComposerPatchesVersion(); - if ($version && version_compare($version, '2.0.0', '<')) { + if ($this->isComposerPatches1()) { $manipulator->addProperty('extra.patches', []); } else { @@ -64,9 +69,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - // Enable patching. - $test = $manipulator->getContents(); - // 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 30b7cb7..d459cce 100644 --- a/src/Composer/PatchMoveToLocalCommand.php +++ b/src/Composer/PatchMoveToLocalCommand.php @@ -37,10 +37,18 @@ protected function execute( $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'; } else { throw new \Exception( 'Composer patches seems to be not enabled. Please enable composer patches first.' diff --git a/src/Composer/PatchRemoveCommand.php b/src/Composer/PatchRemoveCommand.php index f2256e1..0ba3d80 100644 --- a/src/Composer/PatchRemoveCommand.php +++ b/src/Composer/PatchRemoveCommand.php @@ -61,6 +61,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $json_node = null; $json_name = 'patches'; } + elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) { + $manipulator_filename = $extra['patches-file']; + $json_node = null; + $json_name = 'patches'; + } else { throw new \Exception('Composer patches seems to be not enabled. Please enable composer patches first.'); } @@ -90,14 +95,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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.'); diff --git a/tests/PatchEnableCommandTest.php b/tests/PatchEnableCommandTest.php index 1da5fe3..91b9fc0 100644 --- a/tests/PatchEnableCommandTest.php +++ b/tests/PatchEnableCommandTest.php @@ -81,6 +81,34 @@ public function testEnableWithExternalFile() { $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); + + $patchesJson = json_decode(file_get_contents($this->tempDir . '/' . $patchesFilename), TRUE); + $this->assertArrayHasKey('patches', $patchesJson); + } + /** * Tests that it fails if patches-file is already defined. */ diff --git a/tests/PatchMoveToLocalCommandTest.php b/tests/PatchMoveToLocalCommandTest.php index 41f5168..6846f4e 100644 --- a/tests/PatchMoveToLocalCommandTest.php +++ b/tests/PatchMoveToLocalCommandTest.php @@ -112,4 +112,89 @@ public function testNoPatchesModified() { $json = json_decode(file_get_contents($this->composerJsonPath), TRUE); $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)); + } }