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