#!/usr/perl5/bin/perl -w
#
# Script for generating code review pages similar to those generated by
# ON's webrev tool
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License, Version 1.0 only
# (the "License").  You may not use this file except in compliance
# with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#

# FIXMEs: 
#   - require the target dir to be empty
#   - breaks if you have a new subdir that is not under svn control
#   - should have a way to exclude some or all not-svn-controlled files

use strict;
use Fcntl;
use File::Basename;

#
# Usage: webrev /path/to/output/dir
#
# creates the html report of changed in the current svn workspace (current dir)
#

# FIXME: would be nice to turn these into command line options
# max number of chars in each line, above which lines are wrapped in
# side-by-side diffs
my $SDIFF_MAX_LINE=80;
# number of lines of context in sdiffs
my $SDIFF_CONTEXT=20;

# Valid @FOO@ tags in the HTML templates:
#
# @TITLE@     - page title
# @AUTHOR@    - real name of the current user according to the passwd entry
# @COPYRIGHT@ - copyright statement (not implemented)
# @UNAME@     - current user name
# @HOSTNAME@  - hostname of the current host as printed by /bin/hostname
# @DATE@      - current date string as printed by /bin/date
#

# HTML page header template for index.html
my $index_page_header =
    '<HTML>\n' .
    ' <HEAD>\n' .
    '  <TITLE>@TITLE@</TITLE>\n' .
    '  <META NAME="author" CONTENT="@AUTHOR@">\n' .
    '  <META NAME="generator" CONTENT="webrev for svn">\n' .
#    '  <META NAME="copyright" CONTENT="@COPYRIGHT@">\n' .
    ' </HEAD>\n' .
    ' <BODY BGCOLOR="#FFFFFF">\n' .
    '  <FONT FACE="arial,sans">\n' .
    '  <CENTER><FONT SIZE=+1><B>@TITLE@</B></FONT></CENTER><P>\n';
# HTML page footer template for index.html
my $index_page_footer =
    '  </FONT>\n' .
    '  <HR SIZE=1 NOSHADE>\n' .
    '  <FONT FACE="arial,sans" SIZE="-2">\n' .
    '   Webrev report generated by @UNAME@@@HOSTNAME@ on @DATE@.\n' .
    '  </FONT>\n' .
    ' </BODY>\n' .
    '</HTML>\n';

# HTML page header template for the diff pages
my $file_page_header = $index_page_header;
# HTML page footer template for the diff pages
my $file_page_footer = $index_page_footer;

# Map status to file name
my %file_status;
# Descriptions of file status flags
my %status_desc = (' ', 'No change',
		   'A', 'New',
		   'C', '<FONT COLOR="#FF4444">Conflicted</FONT>',
		   'D', 'Deleted',
		   'G', 'Merged',
		   'I', 'Ignored',
		   'M', 'Modified',
		   'R', 'Replaced',
		   '?', '<FONT COLOR="#FF4444">Not under version control</FONT>',
		   '!', 'Missing');

# Map property change status to file name
# FIXME: currently these are not used, but should be.
my %file_prop;
# Descriptions of file status flags
my %prop_desc = (' ', 'No change',
		 'C', 'Conflicted',
		 'M', 'Modified');

my $scm;

my %file_hist;

my $overwrite = O_EXCL;

sub msg_fatal ($) {
    my $msg = shift;
    print STDERR "ERROR: $msg\n";
    exit (1);
}

sub msg_error ($) {
    my $msg = shift;
    print STDERR "ERROR: $msg\n";
}

sub msg_warning ($) {
    my $msg = shift;
    print STDERR "WARNING: $msg\n";
}

# fill the %file_status map based on svn status / cvs status output
sub get_changed_files () {
    if ($scm eq "svn") {
	my @lines = `LC_ALL=C svn --non-interactive status` or msg_fatal ('"svn status" failed');
	foreach my $line (@lines) {
	    chomp ($line);
	    if ($line =~ /^(.)(.)(.)(.)(.)(.) (.*)/) {
		$file_status{$7} = $1;
		$file_prop{$7} = $2;
		$file_hist{$7} = $4;
	    } else {
		msg_warning ("Cannot process svn status output: $line");
	    }
	}
    } elsif ($scm eq "cvs") {
	# map CVS status names to svn status flags
	my %status_map = ('Locally Added', 'A',
			  'Locally Modified', 'M',
			  'Needs Merge', 'M',
			  'Needs Checkout', '!',
			  'File had conflicts on merge', 'C');
	my @lines = `LC_ALL=C cvs -z3 status 2>&1 | egrep '(^\\? |^cvs status: Examining |Status:)' | grep -v Up-to-date`
	    or msg_fatal ('"cvs status" failed');
	my $dir = "";
	foreach my $line (@lines) {
	    chomp ($line);
	    if ($line =~ /^cvs status: Examining (.*)/) {
		$dir = "$1/";
	    } elsif ($line =~ /^File: no file (.+)\s+Status: (.*)/) {
		if (defined ($status_map{$2})) {
		    $file_status{"$dir$1"} = $status_map{$2};
		}
	    } elsif ($line =~ /^File: (.*\S)\s+Status: (.*)/) {
		if (defined ($status_map{$2})) {
		    $file_status{"$dir$1"} = $status_map{$2};
		}
	    } elsif ($line =~ /^? (.*)/) {
		my $f0 = $1;
		if (-d $f0) {
		    my @files = `find $f0 -type f -print | sort`;
		    foreach my $f (@files) {
			chomp ($f);
			$file_status{"$f"} = '?';
		    }
		} else {
		    $file_status{"$f0"} = '?';
		}
	    } else {
		msg_warning ("Cannot process cvs status output: $line");
	    }
	}
    }
}

# fill in values in the HTML templates
my $uname;
my $author;
my $hostname;
sub eval_template ($;$) {
    my $str = shift;
    my $title = shift;

    $title = "" unless defined $title;

    if (not defined ($uname)) {
	$uname = `logname`;
	chomp ($uname);
    }

    if (not defined ($author)) {
	$author = (getpwnam $uname)[6];
    }


    if (not defined ($hostname)) {
	$hostname = `/bin/hostname`;
	chomp ($hostname);
    }

    my $date = `/bin/date`;
    chomp ($date);

    $str =~ s/\@TITLE\@/$title/g;
    $str =~ s/\@AUTHOR\@/$author/g;
    $str =~ s/\@UNAME\@/$uname/g;
    $str =~ s/\@HOSTNAME\@/$hostname/g;
    $str =~ s/\@DATE\@/$date/g;
    $str =~ s/\\n/\n/g;

    return $str;
}

# replace html special chars with corresponding entities
sub html_encode ($) {
    my $str = shift;

    $str =~ s/&/&amp;/g;
    $str =~ s/</&lt;/g;
    $str =~ s/>/&gt;/g;
    $str =~ s/\t/        /g;

    return $str;
}

sub make_base_dir ($$) {
    my $webrev_dir = shift;
    my $file = shift;

    system ("mkdir -p $webrev_dir/$file");
    if ($? != 0) {
	msg_error ("Failed to create directory $webrev_dir/$file");
	return 0;
    }
    return 1;
}

sub gen_diff_new ($$) {
    my $webrev_dir = shift;
    my $file = shift;
    my $basename = basename ($file);
    system ("rm -f $webrev_dir/$file/new.$basename; cp $file $webrev_dir/$file/new.$basename");
    if ($? != 0) {
	msg_fatal ("failed to copy file $file to $webrev_dir/$file");
    }
    return "[<A HREF=\"$file/new.$basename\">new</A>] ";
}

sub gen_diff_old ($$) {
    my $webrev_dir = shift;
    my $file = shift;
    my $basename = basename ($file);
    system ("rm -f $webrev_dir/$file/old.$basename");
    if ($? != 0) {
	return undef;
    }
    if ($scm eq 'svn') {
	system ("svn --non-interactive cat -r BASE $file > $webrev_dir/$file/old.$basename");
	if ($? != 0) {
	    return undef;
	}
    } elsif ($scm eq 'cvs') {
	my $rev=`LC_ALL=C cvs -z3 status $file | grep 'Working revision' | cut -f2 -d:`;
	chomp ($rev);
	my $CVSDIR = dirname ($file) . "/CVS";
	my $CVSROOT = `cat $CVSDIR/Root`;
	chomp ($CVSROOT);
	my $REPO = `cat $CVSDIR/Repository`;
	chomp ($REPO);
	system ("mkdir -p $webrev_dir/tmp");
	if ($? != 0) {
	    return undef;
	}
	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");
	if ($? != 0) {
	    return undef;
	}
    }
    return "[<A HREF=\"$file/old.$basename\">old</A>] ";
}

# create the unified diff page and return the [udiff] link
sub gen_diff_udiff ($$) {
    my $webrev_dir = shift;
    my $file = shift;
    my $basename = basename ($file);
    my @diff;
    if ($scm eq 'svn') {
	@diff = `svn --non-interactive diff $file`;
    } elsif ($scm eq 'cvs') {
	@diff = `cd $webrev_dir/$file; /usr/bin/diff -u old.$basename new.$basename`;
    }
    system ("rm -f $webrev_dir/$file/udiff.html");
    if ($? != 0) {
	return undef;
    }
    sysopen (DIFF, "$webrev_dir/$file/udiff.html", O_WRONLY | $overwrite | O_CREAT) or
	msg_error ("failed to create file $webrev_dir/$file/udiff.html");

    print DIFF eval_template ($file_page_header, "Unified diff of $file");
    print DIFF "<TT><PRE>\n";
    foreach my $line (@diff) {
	chomp ($line);
	$line = html_encode ($line);
	if ($line =~ /^---/) {
	    print DIFF "<FONT COLOR=\"green\" SIZE=\"+1\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^@@/) {
	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^\+\+\+/) {
	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^\+/) {
	    print DIFF "<FONT COLOR=\"blue\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^\*\*\*/) {
	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^-/) {
	    print DIFF "<FONT COLOR=\"brown\">$line</FONT>\n";
	} else {
	    print DIFF "$line\n";
	}
    }
    print DIFF "</PRE></TT>\n";
    print DIFF eval_template ($file_page_footer);
    close DIFF;
    return "[<A HREF=\"$file/udiff.html\">udiff</A>] ";
}

# create the context diff page and return the [cdiff] link
sub gen_diff_cdiff ($$) {
    my $webrev_dir = shift;
    my $file = shift;
    my $basename = basename ($file);
    if (! -f "$webrev_dir/$file/new.$basename") {
	gen_diff_new ($webrev_dir, $file);
    }
    if (! -f "$webrev_dir/$file/old.$basename") {
	gen_diff_old ($webrev_dir, $file);
    }
    my @diff = `cd $webrev_dir/$file; /usr/bin/diff -c old.$basename new.$basename`;
    system ("rm -f $webrev_dir/$file/cdiff.html");
    if ($? != 0) {
	return undef;
    }
    sysopen (DIFF, "$webrev_dir/$file/cdiff.html", O_WRONLY | $overwrite | O_CREAT) or
	msg_error ("failed to create file $webrev_dir/$file/cdiff.html");
    print DIFF eval_template ($file_page_header, "Context diff of $file");
    print DIFF "<TT><PRE>\n";
    foreach my $line (@diff) {
	chomp ($line);
	$line = html_encode ($line);
	if ($line =~ /^\+/) {
	    print DIFF "<FONT COLOR=\"blue\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^---/) {
	    print DIFF "<FONT COLOR=\"green\" SIZE=\"+1\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^\*\*\*/) {
	    print DIFF "<FONT COLOR=\"red\" SIZE=\"+1\"><b>$line</b></FONT>\n";
	} elsif ($line =~ /^-/) {
	    print DIFF "<FONT COLOR=\"brown\">$line</FONT>\n";
	} elsif ($line =~ /^!/) {
	    print DIFF "<FONT COLOR=\"blue\">$line</FONT>\n";
	} else {
	    print DIFF "$line\n";
	}
    }
    print DIFF "</PRE></TT>\n";
    print DIFF eval_template ($file_page_footer);
    close DIFF;
    return "[<A HREF=\"$file/cdiff.html\">cdiff</A>] ";
}

# add a line to the array representing either the left of the right side
# of an sdiff.  Lines are wrapped if longer than $SDIFF_MAX_LINE
# returns the number of lines actually added to the array
#
# $ref is a reference to the array
# $start is printed before the line
# $line is the line itself
# $end is printed to the end of the line
# $indent_len is the number of chars to indent wrapped lines (because of the
#             line numbers
sub push_line ($$$$$) {
    my $ref = shift;
    my $start = shift;
    my $line = shift;
    my $end = shift;
    my $indent_len = shift;

    my $indent_str = sprintf ("%${indent_len}s  ", "");

    if (length ($line) <= $SDIFF_MAX_LINE) {
	$line = html_encode ($line);
	push (@$ref, "$start$line$end");
	return 1;
    }
    my $l = 0;
    my $lstart = substr ($line, 0, $SDIFF_MAX_LINE);
    $line = substr ($line, $SDIFF_MAX_LINE);
    $lstart = html_encode ($lstart);
    my $the_line = "$start$lstart";
    $l++;
    while (length($line) > $SDIFF_MAX_LINE) {
	$lstart = substr ($line, 0, $SDIFF_MAX_LINE);
	$line = substr ($line, $SDIFF_MAX_LINE);
	$lstart = html_encode ($lstart);
	$the_line = "$the_line\n$indent_str$lstart";
	$l++;
    }
    $line = html_encode ($line);
    $the_line = "$the_line\n$indent_str$line$end";
    push (@$ref, $the_line);
    $l++;
    return $l;
}

sub add_empty_line ($$) {
    my $ref = shift;
    my $len = shift;

    my $line = "";
    while ($len) {
	$line = "$line\n";
	$len--;
    }
#    push (@$ref, "<PRE STYLE=\"margin: 1pt\">$line</PRE>");
    push (@$ref, $line);
}

sub extend_last_line ($$) {
    my $ref = shift;
    my $len = shift;

    my $line = pop (@$ref);
    while ($len) {
	$line = "$line\n";
	$len--;
    }
    push (@$ref, $line);
}

# generate the sdiff page and return the [sdiff] link
sub gen_diff_sdiff ($$) {
    my $webrev_dir = shift;
    my $file = shift;
    my $basename = basename ($file);

    # we're going to work from a unified diff between the old and the new files
    # make sure they exist
    if (! -f "$webrev_dir/$file/new.$basename") {
	gen_diff_new ($webrev_dir, $file);
    }
    if (! -f "$webrev_dir/$file/old.$basename") {
	gen_diff_old ($webrev_dir, $file);
    }
    my $total_lines = `cat $webrev_dir/$file/old.$basename | wc -l`;
    chomp ($total_lines);
    $total_lines++;
    my $line_nr_len = length ("$total_lines");
    my @diff = `cd $webrev_dir/$file; /usr/bin/diff -U $SDIFF_CONTEXT old.$basename new.$basename`;

    # the 1st 2 lines are the file names
    my $l = shift (@diff); chomp ($l);
    my @left = ("<FONT COLOR=\"red\" SIZE=\"+1\"><b>$l</b></FONT>\n");
    $l = shift (@diff); chomp ($l);
    my @right = ("<FONT COLOR=\"green\" SIZE=\"+1\"><b>$l</b></FONT>\n");

    # line numbers on the left and right side
    my $left_line;
    my $right_line;

    my $line = shift (@diff);
    chomp ($line);
    while (@diff) {
	# start of a block
	if ($line =~ /^\@\@ -([0-9]+),[0-9]+ \+([0-9]+),[0-9]+ \@\@/) {
	    $left_line = sprintf ("%${line_nr_len}s",$1);
	    $right_line = sprintf ("%${line_nr_len}s",$2);
	    push (@left, "<HR SIZE=1 NOSHADE>");
	    push (@right, "<HR SIZE=1 NOSHADE>");
	    $line = shift (@diff);
	    chomp ($line);
	    next;
	}

	# new lines added to the file: print them on the right side in blue
	if ($line =~ /^\+(.*)/) {
	    my $n = push_line (\@right, "<FONT COLOR=\"blue\">$right_line  ", $1, "</FONT>", $line_nr_len);
	    # print an equal number of blank lines on the left side
	    add_empty_line (\@left, $n);
	    $right_line = sprintf ("%${line_nr_len}s", ++$right_line);
	    $line = shift (@diff);
	    chomp ($line);
	    next;
	}

	# lines deleted
	my @dellines;
	while ($line =~ /^\-(.*)/) {
	    push (@dellines, $1);
	    $line = shift (@diff);
	    chomp ($line);
	}

	# if deleted lines are immediately followed by added lines,
	# then some of the deleted lines are actually changed lines.
	# print them in blue on both sides
	while ($line =~ /^\+(.*)/) {
	    my $line1 = $1;
	    if (@dellines) {
		my $line2 = shift (@dellines);
		my $n1 = push_line (\@left, "<FONT COLOR=\"blue\">$left_line  ", $line2, "</FONT>", $line_nr_len);
		$n1--;
		$left_line = sprintf ("%${line_nr_len}s", ++$left_line);
		my $n2 = push_line (\@right, "<FONT COLOR=\"blue\">$right_line  ", $line1, "</FONT>", $line_nr_len);
		$n2--;
		$right_line = sprintf ("%${line_nr_len}s", ++$right_line);
		if ($n2 > $n1) {
		    $n2 -= $n1;
		    $n1 = 0;
		} else {
		    $n1 -= $n2;
		    $n2 = 0;
		}
		extend_last_line (\@left, $n2) if $n2;
		extend_last_line (\@right, $n1) if $n1;
	    } else {
		# no deleted lines: print the new lines on the right side
		my $n = push_line (\@right, "<FONT COLOR=\"blue\">$right_line  ", $line1, "</FONT>", $line_nr_len);
		add_empty_line (\@left, $n);
		$right_line = sprintf ("%${line_nr_len}s", ++$right_line);
	    }
	    $line = shift (@diff);
	    chomp ($line);
	}
	# deleted lines remain, print them in brown on the left side
	while (@dellines) {
	    my $line2 = shift (@dellines);
	    my $n = push_line (\@left, "<FONT COLOR=\"brown\">$left_line  ", $line2, "</FONT>", $line_nr_len);
	    $left_line = sprintf ("%${line_nr_len}s", ++$left_line);
	    add_empty_line (\@right, $n);
	}
	# unchanged (context) lines
	if ($line =~ /^[^+-]/) {
	    push_line (\@right, "$right_line ", $line, "", $line_nr_len);
	    push_line (\@left, "$left_line ", $line, "", $line_nr_len);
	    $left_line = sprintf ("%${line_nr_len}s", ++$left_line);
	    $right_line = sprintf ("%${line_nr_len}s", ++$right_line);
	} else {
	    next;
	}
	# fetch the next line if exists
	if (@diff) {
	    $line = shift (@diff);
	    chomp ($line);
	}
    }

    # write out the report
    system ("rm -f $webrev_dir/$file/sdiff.html");
    if ($? != 0) {
	return undef;
    }
    sysopen (DIFF, "$webrev_dir/$file/sdiff.html", O_WRONLY | $overwrite | O_CREAT) or
	msg_error ("failed to create file $webrev_dir/$file/sdiff.html");
    print DIFF eval_template ($file_page_header, "Side by side diff of $file");
    print DIFF "<TABLE COLS=2 BORDER=1 CELLSPACING=0>\n";
    print DIFF "<TR><TD VALIGN=top>\n";
    print DIFF "<TABLE WIDTH=100% COLS=1 BORDER=0 CELLSPACING=0 CELLPADDING=0>\n";
    my $col = 1;
    foreach my $line (@left) {
	if ($col) {
	    print DIFF "<TR><TD BGCOLOR=#DDDDDD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
	} else {
	    print DIFF "<TR><TD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
	}
	$col = 1 - $col;
    }
    print DIFF "</TABLE></TD>\n";
    print DIFF "<TD VALIGN=top>\n";
    print DIFF "<TABLE WIDTH=100% COLS=1 BORDER=0 CELLSPACING=0 CELLPADDING=0>\n";
    $col = 1;
    foreach my $line (@right) {
	if ($col) {
	    print DIFF "<TR><TD BGCOLOR=#DDDDDD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
	} else {
	    print DIFF "<TR><TD><PRE STYLE=\"margin: 1pt;\">$line\n</PRE></TD></TR>\n";
	}
	$col = 1 - $col;
    }
    print DIFF "</TABLE></TD></TR>\n";
    print DIFF "</TABLE>\n";
    print DIFF eval_template ($file_page_footer);
    close DIFF;
    return "[<A HREF=\"$file/sdiff.html\">sdiff</A>] ";
}

sub gen_diff_patch ($$) {
    my $webrev_dir = shift;
    my $file = shift;
    my $basename = basename ($file);
    if ($scm eq 'svn') {
	system ("rm -f $webrev_dir/$file/$basename.diff; svn --non-interactive diff $file > $webrev_dir/$file/$basename.diff");
    } elsif ($scm eq 'cvs') {
	system ("rm -f $webrev_dir/$file/$basename.diff; cvs -q diff -up $file > $webrev_dir/$file/$basename.diff");
    }
    if ($? != 0) {
	return undef;
    }
    return "[<A HREF=\"$file/$basename.diff\">patch</A>] ";
}

# map ChangeLog entries to files
my %changelog_entry;

# find updated ChangeLog files and extract the entries for each file
sub read_changelog_entries () {
    foreach my $file (sort keys %file_status) {
	if ($file eq "ChangeLog" or $file =~ /\/ChangeLog$/) {
	    my @chlog_lines;
	    if ($scm eq 'svn') {
		@chlog_lines = `svn --non-interactive diff $file | grep "^\+"`;
	    } elsif ($scm eq 'cvs') {
		@chlog_lines = `cvs -q diff -u $file | grep "^\+"`;
	    }
	    my $dirname = dirname ($file);
	    if ($dirname eq ".") {
		$dirname = "";
	    } else {
		$dirname = "$dirname/";
	    }
	    while (@chlog_lines) {
		my $line = shift (@chlog_lines);
		chomp ($line);
		#      * file: foo bar
		if ($line =~ /^\+(\s+\* \S+.*)/) {
		    my $entry = $1;
		    my $ecat = $1;
		    $line = shift (@chlog_lines);
		    chomp ($line);
		    # read all lines until the next
		    #     * file: foo bar
		    # entry
		    while (defined ($line) and $line =~ /^\+(\s+[^*].*)/) {
			$entry = "$entry\n$1";
			$ecat = "$ecat$1";
			$line = shift (@chlog_lines);
			chomp ($line);
		    }
		    $ecat =~ s/^\s*\*\s*//;
		    # assign the same entry to each file listed with
		    # commas before the first :
		    while ($ecat =~ /^([^:,]+)[:,]\s*(.*)/) {
			$changelog_entry{"$dirname$1"} = "$entry\n";
			$ecat = $2;
		    }
		    unshift (@chlog_lines, $line);
		}
	    }
	}
    }
}

################ MAIN ###################################################

sub main ($) {
    my $webrev_dir = shift;

    system ("mkdir -p $webrev_dir");
    if ($? != 0) {
	msg_fatal ("Webrev directory could not be created");
    }

    sysopen (INDEX, "$webrev_dir/index.html", O_WRONLY | $overwrite | O_CREAT) or
	msg_fatal ("failed to create file $webrev_dir/index.html");

    print "Finding changed files...\n";
    get_changed_files ();

    print "Reading ChangeLogs...\n";
    read_changelog_entries ();

    my $title = `pwd`;
    chomp ($title);
    $title = basename ($title . " Webrev");

    print INDEX eval_template ($index_page_header, $title);

    my $total_new = 0;
    my $total_deleted = 0;
    my $total_changed = 0;
    my $total_unchanged = 0;
    my $total_non_svn = 0;

    print "Processing files...\n";
    foreach my $file (sort keys %file_status) {
	print "       $file\n";
	print INDEX "<P><B>$file</B> ($status_desc{$file_status{$file}})<BR>\n";
	print INDEX "&nbsp; &nbsp; &nbsp; &nbsp;";
	make_base_dir ($webrev_dir, $file) or
	    print INDEX "<P>\n", next;
	if ($file_status{$file} eq 'A') {
	    print INDEX gen_diff_new ($webrev_dir, $file);
	    my $lines = `cat $file | wc -l`;
	    chomp ($lines);
	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
	    print INDEX "$lines new line(s)\n";
	    $total_new += $lines;
	} elsif ($file_status{$file} eq 'D') {
	    print INDEX gen_diff_old ($webrev_dir, $file);
	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
	    my $basename = basename ($file);
	    my $lines = `cat $webrev_dir/$file/old.$basename | wc -l`;
	    chomp ($lines);
	    print INDEX "$lines deleted line(s)\n";
	    $total_deleted += $lines;
	} elsif ($file_status{$file} eq '?') {
	    next if -d $file;
	    print INDEX gen_diff_new ($webrev_dir, $file);
	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
	    my $lines = `cat $file | wc -l`;
	    chomp ($lines);
	    print INDEX "$lines new line(s) not under svn control\n";
	    $total_non_svn += $lines;
	} elsif ($file_status{$file} eq 'M') {
	    my $label;
	    $label = gen_diff_old ($webrev_dir, $file);
	    next if not defined $label;
	    print INDEX $label;
	    $label = gen_diff_new ($webrev_dir, $file);
	    next if not defined $label;
	    print INDEX $label;
	    $label = gen_diff_udiff ($webrev_dir, $file);
	    next if not defined $label;
	    print INDEX $label;
	    $label = gen_diff_cdiff ($webrev_dir, $file);
	    next if not defined $label;
	    print INDEX $label;
	    $label = gen_diff_sdiff ($webrev_dir, $file);
	    next if not defined $label;
	    print INDEX $label;
	    $label = gen_diff_patch ($webrev_dir, $file);	
	    my $basename = basename ($file);
	    my $changed_lines = `diff -c $webrev_dir/$file/old.$basename $webrev_dir/$file/new.$basename | grep '^! ' | wc -l`;
	    chomp ($changed_lines);
	    my $deleted_lines = `diff -c $webrev_dir/$file/old.$basename $webrev_dir/$file/new.$basename | grep '^- ' | wc -l`;
	    chomp ($deleted_lines);
	    my $new_lines = `diff -c $webrev_dir/$file/old.$basename $webrev_dir/$file/new.$basename | grep '^+ ' | wc -l`;
	    chomp ($new_lines);
	    my $total_lines = `cat $webrev_dir/$file/old.$basename | wc -l`;
	    chomp ($total_lines);
	    my $unchanged_lines = $total_lines - $deleted_lines - $changed_lines;
	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
	    print INDEX "$new_lines line(s) new / $deleted_lines line(s) deleted / $changed_lines line(s) updated / $unchanged_lines line(s) unchanged\n";
	    $total_new += $new_lines;
	    $total_deleted += $deleted_lines;
	    $total_changed += $changed_lines;
	    $total_unchanged += $unchanged_lines;
	}
	if (defined ($changelog_entry{$file})) {
	    print INDEX "<PRE>\n";
	    print INDEX $changelog_entry{$file};
	    print INDEX "</PRE>\n";
	} elsif ($file ne "ChangeLog" and not $file =~ /\/ChangeLog$/) {
	    print INDEX "<BR>&nbsp; &nbsp; &nbsp; &nbsp;";
	    print INDEX "<FONT COLOR=red>No ChangeLog entry found</FONT><BR>\n";
	}
    }

    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";
    if ($total_non_svn) {
	print INDEX "An additional $total_non_svn line(s) not under source control<P>\n";
    }
    print INDEX eval_template ($index_page_footer);
    close INDEX;
    print "Done.\n"
}

if ((@ARGV != 1) or ($ARGV[0] eq "-h") or ($ARGV[0] eq "--help")) {
    print "Usage: webrev /path/to/webrev/dir\n\n";
    print "Run this script inside a Subversion or CVS controlled directory\n";
    print "to create an html code review document.\n";
    print "The argument is a directory where the output is written.\n";
    print "The svn or cvs command must be in your PATH and should not\n";
    print "require interaction (e.g. use ssh-add first)\n";
    exit(1);
}

if (-d '.svn') {
    $scm = 'svn';
} elsif (-d 'CVS') {
    $scm = 'cvs';
} else {
    msg_fatal ("No CVS or Subversion control files found in this directory");
}

if (-d "$ARGV[0]/.svn" or -d "$ARGV[0]/CVS") {
    msg_fatal ("The target directory should not be svn or CVS controlled");
}

if (-f "$ARGV[0]/index.html") {
    print "Overwrite files in $ARGV[0] (y/n)? ";
    my $ans = lc(<STDIN>);
    chomp ($ans);
    if ($ans eq 'y' or $ans eq 'yes') {
	$overwrite = O_CREAT;
    }
}

main ($ARGV[0]);
