- Make sure that generated files always come last in the diff.
[rsync-patches.git] / verify-patches
1 #!/usr/bin/perl
2
3 use strict;
4 use Getopt::Long;
5
6 my @generated_files = qw( proto.h configure config.h.in rsync.1 rsyncd.conf.5 );
7
8 my($no_cvs, $failures_only, $minor_updates, $prepare_source);
9
10 &Getopt::Long::Configure('bundling');
11 GetOptions(
12     'no-cvs|n' => \$no_cvs,
13     'failures-only|f' => \$failures_only,
14     'minor-updates|u' => \$minor_updates,
15     'prepare-source|p' => \$prepare_source,
16 ) or &usage;
17
18 my $interesting_fuzz = $minor_updates ? '\d' : '[2-9]';
19
20 chdir('patches') if -d 'patches';
21
22 if (!-f 'verify-patches') {
23     die <<EOT;
24 Please run this script from the root of the rsync dir or
25 from inside the patches subdir.
26 EOT
27 }
28
29 $| = 1;
30 my $CONF_OPTS = '-C';
31
32 my($has_dependencies, @new, @rejects);
33
34 END {
35     &restore_cvsdir;
36     system "rsync -a --del cvsdir/ workdir/" if -d 'cvsdir';
37 };
38
39 my $root;
40 open(IN, '../CVS/Root') or die $!;
41 chomp($root = <IN>);
42 close IN;
43
44 mkdir('tmp', 0777) unless -d 'tmp';
45 chdir('tmp') or die "Unable to chdir to 'tmp'";
46
47 mkdir('workdir') unless -d 'workdir';
48 open(OUT, '>exclude') or die $!;
49 print OUT join("\n", 'CVS', @generated_files), "\n";
50 close OUT;
51
52 unless ($no_cvs) {
53     print "Using CVS to update the tmp/cvsdir copy of the source.\n";
54     system qq|cvs -qd "$root" co -P -d cvsdir rsync|;
55 }
56
57 @ARGV = glob('../*.diff') unless @ARGV;
58
59 DIFF:
60 foreach my $diff (@ARGV) {
61     next unless $diff =~ /\.diff$/;
62     next if $diff =~ /gzip-rsyncable[-_a-z]*\.diff$/;
63     $diff =~ s#^(patches|\.\.)/##;
64
65     my $conf_opts;
66     open(IN, "../$diff") or die $!;
67     while (<IN>) {
68         last if /^--- /;
69         if (/^Depends-On-Patch: (\S+.diff)$/) {
70             my $dep = $1;
71             $has_dependencies = 1;
72             print "\nApplying dependency patch $dep...\n";
73             if (system("patch -d cvsdir -p1 -b -Vt <../$dep") != 0) {
74                 print "Unable to cleanly apply dependency patch -- skipping $diff\n";
75                 system "rm -f cvsdir/*.rej cvsdir/*/*.rej";
76                 &restore_cvsdir;
77                 next DIFF;
78             }
79             sleep(1) if $prepare_source; # Ensure later diffs get later times.
80         }
81         if (!defined($conf_opts) && m#^\s*\./configure( .+)#) {
82             $conf_opts = $1;
83             $conf_opts =~ s/\s+\(.*?\)//;
84         }
85     }
86     close IN;
87     $conf_opts = '' unless defined $conf_opts;
88
89     my $default = apply_patch($diff);
90
91     if ($prepare_source) {
92         print "\nPreparing the source...\n";
93         chdir('workdir') or die $!;
94         system "./prepare-source";
95         chdir('..') or die $!;
96     }
97
98     if ($default =~ s/^D,// || $default eq 'N') {
99         my $def = generate_new_patch($diff);
100         $default = 'U,N' if $default eq 'N' && $def eq 'E';
101         $default = 'N' if !$minor_updates && $default eq 'U,N';
102     }
103
104     PROMPT:
105     while (1) {
106         print "\n----------- $diff ------------\n",
107             "\nFix rejects, Diff create, Edit both diffs, Update patch,\n",
108             "Apply patch again, !(CMD), Build rsync, Next, Quit: [$default] ";
109         my $ans = <STDIN>;
110         chomp $ans;
111         $ans = $default if $ans eq '';
112         while ($ans =~ s/^\s*(!|\w)((?<!!)[^;,]*|[^;]*)[;,]?//) {
113             my $cmd = "\U$1\E";
114             if ($cmd eq '!') {
115                 $cmd = $2 || $ENV{'SHELL'};
116                 chdir('workdir') or die $!;
117                 system $cmd;
118                 chdir('..') or die $!;
119                 $default = 'D';
120                 next;
121             }
122             if ($cmd eq 'A') {
123                 $default = apply_patch($diff);
124                 next;
125             }
126             if ($cmd eq 'B') {
127                 chdir('workdir') or die $!;
128                 system "./prepare-source && ./configure $CONF_OPTS $conf_opts && make";
129                 chdir('..') or die $!;
130                 $default = '!make test';
131                 next;
132             }
133             if ($cmd eq 'D') {
134                 $default = generate_new_patch($diff);
135                 next;
136             }
137             if ($cmd eq 'E') {
138                 chdir('workdir') or die $!;
139                 system "vim -d ../../$diff ../new.patch";
140                 chdir('..') or die $!;
141                 $default = 'U,A,D';
142                 next;
143             }
144             if ($cmd eq 'F') {
145                 chdir('workdir') or die $!;
146                 system "vim @rejects";
147                 chdir('..') or die $!;
148                 $default = 'D,E';
149                 next;
150             }
151             if ($cmd eq 'N') {
152                 last PROMPT;
153             }
154             if ($cmd eq 'Q') {
155                 exit;
156             }
157             if ($cmd eq 'U') {
158                 system "cp -p new.patch ../$diff";
159                 print "\nUpdated $diff from new.patch\n";
160                 $default = 'A';
161                 next;
162             }
163         }
164     }
165
166     &restore_cvsdir;
167 }
168
169 exit;
170
171
172 sub apply_patch
173 {
174     my($diff) = @_;
175
176     undef @new;
177     system "rsync -a --delete --exclude='*~' cvsdir/ workdir/";
178     print "\nApplying patch $diff...\n";
179     undef @rejects;
180     my($saw_failure, $saw_offset, $saw_fuzz);
181     open(IN, "patch -d workdir -p1 --no-backup-if-mismatch <../$diff |") or die $!;
182     while (<IN>) {
183         print $_;
184         chomp;
185         if (s/^patching file //) {
186             push(@new, $_) unless -f "cvsdir/$_";
187         } elsif (s/.* saving rejects to file //) {
188             push(@rejects, $_);
189         } elsif (/^Hunk #\d+ FAILED/) {
190             $saw_failure = 1;
191         } elsif (/^Hunk #\d+ succeeded at \d+( with fuzz $interesting_fuzz)?/o) {
192             $saw_fuzz ||= defined $1;
193             $saw_offset = 1;
194         }
195     }
196     close IN;
197     return 'F,D,E' if $saw_failure;
198     return 'D,E' if $saw_fuzz && !$failures_only;
199     return 'D,U,N' if $saw_offset && !$failures_only;
200     'N';
201 }
202
203 sub filter_diff
204 {
205     my($cmd) = @_;
206     open(IN, '-|', $cmd) or die $!;
207     while (<IN>) {
208         next if /^(diff -|Index: |Only in )/;
209         s#^\Q--- cvsdir/\E([^\t]+).*#--- old/$1#;
210         s#^\Q+++ workdir/\E([^\t]+).*#+++ new/$1#;
211         print OUT $_;
212     }
213     close IN;
214 }
215
216 sub generate_new_patch
217 {
218     my($diff) = @_;
219
220     foreach (@new) {
221         system "touch -r workdir/$_ cvsdir/$_";
222     }
223     open(IN, "../$diff") or die $!;
224     open(OUT, '>new.patch') or die $!;
225     while (<IN>) {
226         last if /^--- /;
227         print OUT $_;
228     }
229     close IN;
230     &filter_diff('diff --exclude-from=exclude -upr cvsdir workdir');
231     if ($prepare_source) {
232         # These are not included in the diff above so that patch will give
233         # generated files a later timestamp than the source files.
234         foreach my $fn (@generated_files) {
235             &filter_diff("diff -up cvsdir/$fn workdir");
236         }
237     }
238     close OUT;
239     foreach (@new) {
240         unlink("cvsdir/$_");
241     }
242     print "\nDiffing... ";
243     if (system("diff ../$diff new.patch >/dev/null") == 0) {
244         print "new patch is identical to old.\n";
245         return 'N';
246     }
247     print "New patch DIFFERS from old.\n";
248     'E';
249 }
250
251 sub restore_cvsdir
252 {
253     return unless $has_dependencies;
254     $has_dependencies = 0;
255
256     chdir('cvsdir') or die $!;
257     foreach (glob('*.~[1-9]~'), glob('*/*.~[1-9]~')) {
258         my $fn;
259         ($fn = $_) =~ s/\.~1~$//;
260         if ($fn eq $_) {
261             unlink($_);
262         } elsif (-r $_) {
263             rename($_, $fn);
264         } else {
265             unlink($_);
266             unlink($fn);
267         }
268     }
269     chdir('..') or die $!;
270 }
271
272 sub usage
273 {
274     die <<EOT;
275 Usage: $0 [OPTS] [DIFF-FILE...]
276  -f, --failures-only   Suggest skipping patches that don't have failing hunks
277  -n, --no-cvs          Don't update tmp/cvsdir at the start of the run
278  -p, --prepare-source  Run ./prepare-source and include generated files in diff
279  -u, --minor-updates   Suggest 'U' for even minor changes in the diff
280 EOT
281 }