Home | History | Annotate | Download | only in scripts
      1 #!/usr/perl5/bin/perl -w
      2 #
      3 # Script for generating code review pages similar to those generated by
      4 # ON's webrev tool
      5 #
      6 # CDDL HEADER START
      7 #
      8 # The contents of this file are subject to the terms of the
      9 # Common Development and Distribution License, Version 1.0 only
     10 # (the "License").  You may not use this file except in compliance
     11 # with the License.
     12 #
     13 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
     14 # or http://www.opensolaris.org/os/licensing.
     15 # See the License for the specific language governing permissions
     16 # and limitations under the License.
     17 #
     18 # When distributing Covered Code, include this CDDL HEADER in each
     19 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     20 # If applicable, add the following below this CDDL HEADER, with the
     21 # fields enclosed by brackets "[]" replaced with your own identifying
     22 # information: Portions Copyright [yyyy] [name of copyright owner]
     23 #
     24 # CDDL HEADER END
     25 #
     26 #
     27 # Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
     28 # Use is subject to license terms.
     29 #
     30 
     31 # FIXMEs: 
     32 #   - require the target dir to be empty
     33 #   - breaks if you have a new subdir that is not under svn control
     34 #   - should have a way to exclude some or all not-svn-controlled files
     35 
     36 use strict;
     37 use Fcntl;
     38 use File::Basename;
     39 
     40 #
     41 # Usage: webrev /path/to/output/dir
     42 #
     43 # creates the html report of changed in the current svn workspace (current dir)
     44 #
     45 
     46 # FIXME: would be nice to turn these into command line options
     47 # max number of chars in each line, above which lines are wrapped in
     48 # side-by-side diffs
     49 my $SDIFF_MAX_LINE=80;
     50 # number of lines of context in sdiffs
     51 my $SDIFF_CONTEXT=20;
     52 
     53 # Valid @FOO@ tags in the HTML templates:
     54 #
     55 # @TITLE@     - page title
     56 # @AUTHOR@    - real name of the current user according to the passwd entry
     57 # @COPYRIGHT@ - copyright statement (not implemented)
     58 # @UNAME@     - current user name
     59 # @HOSTNAME@  - hostname of the current host as printed by /bin/hostname
     60 # @DATE@      - current date string as printed by /bin/date
     61 #
     62 
     63 # HTML page header template for index.html
     64 my $index_page_header =
     65     '<HTML>\n' .
     66     ' <HEAD>\n' .
     67     '  <TITLE>@TITLE@</TITLE>\n' .
     68     '  <META NAME="author" CONTENT="@AUTHOR@">\n' .
     69     '  <META NAME="generator" CONTENT="webrev for svn">\n' .
     70 #    '  <META NAME="copyright" CONTENT="@COPYRIGHT@">\n' .
     71     ' </HEAD>\n' .
     72     ' <BODY BGCOLOR="#FFFFFF">\n' .
     73     '  <FONT FACE="arial,sans">\n' .
     74     '  <CENTER><FONT SIZE=+1><B>@TITLE@</B></FONT></CENTER><P>\n';
     75 # HTML page footer template for index.html
     76 my $index_page_footer =
     77     '  </FONT>\n' .
     78     '  <HR SIZE=1 NOSHADE>\n' .
     79     '  <FONT FACE="arial,sans" SIZE="-2">\n' .
     80     '   Webrev report generated by @UNAME@@@HOSTNAME@ on @DATE@.\n' .
     81     '  </FONT>\n' .
     82     ' </BODY>\n' .
     83     '</HTML>\n';
     84 
     85 # HTML page header template for the diff pages
     86 my $file_page_header = $index_page_header;
     87 # HTML page footer template for the diff pages
     88 my $file_page_footer = $index_page_footer;
     89 
     90 # Map status to file name
     91 my %file_status;
     92 # Descriptions of file status flags
     93 my %status_desc = (' ', 'No change',
     94 		   'A', 'New',
     95 		   'C', '<FONT COLOR="#FF4444">Conflicted</FONT>',
     96 		   'D', 'Deleted',
     97 		   'G', 'Merged',
     98 		   'I', 'Ignored',
     99 		   'M', 'Modified',
    100 		   'R', 'Replaced',
    101 		   '?', '<FONT COLOR="#FF4444">Not under version control</FONT>',
    102 		   '!', 'Missing');
    103 
    104 # Map property change status to file name
    105 # FIXME: currently these are not used, but should be.
    106 my %file_prop;
    107 # Descriptions of file status flags
    108 my %prop_desc = (' ', 'No change',
    109 		 'C', 'Conflicted',
    110 		 'M', 'Modified');
    111 
    112 my $scm;
    113 
    114 my %file_hist;
    115 
    116 my $overwrite = O_EXCL;
    117 
    118 sub msg_fatal ($) {
    119     my $msg = shift;
    120     print STDERR "ERROR: $msg\n";
    121     exit (1);
    122 }
    123 
    124 sub msg_error ($) {
    125     my $msg = shift;
    126     print STDERR "ERROR: $msg\n";
    127 }
    128 
    129 sub msg_warning ($) {
    130     my $msg = shift;
    131     print STDERR "WARNING: $msg\n";
    132 }
    133 
    134 # fill the %file_status map based on svn status / cvs status output
    135 sub get_changed_files () {
    136     if ($scm eq "svn") {
    137 	my @lines = `LC_ALL=C svn --non-interactive status` or msg_fatal ('"svn status" failed');
    138 	foreach my $line (@lines) {
    139 	    chomp ($line);
    140 	    if ($line =~ /^(.)(.)(.)(.)(.)(.) (.*)/) {
    141 		$file_status{$7} = $1;
    142 		$file_prop{$7} = $2;
    143 		$file_hist{$7} = $4;
    144 	    } else {
    145 		msg_warning ("Cannot process svn status output: $line");
    146 	    }
    147 	}
    148     } elsif ($scm eq "cvs") {
    149 	# map CVS status names to svn status flags
    150 	my %status_map = ('Locally Added', 'A',
    151 			  'Locally Modified', 'M',
    152 			  'Needs Merge', 'M',
    153 			  'Needs Checkout', '!',
    154 			  'File had conflicts on merge', 'C');
    155 	my @lines = `LC_ALL=C cvs -z3 status 2>&1 | egrep '(^\\? |^cvs status: Examining |Status:)' | grep -v Up-to-date`
    156 	    or msg_fatal ('"cvs status" failed');
    157 	my $dir = "";
    158 	foreach my $line (@lines) {
    159 	    chomp ($line);
    160 	    if ($line =~ /^cvs status: Examining (.*)/) {
    161 		$dir = "$1/";
    162 	    } elsif ($line =~ /^File: no file (.+)\s+Status: (.*)/) {
    163 		if (defined ($status_map{$2})) {
    164 		    $file_status{"$dir$1"} = $status_map{$2};
    165 		}
    166 	    } elsif ($line =~ /^File: (.*\S)\s+Status: (.*)/) {
    167 		if (defined ($status_map{$2})) {
    168 		    $file_status{"$dir$1"} = $status_map{$2};
    169 		}
    170 	    } elsif ($line =~ /^? (.*)/) {
    171 		my $f0 = $1;
    172 		if (-d $f0) {
    173 		    my @files = `find $f0 -type f -print | sort`;
    174 		    foreach my $f (@files) {
    175 			chomp ($f);
    176 			$file_status{"$f"} = '?';
    177 		    }
    178 		} else {
    179 		    $file_status{"$f0"} = '?';
    180 		}
    181 	    } else {
    182 		msg_warning ("Cannot process cvs status output: $line");
    183 	    }
    184 	}
    185     }
    186 }
    187 
    188 # fill in values in the HTML templates
    189 my $uname;
    190 my $author;
    191 my $hostname;
    192 sub eval_template ($;$) {
    193     my $str = shift;
    194     my $title = shift;
    195 
    196     $title = "" unless defined $title;
    197 
    198     if (not defined ($uname)) {
    199 	$uname = `logname`;
    200 	chomp ($uname);
    201     }
    202 
    203     if (not defined ($author)) {
    204 	$author = (getpwnam $uname)[6];
    205     }
    206 
    207 
    208     if (not defined ($hostname)) {
    209 	$hostname = `/bin/hostname`;
    210 	chomp ($hostname);
    211     }
    212 
    213     my $date = `/bin/date`;
    214     chomp ($date);
    215 
    216     $str =~ s/\@TITLE\@/$title/g;
    217     $str =~ s/\@AUTHOR\@/$author/g;
    218     $str =~ s/\@UNAME\@/$uname/g;
    219     $str =~ s/\@HOSTNAME\@/$hostname/g;
    220     $str =~ s/\@DATE\@/$date/g;
    221     $str =~ s/\\n/\n/g;
    222 
    223     return $str;
    224 }
    225 
    226 # replace html special chars with corresponding entities
    227 sub html_encode ($) {
    228     my $str = shift;
    229 
    230     $str =~ s/&/&amp;/g;
    231     $str =~ s/</&lt;/g;
    232     $str =~ s/>/&gt;/g;
    233     $str =~ s/\t/        /g;
    234 
    235     return $str;
    236 }
    237 
    238 sub make_base_dir ($$) {
    239     my $webrev_dir = shift;
    240     my $file = shift;
    241 
    242     system ("mkdir -p $webrev_dir/$file");
    243     if ($? != 0) {
    244 	msg_error ("Failed to create directory $webrev_dir/$file");
    245 	return 0;
    246     }
    247     return 1;
    248 }
    249 
    250 sub gen_diff_new ($$) {
    251     my $webrev_dir = shift;
    252     my $file = shift;
    253     my $basename = basename ($file);
    254     system ("rm -f $webrev_dir/$file/new.$basename; cp $file $webrev_dir/$file/new.$basename");
    255     if ($? != 0) {
    256 	msg_fatal ("failed to copy file $file to $webrev_dir/$file");
    257     }
    258     return "[<A HREF=\"$file/new.$basename\">new</A>] ";
    259 }
    260 
    261 sub gen_diff_old ($$) {
    262     my $webrev_dir = shift;
    263     my $file = shift;
    264     my $basename = basename ($file);
    265     system ("rm -f $webrev_dir/$file/old.$basename");
    266     if ($? != 0) {
    267 	return undef;
    268     }
    269     if ($scm eq 'svn') {
    270 	system ("svn --non-interactive cat -r BASE $file > $webrev_dir/$file/old.$basename");
    271 	if ($? != 0) {
    272 	    return undef;
    273 	}
    274     } elsif ($scm eq 'cvs') {
    275 	my $rev=`LC_ALL=C cvs -z3 status $file | grep 'Working revision' | cut -f2 -d:`;
    276 	chomp ($rev);
    277 	my $CVSDIR = dirname ($file) . "/CVS";
    278 	my $CVSROOT = `cat $CVSDIR/Root`;
    279 	chomp ($CVSROOT);
    280 	my $REPO = `cat $CVSDIR/Repository`;
    281 	chomp ($REPO);
    282 	system ("mkdir -p $webrev_dir/tmp");
    283 	if ($? != 0) {
    284 	    return undef;
    285 	}
    286 	system ("cd $webrev_dir/tmp && LC_ALL=C cvs -q -z3 -d $CVSROOT co -r$rev $REPO/$basename >/dev/null 2>&1 && mv $REPO/$basename $webrev_dir/$file/old.$basename && cd / && rm -rf $webrev_dir/tmp");
    287 	if ($? != 0) {
    288 	    return undef;
    289 	}
    290     }
    291     return "[<A HREF=\"$file/old.$basename\">old</A>] ";
    292 }
    293 
    294 # create the unified diff page and return the [udiff] link
    295 sub gen_diff_udiff ($$) {
    296     my $webrev_dir = shift;
    297     my $file = shift;
    298     my $basename = basename ($file);
    299     my @diff;
    300     if ($scm eq 'svn') {
    301 	@diff = `svn --non-interactive diff $file`;
    302     } elsif ($scm eq 'cvs') {
    303 	@diff = `cd $webrev_dir/$file; /usr/bin/diff -u old.$basename new.$basename`;
    304     }
    305     system ("rm -f $webrev_dir/$file/udiff.html");
    306     if ($? != 0) {
    307 	return undef;
    308     }
    309     sysopen (DIFF, "$webrev_dir/$file/udiff.html", O_WRONLY | $overwrite | O_CREAT) or
    310 	msg_error ("failed to create file $webrev_dir/$file/udiff.html");
    311 
    312     print DIFF eval_template ($file_page_header, "Unified diff of $file");
    313     print DIFF "<TT><PRE>\n";
    314     foreach my $line (@diff) {
    315 	chomp ($line);
    316 	$line = html_encode ($line);
    317 	if ($line =~ /^---/) {
    318 	    print DIFF "<FONT COLOR=\"green\" SIZE=\"+1\"><b>$line</b></FONT>\n";
    319 	} elsif ($line =~ /^@@/) {
    320 	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
    321 	} elsif ($line =~ /^\+\+\+/) {
    322 	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
    323 	} elsif ($line =~ /^\+/) {
    324 	    print DIFF "<FONT COLOR=\"blue\"><b>$line</b></FONT>\n";
    325 	} elsif ($line =~ /^\*\*\*/) {
    326 	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
    327 	} elsif ($line =~ /^-/) {
    328 	    print DIFF "<FONT COLOR=\"brown\">$line</FONT>\n";
    329 	} else {
    330 	    print DIFF "$line\n";
    331 	}
    332     }
    333     print DIFF "</PRE></TT>\n";
    334     print DIFF eval_template ($file_page_footer);
    335     close DIFF;
    336     return "[<A HREF=\"$file/udiff.html\">udiff</A>] ";
    337 }
    338 
    339 # create the context diff page and return the [cdiff] link
    340 sub gen_diff_cdiff ($$) {
    341     my $webrev_dir = shift;
    342     my $file = shift;
    343     my $basename = basename ($file);
    344     if (! -f "$webrev_dir/$file/new.$basename") {
    345 	gen_diff_new ($webrev_dir, $file);
    346     }
    347     if (! -f "$webrev_dir/$file/old.$basename") {
    348 	gen_diff_old ($webrev_dir, $file);
    349     }
    350     my @diff = `cd $webrev_dir/$file; /usr/bin/diff -c old.$basename new.$basename`;
    351     system ("rm -f $webrev_dir/$file/cdiff.html");
    352     if ($? != 0) {
    353 	return undef;
    354     }
    355     sysopen (DIFF, "$webrev_dir/$file/cdiff.html", O_WRONLY | $overwrite | O_CREAT) or
    356 	msg_error ("failed to create file $webrev_dir/$file/cdiff.html");
    357     print DIFF eval_template ($file_page_header, "Context diff of $file");
    358     print DIFF "<TT><PRE>\n";
    359     foreach my $line (@diff) {
    360 	chomp ($line);
    361 	$line = html_encode ($line);
    362 	if ($line =~ /^\+/) {
    363 	    print DIFF "<FONT COLOR=\"blue\"><b>$line</b></FONT>\n";
    364 	} elsif ($line =~ /^---/) {
    365 	    print DIFF "<FONT COLOR=\"green\" SIZE=\"+1\"><b>$line</b></FONT>\n";
    366 	} elsif ($line =~ /^\*\*\*/) {
    367 	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
    368 	} elsif ($line =~ /^-/) {
    369 	    print DIFF "<FONT COLOR=\"brown\">$line</FONT>\n";
    370 	} elsif ($line =~ /^!/) {
    371 	    print DIFF "<FONT COLOR=\"blue\">$line</FONT>\n";
    372 	} else {
    373 	    print DIFF "$line\n";
    374 	}
    375     }
    376     print DIFF "</PRE></TT>\n";
    377     print DIFF eval_template ($file_page_footer);
    378     close DIFF;
    379     return "[<A HREF=\"$file/cdiff.html\">cdiff</A>] ";
    380 }
    381 
    382 # add a line to the array representing either the left of the right side
    383 # of an sdiff.  Lines are wrapped if longer than $SDIFF_MAX_LINE
    384 # returns the number of lines actually added to the array
    385 #
    386 # $ref is a reference to the array
    387 # $start is printed before the line
    388 # $line is the line itself
    389 # $end is printed to the end of the line
    390 # $indent_len is the number of chars to indent wrapped lines (because of the
    391 #             line numbers
    392 sub push_line ($$$$$) {
    393     my $ref = shift;
    394     my $start = shift;
    395     my $line = shift;
    396     my $end = shift;
    397     my $indent_len = shift;
    398 
    399     my $indent_str = sprintf ("%${indent_len}s  ", "");
    400 
    401     if (length ($line) <= $SDIFF_MAX_LINE) {
    402 	$line = html_encode ($line);
    403 	push (@$ref, "$start$line$end");
    404 	return 1;
    405     }
    406     my $l = 0;
    407     my $lstart = substr ($line, 0, $SDIFF_MAX_LINE);
    408     $line = substr ($line, $SDIFF_MAX_LINE);
    409     $lstart = html_encode ($lstart);
    410     my $the_line = "$start$lstart";
    411     $l++;
    412     while (length($line) > $SDIFF_MAX_LINE) {
    413 	$lstart = substr ($line, 0, $SDIFF_MAX_LINE);
    414 	$line = substr ($line, $SDIFF_MAX_LINE);
    415 	$lstart = html_encode ($lstart);
    416 	$the_line = "$the_line\n$indent_str$lstart";
    417 	$l++;
    418     }
    419     $line = html_encode ($line);
    420     $the_line = "$the_line\n$indent_str$line$end";
    421     push (@$ref, $the_line);
    422     $l++;
    423     return $l;
    424 }
    425 
    426 sub add_empty_line ($$) {
    427     my $ref = shift;
    428     my $len = shift;
    429 
    430     my $line = "";
    431     while ($len) {
    432 	$line = "$line\n";
    433 	$len--;
    434     }
    435 #    push (@$ref, "<PRE STYLE=\"margin: 1pt\">$line</PRE>");
    436     push (@$ref, $line);
    437 }
    438 
    439 sub extend_last_line ($$) {
    440     my $ref = shift;
    441     my $len = shift;
    442 
    443     my $line = pop (@$ref);
    444     while ($len) {
    445 	$line = "$line\n";
    446 	$len--;
    447     }
    448     push (@$ref, $line);
    449 }
    450 
    451 # generate the sdiff page and return the [sdiff] link
    452 sub gen_diff_sdiff ($$) {
    453     my $webrev_dir = shift;
    454     my $file = shift;
    455     my $basename = basename ($file);
    456 
    457     # we're going to work from a unified diff between the old and the new files
    458     # make sure they exist
    459     if (! -f "$webrev_dir/$file/new.$basename") {
    460 	gen_diff_new ($webrev_dir, $file);
    461     }
    462     if (! -f "$webrev_dir/$file/old.$basename") {
    463 	gen_diff_old ($webrev_dir, $file);
    464     }
    465     my $total_lines = `cat $webrev_dir/$file/old.$basename | wc -l`;
    466     chomp ($total_lines);
    467     $total_lines++;
    468     my $line_nr_len = length ("$total_lines");
    469     my @diff = `cd $webrev_dir/$file; /usr/bin/diff -U $SDIFF_CONTEXT old.$basename new.$basename`;
    470 
    471     # the 1st 2 lines are the file names
    472     my $l = shift (@diff); chomp ($l);
    473     my @left = ("<FONT COLOR=\"red\" SIZE=\"+1\"><b>$l</b></FONT>\n");
    474     $l = shift (@diff); chomp ($l);
    475     my @right = ("<FONT COLOR=\"green\" SIZE=\"+1\"><b>$l</b></FONT>\n");
    476 
    477     # line numbers on the left and right side
    478     my $left_line;
    479     my $right_line;
    480 
    481     my $line = shift (@diff);
    482     chomp ($line);
    483     while (@diff) {
    484 	# start of a block
    485 	if ($line =~ /^\@\@ -([0-9]+),[0-9]+ \+([0-9]+),[0-9]+ \@\@/) {
    486 	    $left_line = sprintf ("%${line_nr_len}s",$1);
    487 	    $right_line = sprintf ("%${line_nr_len}s",$2);
    488 	    push (@left, "<HR SIZE=1 NOSHADE>");
    489 	    push (@right, "<HR SIZE=1 NOSHADE>");
    490 	    $line = shift (@diff);
    491 	    chomp ($line);
    492 	    next;
    493 	}
    494 
    495 	# new lines added to the file: print them on the right side in blue
    496 	if ($line =~ /^\+(.*)/) {
    497 	    my $n = push_line (\@right, "<FONT COLOR=\"blue\">$right_line  ", $1, "</FONT>", $line_nr_len);
    498 	    # print an equal number of blank lines on the left side
    499 	    add_empty_line (\@left, $n);
    500 	    $right_line = sprintf ("%${line_nr_len}s", ++$right_line);
    501 	    $line = shift (@diff);
    502 	    chomp ($line);
    503 	    next;
    504 	}
    505 
    506 	# lines deleted
    507 	my @dellines;
    508 	while ($line =~ /^\-(.*)/) {
    509 	    push (@dellines, $1);
    510 	    $line = shift (@diff);
    511 	    chomp ($line);
    512 	}
    513 
    514 	# if deleted lines are immediately followed by added lines,
    515 	# then some of the deleted lines are actually changed lines.
    516 	# print them in blue on both sides
    517 	while ($line =~ /^\+(.*)/) {
    518 	    my $line1 = $1;
    519 	    if (@dellines) {
    520 		my $line2 = shift (@dellines);
    521 		my $n1 = push_line (\@left, "<FONT COLOR=\"blue\">$left_line  ", $line2, "</FONT>", $line_nr_len);
    522 		$n1--;
    523 		$left_line = sprintf ("%${line_nr_len}s", ++$left_line);
    524 		my $n2 = push_line (\@right, "<FONT COLOR=\"blue\">$right_line  ", $line1, "</FONT>", $line_nr_len);
    525 		$n2--;
    526 		$right_line = sprintf ("%${line_nr_len}s", ++$right_line);
    527 		if ($n2 > $n1) {
    528 		    $n2 -= $n1;
    529 		    $n1 = 0;
    530 		} else {
    531 		    $n1 -= $n2;
    532 		    $n2 = 0;
    533 		}
    534 		extend_last_line (\@left, $n2) if $n2;
    535 		extend_last_line (\@right, $n1) if $n1;
    536 	    } else {
    537 		# no deleted lines: print the new lines on the right side
    538 		my $n = push_line (\@right, "<FONT COLOR=\"blue\">$right_line  ", $line1, "</FONT>", $line_nr_len);
    539 		add_empty_line (\@left, $n);
    540 		$right_line = sprintf ("%${line_nr_len}s", ++$right_line);
    541 	    }
    542 	    $line = shift (@diff);
    543 	    chomp ($line);
    544 	}
    545 	# deleted lines remain, print them in brown on the left side
    546 	while (@dellines) {
    547 	    my $line2 = shift (@dellines);
    548 	    my $n = push_line (\@left, "<FONT COLOR=\"brown\">$left_line  ", $line2, "</FONT>", $line_nr_len);
    549 	    $left_line = sprintf ("%${line_nr_len}s", ++$left_line);
    550 	    add_empty_line (\@right, $n);
    551 	}
    552 	# unchanged (context) lines
    553 	if ($line =~ /^[^+-]/) {
    554 	    push_line (\@right, "$right_line ", $line, "", $line_nr_len);
    555 	    push_line (\@left, "$left_line ", $line, "", $line_nr_len);
    556 	    $left_line = sprintf ("%${line_nr_len}s", ++$left_line);
    557 	    $right_line = sprintf ("%${line_nr_len}s", ++$right_line);
    558 	} else {
    559 	    next;
    560 	}
    561 	# fetch the next line if exists
    562 	if (@diff) {
    563 	    $line = shift (@diff);
    564 	    chomp ($line);
    565 	}
    566     }
    567 
    568     # write out the report
    569     system ("rm -f $webrev_dir/$file/sdiff.html");
    570     if ($? != 0) {
    571 	return undef;
    572     }
    573     sysopen (DIFF, "$webrev_dir/$file/sdiff.html", O_WRONLY | $overwrite | O_CREAT) or
    574 	msg_error ("failed to create file $webrev_dir/$file/sdiff.html");
    575     print DIFF eval_template ($file_page_header, "Side by side diff of $file");
    576     print DIFF "<TABLE COLS=2 BORDER=1 CELLSPACING=0>\n";
    577     print DIFF "<TR><TD VALIGN=top>\n";
    578     print DIFF "<TABLE WIDTH=100% COLS=1 BORDER=0 CELLSPACING=0 CELLPADDING=0>\n";
    579     my $col = 1;
    580     foreach my $line (@left) {
    581 	if ($col) {
    582 	    print DIFF "<TR><TD BGCOLOR=#DDDDDD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
    583 	} else {
    584 	    print DIFF "<TR><TD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
    585 	}
    586 	$col = 1 - $col;
    587     }
    588     print DIFF "</TABLE></TD>\n";
    589     print DIFF "<TD VALIGN=top>\n";
    590     print DIFF "<TABLE WIDTH=100% COLS=1 BORDER=0 CELLSPACING=0 CELLPADDING=0>\n";
    591     $col = 1;
    592     foreach my $line (@right) {
    593 	if ($col) {
    594 	    print DIFF "<TR><TD BGCOLOR=#DDDDDD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
    595 	} else {
    596 	    print DIFF "<TR><TD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
    597 	}
    598 	$col = 1 - $col;
    599     }
    600     print DIFF "</TABLE></TD></TR>\n";
    601     print DIFF "</TABLE>\n";
    602     print DIFF eval_template ($file_page_footer);
    603     close DIFF;
    604     return "[<A HREF=\"$file/sdiff.html\">sdiff</A>] ";
    605 }
    606 
    607 sub gen_diff_patch ($$) {
    608     my $webrev_dir = shift;
    609     my $file = shift;
    610     my $basename = basename ($file);
    611     if ($scm eq 'svn') {
    612 	system ("rm -f $webrev_dir/$file/$basename.diff; svn --non-interactive diff $file > $webrev_dir/$file/$basename.diff");
    613     } elsif ($scm eq 'cvs') {
    614 	system ("rm -f $webrev_dir/$file/$basename.diff; cvs -q diff -up $file > $webrev_dir/$file/$basename.diff");
    615     }
    616     if ($? != 0) {
    617 	return undef;
    618     }
    619     return "[<A HREF=\"$file/$basename.diff\">patch</A>] ";
    620 }
    621 
    622 # map ChangeLog entries to files
    623 my %changelog_entry;
    624 
    625 # find updated ChangeLog files and extract the entries for each file
    626 sub read_changelog_entries () {
    627     foreach my $file (sort keys %file_status) {
    628 	if ($file eq "ChangeLog" or $file =~ /\/ChangeLog$/) {
    629 	    my @chlog_lines;
    630 	    if ($scm eq 'svn') {
    631 		@chlog_lines = `svn --non-interactive diff $file | grep "^\+"`;
    632 	    } elsif ($scm eq 'cvs') {
    633 		@chlog_lines = `cvs -q diff -u $file | grep "^\+"`;
    634 	    }
    635 	    my $dirname = dirname ($file);
    636 	    if ($dirname eq ".") {
    637 		$dirname = "";
    638 	    } else {
    639 		$dirname = "$dirname/";
    640 	    }
    641 	    while (@chlog_lines) {
    642 		my $line = shift (@chlog_lines);
    643 		chomp ($line);
    644 		#      * file: foo bar
    645 		if ($line =~ /^\+(\s+\* \S+.*)/) {
    646 		    my $entry = $1;
    647 		    my $ecat = $1;
    648 		    $line = shift (@chlog_lines);
    649 		    chomp ($line);
    650 		    # read all lines until the next
    651 		    #     * file: foo bar
    652 		    # entry
    653 		    while (defined ($line) and $line =~ /^\+(\s+[^*].*)/) {
    654 			$entry = "$entry\n$1";
    655 			$ecat = "$ecat$1";
    656 			$line = shift (@chlog_lines);
    657 			chomp ($line);
    658 		    }
    659 		    $ecat =~ s/^\s*\*\s*//;
    660 		    # assign the same entry to each file listed with
    661 		    # commas before the first :
    662 		    while ($ecat =~ /^([^:,]+)[:,]\s*(.*)/) {
    663 			$changelog_entry{"$dirname$1"} = "$entry\n";
    664 			$ecat = $2;
    665 		    }
    666 		    unshift (@chlog_lines, $line);
    667 		}
    668 	    }
    669 	}
    670     }
    671 }
    672 
    673 ################ MAIN ###################################################
    674 
    675 sub main ($) {
    676     my $webrev_dir = shift;
    677 
    678     system ("mkdir -p $webrev_dir");
    679     if ($? != 0) {
    680 	msg_fatal ("Webrev directory could not be created");
    681     }
    682 
    683     sysopen (INDEX, "$webrev_dir/index.html", O_WRONLY | $overwrite | O_CREAT) or
    684 	msg_fatal ("failed to create file $webrev_dir/index.html");
    685 
    686     print "Finding changed files...\n";
    687     get_changed_files ();
    688 
    689     print "Reading ChangeLogs...\n";
    690     read_changelog_entries ();
    691 
    692     my $title = `pwd`;
    693     chomp ($title);
    694     $title = basename ($title . " Webrev");
    695 
    696     print INDEX eval_template ($index_page_header, $title);
    697 
    698     my $total_new = 0;
    699     my $total_deleted = 0;
    700     my $total_changed = 0;
    701     my $total_unchanged = 0;
    702     my $total_non_svn = 0;
    703 
    704     print "Processing files...\n";
    705     foreach my $file (sort keys %file_status) {
    706 	print "       $file\n";
    707 	print INDEX "<P><B>$file</B> ($status_desc{$file_status{$file}})<BR>\n";
    708 	print INDEX "&nbsp; &nbsp; &nbsp; &nbsp;";
    709 	make_base_dir ($webrev_dir, $file) or
    710 	    print INDEX "<P>\n", next;
    711 	if ($file_status{$file} eq 'A') {
    712 	    print INDEX gen_diff_new ($webrev_dir, $file);
    713 	    my $lines = `cat $file | wc -l`;
    714 	    chomp ($lines);
    715 	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
    716 	    print INDEX "$lines new line(s)\n";
    717 	    $total_new += $lines;
    718 	} elsif ($file_status{$file} eq 'D') {
    719 	    print INDEX gen_diff_old ($webrev_dir, $file);
    720 	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
    721 	    my $basename = basename ($file);
    722 	    my $lines = `cat $webrev_dir/$file/old.$basename | wc -l`;
    723 	    chomp ($lines);
    724 	    print INDEX "$lines deleted line(s)\n";
    725 	    $total_deleted += $lines;
    726 	} elsif ($file_status{$file} eq '?') {
    727 	    next if -d $file;
    728 	    print INDEX gen_diff_new ($webrev_dir, $file);
    729 	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
    730 	    my $lines = `cat $file | wc -l`;
    731 	    chomp ($lines);
    732 	    print INDEX "$lines new line(s) not under svn control\n";
    733 	    $total_non_svn += $lines;
    734 	} elsif ($file_status{$file} eq 'M') {
    735 	    my $label;
    736 	    $label = gen_diff_old ($webrev_dir, $file);
    737 	    next if not defined $label;
    738 	    print INDEX $label;
    739 	    $label = gen_diff_new ($webrev_dir, $file);
    740 	    next if not defined $label;
    741 	    print INDEX $label;
    742 	    $label = gen_diff_udiff ($webrev_dir, $file);
    743 	    next if not defined $label;
    744 	    print INDEX $label;
    745 	    $label = gen_diff_cdiff ($webrev_dir, $file);
    746 	    next if not defined $label;
    747 	    print INDEX $label;
    748 	    $label = gen_diff_sdiff ($webrev_dir, $file);
    749 	    next if not defined $label;
    750 	    print INDEX $label;
    751 	    $label = gen_diff_patch ($webrev_dir, $file);	
    752 	    my $basename = basename ($file);
    753 	    my $changed_lines = `diff -c $webrev_dir/$file/old.$basename $webrev_dir/$file/new.$basename | grep '^! ' | wc -l`;
    754 	    chomp ($changed_lines);
    755 	    my $deleted_lines = `diff -c $webrev_dir/$file/old.$basename $webrev_dir/$file/new.$basename | grep '^- ' | wc -l`;
    756 	    chomp ($deleted_lines);
    757 	    my $new_lines = `diff -c $webrev_dir/$file/old.$basename $webrev_dir/$file/new.$basename | grep '^+ ' | wc -l`;
    758 	    chomp ($new_lines);
    759 	    my $total_lines = `cat $webrev_dir/$file/old.$basename | wc -l`;
    760 	    chomp ($total_lines);
    761 	    my $unchanged_lines = $total_lines - $deleted_lines - $changed_lines;
    762 	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
    763 	    print INDEX "$new_lines line(s) new / $deleted_lines line(s) deleted / $changed_lines line(s) updated / $unchanged_lines line(s) unchanged\n";
    764 	    $total_new += $new_lines;
    765 	    $total_deleted += $deleted_lines;
    766 	    $total_changed += $changed_lines;
    767 	    $total_unchanged += $unchanged_lines;
    768 	}
    769 	if (defined ($changelog_entry{$file})) {
    770 	    print INDEX "<PRE>\n";
    771 	    print INDEX $changelog_entry{$file};
    772 	    print INDEX "</PRE>\n";
    773 	} elsif ($file ne "ChangeLog" and not $file =~ /\/ChangeLog$/) {
    774 	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
    775 	    print INDEX "<FONT COLOR=red>No ChangeLog entry found</FONT><BR>\n";
    776 	}
    777     }
    778 
    779     print INDEX "<P><B>Total</B>: $total_new line(s) new / $total_deleted line(s) deleted / $total_changed line(s) updated / $total_unchanged line(s) unchanged<BR>\n";
    780     if ($total_non_svn) {
    781 	print INDEX "An additional $total_non_svn line(s) not under source control<P>\n";
    782     }
    783     print INDEX eval_template ($index_page_footer);
    784     close INDEX;
    785     print "Done.\n"
    786 }
    787 
    788 if ((@ARGV != 1) or ($ARGV[0] eq "-h") or ($ARGV[0] eq "--help")) {
    789     print "Usage: webrev /path/to/webrev/dir\n\n";
    790     print "Run this script inside a Subversion or CVS controlled directory\n";
    791     print "to create an html code review document.\n";
    792     print "The argument is a directory where the output is written.\n";
    793     print "The svn or cvs command must be in your PATH and should not\n";
    794     print "require interaction (e.g. use ssh-add first)\n";
    795     exit(1);
    796 }
    797 
    798 if (-d '.svn') {
    799     $scm = 'svn';
    800 } elsif (-d 'CVS') {
    801     $scm = 'cvs';
    802 } else {
    803     msg_fatal ("No CVS or Subversion control files found in this directory");
    804 }
    805 
    806 if (-d "$ARGV[0]/.svn" or -d "$ARGV[0]/CVS") {
    807     msg_fatal ("The target directory should not be svn or CVS controlled");
    808 }
    809 
    810 if (-f "$ARGV[0]/index.html") {
    811     print "Overwrite files in $ARGV[0] (y/n)? ";
    812     my $ans = lc(<STDIN>);
    813     chomp ($ans);
    814     if ($ans eq 'y' or $ans eq 'yes') {
    815 	$overwrite = O_CREAT;
    816     }
    817 }
    818 
    819 main ($ARGV[0]);
    820