Add support for applying multiple patches with patch command

ⓘ Allow passing several patch URLs or files at once, e.g. webmin patch url1 url2 url3. Patches are applied in the given order, stopping at the first failure, and Webmin is reloaded only once after all of them are applied.
This commit is contained in:
Ilia Ross
2026-08-17 21:43:28 +02:00
parent 77f78cdb75
commit c76fda35ce

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env perl
# patch - Apply a patch to Webmin core or its modules from GitHub or a local file
# patch - Apply one or more patches to Webmin core or its modules from
# GitHub or a local file
use strict;
use warnings;
@@ -49,20 +50,55 @@ if (!has_command('patch')) {
}
}
# Get patch URL or file
my $patch = $ARGV[0];
# Get patch URLs or files
my @patches = @ARGV;
# Params check
if (!$patch) {
if (!@patches) {
pod2usage(0);
exit 1;
}
# Apply all patches in the given order, stopping at the first failure
my $orig_cwd = cwd();
my $applied = 0;
my $failed = 0;
foreach my $patch (@patches) {
# Show which patch is being applied if there are multiple
print "Applying $patch ..\n" if (@patches > 1);
# Always start from the original directory, as applying a patch
# may change it
chdir($orig_cwd);
if (!apply_patch($patch)) {
$failed = 1;
last;
}
$applied++;
}
# Reload Webmin once, if any patches were applied
reload_miniserv() if ($applied);
# Report if some patches were not applied
if ($failed) {
print "Stopped: $applied of ".scalar(@patches)." patches applied\n"
if (@patches > 1);
exit 1;
}
exit 0;
# apply_patch(patch)
# Apply a single patch given as URL or local file, printing the outcome
# and returning 1 on success or 0 on failure
sub apply_patch
{
my ($patch) = @_;
# Patch check
if ($patch !~ /^https?:\/\//) {
if (!-r $patch) {
print "Patch file $patch doesn't exist\n";
exit 1;
return 0;
}
}
elsif ($patch =~ /^https?:\/\/(github|gitlab)\.com/ &&
@@ -101,9 +137,9 @@ if ($patch =~ m{
# Check if module exists
if (!-d "$path/$module") {
print "Module '$module' doesn't exist\n";
exit 1;
}
print "Module '$module' doesn't exist\n";
return 0;
}
# Prepare patch command
my $cmd;
@@ -128,7 +164,7 @@ if ($patch =~ m{^https?://raw\.githubusercontent\.com/} ||
}
else {
print "Patch failed: Can't parse file name from URL\n";
exit 1;
return 0;
}
my $cd = "$path/$module/$dir";
$cd =~ s|/+|/|g;
@@ -149,9 +185,9 @@ else {
if ($direct) {
$output = `$cmd 2>&1`;
if ($output != 200) {
print "Patch failed: Cannot download '$filename'. HTTP status code: $output\n";
exit 1;
}
print "Patch failed: Cannot download '$filename'. HTTP status code: $output\n";
return 0;
}
}
# Apply patch using patch command
elsif (has_command('patch')) {
@@ -159,7 +195,7 @@ elsif (has_command('patch')) {
$output = `$cmd 2>&1 | patch -p1 --verbose --no-backup-if-mismatch 2>&1`;
if ($output !~ /succeeded/i) {
print "Patch failed: $output\n";
exit 1;
return 0;
}
}
# Apply patch using git command
@@ -174,9 +210,9 @@ else {
$output = `$cmd 2>&1 | git apply --reject --verbose -C1 --whitespace=fix 2>&1`;
if ($output !~ /applied patch.*?cleanly/i) {
print "Patch failed: $output\n";
exit 1;
return 0;
}
}
}
# Print results
if ($direct) {
@@ -191,11 +227,10 @@ if ($direct) {
else {
print "Patch applied successfully to:\n";
print " $1\n" while $output =~ /^(?|Applied patch\s+(\S+)|patching file\s+(\S+))/mg;
}
return 1;
}
# Reload Webmin
reload_miniserv();
=pod
=head1 NAME
@@ -207,9 +242,13 @@ patch
Apply a patch to Webmin core or its modules from GitHub/GitLab, a local
file, or by downloading and replacing the entire file from a raw URL.
Multiple patches can be given at once and are applied in the given order,
stopping at the first failure. Webmin is reloaded only once after all
patches are applied.
=head1 SYNOPSIS
webmin patch patch-url/file
webmin patch patch-url/file [patch-url/file ...]
=head1 OPTIONS
@@ -237,6 +276,11 @@ Examples of usage:
- cd /usr/libexec/webmin/virtual-server/pro &&
webmin patch /root/virtualmin-pro/patches/patch-1.patch
Apply multiple patches at once.
- webmin patch https://github.com/webmin/webmin/commit/e6a2bb15b0 \
https://github.com/virtualmin/virtualmin-gpl/commit/f4433153d
=back
=head1 LICENSE AND COPYRIGHT