mirror of
https://github.com/webmin/webmin.git
synced 2026-08-22 23:10:34 +01:00
ⓘ When GNU patch is not installed, git apply was run inside the module directory, and if the Webmin root itself was a git repository, git resolved paths relative to that root and silently skipped the patched files. Limit repository discovery to the current directory and allow reduced context, similar to the default fuzz used by patch, so slightly drifted context lines no longer cause a rejection.
244 lines
5.5 KiB
Perl
Executable File
244 lines
5.5 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
# patch - Apply a patch 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 URL or file
|
|
my $patch = $ARGV[0];
|
|
|
|
# Params check
|
|
if (!$patch) {
|
|
pod2usage(0);
|
|
exit 1;
|
|
}
|
|
|
|
# Patch check
|
|
if ($patch !~ /^https?:\/\//) {
|
|
if (!-r $patch) {
|
|
print "Patch file $patch doesn't exist\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
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";
|
|
exit 1;
|
|
}
|
|
|
|
# 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";
|
|
exit 1;
|
|
}
|
|
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";
|
|
exit 1;
|
|
}
|
|
}
|
|
# Apply patch using patch command
|
|
elsif (has_command('patch')) {
|
|
$output = `$cmd 2>&1 | patch -p1 --verbose 2>&1`;
|
|
if ($output !~ /succeeded/i) {
|
|
print "Patch failed: $output\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
# 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";
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
# 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;
|
|
}
|
|
|
|
# Reload Webmin
|
|
reload_miniserv();
|
|
|
|
=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.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
webmin patch 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
|
|
|
|
=back
|
|
|
|
=head1 LICENSE AND COPYRIGHT
|
|
|
|
Copyright 2024 Ilia Ross <ilia@virtualmin.com>
|