3 # patchls - patch listing utility
5 # Input is one or more patchfiles, output is a list of files to be patched.
7 # Copyright (c) 1997 Tim Bunce. All rights reserved.
8 # This program is free software; you can redistribute it and/or
9 # modify it under the same terms as Perl itself.
11 # With thanks to Tom Horsley for the seed code.
15 use Text::Wrap qw(wrap $columns);
16 use Text::Tabs qw(expand unexpand);
18 use vars qw($VERSION);
24 patchls [options] patchfile [ ... ]
26 -h no filename headers (like grep), only the listing.
27 -l no listing (like grep), only the filename headers.
28 -i Invert: for each patched file list which patch files patch it.
29 -c Categorise the patch and sort by category (perl specific).
30 -m print formatted Meta-information (Subject,From,Msg-ID etc).
31 -p N strip N levels of directory Prefix (like patch), else automatic.
32 -v more verbose (-d for noisy debugging).
33 -n give a count of the number of patches applied to a file if >1.
34 -f F only list patches which patch files matching regexp F
35 (F has \$ appended unless it contains a /).
36 -e Expect patched files to Exist (relative to current directory)
37 Will print warnings for files which don't. Also affects -4 option.
38 - Read patch from STDIN
39 other options for special uses:
40 -I just gather and display summary Information about the patches.
41 -4 write to stdout the PerForce commands to prepare for patching.
42 -5 like -4 but add "|| exit 1" after each command
43 -M T Like -m but only output listed meta tags (eg -M 'Title From')
44 -W N set wrap width to N (defaults to 70, use 0 for no wrap)
45 -X list patchfiles that may clash (i.e. patch the same file)
47 patchls version $VERSION by Tim Bunce
51 $::opt_p = undef; # undef != 0
63 # special purpose options
65 $::opt_4 = 0; # output PerForce commands to prepare for patching
67 $::opt_M = ''; # like -m but only output these meta items (-M Title)
68 $::opt_W = 70; # set wrap width columns (see Text::Wrap module)
69 $::opt_C = 0; # 'Chip' mode (handle from/tags/article/bug files) undocumented
70 $::opt_X = 0; # list patchfiles that patch the same file
74 getopts("dmnihlvecC45Xp:f:IM:W:") or usage;
76 $columns = $::opt_W || 9999999;
78 $::opt_m = 1 if $::opt_M;
79 $::opt_4 = 1 if $::opt_5;
80 $::opt_i = 1 if $::opt_X;
83 my @show_meta = split(' ', $::opt_M || 'Title From Msg-ID Files');
84 my %show_meta = map { ($_,1) } @show_meta;
87 'BUILD' => 'BUILD PROCESS',
88 'CORE' => 'CORE LANGUAGE',
89 'DOC' => 'DOCUMENTATION',
91 'PORT1' => 'PORTABILITY - WIN32',
92 'PORT2' => 'PORTABILITY - GENERAL',
94 'UTIL' => 'UTILITIES',
95 'OTHER' => 'OTHER CHANGES',
96 'EXT' => 'EXTENSIONS',
97 'UNKNOWN' => 'UNKNOWN - NO FILES PATCHED',
104 if (/^From:\s+(.*\S)/i) {;
105 my $from = $1; # temporary measure for Chip Salzenberg
106 $from =~ s/chip\@(atlantic\.net|perlsupport\.com)/chip\@pobox.com/;
107 $from =~ s/\(Tim Bunce\) \(Tim Bunce\)/(Tim Bunce)/;
108 $ls->{From}{$from} = 1
110 if (/^Subject:\s+(?:Re: )?(.*\S)/i) {
112 $title =~ s/\[(PATCH|PERL)[\w\. ]*\]:?//g;
113 $title =~ s/\b(PATCH|PERL)[\w\.]*://g;
114 $title =~ s/\bRe:\s+/ /g;
116 $title =~ s/^\s*(.*?)\s*$/$1/g;
117 $ls->{Title}{$title} = 1;
119 $ls->{'Msg-ID'}{$1}=1 if /^Message-Id:\s+(.*\S)/i;
120 $ls->{Date}{$1}=1 if /^Date:\s+(.*\S)/i;
121 $ls->{$1}{$2}=1 if $::opt_M && /^([-\w]+):\s+(.*\S)/;
126 # *** perl-5.004/embed.h Sat May 10 03:39:32 1997
127 # --- perl-5.004.fixed/embed.h Thu May 29 19:48:46 1997
133 # --- perl5.004001/mg.c Sun Jun 08 12:26:24 1997
134 # +++ perl5.004-bc/mg.c Sun Jun 08 11:56:08 1997
137 # --- perl5.004001/mg.c Sun Jun 08 12:26:24 1997
138 # +++ /dev/null Sun Jun 08 11:56:08 1997
140 # or (rcs, note the different date format)
141 # --- 1.18 1997/05/23 19:22:04
142 # +++ ./pod/perlembed.pod 1997/06/03 21:41:38
153 my (%removed, %added);
154 my $prologue = 1; # assume prologue till patch or /^exit\b/ seen
157 foreach my $argv (@ARGV) {
160 warn "Ignored directory $in\n";
165 } elsif (not open F, "<$in") {
166 warn "Unable to open $in: $!\n";
169 print "Reading $in...\n" if $::opt_v and @ARGV > 1;
170 $ls = $ls{$in} ||= { is_in => 1, in => $in };
173 unless (/^([-+*]{3}) / || /^(Index):/) {
174 # not an interesting patch line
175 # but possibly meta-information or prologue
177 $added{$1} = 1 if /^touch\s+(\S+)/;
178 $removed{$1} = 1 if /^rm\s+(?:-f)?\s*(\S+)/;
179 $prologue = 0 if /^exit\b/;
181 get_meta_info($ls, $_) if $::opt_m;
185 next if /^--- [0-9,]+ ----$/ || /^\*\*\* [0-9,]+ \*\*\*\*$/;
188 print "Last: $prevline","This: ${_}Got: $type\n\n" if $::opt_d;
190 # Some patches have Index lines but not diff headers
191 # Patch copes with this, so must we. It's also handy for
192 # documenting manual changes by simply adding Index: lines
193 # to the file which describes the problem being fixed.
194 if (/^Index:\s+(.*)/) {
196 foreach $f (split(/ /, $1)) { add_patched_file($ls, $f) }
200 if ( ($type eq '---' and $prevtype eq '***') # Style 1
201 or ($type eq '+++' and $prevtype eq '---') # Style 2
203 if (/^[-+*]{3} (\S+)\s*(.*?\d\d:\d\d:\d\d)?/) { # double check
204 if ($1 eq "/dev/null") {
205 $prevline =~ /^[-+*]{3} (\S+)\s*/;
206 add_deleted_file($ls, $1);
209 add_patched_file($ls, $1);
213 warn "$in $.: parse error (prev $prevtype, type $type)\n$prevline$_";
219 $prevtype = $type || '';
223 # special mode for patch sets from Chip
224 if ($in =~ m:[\\/]patch$:) {
227 my $dir; ($dir = $in) =~ s:[\\/]patch$::;
228 if (!$ls->{From} && (open(CHIP,"$dir/article") || open(CHIP,"$dir/bug"))) {
229 get_meta_info($ls, $_) while (<CHIP>);
232 if (open CHIP,"<$dir/from") {
233 chop($chip = <CHIP>);
234 $ls->{From} = { $chip => 1 };
237 if (open CHIP,"<$dir/tag") {
238 chop($chip = <CHIP>);
239 $ls->{Title} = { $chip => 1 };
242 $ls->{From} = { "Chip Salzenberg" => 1 } if $is_chip && !$ls->{From};
245 # if we don't have a title for -m then use the file name
246 $ls->{Title}{"Untitled: $in"}=1 if $::opt_m
247 and !$ls->{Title} and $ls->{out};
249 $ls->{category} = $::opt_c
250 ? categorize_files([keys %{ $ls->{out} }], $::opt_v) : '';
252 print scalar(@ARGV)." files read.\n" if $::opt_v and @ARGV > 1;
255 # --- Firstly we filter and sort as needed ---
259 if ($::opt_f) { # filter out patches based on -f <regexp>
260 $::opt_f .= '$' unless $::opt_f =~ m:/:;
264 my @out = keys %{ $_->{out} };
265 $match=1 if grep { m/$::opt_f/o } @out;
268 $match=1 if $_->{in} =~ m/$::opt_f/o;
275 $a->{category} cmp $b->{category} || $a->{in} cmp $b->{in}
279 # --- Handle special modes ---
282 my $tail = ($::opt_5) ? "|| exit 1" : "";
283 print map { "p4 delete $_$tail\n" } sort keys %removed if %removed;
284 print map { "p4 add $_$tail\n" } sort keys %added if %added;
285 my @patches = sort grep { $_->{is_in} } @ls;
286 my @no_outs = grep { keys %{$_->{out}} == 0 } @patches;
287 warn "Warning: Some files contain no patches:",
288 join("\n\t", '', map { $_->{in} } @no_outs), "\n" if @no_outs;
290 my %patched = map { ($_, 1) } map { keys %{$_->{out}} } @patches;
291 delete @patched{keys %added};
292 my @patched = sort keys %patched;
294 next if $removed{$_};
295 my $edit = ($::opt_e && !-f $_) ? "add " : "edit";
296 print "p4 $edit $_$tail\n";
298 exit 0 unless $::opt_C;
308 next unless $in->{is_in};
310 my @outs = keys %{$in->{out}};
311 push @no_outs, $in unless @outs;
312 @all_out{@outs} = ($in->{in}) x @outs;
314 my @all_out = sort keys %all_out;
315 my @missing = grep { ! -f $_ } @all_out;
316 print "$n_patches patch files patch ".@all_out." files (".@missing." missing)\n";
317 print @no_outs." patch files don't contain patches.\n" if @no_outs;
318 print "(use -v to list patches which patch 'missing' files)\n"
319 if (@missing || @no_outs) && !$::opt_v;
320 if ($::opt_v and @no_outs) {
321 print "Patch files which don't contain patches:\n";
322 foreach $out (@no_outs) {
323 printf " %-20s\n", $out->{in};
326 if ($::opt_v and @missing) {
327 print "Missing files:\n";
328 foreach $out (@missing) {
329 printf " %-20s\t", $out unless $::opt_h;
330 print $all_out{$out} unless $::opt_l;
334 print "Added files: ".join(" ",sort keys %added )."\n" if %added;
335 print "Removed files: ".join(" ",sort keys %removed)."\n" if %removed;
339 unless ($::opt_c and $::opt_m) {
341 next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in};
342 next if $::opt_X and keys %{$ls->{out}} <= 1;
343 list_files_by_patch($ls);
349 next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in};
350 print "\n ------ $cat_title{$ls->{category}} ------\n"
351 if $ls->{category} ne $c;
352 $c = $ls->{category};
354 list_files_by_patch($ls);
358 print "\n$out patched by:\n";
359 # find all the patches which patch $out and list them
360 my @p = grep { $_->{out}->{$out} } values %ls;
362 list_files_by_patch($ls, '');
375 sub add_patched_file {
377 my $raw_name = shift;
378 my $action = shift || 1; # 1==patched, 2==deleted
380 my $out = trim_name($raw_name);
381 print "add_patched_file '$out' ($raw_name, $action)\n" if $::opt_d;
383 $ls->{out}->{$out} = $action;
385 warn "$out patched but not present\n" if $::opt_e && !-f $out;
387 # do the -i inverse as well, even if we're not doing -i
388 my $i = $ls{$out} ||= {
391 category => $::opt_c ? categorize_files([ $out ], $::opt_v) : '',
393 $i->{out}->{$in} = 1;
396 sub add_deleted_file {
398 my $raw_name = shift;
399 my $out = trim_name($raw_name);
400 print "add_deleted_file '$out' ($raw_name)\n" if $::opt_d;
402 #add_patched_file(@_[0,1], 2);
406 sub trim_name { # reduce/tidy file paths from diff lines
408 $name =~ s:\\:/:g; # adjust windows paths
409 $name =~ s://:/:g; # simplify (and make win \\share into absolute path)
410 if ($name eq "/dev/null") {
411 # do nothing (XXX but we need a way to record deletions)
413 elsif (defined $::opt_p) {
414 # strip on -p levels of directory prefix
416 $name =~ s:^[^/]+/(.+)$:$1: while $dc-- > 0;
418 else { # try to strip off leading path to perl directory
419 # if absolute path, strip down to any *perl* directory first
420 $name =~ s:^/.*?perl.*?/::i;
421 $name =~ s:.*(perl|maint)[-_]?5?[._]?[-_a-z0-9.+]*/::i;
428 sub list_files_by_patch {
430 $name = $ls->{in} unless defined $name;
434 foreach $meta (@show_meta) {
435 next unless $ls->{$meta};
436 my @list = sort keys %{$ls->{$meta}};
437 push @meta, sprintf "%7s: ", $meta;
438 if ($meta eq 'Title') {
439 @list = map { "\"$_\""; } @list;
440 push @list, "#$1" if $::opt_C && $ls->{in} =~ m:\b(\w\d+)/patch$:;
442 elsif ($meta eq 'From') {
443 # fix-up bizzare addresses from japan and ibm :-)
446 s/\d\d-\w\w\w-\d{4}\s+\d\d:\S+\s*//;
449 elsif ($meta eq 'Msg-ID') {
450 my %from; # limit long threads to one msg-id per site
452 $from{(/@(.*?)>/ ? $1 : $_)}++ ? () : ($_);
455 push @meta, my_wrap(""," ", join(", ",@list)."\n");
457 $name = "\n$name" if @meta and $name;
459 # don't print the header unless the file contains something interesting
460 return if !@meta and !$ls->{out} and !$::opt_v;
461 if ($::opt_l) { # -l = no listing, just names
463 my $n = keys %{ $ls->{out} };
464 print " ($n patches)" if $::opt_n and $n>1;
469 # a twisty maze of little options
470 my $cat = ($ls->{category} and !$::opt_m) ? "\t$ls->{category}" : "";
471 print "$name$cat: " unless ($::opt_h and !$::opt_v) or !"$name$cat";
473 $sep = "" if @show_meta==1 && $::opt_c && $::opt_h;
474 print join('', $sep, @meta) if @meta;
476 return if $::opt_m && !$show_meta{Files};
477 my @v = sort PATORDER keys %{ $ls->{out} };
480 print $::opt_m ? " Files: ".my_wrap(""," ",$v) : $v;
481 print " ($n patches)" if $::opt_n and $n>1;
487 my $txt = eval { expand(wrap(@_)) }; # die's on long lines!
488 return $txt unless $@;
494 sub categorize_files {
495 my($files, $verb) = @_;
498 foreach (@$files) { # assign a score to a file path
499 # the order of some of the tests is important
500 $c{TEST} += 5,next if m:^t/:;
501 $c{DOC} += 5,next if m:^pod/:;
502 $c{UTIL} += 10,next if m:^(utils|x2p|h2pl)/:;
503 $c{PORT1}+= 15,next if m:^win32:;
505 if m:^(cygwin|os2|plan9|qnx|vms)/:
506 or m:^(hints|Porting|ext/DynaLoader)/:
509 if m:^(ext|lib/ExtUtils)/:;
512 $c{'CORE'} += 15,next
513 if m:^[^/]+[\._]([chH]|sym|pl)$:;
515 if m:^[A-Z]+$: or m:^[^/]+\.SH$:
516 or m:^(install|configure|configpm):i;
517 print "Couldn't categorise $_\n" if $::opt_v;
520 if (keys %c > 1) { # sort to find category with highest score
523 my @c = sort { $c{$b} <=> $c{$a} || $a cmp $b } keys %c;
524 my @v = map { $c{$_} } @c;
525 if (@v > 1 and $refine <= 1 and "@v" =~ /^(\d) \1/
526 and $c[0] =~ m/^(DOC|TESTS|OTHER)/) { # rare
527 print "Tie, promoting $c[1] over $c[0]\n" if $::opt_d;
531 print " ".@$files." patches: ", join(", ", map { "$_: $c{$_}" } @c),".\n"
533 return $c[0] || 'OTHER';
537 $c ||= 'UNKNOWN'; $v ||= 0;
538 print " ".@$files." patches: $c: $v\n" if $verb;
544 sub PATORDER { # PATORDER sort by Chip Salzenberg
547 $i = ($a =~ m#^[A-Z]+$#);
548 $j = ($b =~ m#^[A-Z]+$#);
549 return $j - $i if $i != $j;
551 $i = ($a =~ m#configure|hint#i) || ($a =~ m#[S_]H$#);
552 $j = ($b =~ m#configure|hint#i) || ($b =~ m#[S_]H$#);
553 return $j - $i if $i != $j;
555 $i = ($a =~ m#\.pod$#);
556 $j = ($b =~ m#\.pod$#);
557 return $j - $i if $i != $j;
559 $i = ($a =~ m#include/#);
560 $j = ($b =~ m#include/#);
561 return $j - $i if $i != $j;
563 if ((($i = $a) =~ s#/+[^/]*$##)
564 && (($j = $b) =~ s#/+[^/]*$##)) {
565 return $i cmp $j if $i ne $j;
568 $i = ($a =~ m#\.h$#);
569 $j = ($b =~ m#\.h$#);
570 return $j - $i if $i != $j;