#!/bin/perl -w # # patchls - patch listing utility # # Input is one or more patchfiles, output is a list of files to be patched. # # 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. use Getopt::Std; use Text::Wrap qw(wrap $columns); use Text::Tabs qw(expand unexpand); use strict; use vars qw($VERSION); $VERSION = 2.05; sub usage { die qq{ patchls [options] patchfile [ ... ] -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 /). -e Expect patched files to Exist (relative to current directory) Will print warnings for files which don't. Also affects -4 option. 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. -5 like -4 but add "|| exit 1" after each command -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) patchls version $VERSION by Tim Bunce } } $::opt_p = undef; # undef != 0 $::opt_d = 0; $::opt_v = 0; $::opt_m = 0; $::opt_i = 0; $::opt_h = 0; $::opt_l = 0; $::opt_c = 0; $::opt_f = ''; $::opt_e = 0; # special purpose options $::opt_I = 0; $::opt_4 = 0; # output PerForce commands to prepare for patching $::opt_5 = 0; $::opt_M = ''; # like -m but only output these meta items (-M Title) $::opt_W = 70; # set wrap width columns (see Text::Wrap module) $::opt_C = 0; # 'Chip' mode (handle from/tags/article/bug files) undocumented usage unless @ARGV; getopts("mihlvecC45p:f:IM:W:") or usage; $columns = $::opt_W || 9999999; $::opt_m = 1 if $::opt_M; $::opt_4 = 1 if $::opt_5; my @show_meta = split(' ', $::opt_M || 'Title From Msg-ID'); # see get_meta_info() 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', ); sub get_meta_info { my $ls = shift; local($_) = shift; $ls->{From}{$1}=1 if /^From:\s+(.*\S)/i; $ls->{Title}{$1}=1 if /^Subject:\s+(?:Re: )?(.*\S)/i; $ls->{'Msg-ID'}{$1}=1 if /^Message-Id:\s+(.*\S)/i; $ls->{Date}{$1}=1 if /^Date:\s+(.*\S)/i; $ls->{$1}{$2}=1 if $::opt_M && /^([-\w]+):\s+(.*\S)/; } # Style 1: # *** perl-5.004/embed.h Sat May 10 03:39:32 1997 # --- perl-5.004.fixed/embed.h Thu May 29 19:48:46 1997 # *************** # *** 308,313 **** # --- 308,314 ---- # # Style 2: # --- perl5.004001/mg.c Sun Jun 08 12:26:24 1997 # +++ perl5.004-bc/mg.c Sun Jun 08 11:56:08 1997 # @@ -656,9 +656,27 @@ # or (rcs, note the different date format) # --- 1.18 1997/05/23 19:22:04 # +++ ./pod/perlembed.pod 1997/06/03 21:41:38 # # Variation: # Index: embed.h my %ls; my ($in, $prevline, $ls); my $prevtype = ''; my (@removed, @added); my $prologue = 1; # assume prologue till patch or /^exit\b/ seen foreach my $argv (@ARGV) { $in = $argv; unless (open F, "<$in") { warn "Unable to open $in: $!\n"; next; } print "Reading $in...\n" if $::opt_v and @ARGV > 1; $ls = $ls{$in} ||= { is_in => 1, in => $in }; my $type; while () { unless (/^([-+*]{3}) / || /^(Index):/) { # 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/; } get_meta_info($ls, $_) if $::opt_m; 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. 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 add_file($ls, $1); } else { warn "$in $.: parse error (prev $prevtype, type $type)\n$prevline$_"; } } } continue { $prevline = $_; $prevtype = $type; $type = ''; } # special mode for patch sets from Chip if ($::opt_C && $in =~ m:[\\/]patch$:) { my $chip; my $dir; ($dir = $in) =~ s:[\\/]patch$::; if (!$ls->{From} && (open(CHIP,"$dir/article") || open(CHIP,"$dir/bug"))) { get_meta_info($ls, $_) while (); } if (open CHIP,"<$dir/from") { chop($chip = ); $ls->{From} = { $chip => 1 }; } if (open CHIP,"<$dir/tag") { chop($chip = ); $ls->{Title} = { $chip => 1 }; } $ls->{From} = { "Chip Salzenberg" => 1 } unless $ls->{From}; } # 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 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) { my $tail = ($::opt_5) ? "|| exit 1" : ""; print map { "p4 delete $_$tail\n" } @removed if @removed; print map { "p4 add $_$tail\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 { my $edit = ($::opt_e && !-f $_) ? "add " : "edit"; "p4 $edit $_$tail\n" } @patched if @patched; exit 0 unless $::opt_C; } 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; } unless ($::opt_c and $::opt_m) { foreach $ls (@ls) { next unless ($::opt_i) ? $ls->{is_out} : $ls->{is_in}; list_files_by_patch($ls); } } else { my $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}; 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}->{$out} = 1; warn "$out patched but not present\n" if $::opt_e && !-f $out; # 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; $name =~ s:^[^/]+/(.+)$:$1: while $dc-- > 0; } 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:^\./::; } return $name; } sub list_files_by_patch { my($ls, $name) = @_; $name = $ls->{in} unless defined $name; my @meta; if ($::opt_m) { 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|PERL)\]?:?\s*//g; "\"$_\""; } @list; push @list, "#$1" if $::opt_C && $ls->{in} =~ m:\b(\w\d+)/patch$:; } 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 and $name; } # don't print the header unless the file contains something interesting return if !@meta and !$ls->{out}; print("$ls->{in}\n"),return if $::opt_l; # -l = no listing, just names # 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->{out} }; my $v = "@v\n"; print $::opt_m ? " Files: ".my_wrap(""," ",$v) : $v; } sub my_wrap { my $txt = eval { expand(wrap(@_)) }; # die's on long lines! return $txt unless $@; return expand("@_"); } sub categorize_files { my($files, $verb) = @_; my(%c, $refine); 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{LIB} += 10,next if m:^(lib|ext)/:; $c{'CORE'} += 15,next if m:^[^/]+[\._]([chH]|sym|pl)$:; $c{BUILD} += 10,next if m:^[A-Z]+$: or m:^[^/]+\.SH$: or m:^(install|configure|configpm):i; print "Couldn't categorise $_\n" if $::opt_v; $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'; } else { my($c, $v) = %c; $c ||= 'OTHER'; $v ||= 0; print " ".@$files." patches: $c: $v\n" if $verb; return $c; } } sub PATORDER { # PATORDER sort by Chip Salzenberg my ($i, $j); $i = ($a =~ m#^[A-Z]+$#); $j = ($b =~ m#^[A-Z]+$#); return $j - $i if $i != $j; $i = ($a =~ m#configure|hint#i) || ($a =~ m#[S_]H$#); $j = ($b =~ m#configure|hint#i) || ($b =~ m#[S_]H$#); return $j - $i if $i != $j; $i = ($a =~ m#\.pod$#); $j = ($b =~ m#\.pod$#); return $j - $i if $i != $j; $i = ($a =~ m#include/#); $j = ($b =~ m#include/#); return $j - $i if $i != $j; if ((($i = $a) =~ s#/+[^/]*$##) && (($j = $b) =~ s#/+[^/]*$##)) { return $i cmp $j if $i ne $j; } $i = ($a =~ m#\.h$#); $j = ($b =~ m#\.h$#); return $j - $i if $i != $j; return $a cmp $b; }