More Chip patches:
[p5sagit/p5-mst-13.2.git] / Porting / patchls
index e9e902f..1d4bd5a 100644 (file)
@@ -1,20 +1,46 @@
 #!/bin/perl -w
 # 
-# Originally from Tom Horsley. Generally hacked and extended by Tim Bunce.
+#      patchls - patch listing utility
 #
 # Input is one or more patchfiles, output is a list of files to be patched.
 #
-# $Id: patchls,v 1.3 1997/06/10 21:38:45 timbo Exp $
+# Copyright (c) 1997 Tim Bunce. All rights reserved.
+# This program is free software; you can redistribute it and/or
+# modify it under the same terms as Perl itself.
+#
+# With thanks to Tom Horsley for the seed code.
 
-require "getopts.pl";
 
+use Getopt::Std;
 use Text::Wrap qw(wrap $columns);
 use Text::Tabs qw(expand unexpand);
 use strict;
+use vars qw($VERSION);
+
+$VERSION = 2.04;
+
+sub usage {
+die q{
+  patchls [options] patchfile [ ... ]
 
-$columns = 70;
+    -h     no filename headers (like grep), only the listing.
+    -l     no listing (like grep), only the filename headers.
+    -i     Invert: for each patched file list which patch files patch it.
+    -c     Categorise the patch and sort by category (perl specific).
+    -m     print formatted Meta-information (Subject,From,Msg-ID etc).
+    -p N   strip N levels of directory Prefix (like patch), else automatic.
+    -v     more verbose (-d for noisy debugging).
+    -f F   only list patches which patch files matching regexp F
+           (F has $ appended unless it contains a /).
+  other options for special uses:
+    -I     just gather and display summary Information about the patches.
+    -4     write to stdout the PerForce commands to prepare for patching.
+    -M T   Like -m but only output listed meta tags (eg -M 'Title From')
+    -W N   set wrap width to N (defaults to 70, use 0 for no wrap)
+}
+}
 
-$::opt_p = undef;      # like patch -pN, strip off N dir levels from file names
+$::opt_p = undef;      # undef != 0
 $::opt_d = 0;
 $::opt_v = 0;
 $::opt_m = 0;
@@ -22,23 +48,34 @@ $::opt_i = 0;
 $::opt_h = 0;
 $::opt_l = 0;
 $::opt_c = 0;
+$::opt_f = '';
 
-die qq{
+# special purpose options
+$::opt_I = 0;
+$::opt_4 = 0;  # output PerForce commands to prepare for patching
+$::opt_M = ''; # like -m but only output these meta items (-M Title)
+$::opt_W = 70; # set wrap width columns (see Text::Wrap module)
 
-  patchls [options] patchfile [ ... ]
+usage unless @ARGV;
+
+getopts("mihlvc4p:f:IM:W:") or usage;
 
-    -m     print formatted Meta-information (Subject,From,Msg-ID etc)
-    -p N   strip N levels of directory Prefix (like patch), else automatic
-    -i     Invert: for each patched file list which patch files patch it
-    -h     no filename headers (like grep), only the listing
-    -l     no listing (like grep), only the filename headers
-    -c     attempt to Categorise the patch (sort by category with -m)
-    -v     more verbose
-    -d     still more verbosity for debugging
+$columns = $::opt_W || 9999999;
 
-} unless @ARGV;
+$::opt_m = 1 if $::opt_M;
+my @show_meta = split(' ', $::opt_M || 'Title From Msg-ID');
 
-&Getopts("mihlvcp:");
+my %cat_title = (
+    'BUILD'    => 'BUILD PROCESS',
+    'CORE'     => 'CORE LANGUAGE',
+    'DOC'      => 'DOCUMENTATION',
+    'LIB'      => 'LIBRARY AND EXTENSIONS',
+    'PORT1'    => 'PORTABILITY - WIN32',
+    'PORT2'    => 'PORTABILITY - GENERAL',
+    'TEST'     => 'TESTS',
+    'UTIL'     => 'UTILITIES',
+    'OTHER'    => 'OTHER CHANGES',
+);
 
 my %ls;
 
@@ -61,6 +98,8 @@ my %ls;
 #      Index: embed.h
 
 my($in, $prevline, $prevtype, $ls);
+my(@removed, @added);
+my $prologue = 1;      # assume prologue till patch or /^exit\b/ seen
 
 foreach my $argv (@ARGV) {
     $in = $argv;
@@ -69,31 +108,41 @@ foreach my $argv (@ARGV) {
        next;
     }
     print "Reading $in...\n" if $::opt_v and @ARGV > 1;
-    $ls = $ls{$in} ||= { in => $in };
+    $ls = $ls{$in} ||= { is_in => 1, in => $in };
     my $type;
     while (<F>) {
        unless (/^([-+*]{3}) / || /^(Index):/) {
-           # not an interesting patch line but possibly meta-information
+           # not an interesting patch line
+           # but possibly meta-information or prologue
+           if ($prologue) {
+               push @added, $1     if /^touch\s+(\S+)/;
+               push @removed, $1   if /^rm\s+(?:-f)?\s*(\S+)/;
+               $prologue = 0       if /^exit\b/;
+           }
            next unless $::opt_m;
-           $ls->{From}{$1}=1       if /^From: (.*\S)/i;
-           $ls->{Title}{$1}=1      if /^Subject: (?:Re: )?(.*\S)/i;
-           $ls->{'Msg-ID'}{$1}=1   if /^Message-Id: (.*\S)/i;
-           $ls->{Date}{$1}=1       if /^Date: (.*\S)/i;
+           $ls->{From}{$1}=1,next     if /^From:\s+(.*\S)/i;
+           $ls->{Title}{$1}=1,next    if /^Subject:\s+(?:Re: )?(.*\S)/i;
+           $ls->{'Msg-ID'}{$1}=1,next if /^Message-Id:\s+(.*\S)/i;
+           $ls->{Date}{$1}=1,next     if /^Date:\s+(.*\S)/i;
+           $ls->{$1}{$2}=1,next       if /^([-\w]+):\s+(.*\S)/;
            next;
        }
        $type = $1;
        next if /^--- [0-9,]+ ----$/ || /^\*\*\* [0-9,]+ \*\*\*\*$/;
+       $prologue = 0;
 
        print "Last: $prevline","This: ${_}Got:  $1\n\n" if $::opt_d;
 
        # Some patches have Index lines but not diff headers
-       # Patch copes with this, so must we
-       add_file($ls, $1), next if /^Index:\s+(.*)/;
+       # Patch copes with this, so must we. It's also handy for
+       # documenting manual changes by simply adding Index: lines
+       # to the file which describes the problem bing fixed.
+       add_file($ls, $1), next if /^Index:\s+(\S+)/;
 
        if (    ($type eq '---' and $prevtype eq '***') # Style 1
            or  ($type eq '+++' and $prevtype eq '---') # Style 2
        ) {
-           if (/^[-+*]{3} (\S+)\s+.*\d\d:\d\d:\d\d/) { # double check
+           if (/^[-+*]{3} (\S+)\s*(.*?\d\d:\d\d:\d\d)?/) {     # double check
                add_file($ls, $1);
            }
            else {
@@ -106,40 +155,133 @@ foreach my $argv (@ARGV) {
        $prevtype = $type;
        $type = '';
     }
-    $ls->{Title}{$in}=1 if !$ls->{Title} and $::opt_m and $::opt_c
-                               and $ls->{files_by_patch};
-    $ls->{category} = intuit_category($ls, $::opt_v) if $::opt_c;
+    # if we don't have a title for -m then use the file name
+    $ls->{Title}{$in}=1 if $::opt_m
+       and !$ls->{Title} and $ls->{out};
+
+    $ls->{category} = $::opt_c
+       ? categorize_files([keys %{ $ls->{out} }], $::opt_v) : '';
+}
+print scalar(@ARGV)." files read.\n" if $::opt_v and @ARGV > 1;
+
+
+# --- Firstly we filter and sort as needed ---
+
+my @ls  = values %ls;
+
+if ($::opt_f) {                # filter out patches based on -f <regexp>
+    my $out;
+    $::opt_f .= '$' unless $::opt_f =~ m:/:;
+    @ls = grep {
+       my @out = keys %{$_->{out}};
+       my $match = 0;
+       for $out (@out) {
+           ++$match if $out =~ m/$::opt_f/o;
+       }
+       $match;
+    } @ls;
+}
+
+@ls  = sort {
+    $a->{category} cmp $b->{category} || $a->{in} cmp $b->{in}
+} @ls;
+
+
+# --- Handle special modes ---
+
+if ($::opt_4) {
+    print map { "p4 delete $_\n" } @removed if @removed;
+    print map { "p4 add    $_\n" } @added   if @added;
+    my @patches = grep { $_->{is_in} } @ls;
+    my %patched = map { ($_, 1) } map { keys %{$_->{out}} } @patches;
+    delete @patched{@added};
+    my @patched = sort keys %patched;
+    print map { "p4 edit   $_\n" } @patched if @patched;
+    exit 0;
+}
+
+if ($::opt_I) {
+    my $n_patches = 0;
+    my($in,$out);
+    my %all_out;
+    foreach $in (@ls) {
+       next unless $in->{is_in};
+       ++$n_patches;
+       my @outs = keys %{$in->{out}};
+       @all_out{@outs} = ($in->{in}) x @outs;
+    }
+    my @all_out = sort keys %all_out;
+    my @missing = grep { ! -f $_ } @all_out;
+    print "$n_patches patch files patch ".@all_out." files (".@missing." missing)\n";
+    print "(use -v to list patches which patch 'missing' files)\n"
+           if @missing && !$::opt_v;
+    if ($::opt_v and @missing) {
+       print "Missing files:\n";
+       foreach $out (@missing) {
+           printf "  %-20s\t%s\n", $out, $all_out{$out};
+       }
+    }
+    print "Added files:   @added\n"   if @added;
+    print "Removed files: @removed\n" if @removed;
+    exit 0+@missing;
 }
-print "All files read.\n" if $::opt_v and @ARGV > 1;
 
 unless ($::opt_c and $::opt_m) {
-    foreach $in (sort keys %ls) {
-       $ls = $ls{$in};
+    foreach $ls (@ls) {
+       next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in};
        list_files_by_patch($ls);
     }
 }
 else {
     my $c = '';
-    foreach $ls (sort { $a->{category} cmp $b->{category} } values %ls) {
-       print "\n  $ls->{category}\n" if $ls->{category} ne $c;
+    foreach $ls (@ls) {
+       next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in};
+       print "\n  ------  $cat_title{$ls->{category}}  ------\n"
+           if $ls->{category} ne $c;
        $c = $ls->{category};
-       list_files_by_patch($ls);
+       unless ($::opt_i) {
+           list_files_by_patch($ls);
+       }
+       else {
+           my $out = $ls->{in};
+           print "\n$out patched by:\n";
+           # find all the patches which patch $out and list them
+           my @p = grep { $_->{out}->{$out} } values %ls;
+           foreach $ls (@p) {
+               list_files_by_patch($ls, '');
+           }
+       }
     }
     print "\n";
 }
 
+exit 0;
+
+
+# ---
+
 
 sub add_file {
     my $ls = shift;
     my $out = trim_name(shift);
-    ($ls, $out) = ($ls{$out} ||= { in => $out }, $in) if $::opt_i;
-    $ls->{files_by_patch}->{$out} = 1;
+
+    $ls->{out}->{$out} = 1;
+
+    # do the -i inverse as well, even if we're not doing -i
+    my $i = $ls{$out} ||= {
+       is_out   => 1,
+       in       => $out,
+       category => $::opt_c ? categorize_files([ $out ], $::opt_v) : '',
+    };
+    $i->{out}->{$in} = 1;
 }
 
 
 sub trim_name {                # reduce/tidy file paths from diff lines
     my $name = shift;
     $name = "$name ($in)" if $name eq "/dev/null";
+    $name =~ s:\\:/:g; # adjust windows paths
+    $name =~ s://:/:g; # simplify (and make win \\share into absolute path)
     if (defined $::opt_p) {
        # strip on -p levels of directory prefix
        my $dc = $::opt_p;
@@ -148,7 +290,7 @@ sub trim_name {             # reduce/tidy file paths from diff lines
     else {     # try to strip off leading path to perl directory
        # if absolute path, strip down to any *perl* directory first
        $name =~ s:^/.*?perl.*?/::i;
-       $name =~ s:.*perl[-_]?5\.[-_a-z0-9.]+/::i;
+       $name =~ s:.*perl[-_]?5?[._]?[-_a-z0-9.+]*/::i;
        $name =~ s:^\./::;
     }
     return $name;
@@ -156,87 +298,103 @@ sub trim_name {          # reduce/tidy file paths from diff lines
 
 
 sub list_files_by_patch {
-    my $ls = shift;
-    my $name = $ls->{in};
+    my($ls, $name) = @_;
+    $name = $ls->{in} unless defined $name;
     my @meta;
     if ($::opt_m) {
-       foreach(qw(Title From Msg-ID)) {
-           next unless $ls->{$_};
-           my @list = sort keys %{$ls->{$_}};
-           push @meta, sprintf "%7s:  ", $_;
-           @list = map { "\"$_\"" } @list if $_ eq 'Title';
+       my $meta;
+       foreach $meta (@show_meta) {
+           next unless $ls->{$meta};
+           my @list = sort keys %{$ls->{$meta}};
+           push @meta, sprintf "%7s:  ", $meta;
+           if ($meta eq 'Title') {
+               @list = map { s/\[?PATCH\]?:?\s*//g; "\"$_\""; } @list
+           }
+           elsif ($meta eq 'From') {
+               # fix-up bizzare addresses from japan and ibm :-)
+               foreach(@list) {
+                   s:\W+=?iso.*?<: <:;
+                   s/\d\d-\w\w\w-\d{4}\s+\d\d:\S+\s*//;
+               }
+           }
+           elsif ($meta eq 'Msg-ID') {
+               my %from; # limit long threads to one msg-id per site
+               @list = map {
+                   $from{(/@(.*?)>/ ? $1 : $_)}++ ? () : ($_);
+               } @list;
+           }
            push @meta, my_wrap("","          ", join(", ",@list)."\n");
        }
-       $name = "\n$name" if @meta;
+       $name = "\n$name" if @meta and $name;
     }
     # don't print the header unless the file contains something interesting
-    return if !@meta and !$ls->{files_by_patch};
+    return if !@meta and !$ls->{out};
     print("$ls->{in}\n"),return  if $::opt_l;  # -l = no listing
 
-       # a twisty maze of little options
-    my $cat = ($ls->{category} and !$::opt_m) ? " $ls->{category}" : "";
-    print "$name$cat: "        unless $::opt_h and !$::opt_v;
+    # a twisty maze of little options
+    my $cat = ($ls->{category} and !$::opt_m) ? "\t$ls->{category}" : "";
+    print "$name$cat: "        unless ($::opt_h and !$::opt_v) or !"$name$cat";
     print join('',"\n",@meta) if @meta;
 
-    my @v = sort PATORDER keys %{ $ls->{files_by_patch} };
+    my @v = sort PATORDER keys %{ $ls->{out} };
     my $v = "@v\n";
     print $::opt_m ? "  Files:  ".my_wrap("","          ",$v) : $v;
 }
 
 
 sub my_wrap {
-    return expand(wrap(@_));
+       my $txt = eval { expand(wrap(@_)) };    # die's on long lines!
+    return $txt unless $@;
+       return expand("@_");
 }
 
 
 
-# CORE LANGUAGE CHANGES
-# CORE PORTABILITY
-# OTHER CORE CHANGES
-# BUILD PROCESS
-# LIBRARY AND EXTENSIONS
-# TESTS
-# UTILITIES
-# DOCUMENTATION
-
-sub intuit_category {
-    my($ls, $verb) = @_;
-    return 'OTHER' unless $ls->{files_by_patch};
+sub categorize_files {
+    my($files, $verb) = @_;
     my(%c, $refine);
-    foreach (keys %{ $ls->{files_by_patch} }) {
-       ++$c{'DOCUMENTATION'},next
-           if m:^pod/:;
-       ++$c{'UTILITIES'},next
-           if m:^(utils|x2p|h2pl)/:;
-       ++$c{'PORTABILITY'},next
-           if m:^(cygwin32|os2|plan9|qnx|vms|win32)/:
+
+    foreach (@$files) {        # assign a score to a file path
+       # the order of some of the tests is important
+       $c{TEST} += 5,next   if m:^t/:;
+       $c{DOC}  += 5,next   if m:^pod/:;
+       $c{UTIL} += 10,next  if m:^(utils|x2p|h2pl)/:;
+       $c{PORT1}+= 15,next  if m:^win32:;
+       $c{PORT2} += 15,next
+           if m:^(cygwin32|os2|plan9|qnx|vms)/:
            or m:^(hints|Porting|ext/DynaLoader)/:
            or m:^README\.:;
-       ++$c{'LIBRARY AND EXTENSIONS'},next
+       $c{LIB}  += 10,next
            if m:^(lib|ext)/:;
-       ++$c{'TESTS'},next
-           if m:^t/:;
-       ++$c{'CORE LANGUAGE'},next
-           if m:^[^/]+\.([chH]|sym)$:;
-       ++$c{'BUILD PROCESS'},next
+       $c{'CORE'} += 15,next
+           if m:^[^/]+[\._]([chH]|sym|pl)$:;
+       $c{BUILD} += 10,next
            if m:^[A-Z]+$: or m:^[^/]+\.SH$:
-           or m:^(install|configure):i;
+           or m:^(install|configure|configpm):i;
        print "Couldn't categorise $_\n" if $::opt_v;
-       ++$c{OTHER};
+       $c{OTHER} += 1;
+    }
+    if (keys %c > 1) { # sort to find category with highest score
+      refine:
+       ++$refine;
+       my @c = sort { $c{$b} <=> $c{$a} || $a cmp $b } keys %c;
+       my @v = map  { $c{$_} } @c;
+       if (@v > 1 and $refine <= 1 and "@v" =~ /^(\d) \1/
+               and $c[0] =~ m/^(DOC|TESTS|OTHER)/) { # rare
+           print "Tie, promoting $c[1] over $c[0]\n" if $::opt_d;
+           ++$c{$c[1]};
+           goto refine;
+       }
+       print "  ".@$files." patches: ", join(", ", map { "$_: $c{$_}" } @c),".\n"
+           if $verb;
+       return $c[0] || 'OTHER';
     }
-refine:
-    ++$refine;
-    my @c = sort { $c{$b} <=> $c{$a} || $a cmp $b } keys %c;
-    my @v = map  { $c{$_} } @c;
-    if (@v > 1 and $refine <= 1 and "@v" =~ /^(\d) \1/
-           and $c[0] =~ m/^(DOC|TESTS|OTHER)/) {
-       print "Tie, promoting $c[1] over $c[0]\n" if $::opt_d;
-       ++$c{$c[1]};
-       goto refine;
+    else {
+       my($c, $v) = %c;
+       $c ||= 'OTHER'; $v ||= 0;
+       print "  ".@$files." patches: $c: $v\n" if $verb;
+       return $c;
     }
-    print "  ", join(", ", map { "$_: $c{$_}" } @c),".\n"
-       if $verb and @v > 1;
-    return $c[0];
 }