Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnPhpunitDeprecations="true">
<testsuites>
<testsuite name="Tests">
Expand Down
23 changes: 3 additions & 20 deletions src/Composer/PatchAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,32 +146,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('The patch was successfully added.');

if (!$input->getOption('no-update')) {
$application = $this->getApplication();
$application->setAutoExit(FALSE);

if ($this->isComposerPatches1()) {
// Trigger install command after adding a patch.
$install = Installer::create($this->getIO(), $this->requireComposer());
$install->setUpdate(TRUE)
// Forward the option
->setVerbose($input->getOption('verbose'))
// Only update the current package
->setUpdateAllowList([$package])
// Don't update the dependencies of the patched package.
->setUpdateAllowTransitiveDependencies(Request::UPDATE_ONLY_LISTED)
// Patches are always considered to be applied in "dev mode".
// This is also required to prevent composer from removing all installed
// dev dependencies.
->setDevMode($updateDevMode)
->run();
$this->runReinstall($package, $updateDevMode);
}
else {
$output->writeln('<info>Relocking patches...</info>');
$application->run(new ArrayInput(['command' => 'patches-relock']), $output);
$this->runPatchesRelock();
$output->writeln('<info>Repatching dependencies...</info>');
$application->run(new ArrayInput(['command' => 'patches-repatch']), $output);
$output->writeln('<info>Reinstalling package...</info>');
$application->run(new ArrayInput(['command' => 'reinstall', 'packages' => [$package]]), $output);
$this->runRepatch();
}
}

Expand Down
92 changes: 92 additions & 0 deletions src/Composer/PatchBaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

namespace szeidler\ComposerPatchesCLI\Composer;

use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\Factory;
use Composer\Command\BaseCommand;
use Composer\Json\JsonFile;
use Composer\Semver\Comparator;
use Composer\Installer;
use Composer\DependencyResolver\Request;
use cweagans\Composer\Plugin\Patches;

class PatchBaseCommand extends BaseCommand {

Expand Down Expand Up @@ -87,6 +92,93 @@ protected function isComposerPatches1() {
return $version && version_compare($version, '2.0.0', '<');
}

/**
* Get the Patches plugin instance.
*/
protected function getPatchesPluginInstance() {
foreach ($this->requireComposer()->getPluginManager()->getPlugins() as $plugin) {
$className = get_class($plugin);
if (str_starts_with($className, 'cweagans\Composer\Plugin\Patches')) {
return $plugin;
}
}
return NULL;
}

/**
* Run the patches-relock command.
*/
protected function runPatchesRelock(): void {
$plugin = $this->getPatchesPluginInstance();
if ($plugin) {
if (file_exists($plugin->getLockFile()->getPath())) {
unlink($plugin->getLockFile()->getPath());
}
$plugin->createNewPatchesLock();
}
}

/**
* Run the patches-repatch command.
*/
protected function runRepatch(): void {
$plugin = $this->getPatchesPluginInstance();
if ($plugin) {
$plugin->loadLockedPatches();
$patchCollection = $plugin->getPatchCollection();
if ($patchCollection) {
$localRepository = $this->requireComposer()
->getRepositoryManager()
->getLocalRepository();

$patched_packages = $patchCollection->getPatchedPackages();
$packages = array_filter($localRepository->getPackages(), function ($val) use ($patched_packages) {
return in_array($val->getName(), $patched_packages);
});

$promises = [];
foreach ($packages as $package) {
$uninstallOperation = new UninstallOperation($package);
$promises[] = $this->requireComposer()
->getInstallationManager()
->uninstall($localRepository, $uninstallOperation);
}

$promises = array_filter($promises);
if (!empty($promises)) {
$this->requireComposer()->getLoop()->wait($promises);
}

$install = Installer::create($this->getIO(), $this->requireComposer());
$install->run();
}
}
}

/**
* Run the reinstall command for a package.
*
* @param string $package
* @param bool $devMode
*/
protected function runReinstall(string $package, bool $devMode = TRUE): void {
$install = Installer::create($this->getIO(), $this->requireComposer());
$install->setUpdate(TRUE)
->setUpdateAllowList([$package])
->setUpdateAllowTransitiveDependencies(Request::UPDATE_ONLY_LISTED)
->setDevMode($devMode)
->run();
}

/**
* Updates the lock file hash.
*/
protected function updateLockFile(): void {
$composerJsonPath = Factory::getComposerFile();
$composerJson = new JsonFile($composerJsonPath);
$this->requireComposer()->getLocker()->updateHash($composerJson);
}

/**
* Get the patches from root composer or external file
*
Expand Down
1 change: 1 addition & 0 deletions src/Composer/PatchEnableCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$patches_file = new JsonFile($patches_filename);
if (!$patches_file->exists()) {
if (copy(dirname(__FILE__) . '/../Fixtures/composer.patches.json', $patches_filename)) {
print_r(dirname(__FILE__));
$output->writeln('The composer patches file was created.');
}
else {
Expand Down
112 changes: 50 additions & 62 deletions src/Composer/PatchMigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,57 +39,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int

if ($patchType === self::PATCHTYPE_ROOT_CP1) {
$output->writeln('<info>Migrating patches from root composer.json...</info>');

// Move patches to the new location.
$composer_manipulator->removeSubNode('extra', 'patches');
$composer_manipulator->addSubNode('extra', 'composer-patches.patches', $patches);

// Handle patches-ignore -> ignore-dependency-patches
if (isset($extra['patches-ignore'])) {
$ignored = [];
foreach ($extra['patches-ignore'] as $package => $package_patches) {
// CP1 patches-ignore format is slightly different, but often it was just a list of patches.
// "patches-ignore": { "source/package": { "target/package": { "description": "url" } } }
// CP2 ignore-dependency-patches is just a list of packages whose patches should be ignored.
// "ignore-dependency-patches": ["some/package"]
$ignored[] = $package;
}
$composer_manipulator->removeSubNode('extra', 'patches-ignore');
$composer_manipulator->addSubNode('extra', 'composer-patches.ignore-dependency-patches', array_unique($ignored));
}

// Handle patchLevel -> package-depths
if (isset($extra['patchLevel'])) {
$depths = [];
foreach ($extra['patchLevel'] as $package => $level) {
// Convert -p1 to 1
$depths[$package] = (int) str_replace('-p', '', $level);
}
$composer_manipulator->removeSubNode('extra', 'patchLevel');
$composer_manipulator->addSubNode('extra', 'composer-patches.package-depths', $depths);
}

// Handle composer-exit-on-patch-failure -> exit-on-patch-failure
if (isset($extra['composer-exit-on-patch-failure'])) {
$composer_manipulator->removeSubNode('extra', 'composer-exit-on-patch-failure');
$composer_manipulator->addSubNode('extra', 'composer-patches.exit-on-patch-failure', $extra['composer-exit-on-patch-failure']);
}

// Handle composer-patches-skip-reporting -> skip-reporting
if (isset($extra['composer-patches-skip-reporting'])) {
$composer_manipulator->removeSubNode('extra', 'composer-patches-skip-reporting');
$composer_manipulator->addSubNode('extra', 'composer-patches.skip-reporting', $extra['composer-patches-skip-reporting']);
}

// Handle enable-patching (cleanup)
if (isset($extra['enable-patching'])) {
$composer_manipulator->removeSubNode('extra', 'enable-patching');
}

// Store the manipulated JSON file.
if (!file_put_contents($composer_filename, $composer_manipulator->getContents())) {
throw new \Exception('Composer file could not be saved.');
}
}
elseif ($patchType === self::PATCHTYPE_FILE_CP1) {
$patches_filename = $extra['patches-file'];
Expand All @@ -98,28 +51,63 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// Update composer.json to use the new patches-file location.
$composer_manipulator->removeSubNode('extra', 'patches-file');
$composer_manipulator->addSubNode('extra', 'composer-patches.patches-file', $patches_filename);

if (!file_put_contents($composer_filename, $composer_manipulator->getContents())) {
throw new \Exception('Composer file could not be saved.');
}

// Handle patches-ignore -> ignore-dependency-patches
if (isset($extra['patches-ignore'])) {
$ignored = [];
foreach ($extra['patches-ignore'] as $package => $package_patches) {
// CP1 patches-ignore format is slightly different, but often it was just a list of patches.
// "patches-ignore": { "source/package": { "target/package": { "description": "url" } } }
// CP2 ignore-dependency-patches is just a list of packages whose patches should be ignored.
// "ignore-dependency-patches": ["some/package"]
$ignored[] = $package;
}
$composer_manipulator->removeSubNode('extra', 'patches-ignore');
$composer_manipulator->addSubNode('extra', 'composer-patches.ignore-dependency-patches', array_unique($ignored));
}

// Handle patchLevel -> package-depths
if (isset($extra['patchLevel'])) {
$depths = [];
foreach ($extra['patchLevel'] as $package => $level) {
// Convert -p1 to 1
$depths[$package] = (int) str_replace('-p', '', $level);
}
$composer_manipulator->removeSubNode('extra', 'patchLevel');
$composer_manipulator->addSubNode('extra', 'composer-patches.package-depths', $depths);
}

// Handle enable-patching, composer-patches-skip-reporting and composer-exit-on-patch-failure (cleanup)
$cleanup_keys = [
'enable-patching',
'composer-patches-skip-reporting',
'composer-exit-on-patch-failure',
];
foreach ($cleanup_keys as $cleanup_key) {
if (isset($extra[$cleanup_key])) {
$composer_manipulator->removeSubNode('extra', $cleanup_key);
}
}

// Update the patches file itself.
$patches_file = new JsonFile($patches_filename);
$patches_manipulator = new JsonManipulator(file_get_contents($patches_file->getPath()));
// CP2 expects patches to be in the root "patches" key of the patches-file, which is the same as CP1.
// So no changes might be needed to the content of the file itself if it only contains "patches".
// However, we should check if there's anything else in there.
// Store the manipulated JSON file.
if (!file_put_contents($composer_filename, $composer_manipulator->getContents())) {
throw new \Exception('Composer file could not be saved.');
}

$output->writeln('Migration completed successfully.');

$application = $this->getApplication();
$application->setAutoExit(FALSE);

$output->writeln(
'<info>Running composer update nothing to refresh lock file...</info>',
);
$this->updateLockFile();
$this->resetComposer();

$output->writeln('<info>Relocking patches...</info>');
$application->run(new ArrayInput(['command' => 'patches-relock']), $output);
$this->runPatchesRelock();

$output->writeln('<info>Repatching dependencies...</info>');
$application->run(new ArrayInput(['command' => 'patches-repatch']), $output);
$this->runRepatch();

return 0;
}
Expand Down
7 changes: 2 additions & 5 deletions src/Composer/PatchMoveToLocalCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,11 @@ protected function execute(
'<info>Remote Composer patches got successfully moved to local files and got updated in the composer.json or composer.patches.json.</info>'
);

$application = $this->getApplication();
$application->setAutoExit(FALSE);

if (!$this->isComposerPatches1()) {
$output->writeln('<info>Relocking patches...</info>');
$application->run(new ArrayInput(['command' => 'patches-relock']), $output);
$this->runPatchesRelock();
$output->writeln('<info>Repatching dependencies...</info>');
$application->run(new ArrayInput(['command' => 'patches-repatch']), $output);
$this->runRepatch();
}
} else {
throw new \Exception(
Expand Down
10 changes: 4 additions & 6 deletions src/Composer/PatchRemoveCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('The patch was successfully removed.');

if (!$input->getOption('no-update')) {
$application = $this->getApplication();
$application->setAutoExit(FALSE);

$updateDevMode = !$input->hasOption('no-dev') || !$input->getOption('no-dev');
if (!$this->isComposerPatches1()) {
$output->writeln('<info>Relocking patches...</info>');
$application->run(new ArrayInput(['command' => 'patches-relock']), $output);
$this->runPatchesRelock();
$output->writeln('<info>Repatching dependencies...</info>');
$application->run(new ArrayInput(['command' => 'patches-repatch']), $output);
$this->runRepatch();
}
$output->writeln('<info>Reinstalling package...</info>');
$application->run(new ArrayInput(['command' => 'reinstall', 'packages' => [$package]]), $output);
$this->runReinstall($package, $updateDevMode);
}

return 0;
Expand Down
2 changes: 1 addition & 1 deletion src/Fixtures/composer.patches.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"patches": {}
}
}
24 changes: 24 additions & 0 deletions tests/Fixtures/composer-migrate-root.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "szeidler/composer-patches-cli-test",
"description": "Test package",
"require": {
"cweagans/composer-patches": "^2.0"
},
"extra": {
"composer-exit-on-patch-failure": true,
"patchLevel": {
"drupal/core": "-p2"
},
"enable-patching": true,
"patches": {
"vendor/package": {
"test patch": "test.patch"
}
}
},
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
}
}
Loading