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/&/&/g; 231 $str =~ s/</</g; 232 $str =~ s/>/>/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 " "; 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> "; 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> "; 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> "; 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> "; 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> "; 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