Matt's transliterate patch.
[rsync/rsync-patches.git] / verify-patches
index 7100d32..d6d7c3a 100755 (executable)
-#!/bin/sh
-
-[ -d patches ] && cd patches
-
-if [ ! -f verify-patches ]; then
-    echo "Please run this script from the root of the rsync dir"
-    echo "or from inside the patches subdir."
-    exit 1
-fi
-
-root=`cat ../CVS/Root`
-tmpdir=,tmp-for-patch-tests
-
-[ -d $tmpdir ] || mkdir $tmpdir
-cd $tmpdir || exit 1
-
-[ -d workdir ] || mkdir workdir
-echo "Using CVS to update the $tmpdir/cvsdir copy of the source."
-cvs -d "$root" co -d cvsdir rsync
-
-cd workdir || exit 1
-
-if [ -z "$1" ]; then
-    set -- ../../*.diff
-fi
-
-for xx in "${@}"; do
-    case "$xx" in
-    *gzip-rsyncable.diff) continue ;;
-    patches/*) xx=`echo $xx | sed 's:patches:../..:'` ;;
-    */*.diff) ;;
-    *.diff) xx=../../$xx ;;
-    *) continue ;;
-    esac
-    apply=y
-    while : ; do
-       echo -e "\n----------- $xx ------------"
-       if [ $apply = y ]; then
-           rsync -a --delete ../cvsdir/ .
-           patch -p0 <$xx | tee ,patch.output
-           new=''
-           for nn in `sed -n 's/^patching file //p' ,patch.output`; do
-               [ -f ../cvsdir/$nn ] || new="$new $nn"
-           done
-           if grep "^Hunk #[0-9]* FAILED" ,patch.output >/dev/null; then
-               default=F
-           elif grep "^Hunk #[0-9]* succeeded" ,patch.output >/dev/null; then
-               default=E
-           else
-               default=N
-           fi
-           apply=n
-       fi
-       echo -e "\nFix rejects, Edit both diffs, Update patch,"
-       echo -n "Apply patch again, Next, Quit: [$default] "
-       read ans
-       [ -z "$ans" ] && ans=$default
-       case "$ans" in
-       [Ee]*)
-           [ ! -z "$new" ] && cvs add $new
-           new=''
-           rm -f *.rej *.orig */*.rej */*.orig
-           sed '/^--- /,$ d' $xx >,new.patch
-           cvs diff -N | egrep -v '^(diff -|===============|RCS file: |retrieving revision |Index: )' >>,new.patch
-           vim -d $xx ,new.patch
-           default=U
-           ;;
-       [Ff]*)
-           vim `sed -ne 's/.* saving rejects to file //p' ,patch.output`
-           default=E
-           ;;
-       [Uu]*)
-           if [ -f ,new.patch ]; then
-               cp -p ,new.patch $xx
-               echo -e "\nCopied ,new.patch to $xx"
-               default=A
-           else
-               echo -e "\n*** Edit the diffs first. ***"
-               default=E
-           fi
-           ;;
-       [Aa]*)
-           apply=y
-           ;;
-       [Nn]*)
-           break
-           ;;
-       [Qq]*)
-           exit 0
-           ;;
-       esac
-    done
-done
+#!/usr/bin/perl
+
+use strict;
+use Getopt::Long;
+use Cwd;
+
+my @generated_files = qw( proto.h configure config.h.in rsync.1 rsyncd.conf.5 );
+
+my($no_cvs, $failures_only, $minor_updates, $prepare_source);
+my @auto_cmds;
+
+&Getopt::Long::Configure('bundling');
+GetOptions(
+    'no-cvs|n' => \$no_cvs,
+    'failures-only|f' => \$failures_only,
+    'minor-updates|u' => \$minor_updates,
+    'prepare-source|p' => \$prepare_source,
+    'auto-cmd|a=s' => sub { push(@auto_cmds, $_[1]) },
+) or &usage;
+
+$" = '|';
+my $auto_regex = @auto_cmds ? qr/^(@auto_cmds)$/i : qr/^never$/;
+my $interesting_fuzz = $minor_updates ? '\d' : '[2-9]';
+$" = ' ';
+
+chdir('patches') if -d 'patches';
+
+if (!-f 'verify-patches') {
+    die <<EOT;
+Please run this script from the root of the rsync dir or
+from inside the patches subdir.
+EOT
+}
+
+my $patches_dir = Cwd::cwd();
+
+$ENV{'LC_COLLATE'} = 'C';
+$| = 1;
+my $CONF_OPTS = '--cache-file=../config.cache';
+
+my($has_dependencies, @new, @rejects);
+
+END {
+    &restore_cvsdir;
+    system "rsync -a --del cvsdir/ workdir/" if -d 'cvsdir';
+    my $pid = readlink('lock') || 0;
+    unlink('lock') if $pid == $$;
+};
+
+my $root;
+open(IN, '../CVS/Root') or die $!;
+chomp($root = <IN>);
+close IN;
+
+mkdir('tmp', 0777) unless -d 'tmp';
+chdir('tmp') or die "Unable to chdir to 'tmp'";
+unlink('patches');
+symlink($patches_dir, 'patches');
+
+symlink($$, 'lock') or die "Unable to create lock file: $!\n";
+
+mkdir('workdir') unless -d 'workdir';
+open(OUT, '>exclude') or die $!;
+print OUT join("\n", 'CVS', @generated_files), "\n";
+close OUT;
+
+unless ($no_cvs) {
+    print "Using CVS to update the tmp/cvsdir copy of the source.\n";
+    system qq|cvs -qd "$root" co -P -d cvsdir rsync|;
+    @_ = qw( configure configure.in config.h.in configure.in
+            rsync.1 rsync.yo rsyncd.conf.5 rsyncd.conf.yo );
+    while (@_) {
+       my $gen = 'cvsdir/' . shift(@_);
+       my $src = 'cvsdir/' . shift(@_);
+       if ((-M $gen) > (-M $src)) {
+           system "touch $gen";
+       }
+    }
+}
+
+@ARGV = glob('patches/*.diff') unless @ARGV;
+
+DIFF:
+foreach my $diff (@ARGV) {
+    next unless $diff =~ /\.diff$/;
+    next if $diff =~ /gzip-rsyncable[-_a-z]*\.diff$/;
+    $diff =~ s#^(patches|\.\.)/##;
+
+    my $conf_opts;
+    open(IN, "patches/$diff") or die $!;
+    while (<IN>) {
+       last if /^--- /;
+       if (m#^\s+patch -p1 <patches/(\S+.diff)# && $1 ne $diff) {
+           my $dep = $1;
+           $has_dependencies = 1;
+           print "\nApplying dependency patch $dep...\n";
+           if (system("patch -f -d cvsdir -p1 -b -Vt <patches/$dep") != 0) {
+               print "Unable to cleanly apply dependency patch -- skipping $diff\n";
+               system "rm -f cvsdir/*.rej cvsdir/*/*.rej";
+               &restore_cvsdir;
+               next DIFF;
+           }
+           sleep(1); # Ensure later diffs get later times.
+       }
+       if (!defined($conf_opts) && s#^\s+\./configure\s+##) {
+           chomp($conf_opts = $_);
+           $conf_opts =~ s/\s*\(.*?\)//;
+       }
+    }
+    close IN;
+    $conf_opts = '' unless defined $conf_opts;
+
+    my $default = apply_patch($diff);
+
+    if ($prepare_source) {
+       print "\nPreparing the source...\n";
+       chdir('workdir') or die $!;
+       system "./prepare-source";
+       chdir('..') or die $!;
+    }
+
+    if ($default =~ s/^D,// || $default eq 'N') {
+       my $def = generate_new_patch($diff);
+       $default = 'U,N' if $default eq 'N' && $def eq 'E';
+       $default = 'N' if !$minor_updates && $default eq 'U,N';
+    }
+
+    my $first_time = 1;
+    PROMPT:
+    while (1) {
+       print "\n----------- $diff ------------\n",
+           "\nFix rejects, Diff create, Edit both diffs, Update patch,\n",
+           "Apply patch again, !(CMD), Build rsync, Next, Quit: [$default] ";
+       my $ans = $default;
+       if ($first_time && $default =~ /$auto_regex/) {
+           print $default, "\n";
+       } else {
+           my $input = <STDIN>;
+           chomp $input;
+           if ($input =~ s/^(-a|--auto-cmd=?)\s*//) {
+               push(@auto_cmds, $input eq '' ? $default : $input);
+               $" = '|';
+               $auto_regex = qr/^(@auto_cmds)$/i;
+               $" = ' ';
+               next;
+           }
+           $ans = $input if $input ne '';
+       }
+       $first_time = 0;
+       while ($ans =~ s/^\s*(!|\w)((?<!!)[^;,]*|[^;]*)[;,]?//) {
+           my $cmd = "\U$1\E";
+           if ($cmd eq '!') {
+               $cmd = $2 || $ENV{'SHELL'};
+               chdir('workdir') or die $!;
+               system $cmd;
+               chdir('..') or die $!;
+               $default = 'D';
+               next;
+           }
+           if ($cmd eq 'A') {
+               $default = apply_patch($diff);
+               next;
+           }
+           if ($cmd eq 'B') {
+               chdir('workdir') or die $!;
+               my $cmd = "./prepare-source && ./configure $CONF_OPTS $conf_opts && make";
+               print "Running: $cmd\n";
+               system $cmd;
+               chdir('..') or die $!;
+               $default = '!make test';
+               next;
+           }
+           if ($cmd eq 'D') {
+               $default = generate_new_patch($diff);
+               next;
+           }
+           if ($cmd eq 'E') {
+               chdir('workdir') or die $!;
+               system "vim -d ../patches/$diff ../new.patch";
+               chdir('..') or die $!;
+               $default = 'U,A,D';
+               next;
+           }
+           if ($cmd eq 'F') {
+               chdir('workdir') or die $!;
+               system "vim @rejects";
+               chdir('..') or die $!;
+               $default = 'D,E';
+               next;
+           }
+           if ($cmd eq 'N') {
+               last PROMPT;
+           }
+           if ($cmd eq 'Q') {
+               exit;
+           }
+           if ($cmd eq 'U') {
+               system "cp -p new.patch patches/$diff";
+               print "\nUpdated $diff from new.patch\n";
+               $default = 'A';
+               next;
+           }
+       }
+    }
+
+    &restore_cvsdir;
+}
+
+exit;
+
+
+sub apply_patch
+{
+    my($diff) = @_;
+
+    undef @new;
+    system "rsync -a --del --exclude='*~' cvsdir/ workdir/";
+    print "\nApplying patch $diff...\n";
+    undef @rejects;
+    my($saw_offset, $saw_fuzz);
+    open(IN, "patch -f -d workdir -p1 --no-backup-if-mismatch <patches/$diff |") or die $!;
+    while (<IN>) {
+       print $_;
+       chomp;
+       if (s/^patching file //) {
+           push(@new, $_) unless -f "cvsdir/$_";
+       } elsif (s/.* saving rejects to file //) {
+           push(@rejects, $_);
+       } elsif (/No file to patch\.\s+Skipping patch/) {
+           push(@rejects, 'skipped.file');
+       } elsif (/^Hunk #\d+ succeeded at \d+( with fuzz $interesting_fuzz)?/o) {
+           $saw_fuzz ||= defined $1;
+           $saw_offset = 1;
+       }
+    }
+    close IN;
+    return 'F,D,E' if @rejects;
+    return 'D,E' if $saw_fuzz && !$failures_only;
+    return 'D,U,N' if $saw_offset && !$failures_only;
+    'N';
+}
+
+sub filter_diff
+{
+    my($cmd) = @_;
+    open(IN, '-|', $cmd) or die $!;
+    while (<IN>) {
+       next if /^(diff -|Index: |Only in )/;
+       s#^\Q--- cvsdir/\E([^\t]+).*#--- old/$1#;
+       s#^\Q+++ workdir/\E([^\t]+).*#+++ new/$1#;
+       print OUT $_;
+    }
+    close IN;
+}
+
+sub generate_new_patch
+{
+    my($diff) = @_;
+
+    foreach (@new) {
+       system "touch -r workdir/$_ cvsdir/$_";
+    }
+    open(IN, "patches/$diff") or die $!;
+    open(OUT, '>new.patch') or die $!;
+    while (<IN>) {
+       last if /^--- /;
+       print OUT $_;
+    }
+    close IN;
+    &filter_diff('diff --exclude-from=exclude -dupr cvsdir workdir');
+    if ($prepare_source) {
+       # These are not included in the diff above so that patch will give
+       # generated files a later timestamp than the source files.
+       foreach my $fn (@generated_files) {
+           &filter_diff("diff -dupW128 cvsdir/$fn workdir");
+       }
+    }
+    close OUT;
+    foreach (@new) {
+       unlink("cvsdir/$_");
+    }
+    print "\nDiffing... ";
+    if (system("diff patches/$diff new.patch >/dev/null") == 0) {
+       print "new patch is identical to old.\n";
+       return 'N';
+    }
+    print "New patch DIFFERS from old.\n";
+    'E';
+}
+
+sub restore_cvsdir
+{
+    return unless $has_dependencies;
+    $has_dependencies = 0;
+
+    chdir('cvsdir') or die $!;
+    foreach (glob('*.~[1-9]~'), glob('*/*.~[1-9]~')) {
+       my $fn;
+       ($fn = $_) =~ s/\.~1~$//;
+       if ($fn eq $_) {
+           unlink($_);
+       } elsif (-r $_) {
+           rename($_, $fn);
+       } else {
+           unlink($_);
+           unlink($fn);
+       }
+    }
+    chdir('..') or die $!;
+}
+
+sub usage
+{
+    die <<EOT;
+Usage: $0 [OPTS] [DIFF-FILE...]
+ -a, --auto-cmd=REGEX  If default_cmd =~ /^(REGEX)\$/, enter it automatically
+ -f, --failures-only   Suggest skipping patches that don't have failing hunks
+ -n, --no-cvs          Don't update tmp/cvsdir at the start of the run
+ -p, --prepare-source  Run ./prepare-source and include generated files in diff
+ -u, --minor-updates   Suggest 'U' for even minor changes in the diff
+EOT
+}