#! /usr/bin/perl -wT
# Copyright  2001 Martin Kammerhofer <mkamm@gmx.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

# @(#)$CVSHeader: scripts/cvsrdiff2cvsweb.pl,v 1.6 2001/02/23 10:04:39 mkamm Exp $
# Convert "cvs rdiff -s" output to HTML with links to cvsweb.cgi.

require 5.003;
use strict;
use Getopt::Long;
{
    local $^W = 0;
    eval "use URI::Escape";
    if ($@) {
	# provide a dummy function if package URI::Escape is not available.
	sub uri_escape { return $_[0]; }
	# warn "$0: package URI::Escape is not available\n";
    }
}

delete @ENV{qw(IFS CDPATH ENV BASH_ENV LD_LIBRARY_PATH LD_PRELOAD)};
$ENV{PATH} = '/bin:/usr/bin:/usr/local/bin';

my ($cvsweb, $urlsuffix, $branch,
    $repository, $rev1, $rev2,
    $outfile) =
    (
     # "http://www.FreeBSD.org/cgi/cvsweb.cgi",
     "http://localhost/FreeBSD-WWW/cgi/cvsweb.cgi", # URL of cvsweb.cgi
     "",	# suffix to URL e.g. "cvsroot=myproject"
     "",	# RCS branch e.g. "MAIN"
     "", "", "", # repo, r1, r2
     "-",
     );

my %optctl = (
	      "cvsweb" => \$cvsweb,
	      "urlsuffix" => \$urlsuffix,
	      "branch" => \$branch,
	      "outfile" => \$outfile,
	      "repository" => \$repository,
	      "rev1" => \$rev1,
	      "rev2" => \$rev2,
	      );

my $prog=$0;
$prog =~ s+^.*/++; # basename
$prog =~ s/\.p(er)?l$//; # truncate '.pl' suffix
my $version = '$Id: cvsrdiff2cvsweb.pl,v 1.6 2001/02/23 10:04:39 mkamm Exp $'; # '
$version =~ s/^\s*\$Id: //;
$version =~ s/ \$\s*$//;

sub usage () {
    print STDERR
	"usage: $prog [--cvsweb=URL] [--urlsuffix=SFX] [--branch=TAG]",
	" [--outfile=FILE] rdiff-file\n",
	"   or: $prog [--cvsweb=URL] [--urlsuffix=SFX] [--branch=TAG]",
	" [--outfile=FILE] --repository=REPO --rev1=REV1 --rev2=REV2 module...\n",
	"   or: $prog --version\n";
    exit 64;
}

sub html_escape ($) {
    local $_ = shift or die;
    s/\&/&amp;/g;
    s/\"/&quot;/g;
    s/>/&gt;/g;
    s/</&lt;/g;
    return $_;
}

if (!GetOptions(\%optctl, "cvsweb|url=s", "branch|tag=s", "version!",
		"urlsuffix|suffix=s", "outfile|output=s",
		"repository=s", "rev1|r1=s", "rev2|r2=s",)
    || $#ARGV == -1 && -t && !$optctl{version} # only terminal input
    )
{
    usage();
}
if ($optctl{version}) {
    print "$version\n";
    exit 0;
}
if ($repository || $rev1 || $rev2) { # run "cvs rdiff -s"
    if (!$repository && defined($ENV{CVSROOT})) {
	$repository = $ENV{CVSROOT};
    }
    if (!($repository && $rev1 && $rev2)) {
	print STDERR "$prog: options --repo --r1 --r2 are all or none!\n";
	usage();
    }
    if ($#ARGV == -1) {
	print STDERR "$prog: no module(s) specified!\n";
	usage();
    }
}

if ($outfile ne "-") {
    close(STDOUT) or die;
    $outfile = $1 if $outfile =~ /^(.*)$/; # untaint
    open(STDOUT, "> $outfile") or
	die "$prog: redirect output to '$outfile': $!.\nStopped";
}

my $command;
if ($repository) {
    $command = "cvs -lq -d " . quotemeta($repository) . " rdiff -s"
	. " -r " . quotemeta($rev1) . " -r " . quotemeta($rev2);
    while (my $module = shift(@ARGV)) {
	$command .= " " . quotemeta($module);
    }
    $command = $1 if $command =~ /^(.*)$/; # untaint
    close(STDIN) or die;
    open(STDIN, "$command |") or
	die "$prog: running command '$command': $!.\nStopped";
}

my (@f, $fname, $uri);
my $cvsweb_suffix = "";
$cvsweb_suffix = "?only_with_tag=" . $branch if $branch;
$cvsweb_suffix .= ($cvsweb_suffix ? "&" : "?") . $urlsuffix if $urlsuffix;

my $title = "$prog";
$title .= " $ARGV[0]" if $#ARGV == 0;
$title = html_escape($title); # paranoia
print <<EndOfHeader;
<HTML><HEAD>
<META NAME="generator" CONTENT="$version">
<TITLE>$title</TITLE></HEAD>
<BODY BGCOLOR=white><PRE>
EndOfHeader
print "$command\n" if $command;

while (<>) {
    next unless /^File /;
    @f = split;
    if ($f[0] eq 'File') {
	# hyperlink the file name
	$fname = html_escape($f[1]); # be paranoid
	$uri = uri_escape("$cvsweb/$f[1]$cvsweb_suffix");
	s\Q$f[1]\E<A HREF="$uri">$fname</A>;
	# search for "revision" "#"
	# or "revision" "#1" to "#2"
	for (my $i = 2; $i < $#f; $i++) {
	    next unless $f[$i] eq 'revision';
	    last unless $f[++$i] =~ /^\d+(\.\d+)+$/;
	    # hyperlink the revision number
	    $uri = uri_escape("$cvsweb/$f[1]") . "#"
		. uri_escape("rev$f[$i]$cvsweb_suffix");
	    s\b\Q$f[$i]\E\b<A HREF="$uri">$f[$i]</A>;
	    last unless $i+2 <= $#f && $f[++$i] eq 'to'
		&& $f[++$i] =~ /^\d+(\.\d+)+$/;
	    # hyperlink the second revision number
	    $uri = uri_escape("$cvsweb/$f[1]") . "#"
		. uri_escape("rev$f[$i]$cvsweb_suffix");
	    s\b\Q$f[$i]\E\b<A HREF="$uri">$f[$i]</A>;
	    # hyperlink the word "to" to the delta
	    my $suffix = ($cvsweb_suffix ? "$cvsweb_suffix&" : "?");
	    $uri = uri_escape("$cvsweb/$f[1].diff${suffix}r1=$f[$i-2]&r2=$f[$i]");
	    s\bto\b<A HREF="$uri">to</A>;
	    last;
	}
    }
} continue {
    print;
}

print <<'EndOfFooter';
</PRE></BODY></HTML>
EndOfFooter

if ($outfile ne "-") {
    close(STDOUT) or die;
}

exit(1) unless $.;

__END__;

=head1 NAME

cvsrdiff2cvsweb - convert a C<cvs rdiff -s> output to HTML

=head1 SYNOPSIS

=over 4

=item *

cvsrdiff2cvsweb [--cvsweb=I<URL>] [--urlsuffix=I<SFX>] [--branch=I<TAG>]
[--outfile=I<FILE>] I<rdiff-file>

=item *

cvsrdiff2cvsweb [--cvsweb=I<URL>] [--urlsuffix=I<SFX>]
[--branch=I<TAG>] [--outfile=I<FILE>] --repository=I<REPO>
--rev1=I<REV1> --rev2=I<REV2> I<module>...

=item *

cvsrdiff2cvsweb --version

=back

=head1 DESCRIPTION

The cvsrdiff2cvsweb program takes output from C<cvs rdiff -s> (change
summary) and converts it into HTML. Names of changed (added, updatet
or deleted) files are replaced with hyperlinks to a C<cvsweb> CGI
script.

This means you can click on any of the updated files and see the CVS
log (change history) and have access to all the revisions and deltas.

(The cgi-script C<cvsweb.cgi> was originally written by Bill Fenner
<fenner@freebsd.org> for the FreeBSD project. It allows browsing of
CVS-repositories with a HTML-browser. CVS is a popular version control
system.)

Options may be abbreviated to a unique prefix. The options are as
follows:

=over 4

=item --cvsweb=I<URL>

Specify URL of cvsweb.cgi script.

=item --urlsuffix=I<SFX>

Specify some extra information for appending to generated URLs. (You
should not type a leading C<?> or C<&> character because it will be
added automatically.)

=item --branch=I<TAG>

Tell C<cvsweb.cgi> that you are only interested in file revisions on
the specified branch.

=item --outfile=I<FILENAME>

Specify the output file. If no output file is specified standard
output is used.

=item --repository=I<REPOSITORY>

This is used to invoke the C<cvs rdiff> command. This option requires
options C<--rev1> and C<--rev2> too.

=item --rev1=I<REVISION1> --rev2=I<REVISION2>

Specify which revisions to compare. Unless C<CVSROOT> is set in your environment this will also require option C<--repository>.

=item --version

Print version information and exit.

=back

=head1 EXAMPLE

Suppose you are running the FreeBSD operating and want to know in
detail how the networking code changed between S<FreeBSD 4.2> and the
latest 4-X-stable.

C<CVSROOT=:pserver:anoncvs@anoncvs.FreeBSD.org:/home/ncvs>

C<export CVSROOT; cvs login # password 'anoncvs'>

C<cvs rdiff -s -r RELENG_4_2_0_RELEASE -r RELENG_4 sys/netinet E<gt>netinet.rdiff>

C<cvsrdiff2cvsweb -cvsweb http://www.FreeBSD.org/cgi/cvsweb.cgi -branch RELENG_4 -out netinet.html netinet.rdiff>

Now open F<netinet.html> with your favourite browser!

=head1 BUGS

There is currently now way to specify option defaults other than to
edit the program text.

There is no provision to invoke C<cvs rdiff -s> with custom options or
dates (C<-D>) rather than revision numbers or tags. It is however
possible to run C<cvs rdiff -s> manually and feed the output into
C<cvsrdiff2cvsweb>.

You cannot show revisions and diffs of files which have been removed
in the MAIN branch. (This is a limitation of C<cvsweb.cgi>.)

=head1 AUTHOR

Martin Kammerhofer <mkamm@gmx.net>

