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);