mirror of
https://github.com/webmin/webmin.git
synced 2026-08-22 15:00:34 +01:00
ⓘ 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.
289 lines
6.8 KiB
Perl
Executable File
289 lines
6.8 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
# patch - Apply one or more patches to Webmin core or its modules from
|
|
# GitHub or a local file
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use 5.010;
|
|
|
|
use Getopt::Long qw(:config permute pass_through);
|
|
use Pod::Usage;
|
|
use File::Basename;
|
|
use Cwd qw(cwd);
|
|
|
|
my %opt;
|
|
GetOptions(
|
|
'help|h' => \$opt{'help'},
|
|
'config|c=s' => \$opt{'config'},
|
|
);
|
|
pod2usage(0) if ($opt{'help'});
|
|
|
|
# Get Webmin path
|
|
my $path = cwd;
|
|
my $lib = "web-lib-funcs.pl";
|
|
if (!-r "$path/$lib") {
|
|
$path = dirname(dirname($0));
|
|
if (!-r "$path/$lib") {
|
|
$path = $path = Cwd::realpath('..');
|
|
}
|
|
}
|
|
|
|
# Init core
|
|
my $config_dir = $opt{'config'} || '/etc/webmin';
|
|
$ENV{'WEBMIN_CONFIG'} = $config_dir;
|
|
push(@INC, $path);
|
|
eval 'use WebminCore';
|
|
init_config();
|
|
|
|
# Check if curl is installed
|
|
if (!has_command('curl')) {
|
|
print "\"curl\" command is not installed\n";
|
|
exit 1;
|
|
}
|
|
|
|
# Check if git is installed
|
|
if (!has_command('patch')) {
|
|
if (!has_command('git')) {
|
|
print "Neither \"patch\" nor \"git\" commands are installed\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
# Get patch URLs or files
|
|
my @patches = @ARGV;
|
|
|
|
# Params check
|
|
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";
|
|
return 0;
|
|
}
|
|
}
|
|
elsif ($patch =~ /^https?:\/\/(github|gitlab)\.com/ &&
|
|
$patch !~ /\.patch$/ && $patch !~ /\.diff$/) {
|
|
$patch .= '.patch';
|
|
}
|
|
|
|
# Parse module name from URL
|
|
my $module = "";
|
|
if ($patch =~ m{
|
|
(?|
|
|
# GitHub/GitLab commit URL
|
|
https://(?:github|gitlab)\.com/([^/]+)/([^/]+)/commit/([^/]+)
|
|
|
|
|
# GitHub pull request commit URL
|
|
https://github\.com/([^/]+)/([^/]+)/pull/\d+/commits/([^/]+)
|
|
|
|
|
# GitLab merge request URL with commit ID
|
|
https://gitlab\.com/([^/]+)/([^/]+)/-/merge_requests/\d+/diffs\?commit_id=([^&]+)
|
|
|
|
|
# GitHub raw URL
|
|
https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)
|
|
|
|
|
# GitLab raw URL
|
|
https://gitlab\.com/([^/]+)/([^/]+)/-/raw/([^/]+)/(.+)
|
|
)
|
|
}x) {
|
|
$module = $2;
|
|
$module = "" if ($2 eq 'webmin');
|
|
# Special handling for some modules
|
|
$module = $module =~ /^virtualmin-pro$/ ?
|
|
'virtual-server/pro' :
|
|
'virtual-server'
|
|
if $module =~ /^virtualmin-(gpl|pro)$/;
|
|
}
|
|
|
|
# Check if module exists
|
|
if (!-d "$path/$module") {
|
|
print "Module '$module' doesn't exist\n";
|
|
return 0;
|
|
}
|
|
|
|
# Prepare patch command
|
|
my $cmd;
|
|
my $direct;
|
|
my $filename;
|
|
my $dir;
|
|
my $output;
|
|
|
|
# If raw URL given, try to download and just replace the whole file
|
|
if ($patch =~ m{^https?://raw\.githubusercontent\.com/} ||
|
|
$patch =~ m{^https?://gitlab\.com/.*?/-/raw/}) {
|
|
if ($patch =~ m{
|
|
.*? # Non-greedy match of everything up to the branch name
|
|
(?:master|main)/ # Branches name
|
|
(.*/)? # Capture all directories after the branch (group 1)
|
|
([^/]+)$ # Filename (group 2)
|
|
}x)
|
|
{
|
|
$direct = 1;
|
|
$dir = $1 // "";
|
|
$filename = $2;
|
|
}
|
|
else {
|
|
print "Patch failed: Can't parse file name from URL\n";
|
|
return 0;
|
|
}
|
|
my $cd = "$path/$module/$dir";
|
|
$cd =~ s|/+|/|g;
|
|
chdir "$cd";
|
|
$cmd = "curl -w \"%{http_code}\\n\" -s -o $filename @{[quotemeta($patch)]}";
|
|
}
|
|
# Download command
|
|
elsif ($patch =~ /^https?:\/\//) {
|
|
$cmd = "curl -L -s @{[quotemeta($patch)]}";
|
|
chdir "$path/$module";
|
|
}
|
|
# Local file
|
|
else {
|
|
$cmd = "cat @{[quotemeta($patch)]}";
|
|
}
|
|
|
|
# Download file directly
|
|
if ($direct) {
|
|
$output = `$cmd 2>&1`;
|
|
if ($output != 200) {
|
|
print "Patch failed: Cannot download '$filename'. HTTP status code: $output\n";
|
|
return 0;
|
|
}
|
|
}
|
|
# Apply patch using patch command
|
|
elsif (has_command('patch')) {
|
|
# Do not leave .orig backups behind when hunks apply with fuzz or offset
|
|
$output = `$cmd 2>&1 | patch -p1 --verbose --no-backup-if-mismatch 2>&1`;
|
|
if ($output !~ /succeeded/i) {
|
|
print "Patch failed: $output\n";
|
|
return 0;
|
|
}
|
|
}
|
|
# Apply patch using git command
|
|
else {
|
|
# If the current directory is inside some git repository (e.g. Webmin
|
|
# root tracked with git), git would treat patch paths as relative to
|
|
# that repository root and silently skip them, so stop repository
|
|
# discovery at the parent directory to always apply patch paths
|
|
# relative to the current directory
|
|
local $ENV{'GIT_CEILING_DIRECTORIES'} = dirname(cwd());
|
|
# Allow reduced context, similar to the default fuzz used by patch
|
|
$output = `$cmd 2>&1 | git apply --reject --verbose -C1 --whitespace=fix 2>&1`;
|
|
if ($output !~ /applied patch.*?cleanly/i) {
|
|
print "Patch failed: $output\n";
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
# Print results
|
|
if ($direct) {
|
|
print "File replaced successfully:\n";
|
|
if ($dir) {
|
|
$dir = $dir ? "/$dir" : "";
|
|
$dir =~ s|^/||;
|
|
}
|
|
print " $dir$filename\n";
|
|
system("sed -i '1s|^#!/usr/local/bin/perl|#!/usr/bin/perl|' \"$filename\"");
|
|
}
|
|
else {
|
|
print "Patch applied successfully to:\n";
|
|
print " $1\n" while $output =~ /^(?|Applied patch\s+(\S+)|patching file\s+(\S+))/mg;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
=pod
|
|
|
|
=head1 NAME
|
|
|
|
patch
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
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 [patch-url/file ...]
|
|
|
|
=head1 OPTIONS
|
|
|
|
=over
|
|
|
|
=item --help, -h
|
|
|
|
Give this help list.
|
|
|
|
=item --config, -c
|
|
|
|
Specify the full path to the Webmin configuration directory. Defaults to
|
|
C</etc/webmin>
|
|
|
|
Examples of usage:
|
|
|
|
Apply a patch from a URL.
|
|
|
|
- webmin patch https://github.com/webmin/webmin/commit/e6a2bb15b0.patch
|
|
|
|
- webmin patch https://github.com/virtualmin/virtualmin-gpl/commit/f4433153d
|
|
|
|
Apply a patch from local file.
|
|
|
|
- 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
|
|
|
|
Copyright 2024 Ilia Ross <ilia@virtualmin.com>
|