LDP/CVSROOT/cvslog.pl

504 lines
13 KiB
Perl
Executable File

#!/usr/bin/perl -w
#
# cvslog -- Mail the CVS log message to a given address.
#
# Loosely based on cvslog by Russ Allbery <rra@stanford.edu>
# Copyright 1998 Board of Trustees, Leland Stanford Jr. University
#
# Copyright 2001, 2003, 2004 Petr Baudis <pasky@ucw.cz>
#
# 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 do less (it
# doesn't do cvs status).
#
# It should be run from loginfo with something like:
#
# ALL $CVSROOT/CVSROOT/cvslog %{sVv} $USER
#
# Note that it mails everything to the address configured at the top of this
# file.
#
# TODO: attach diffs (under some length, possibly a diffstat instead),
# non-agressively reformat log messages
#
# $Id$
use strict;
use vars qw ($x_mailer $project $repository $from_email $dest_email $reply_email
$CVS $diffstat $cvsweb_url $help_msg $sync_delay $max_diff_lines
$show_diffstat $show_diff $login $subj_files $subj_maxlength
$withthreading_email $messageid_email $mail_contenttype $mail_contenttransen);
### Configuration
# Project name.
$project = 'LDP';
# 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 = '/cvsroot'; # ($0 =~ m#^(.*)/CVSROOT/cvslog$#);
# The from address in the generated mails.
$login = $ENV{'CVS_USER'} || $ENV{'CVS_USERNAME'} || $ENV{'CVSUSER'} ||
getlogin || (getpwuid($<))[0] || "nobody";
$from_email = "$login <$login\@tldp.org>";
# Mail all reports to this address.
#$dest_email = 'elinks-cvs@v.or.cz, pasky@pasky.ji.cz, fonseca@diku.dk, zas@norz.org';
$dest_email = 'cvs-commits@en.tldp.org';
#for testing, sometimes useful:
#$dest_email = 'ser@metalab.unc.edu';
# Email address all the replies should go at.
$reply_email = 'discuss@en.tldp.org';
# X-mailer for better identification
$x_mailer = '$Id$';
# suffix of Message-ID in email header
$messageid_email = '@tldp.org';
# with threading?
$withthreading_email = 1;
# mail content-type
$mail_contenttype="text/plain; charset=iso-8859-1";
# mail content-transfer-encoding
$mail_contenttransen="8bit";
# 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://cvsview.tldp.org/index.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;
# Maximal 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 maximal 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 = 0;
# Whether you want the diff of changes to be sent in the message.
$show_diff = 1;
# Whether you want the affected files list in subject
$subj_files=1;
# How long the subject can be
$subj_maxlength=78;
### The code itself
use vars qw (@dirs $module $user $tag $htag $logmsg);
### Load input data
my (@files, %files); # two ways of accessing the same records
# The arguments are from %{sVv}; first the relative path in the repository
# and then the list of files modified.
my @input = split (' ', ($ARGV[0] or ''));
$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';
foreach (@input) {
my ($file);
($file->{name}, $file->{oldrev}, $file->{newrev}) = split (',');
$file->{op} = '?';
push (@files, $file);
$files{$file->{name}} = $file;
push (@{$dirs[0]->{commits}}, $file);
}
}
# Guess module name.
$module = $dirs[0]->{name}; $module =~ s#/.*##;
# Figure out who is doing the update.
$user = $ARGV[1];
# 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 : "HEAD";
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("\t", $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 ("\t");
$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);
}
### Send the mail
# Open our mail program
open (MAIL, '| /var/qmail/bin/sendmail -t -oi -oem')
or die "$0: cannot fork sendmail: $!\n";
# Fill in date
my ($date);
$date = scalar gmtime;
# Compose the mail
# TODO: Use CVSROOT/users to determine the committer's realname and email and
# add it to the reply-to / mail-followup-to list. --pasky
my ($VERSION) = '$Revision$' =~ / (\d+\.\d+) /;
# Subject files, references and
# future "files affected part" (List the files being changed, plus the cvsweb URLs)
my $files_affected_part="";
my $subj_file_list="";
my $references="";
my $messageid="";
my $cvsweb_part="";
for (my $i = 0; $i < @dirs; $i++) {
my $dirs = $dirs[$i];
my $dir = $dirs->{name};
my $subdir = $dir;
$subdir =~ s!^[^/]*/?!!;
$subdir .= '/' if $subdir ne '';
$files_affected_part .= "\n$dir:\n ";
if ($dirs[$i]->{type} eq 'directory') {
$files_affected_part .= "New directory\n";
} else {
my $commits = $dirs->{commits};
for (my $j = 0; $j < @$commits; $j++) {
my $ref;
my $commit = $commits->[$j];
my ($name, $oldrev, $newrev, $op) = ("NAME","NONE","NONE",-1);
($name, $oldrev, $newrev, $op) = ($commit->{name}, $commit->{oldrev}, $commit->{newrev}, $commit->{op});
$subj_file_list .= "$subdir$name";
$subj_file_list .= " (NEW)" if ($op eq 'add');
$subj_file_list .= " (REMOVED)" if ($op eq 'remove');
$subj_file_list .= ", ";
$files_affected_part .= "$name ($oldrev -> $newrev) ";
$files_affected_part .= " (NEW)" if ($op eq 'add');
$files_affected_part .= " (REMOVED)" if ($op eq 'remove');
$files_affected_part .= " (?! contact devils)" if ($op eq '?');
$files_affected_part .= ", " if $j < @$commits-1;
# $files_affected_part .= "\n";
$cvsweb_part .= " $cvsweb_url/$dir/$name?r1=$oldrev&r2=$newrev&f=u\n"
if defined $cvsweb_url and $op ne 'add' and $op ne 'remove';
$ref=$dir."_".$name.".".$newrev;
$ref =~ s/[^a-zA-Z0-9.]/_/g;
$messageid="<".$ref.$messageid_email.">";
$ref=$dir."_".$name.".".$oldrev;
$ref =~ s/[^a-zA-Z0-9.]/_/g;
$references.=" <". $ref. $messageid_email .">";
}
}
}
# Fill in subj and possibly cut it
my ($subj);
$subj_file_list =~ s/, $/ /;
$subj = "Subject: $module".($tag?" ($tag)":"").": ".($subj_files?$subj_file_list:"").$logmsg;
$subj =~ s/\n/ /g; $subj =~ s/ *$//;
$subj = substr($subj, 0, $subj_maxlength-3) . '...' if (length($subj) > $subj_maxlength);
my $keywords="DEV-$user, MOD-$module, TAG-".($tag?$tag:"HEAD").", ". $subj_file_list;
print MAIL <<EOM;
From: $from_email
To: $dest_email
Reply-To: $reply_email
Mail-Followup-To: $reply_email
X-CVS: $user\@$project:$module
X-CVS-Module: $module
User-Agent: cvslog.pl/$VERSION
$subj
Message-ID: $messageid
X-Mailer: $x_mailer
MIME-Version: 1.0
Content-Transfer-Encoding: $mail_contenttransen
Content-Type: $mail_contenttype
Keywords: $keywords
EOM
if ($withthreading_email) {
print MAIL <<EOM;
References: $references
In-Reply-To: $references
EOM
}
my $msgheader = sprintf ("%-36s %s\n%-36s %s", "Author: $user", "Date: $date GMT","Module: $module"," Tag: $htag");
$logmsg =~ s/\n+/\n/gm;
print MAIL <<EOM;
$msgheader
EOM
print MAIL <<EOM;
---- Log message:
$logmsg
EOM
print MAIL <<EOM;
---- Files affected:$files_affected_part
EOM
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, "\n" . ("=" x 64) . "\n");
push (@diff, @difflines);
}
}
push (@diff, ("=" x 64) . "\n");
# 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";
}
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:
if ($cvsweb_part ne ""){
print MAIL <<EOM;
---- CVS-web:
$cvsweb_part
EOM
}
# Send it to the world
close MAIL;
die "$0: sendmail exit status " . $? >> 8 . "\n" unless ($? == 0);
# vi: set sw=2: