#!/usr/bin/perl -w
#
# cvslog -- Mail the CVS log message to a given address.
#
# Copyright 2001, 2003, 2004  Petr Baudis <pasky@ucw.cz>
#
# Contains some ideas from cvslog by Russ Allbery <rra@stanford.edu> and from
# loginfo.pl by <taj@kde.org> and <dirk@kde.org>.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License version 2, as published by the
# Free Software Foundation.
#
# This program is designed to run from CVS's loginfo administrative file and
# takes a log message, massaging it and mailing it away. It's a modified
# version of the log script that comes with CVS, but tries to work better and
# generate more readable and useful output.
#
# First, add the file to your CVSROOT/checkoutlist. Then, insert a variation of
# this line to CVSROOT/loginfo:
#
#       ALL         $CVSROOT/CVSROOT/cvslog %{sVv} $USER
#
# If you are using CVS version 1.12.6 or newer and want to make use of the new
# command format you need to add the following line instead:
#
#       ALL         $CVSROOT/CVSROOT/cvslog %p %{sVv} $USER
#
# Note that it mails everything to the address configured at the top of this
# file.
#
# You can add a specific address to the Cc list of the commit notification
# email, by adding "CCMAIL: email@addy" ON A SEPARATE LINE to the log message
# OR by surrounding the CCMAIL by "~":
# cvs ci -m"Blah blah. ~CCMAIL: email@addy, email@addy.2~ Blah blah? CVSSILENT"
#
# By adding "CVSSILENT" keyword to the log message, it is marked as "noise" ---
# it includes "(silent)" in the subject, and inserts "X-CVS-Silent: Yes" header
# to the email.
#
# TODO: Reformat log messages non-agressively.
#
# $Id: cvslog.pl,v 1.143 2005/09/12 12:11:01 pasky Exp $

use strict;
use vars qw ($project $repository $from_email $dest_email %mod_dest_email
		$reply_email $CVS $diffstat $cvsweb_url $help_msg $sync_delay
		$max_diff_lines $show_diffstat $show_diff $old_cmdargs);




### Configuration

# Project name.
$project = 'ELinks';

# The path to the repository.  If your platform or CVS implementation doesn't
# pass the full path to the cvslog script in $0 (or if your cvslog script
# isn't in the CVSROOT directory of your repository for some reason), you will
# need to explicitly set $REPOSITORY to the root directory of your repository
# (the same thing that you would set CVSROOT to).
$repository = '/home/cvs/elinks'; # ($0 =~ m#^(.*)/CVSROOT/cvslog$#);

# The from address in the generated mails.
$from_email = 'cvs@pasky.or.cz';

# Mail all reports to this address.
#$dest_email = 'elinks-cvs@v.or.cz, pasky@pasky.or.cz, fonseca@diku.dk, zas@norz.org';
$dest_email = 'elinks-cvs@v.or.cz';

# Optionally, you can override $dest_email for chosen modules by a different
# one, like this:
# $mod_dest_email{'module1'} = 'email@addy';
# $mod_dest_email{'module2'} = 'email@addy.2';
# No notifications will be sent for this module:
# $mod_dest_email{'module3'} = '';

# Email address all the replies should go at (in addition to the author's
# email, if specified in CVSROOT/users; do not forget to have the users file
# in your checkoutlist).
$reply_email = 'elinks-dev@linuxfromscratch.org';

# The cvs binary location + name (full path to the executable). If in doubt,
# try just 'cvs' and hope. Otherwise, /usr/bin/cvs or /usr/local/bin/cvs could
# do.
$CVS = '/usr/bin/cvs';

# The diffstat binary location + name (full path to the executable) plus the
# additional arguments you want to pass to it. If in doubt, keep the default
# arguments and try just 'diffstat' and hope. Otherwise, /usr/bin/diffstat or
# /usr/local/bin/diffstat could do. Just comment it out if you don't have
# diffstat enabled.
$diffstat = '/usr/bin/diffstat -p0 -w 72';

# URL of cvsweb. Just comment out if you don't have any.
$cvsweb_url = 'http://cvsweb.elinks.or.cz/cvsweb.cgi';

# The leading message of the mail:
$help_msg = "This is an automated notification of a change to the $project CVS tree.";

# Number of seconds to wait for possible concurrent instances. CVS calls up
# this script for each involved directory separately and this is the sync
# delay. 5s looks as a safe value, but feel free to increase if you are running
# this on a slower (or overloaded) machine or if you have really a lot of
# directories.
$sync_delay = 5;

# Maximum number of lines the diff can contain. If it will be longer, it is
# going to be trimmed to this length.
# Assuming that each line is avg. 60 lines long and we don't want to have mails
# bigger than 35kb, the maximum number of lines would be 597.
$max_diff_lines = 597;

# Whether you want the diffstat of changes to be sent in the message.
$show_diffstat = 1;

# Whether you want the diff of changes to be sent in the message.
$show_diff = 1;

# Whether to assume that the command line arguments for the %{sVv} format
# are passed using the old way.
$old_cmdargs = 0;



### The code itself

use vars qw (@dirs $module $user $tag $htag $logmsg $ccmail $cvssilent);



### Load input data

my (@files, %files); # two ways of accessing the same records


# Figure out who is doing the update by reading the ending $USER arg.

$user = pop(@ARGV);


# The arguments are from %p (implied for the old format) and %{sVv}.
#
# The old format: "<repository-path> <filename1>,<oldrev1>,<newrev1> ..."
#
#	All inputs are in the same string separated by spaces. First the part
#	is the relative path in the repository, then follows the list of files
#	modified with information about filename, old revision and new revision
#	each separated by commas.
#
# The new format: "<repo-path>" "<name1>" "<oldrev1>" "<newrev1>" ...
#
#	All inputs are given in separate args. First the relative path in the
#	repository and then the list of modified files where each file is
#	notified via 3 arguments being the name plus the old and the new
#	revision.

my @input;

# Which command line passing format should we accept?
if ($old_cmdargs) {
  # The old command format is handled by converting it to the new format making
  # @input reflect how @ARGV looks for the new format.
  @input = split (/[ ,]/, ($ARGV[0] or ''));
} else {
  @input = @ARGV;
}

$dirs[0]->{name} = shift @input or die "$0: no directory specified\n";

if ("@input" eq '- New directory') {
  $dirs[0]->{type} = 'directory';

} else {
  $dirs[0]->{type} = 'checkin';

  while ($#input >= 2) {
    my ($file);

    $file->{name} = shift(@input);
    $file->{oldrev} = shift(@input);
    $file->{newrev} = shift(@input);
    $file->{op} = '?';

    push (@files, $file);
    $files{$file->{name}} = $file;

    push (@{$dirs[0]->{commits}}, $file);
  }
}


# Guess module name.

$module = $dirs[0]->{name}; $module =~ s#/.*##;


# Parse stdin

my $state = 0;
my @op = ('add', 'modify', 'remove');

while (<STDIN>) {
  $tag = $1 if /^\s*Tag: ([a-zA-Z0-9_-]+)/;
  $state = 1 if /^Added Files:/;
  $state = 2 if /^Modified Files:/;
  $state = 3 if /^Removed Files:/;
  last if /^Log Message/;
  next unless $state;
  foreach (split) {
    $files{$_}->{op} = $op[$state-1];
  }
}

$htag = $tag ? $tag : "<TRUNK>";

while (<STDIN>) {
  $logmsg .= $_;
}



### Check if we want to waste time at this whole thing at all


# The following is an elinks-specific hack, as we don't want to send
# notifications about this file being changed :).

exit if ($files[0]->{name} and $files[0]->{name} eq "ChangeLog");



### Sync between the multiple instances potentially being ran simultaneously

my $sum; # _VERY_ simple hash of the log message. It is really weak, but I'm
         # lazy and it's really sorta exceptional to even get more commits
         # running simultaneously anyway.
map { $sum += ord $_ } split (//, $logmsg);

my $syncfile; # Name of the file used for syncing
$syncfile = "/tmp/cvslog.$project.$module.$sum";


if (-f $syncfile and -w $syncfile) {
  # The synchronization file for this file already exists, so we are not the
  # first ones. So let's just dump what we know and exit.

  open (FF, ">>$syncfile") or die "aieee... can't log, can't log! $syncfile blocked!";

  {
    my @t;
    foreach (@files) {
      push (@t, join(',', $_->{name}, $_->{oldrev}, $_->{newrev}, $_->{op}));
    }

    print FF join(' ', $dirs[0]->{name}, $dirs[0]->{type}, @t) . "\n";
  }

  close (FF);
  exit;

} else {
  # We are the first one! Thus, we'll fork, exit the original instance, and
  # wait a bit with the new one. Then we'll grab what the others collected and
  # go on.

  # We don't need to care about permissions since all the instances of the one
  # commit will obviously live as the same user.

  # system("touch") in a different way
  open (FF, ">>$syncfile") or die "aieee... can't log, can't log! $syncfile blocked!";
  close (FF);

  exit if (fork);
  sleep ($sync_delay);

  open (FF, $syncfile);
  my ($i) = 1;
  while (<FF>) {
    chomp;

    my ($zdir, $ztype, @zfiles) = split (' ');
    $dirs[$i]->{name} = $zdir;
    $dirs[$i]->{type} = $ztype;

    foreach (@zfiles) {
      my ($commit);
      ($commit->{name}, $commit->{oldrev}, $commit->{newrev}, $commit->{op}) = split (',');
      push (@{$dirs[$i]->{commits}}, $commit);
    }

    $i++;
  }
  close (FF);

  unlink ($syncfile);
}



### Process the log message

$ccmail = "\nCc: $1" if ($logmsg =~ /^\s*CC[-_]?MAIL[:=]\s*(.*?)$/m);
$ccmail = "\nCc: $1" if ($logmsg =~ /\~\s*CC[-_]?MAIL[:=]\s*(.*?)\s*\~/);
$cvssilent = " (silent)" if ($logmsg =~ /CVS.?SILENT/);

$ccmail ||= '';
$cvssilent ||= '';



### Send the mail


# Open our mail program

open (MAIL, '| /usr/lib/sendmail -t -oi -oem')
    or die "$0: cannot fork sendmail: $!\n";


# Fill in date

my ($date);
$date = scalar gmtime;


# Fill in subj and possibly cut it

my ($subj);
$subj = "Subject: [$project] $module".($tag?" ($tag)":"")." - $user: $logmsg$cvssilent";
$subj =~ s/[[:cntrl:]]/ /g; $subj =~ s/\s*$//;
$subj = substr ($subj, 0, 75) . '...' if (length ($subj) > 78);


# Look up the author's email

if (open (USERS, $repository . '/CVSROOT/users')) {
  while (<USERS>) {
    my ($uname, $gecos) = split (/:/);
    next unless $uname eq $user;
    next unless $gecos =~ /<([^>]*@.*)>/;
    if ($reply_email) {
      $reply_email .= ', ' . $1;
    } else {
      $reply_email = $1;
    }
    last;
  }
  close (USERS);
}


# Check the target mail addy

$dest_email = $mod_dest_email{$module} if (defined $mod_dest_email{$module});

exit unless ($dest_email);


# Compose the mail

my ($VERSION) = '$Revision: 1.143 $' =~ / (\d+\.\d+) /;

$cvssilent = "\nX-CVS-Silent: Yes" if $cvssilent;

print MAIL <<EOM;
From: $from_email
To: $dest_email$ccmail
Reply-To: $reply_email
Mail-Followup-To: $reply_email
X-CVS: $user\@$project:$module$cvssilent
User-Agent: cvslog.pl/$VERSION
$subj

$help_msg

Author: $user
Module: $module
   Tag: $htag
  Date: $date GMT
EOM

print MAIL <<EOM;

---- Log message:

$logmsg
EOM

print MAIL <<EOM;

---- Files affected:

EOM


# List the files being changed, plus the cvsweb URLs

for (my $i = 0; $i < @dirs; $i++) {
  my $dirs = $dirs[$i];
  my $dir = $dirs->{name};

  print MAIL "$dir:\n";

  if ($dirs[$i]->{type} eq 'directory') {
    print MAIL "   New directory\n";

  } else {
    my $commits = $dirs->{commits};

    for (my $j = 0; $j < @$commits; $j++) {
      my $commit = $commits->[$j];
      my ($name, $oldrev, $newrev, $op) = ($commit->{name}, $commit->{oldrev}, $commit->{newrev}, $commit->{op});
      print MAIL "   $name ($oldrev -> $newrev) ";
      print MAIL " (new)" if ($op eq 'add');
      print MAIL " (removed)" if ($op eq 'remove');
      print MAIL " (?! contact pasky)" if ($op eq '?');
      print MAIL "\n";
      print MAIL "    $cvsweb_url/$dir/$name.diff?r1=$oldrev&r2=$newrev&f=u\n"
        if defined $cvsweb_url and $op ne 'add' and $op ne 'remove';
    }
  }
}


goto end_diff unless $show_diff or $show_diffstat;

print MAIL <<EOM;


---- Diffs:

EOM


# And now the diffs!

my @diff;

for (my $i = 0; $i < @dirs; $i++) {
  my $dirs = $dirs[$i];
  my $dir = $dirs->{name};

  next if ($dirs[$i]->{type} ne 'checkin');

  my $commits = $dirs->{commits};

  for (my $j = 0; $j < @$commits; $j++) {
    my $commit = $commits->[$j];

    my $oldrev = $commit->{oldrev}; $oldrev = '0.0' if ($oldrev eq 'NONE');
    my $newrev = $commit->{newrev};
    my $name = $commit->{name};

    # Do not print diffs of removed files. Too boring.
    next if ($newrev eq 'NONE');

    my @difflines;

    my $pid = open (CVS, '-|');
    if (!defined $pid) {
      die "$0: can't fork cvs: $!\n";
    } elsif ($pid == 0) {
      open (STDERR, '>&STDOUT') or die "$0: can't reopen stderr: $!\n";
      exec ($CVS, '-fnQq', '-d', $repository, 'rdiff', '-kk', '-u',
            '-r', $oldrev, '-r', $newrev, $dir.'/'.$name)
		or die "$0: can't fork cvs: $!\n";
    } else {
      @difflines = <CVS>;
      close CVS;
      if (@difflines > 1 and $difflines[1] =~ /failed to read diff file header/) {
        @difflines = ($difflines[0], "<<Binary file>>\n");
      }
    }

    push (@diff, @difflines);
  }
}

# Diffstat

my $dstmp = '/tmp/cvslog.diffstat.'.$$;
if ($diffstat and $show_diffstat and open (DSTMP, '>' . $dstmp)) {
  print DSTMP @diff;
  close DSTMP;
  my $pid = open (DIFFSTAT, '-|');
  if (!defined $pid) {
    die "$0: can't fork diffstat: $!\n";
  } elsif ($pid == 0) {
    open (STDERR, '>&STDOUT') or die "$0: can't reopen stderr: $!\n";
    exec (split(/\s+/, $diffstat), $dstmp)
      	or die "$0: can't fork diffstat: $!\n";
  } else {
    my @diffstat = <DIFFSTAT>;
    close DIFFSTAT;
    print MAIL @diffstat;
    print MAIL "\n\n";
  }
  unlink ($dstmp);
}

goto end_diff unless $show_diff;

if (@diff > $max_diff_lines) {
  @diff = splice(@diff, 0, $max_diff_lines);
  print MAIL @diff;
  print MAIL "<<Diff was trimmed, longer than $max_diff_lines lines>>\n";
} else {
  print MAIL @diff;
}

end_diff:


# Send it to the world

close MAIL;
die "$0: sendmail exit status " . $? >> 8 . "\n" unless ($? == 0);


# vi: set sw=2:
