#! @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 cvsup(1) logfile to HTML.
# @(#)$Id: cvsuplog2cvsweb.pl.in,v 1.18 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 PATH)};

my ($cvsweb, $urlsuffix, $branch, $cvsrootdir, $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",
     "",	# e.g. "cvsroot=myproject&content-type=text/x-cvsweb-markup"
     "",	# e.g. "MAIN"
     "",	# e.g. "/home/ncvs"
     "-",
     );

my %optctl = (
	      "branch" => \$branch,
	      "cvsrootdir" => \$cvsrootdir,
	      "cvsweb" => \$cvsweb,
	      "output-file" => \$outfile,
	      "urlsuffix" => \$urlsuffix,
	      );

my $prog=$0;
$prog =~ s+^.*/++; # basename
$prog =~ s/\.p(er)?l$//; # truncate '.pl' suffix
my $version = '$Id: cvsuplog2cvsweb.pl.in,v 1.18 2001/03/17 03:22:05 mkamm Exp $'; # ';
$version =~ s/^\s*\$Id: //;
$version =~ s/ \$\s*$//;

sub usage () {
    print STDERR
	"usage: $prog [--cvsweb=URL] [--urlsuffix=STR] [--branch=TAG]\n",
	"             [--cvsrootdir=DIR] [--output-file=FILE] cvsup.log\n",
	"   or: $prog --help\n",
	"   or: $prog --version\n";
    exit 64;
}

sub help () {
    {exec 'perldoc', $0};
    # try 'perldoc' in the same directory as perl itself
    $ENV{PATH} = $1 if dirname($^X) =~ /^(.*)$/ ; # untaint
    {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 cvsrootdir outfile))
    if $configfile;
if (!GetOptions(\%optctl, "cvsweb|url=s", "branchtag|tag=s", "version!",
		"urlsuffix|suffix=s", "output-file|outfile=s",
		"cvsrootdir|repositorydir=s", "help!", )
    || $#ARGV > 0 # more than one filearg
    )
{
    usage();
}
help() if $optctl{help};
if ($optctl{version}) {
    print "$version\n";
    exit 0;
}
usage() if !@ARGV && -t; # only tty input

$cvsrootdir =~ s/*$;

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

my (@f, $fname, $htm, $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

while (<>) {
    @f = split;
    if ($f[0] eq 'Edit'
	|| $f[0] eq 'Checkout'
	|| $f[0] eq 'Create'
	|| $f[0] eq 'Delete') {
	# hyperlink the file name
	$fname = $f[1];
	$fname =~ s/,v$//;
	$htm = html_escape($fname); # be paranoid
	$uri = uri_escape("$cvsweb/$fname$cvsweb_suffix");
	s\Q$f[1]\E<A HREF="$uri">$htm</A>;
    } elsif ($f[0] eq 'Add' && $#f >= 1 && $f[1] eq 'delta') {
	# make the UTC date more readable
	s{\b(\d{4})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})\b}
	{$1-$2-$3 $4:$5:$6};
	# hyperlink the revision number to the delta (diff)
	my $committer = html_escape($f[4]);
	s/\Q$f[4]\E/$committer/; # be paranoid
	my ($delta, $prev) = ($f[2], $f[2]);
	if ($delta =~ /^\d+(?:\.\d+)*\.(\d+)$/) {
	    my $lsn = $1; # least significant number
	    $lsn -= 1;
	    if ($lsn) { # delta x.y => x.(y+1)
		$prev =~ s/$1$/$lsn/;
	    } else { # delta x.y => x.y.z.1
		$prev =~ s/\.\d+\.\d+$//;
	    }
	    # link the delta revision
	    $uri = uri_escape("$cvsweb/$fname") . "#"
		. uri_escape("rev$delta$cvsweb_suffix");
	    s\b\Q$delta\E\b<A HREF="$uri">$delta</A>;
	    # link the word "delta" to the patch
	    my $suffix = ($cvsweb_suffix ? "$cvsweb_suffix&" : "?");
	    $uri = uri_escape("$cvsweb/$fname.diff${suffix}r1=$prev&r2=$delta");
	    s\bdelta\b<A HREF="$uri">delta</A>;
	}
    } elsif ($f[0] eq 'Rsync'
	     || ($f[0] eq 'Append' && $#f >= 1 && $f[1] eq 'to')) {
	$htm = html_escape($f[-1]);
	$uri = uri_escape("file:$cvsrootdir/$f[-1]");
	s\Q$f[-1]\E<A HREF="$uri">$htm</A>;
    }
} 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

cvsuplog2cvsweb - convert a C<cvsup> log file to HTML

=head1 SYNOPSIS

=over 4

=item .

cvsuplog2cvsweb [--cvsweb=I<URL>] [--urlsuffix=I<STR>] [--branch=I<TAG>]
[--cvsrootdir=I<DIR>] [--output-file=I<FILE>] cvsup.log

=item .

cvsuplog2cvsweb --version

=back

=head1 DESCRIPTION

The cvsuplog2cvsweb program takes a logfile from C<cvsup> and converts
it into HTML. Names of changed (added, updated 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.

(C<cvsup> is written by John Polstra <jdp@polstra.com>. It is a
network distribution package for CVS repositories. 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 then standard
output is used.

=item --cvsrootdir=I<DIR>

Add HTML C<file:> links to non-versioned (rsynced or appended) files.
The directory I<DIR> is prepended to the generated C<file:> URLs.

=item --version

Print version information and exit.

=back

=head1 FILES

C<cvsuplog2cvsweb> 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 system and want to upgrade your
sources from the RELENG_4 branch. You already have a working cvsup
config file in F</usr/share/examples/cvsup/4.x-stable-supfile>. Since
your nearest cvsup mirror is in Germany you use

C<cvsup -g -h cvsup.de.freebsd.org -L2
/usr/share/examples/cvsup/4.x-stable-supfile E<gt>cvsup.log>

Now you want to know what all this source updates are about and invoke

C<cvsuplog2cvsweb --cvsweb=http://www.FreeBSD.org/cgi/cvsweb.cgi
--branch=RELENG_4 E<gt>cvsuplog.html>

Open F<cvsuplog.html> with your favourite browser now!

=head1 AUTHOR

Martin Kammerhofer <mkamm@gmx.net>

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