#!/usr/bin/perl -w
# 
# Complicated notification for CVS checkins.
# 
# This script is used to provide email notifications of changes to the CVS
# repository.  These email changes will include diffs of the changes.
# Really big diffs will be trimmed.
# 
# This script is run from a CVS loginfo file (see $CVSROOT/CVSROOT/loginfo).
# To set this up, create a loginfo entry that looks something like this:
# 
#     mymodule /path/to/this/script -- %%s some-email-addr@your.domain
# 
# In this example, whenever a checkin that matches `mymodule' is made, this
# script is invoked, which will generate the diff containing email, and send it
# to some-email-addr@your.domain.
# 
#
# Note:
#  this program is merely a rewrite in perl of the syncmail script in python
#  provided by mailman.
# 
# Usage:
# 
#     %(PROGRAM)s [options] -- <%%S> email-addr [email-addr ...]
# 
# Where options is:
# 
#     --cvsroot=<path>
#     	Use <path> as the environment variable CVSROOT.  Otherwise this
#     	variable must exist in the environment.
# 
#     --help
#     -h
#         Print this text.
# 
#     --context=#
#     -C #
#         Include # lines of context around lines that differ (default: 0).
# 
#     -c
#         Produce a context diff.
# 
#     -u
#         Produce a unified diff (default).
# 
#     <%%S>
#         CVS %%s loginfo expansion.  When invoked by CVS, this will be a single
#         string containing the directory the checkin is being made in, relative
#         to $CVSROOT, followed by the list of files that are changing. If the
#         %%s in the loginfo file is %%{sVv}, context diffs for each of the
#         modified files are included in any email messages that are generated.
# 
#     email-addrs
#         At least one email address.
# 
# Thanks, pizzas, beer cans, etc. to joerg@dorchain.net (c) 2001
# Flames, Rants to /dev/null
# 
# Use and distribute freely, but at your own risk (to the maximum extend allowed by law)




# Globals (ugly)
$MAILCMD = 'mail -s "__SUBJECT__" __PEOPLE__ 2>&1 >/dev/null';
$CVSWEBBASEURL="http://tech.europeonline.net/cgi-bin/cvsweb.cgi/";

# Diff trimming
$DIFF_HEAD_LINES = 20;
$DIFF_TAIL_LINES = 20;
$DIFF_TRUNCATE_IF_LARGER = 1000;

sub usage {
	print $_[1]."\n" if defined($_[1]);
	exit($_[0]);
};

sub calculate_diff {
	my ($file, $oldrev, $newrev, $contextlines) = @_;
	my ($update_cmd, $diffcmd, @lines);
	local *FP;

	if ($oldrev eq 'NONE') {
		if ( -r $file) {
			open(FP, $file);
		} else {
			$update_cmd = "cvs -fn update -r $newrev -p $file 2>&1 |";
			open(FP, $update_cmd);
		}
		@lines = <FP>;
		close(FP);
		unshift @lines , "--- NEW FILE: $file ---\n";
	} elsif ( $newrev eq 'NONE' ) {
		@lines = ("--- $file DELETED ---\n");
	} else {
		# This has to happen in the background, otherwise we'll
		# run into CVS lock contension.
		if ($contextlines > 0 ) {
			$difftype = "-C $contextlines";
		} else {
			$difftype = "-u";
		}
		$diffcmd = "cvs -f diff -kk $difftype --minimal -r $oldrev -r $newrev $file 2>&1 |";
		open(FP, $diffcmd);
		@lines = <FP>;
		close(FP);
	}
	if ( $#lines > $DIFF_TRUNCATE_IF_LARGER) {
		$removedlines = $#lines - $DIFF_HEAD_LINES - $DIFF_TAIL_LINES;
		splice(@lines, $DIFF_HEAD_LINES, $removedlines, "...$removedlines suppressed...]\n");
	}
	return @lines;
}

sub makecvsurl {
	my ($module, $file, $oldrev, $newrev) = @_;
	return $CVSWEBBASEURL.$module."/".$file.".diff?r1=text&tr1=".$oldrev."&r2=text&tr2=".$newrev."&f+h";
}

sub blast_mail {
	my ($mailcmd, $module, $filestodiff, $contextlines) = @_;
	my $pid = fork;
	# cannot wait for child process or that will cause parent to
	# retain cvs lock for too long
	if ( defined($pid) && $pid == 0) { 
	      my $filerev;
	      local *FP;
	      sleep(2);
	      open(FP,"| ".$mailcmd) or die "Cannot open $mailcmd\n";
	      print FP <STDIN>;
	      print FP "\n";
	      print FP "URLs to diffs:\n";
	      # Append urls if available
	      foreach $filespec (split /\s+/, $filestodiff) {
	      	      my ($file, $oldrev, $newrev) = split(/,/, $filespec);
		      print FP makecvsurl ($module, $file, $oldrev, $newrev, $contextlines);
		      print FP "\n";
	      }
	      print FP "\n";
	      # Append diffs if available
	      foreach $filespec (split /\s+/, $filestodiff) {
	      	      my ($file, $oldrev, $newrev) = split(/,/, $filespec);
		      print FP calculate_diff($file, $oldrev, $newrev, $contextlines);
		      print FP "\n";
	      }
	      close(FP);
	      exit(0);
	}
}

sub countsubstr {
	my ($str, $substr) = @_;
	my ($pos, $count) = ( -1, 0);
	while (($pos = index($str, $substr, $pos)) > -1) {
		$pos++;
		$count++
	}
	return $count;
}

use Getopt::Long;

# Main part
my $contextlines = 0;
my ($help, $cvsroot, $contextdiff, $unidiff);
my (@specs, $mailcmd, $module);
my ($PEOPLE, $SUBJECT);
Getopt::Long::Configure('no_ignore_case');
GetOptions(
	'h|help' => \$help,
	'cvsroot=s' => \$cvsroot,
	'C|context:i' => \$contextlines,
	'c' => \$contextdiff,
	'u' => \$unidiff
	);
if (defined($help)) {
	usage(0,<<__EOF__

Usage:

    $0 [options] -- <%%S> email-addr [email-addr ...]

Where options is:

    --cvsroot=<path>
    	Use <path> as the environment variable CVSROOT.  Otherwise this
    	variable must exist in the environment.

    --help
    -h
        Print this text.

    --context=#
    -C #
        Include # lines of context around lines that differ (default: 0).

    -c
        Produce a context diff.

    -u
        Produce a unified diff (default).

    <%%S>
        CVS %%s loginfo expansion.  When invoked by CVS, this will be a single
        string containing the directory the checkin is being made in, relative
        to \$CVSROOT, followed by the list of files that are changing. If the
        %%s in the loginfo file is %%{sVv}, context diffs for each of the
        modified files are included in any email messages that are generated.

    email-addrs
        At least one email address.

__EOF__
		);
}
if (defined($cvsroot)) {
	$ENV{"CVSROOT"}=$cvsroot;
}
if (defined($contextdiff)) {
	if ($contextlines <= 0) {
		$contextlines = 2;
	}
}
if (defined($unidiff)) {
	$contextlines = 0;
}

# What follows is the specification containing the files that were
# modified. The arguments actually must be split, with the first component
# containing the directory the checkin is being made in, relative to
# $CVROOT, floowed by the list of files that are changing.
if ( ! @ARGV) {
	usage(1, "No CVS modules specified");
}
$SUBJECT = shift;
@specs = split(/\s+/, $SUBJECT);
$module = shift @specs;

# The remaining args should be the email addresses
if ( ! @ARGV) {
	usage(1, "No mailaddrs specified");
}
$PEOPLE = join(" ", @ARGV);
$mailcmd = $MAILCMD;
$mailcmd =~ s/__SUBJECT__/$SUBJECT/;
$mailcmd =~ s/__PEOPLE__/$PEOPLE/;

print "Mailing $PEOPLE...\n";
if (join(' ', @specs) eq '- Imported sources') {
	splice(@specs, -3);
} elsif (($#specs > 2) && (join(' ',  @specs[-3..-1]) eq '- New directory')) {
	splice(@specs, -3);
} elsif ($#specs > 2) {
	my @L = splice(@specs, 0, 2);
	foreach $s (@specs) {
		my $prev = $L[-1];
		if (countsubstr($prev, ",") < 2) {
			$L[-1] = "$prev $s";
		} else {
			push(@L, ( $s ));
		}
	}
	@specs = @L;
}
print "Generating notification message...\n";
blast_mail($mailcmd, $module, join(' ', @specs), $contextlines);
print "Generating notification message... done.\n";
