Skip to main content
Ungathered Thoughts

Convert MR patches to local patches

Many projects use pull or merge requests to collaborate on fixes to software components. Tooling can enable us to apply these patches during build phase, so we can say "Use some/module v1.2.3, but apply the fixes I need from issues #4 and #5 as well".

Collaborating on these changes is great, and it's helpful to be able to pull patchs from patch URLs (Github, Gitlab, Drupal docs) directly when things are in a state of lots of change. You might need to add additional fixes as you go, and tracking that with a local patch copy is a bit of extra work.

It's convenient to use the patch URLs when things are very dynamic, but that adds some risks - other collaborators can push changes which break things unexpectedly, and an external actor could even introduce malicious code to your builds when you're pulling unvalidated code from the internet like that.

So, before things start to "set", you want to turn those URLs into local copies of patches, which aren't mutable by external parties. To that end, here's a quick PHP script to convert remote MR patch/diff URLs to local patches.

This example is hardcoded for a build using cweagans/composer-patches, with a filename of composer.patches.json and merge requests from Drupal's Gitlab, but can be readily adjusted to suit your needs.

#!/usr/bin/env php
<?php

/**
* For remote patches which are at mutable URLs, we make a local copy.
*
* This counters risk of breakage or attack via update to an open MR.
*/

if ($config = json_decode(file_get_contents('composer.patches.json'))) {
foreach ($config->patches as $project => &$patches) {
foreach ($patches as $description => &$url) {
if ($parsed_url = parse_url($url)) {
if (isset($parsed_url['scheme']) && stristr('git.drupalcode.org', $parsed_url['host'])) {
$prefix = preg_replace('#/#', '_', $project);

$filename = preg_replace('/[^0-9a-zA-Z_]/', '-', "{$prefix}-{$description}");
$filename = preg_replace('/-+/', '-', $filename);
$filename = trim($filename, '-');

$filename = "patches/{$filename}.patch";
$cmd = escapeshellcmd("curl -sSo {$filename} {$url}");
if (shell_exec($cmd) == "") {
print "Retrieved {$url} to {$filename}." . PHP_EOL;
$url = $filename;
}
else {
print "Unable to retrieve {$url} for project {$project}." . PHP_EOL;
}
}
// OK to skip; it's not a remote URL.
}
else {
print "Unable to parse patch URL {$url} for project {$project}." . PHP_EOL;
}
}
}
file_put_contents('composer.patches.json', json_encode($config, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
}