#! @PERL5@ -wT
# @configure_input@
# 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.

# Convert "cvs rdiff -s" output to HTML with links to cvsweb.cgi.
# @(#)$Id: cvsrdiff2cvsweb.pl.in,v 1.11 2001/03/17 03:22:05 mkamm Exp $

require 5.003;
use strict;
use Getopt::Long;
use Config::IniFiles;
use constant CONFENV =>  "CVSWEB_CONVERTERS_CONF";	# ENV variable
use constant CONFNAME => "cvsweb-converters.conf";	# file basename
use constant COMMON => "common";	# default section in ini-file
{
    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";
    }
}

sub lookup_configfile();
sub read_configfile ($@);

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

my ($cvsweb, $urlsuffix, $branch,
    $repository, $rev1, $rev2,
    $outfile) =
    # DO NOT EDIT THE DEFAULTS HERE - use the configfile instead!
    # (Otherwise you have to start all over when you upgrade this script.)
    (
     # "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,
	      "output-file" => \$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.in,v 1.11 2001/03/17 03:22:05 mkamm Exp $'; # ';
$version =~ s/^\s*\$Id: //;
$version =~ s/ \$\s*$//;

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

sub help () {
    {exec 'perldoc', $0};
    print STDERR "$prog: cannot exec 'perldoc'\n";
    goto &usage;
}

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

my $configfile = lookup_configfile();
read_configfile($configfile,
		qw(cvsweb urlsuffix branch repository rev1 rev2 outfile))
    if $configfile;
if (!GetOptions(\%optctl, "cvsweb|url=s", "branch|tag=s", "version!",
		"urlsuffix|suffix=s", "output-file|outfile=s", "help!",
		"repository=s", "rev1|r1=s", "rev2|r2=s",)
    )
{
    usage();
}
help() if $optctl{help};
if ($optctl{version}) {
    print "$version\n";
    exit 0;
}
usage() if !@ARGV && -t; # only tty input

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($. ? 0 : 1); # it's an error if no lines have been read

# ----------------------------------------------------------------------

# Look for a config file
#
# try 1. $ENV{CONFENV}		(constants CONF* are defined above)
#     2. "~/.CONFNAME"		(in home directory prefixed with a dot)
# and 3. "@PREFIX@/etc/CONFNAME"		(in "/PREFIX/etc")
sub lookup_configfile () {
    if (exists $ENV{+CONFENV}) {
	return $ENV{+CONFENV};
    } elsif (exists $ENV{HOME} && -e ("$ENV{HOME}/." . CONFNAME)) {
	return "$ENV{HOME}/." . CONFNAME;
    } elsif (-e "@PREFIX@/etc/" . CONFNAME) {
	return "@PREFIX@/etc/" . CONFNAME;
    }
    return undef;
}

# read defaults for commandline options from the config file
# 1st argument is filename, other arguments are names of config variables
sub read_configfile ($@) {
    my ($configfile, @param_names) = @_;
    map {$_ = $1 if /^(.*)/} (@param_names); # untaint
    my %config;
    if (!tie %config, 'Config::IniFiles',
	(-file => $configfile, -default => COMMON)) {
	$" = "\n";
	die "$0: cannot tie to config file '$configfile'\n",
	"@Config::IniFiles::errors\n";
    }
    # get my (sub)section(s)
    my (@sections, %param_hash);
    push @sections, $config{+COMMON} if exists $config{+COMMON};
    while (my ($section, $hashref) = each %config) {
	push @sections, $hashref if $section =~ m\Q$prog\Eio;
    }
    # read and assign my parameters
    my $parameter_count = 0;
    foreach my $name (@param_names) {
	$param_hash{$name} = undef; # remember parameter names
	foreach my $section (@sections) {
	    if (exists $section->{$name}) {
		eval qq \$$name = \$section->{'$name'} ;
		die "$prog: cannot assign to \$$name: $@\n" if $@;
		$parameter_count++;
	    }
	}
    }
    print STDERR "$prog: WARNING: no parameters read from '$configfile'\n"
	unless $parameter_count;
    # warn about unrecognized parameters
    my @unrecognized;
    foreach my $section (@sections) {
	foreach my $parameter (keys %{$section}) {
	    push @unrecognized, $parameter
		unless exists $param_hash{$parameter};
	}
    }
    if (@unrecognized) {
	@unrecognized = sort @unrecognized;
	print STDERR "$prog: WARNING: ",
	"the following parameters are not recognized:\n",
	"@unrecognized\n";
    }
    # untie
    untie %config or die "untie failed";
} # end of sub "read_configfile"

__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>]
[--output-file=I<FILE>] I<rdiff-file>

=item *

cvsrdiff2cvsweb [--cvsweb=I<URL>] [--urlsuffix=I<SFX>]
[--branch=I<TAG>] [--output-file=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 --output-file=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 FILES

C<cvsrdiff2cvsweb> looks for a configuration file in three places.

=over 4

=item *

If the variable C<CVSWEB_CONVERTERS_CONF> is set in the environment its
content is interpreted as the name of the configuration file, otherwise

=item *

the file F<~/.cvsweb-converters.conf> is examined, and finally

=item *

F<@PREFIX@/etc/cvsweb-converters.conf> is tried.

=back

Only the first found file is read.

=head1 EXAMPLE

Suppose you are running the FreeBSD operating and want to know in
detail how the networking code changed between S<FreeBSD 4.3> 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_3_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 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>.

=head1 AUTHOR

Martin Kammerhofer <mkamm@gmx.net>

=cut

# Local Variables:
# mode: perl
# End:
#EOF
