#!/usr/bin/perl
#
# Perl filter to handle the log messages from the checkin of files in
# a directory.  This script will group the lists of files by log
# message, and mail a single consolidated log message at the end of
# the commit.
#
# This file assumes a pre-commit checking program that leaves the
# names of the first and last commit directories in a temporary file.
#
# Contributed by David Hampton <hampton@cisco.com>
# Roy Fielding removed useless code and added log/mail of new files
# Jon Jensen <jon@redhat.com> messed around with a bunch of things

############################################################
#
# Configurable options
#
############################################################
#
# Where do you want the RCS ID and delta info?
# 0 = none,
# 1 = in mail only,
# 2 = rcsids in both mail and logs.
#
$rcsidinfo = 2;

############################################################
#
# Constants
#
############################################################
$STATE_NONE    = 0;
$STATE_CHANGED = 1;
$STATE_ADDED   = 2;
$STATE_REMOVED = 3;
$STATE_LOG     = 4;

$TMPDIR        = $ENV{TMPDIR} || '/tmp';
$FILE_PREFIX   = '#cvs.';

$LAST_FILE     = "$TMPDIR/${FILE_PREFIX}lastdir";
$CHANGED_FILE  = "$TMPDIR/${FILE_PREFIX}files.changed";
$ADDED_FILE    = "$TMPDIR/${FILE_PREFIX}files.added";
$REMOVED_FILE  = "$TMPDIR/${FILE_PREFIX}files.removed";
$LOG_FILE      = "$TMPDIR/${FILE_PREFIX}files.log";
$BRANCH_FILE   = "$TMPDIR/${FILE_PREFIX}files.branch";
$SUMMARY_FILE  = "$TMPDIR/${FILE_PREFIX}files.summary";
$COUNT_FILE    = "$TMPDIR/${FILE_PREFIX}files.count";

$CVSROOT       = $ENV{CVSROOT};

%MAIL_MAP      = qw(
);
$MAIL_TO_ELSE  = 'cvs-log@localhost';

############################################################
#
# Subroutines
#
############################################################

sub format_names {
    local($dir, @files) = @_;
    local(@lines);

    $lines[0] = sprintf(" %-08s", $dir);
    foreach $file (@files) {
		if (length($lines[$#lines]) + length($file) > 60) {
			$lines[++$#lines] = sprintf(" %8s", " ");
		}
		$lines[$#lines] .= " ".$file;
    }
    @lines;
}

sub cleanup_tmpfiles {
    local(@files);

    opendir(DIR, $TMPDIR);
    push(@files, grep(/^${FILE_PREFIX}.*\.${id}$/, readdir(DIR)));
    closedir(DIR);
    foreach (@files) {
		unlink "$TMPDIR/$_";
    }
}

sub write_logfile {
    local($filename, @lines) = @_;

    open(FILE, ">$filename") || die ("Cannot open log file $filename: $!\n");
    print(FILE join("\n", @lines), "\n");
    close(FILE);
}

sub append_to_file {
    local($filename, $dir, @files) = @_;

    if (@files) {
		local(@lines) = &format_names($dir, @files);
		open(FILE, ">>$filename") || die ("Cannot open file $filename: $!\n");
		print(FILE join("\n", @lines), "\n");
		close(FILE);
    }
}

sub write_line {
    local($filename, $line) = @_;

    open(FILE, ">$filename") || die("Cannot open file $filename: $!\n");
    print(FILE $line, "\n");
    close(FILE);
}

sub append_line {
    local($filename, $line) = @_;

    open(FILE, ">>$filename") || die("Cannot open file $filename: $!\n");
    print(FILE $line, "\n");
    close(FILE);
}

sub read_line {
    local($filename) = @_;
    local($line);

    open(FILE, "<$filename") || die("Cannot open file $filename: $!\n");
    $line = <FILE>;
    close(FILE);
    chomp($line);
    $line;
}

sub read_file {
    local($filename, $leader) = @_;
    local(@text) = ();

    open(FILE, "<$filename") || return ();
    while (<FILE>) {
		chomp;
		push @text, sprintf("%-10s", $leader) . $_;
    }
    close FILE;
    @text;
}

sub read_logfile {
    local($filename, $leader) = @_;
    local(@text) = ();

    open(FILE, "<$filename") || die ("Cannot open log file $filename: $!\n");
    while (<FILE>) {
		chomp;
		push(@text, $leader.$_);
    }
    close(FILE);
    @text;
}

#
# do a 'cvs -Qn status' on each file in the arguments, and extract info.
#
sub change_summary {
    local($out, @filenames) = @_;
    local(@revline);
    local($file, $rev, $rcsfile, $line);

    while (@filenames) {

		$file = shift @filenames;
		next if $file eq "";

		open(RCS, "-|") || exec 'cvs', '-Qn', 'status', $file;

		$rev = "";
		$delta = "";
		$rcsfile = "";

		while (<RCS>) {
			if (/^[ \t]*Repository revision/) {
				chomp;
				@revline = split(' ', $_);
				$rev = $revline[2];
				$rcsfile = $revline[3];
				$rcsfile =~ s,^$CVSROOT/,,;
				$rcsfile =~ s/,v$//;
			}
		}
		close(RCS);

		if ($rev ne '' && $rcsfile ne '') {
			open(RCS, "-|") || exec 'cvs', '-Qn', 'log', "-r$rev", $file;
			while (<RCS>) {
				if (/^date:/) {
					chomp;
					$delta = $_;
					$delta =~ s/^.*;//;
					$delta =~ s/^[\s]+lines://;
				}
			}
			close(RCS);
		}

		$diff = "\n\n";

		# If this is a binary file, don't try to report a diff; not only is
		# it meaningless, but it also screws up some mailers.  We rely on
		# Perl's 'is this binary' algorithm; it's pretty good.
		if (-B $file) {
			$filetype = `file $file`;
			chomp $filetype;
			$diff .= "<<$filetype>>\n\n";
		}

		# Get the differences between this and the previous revision,
		# being aware that new files always have revision '1.1' and
		# new branches always end in '.n.1'.
		elsif ($rev =~ /^(.*)\.([0-9]+)$/) {
			$prev = $2 - 1;
			$prev_rev = $1 . '.' .  $prev;
			$prev_rev =~ s/\.[0-9]+\.0$//;  # Truncate if first rev on branch    

			$diff .= "rev $rev, prev_rev $prev_rev\n";

#		    if ($rev eq '1.1' || $prev_rev eq '1.1') {
			if ($rev eq '1.1') {
				open(DIFF, "-|") || exec 'cvs', '-Qn', 'update', '-p',
										 "-r$rev", $file;
				$diff .= "Index: $file\n=================================="
						 . "=================================\n";
			}
			else {
				open(DIFF, "-|") || exec 'cvs', '-Qn', 'diff', '-u',
										 "-r$prev_rev", "-r$rev", $file;
			}

			while (<DIFF>) {
				$diff .= $_;
			}
			close(DIFF);
			$diff .= "\n\n";
		}

		&append_line($out, sprintf("%-9s%-12s%s", $rev, $delta, $rcsfile));
		&append_line($out, $diff);
    }
}


sub build_header {
    my $header;
    my ($sec,$min,$hour,$mday,$mon,$year) = gmtime();

    $header  = "User:      $login\n";
	$header .= "Date:      " . sprintf(
		"%04d-%02d-%02d %02d:%02d:%02d GMT",
		$year+1900, $mon+1, $mday, $hour, $min, $sec
	);
}

# Mailing-list and history file mappings here
sub mlist_map
{
    local($path) = @_;

    if ($path =~ /^([^\/]+)/ and $MAIL_MAP{$1}) {
		return $MAIL_MAP{$1};
	} else {
		return $MAIL_TO_ELSE;
	}
}    

sub do_changes_file
{
	# we don't use this for Interchange at the moment
	return;

    local($category, @text) = @_;
    local($changes);

    $changes = "$CVSROOT/CVSROOT/commitlogs/$category";

    if (open(CHANGES, ">>$changes")) {
		print CHANGES $_, "\n" for @text;
		print CHANGES "\n";
        close(CHANGES);
    } else { 
        warn "Cannot open $changes: $!\n";
    }
}

sub mail_notification
{
    my (@text) = @_;
	my ($repos, $file, $subject);

	$file = $ARGV[0];
	$repos = $1 . ' - ' if $file =~ s:^([^/ ]+)[/ ]::;

	if ($ARGV[0] =~ /New directory/) {
		$file =~ s/\s.*//;
		$subject = $repos . $login . ' added directory ' . $file;
	} else {
		$file =~ s:\s+:/:;
		$subject = $repos . $login . ' modified ';
		my $count = scalar( &read_logfile("$COUNT_FILE.$id", "") );
		$subject .= ($count > 1) ? "$count files" : $file;
	}

	$subject =~ s/"/\\"/g;

	for my $to ($MAIL_TO) {
		#print "Mailing the commit message to '$to' ...\n";
		open MAIL, qq{| $CVSROOT/CVSROOT/mailout -s "$subject" -f $to $to};
		print MAIL $_, "\n" for @text;
		close MAIL;
	}

#    open(MAIL, qq{| /usr/bin/Mail -s "$subject" $MAIL_TO});
#    open(MAIL, "| /usr/exim/bin/exim -t -odq");
#    print MAIL "To: $MAIL_TO\n";
#    print MAIL "Subject: cvs commit: $ARGV[0]\n";
#    print MAIL "\n";

}

#############################################################
#
# Main Body
#
############################################################
#
# Setup environment
#
umask (002);

#
# Initialize basic variables
#
$id = getpgrp();
$state = $STATE_NONE;
$login = $ENV{USER} || getlogin || (getpwuid($<))[0] || sprintf("uid#%d",$<);
@files = split(' ', $ARGV[0]);
@path = split('/', $files[0]);
$repository = $path[0];
if ($#path == 0) {
    $dir = ".";
} else {
    $dir = join('/', @path[1..$#path]);
}
#print("ARGV  - ", join(":", @ARGV), "\n");
#print("files - ", join(":", @files), "\n");
#print("path  - ", join(":", @path), "\n");
#print("dir   - ", $dir, "\n");
#print("id    - ", $id, "\n");

#
# Map the repository directory to a name for commitlogs.
#
$MAIL_TO = &mlist_map($files[0]);

##########################
#
# Check for a new directory first.  This will always appear as a
# single item in the argument list, and an empty log message.
#
if ($ARGV[0] =~ /New directory/) {
	&append_line("$COUNT_FILE.$id", "NEW-DIRECTORY");
    $header = &build_header;
    @text = ();
    push(@text, $header);
	push(@text, "");
    push(@text, $ARGV[0]);
    &do_changes_file($mlist, @text);
    &mail_notification(@text) if defined($MAIL_TO);
    exit 0;
}

# add filenames for this pass for future counting
&append_line("$COUNT_FILE.$id", join("\n", @files[1..$#files]));

#
# Iterate over the body of the message collecting information.
#
while (<STDIN>) {
    chomp;
    if (/^Revision\/Branch:/) {
        s,^Revision/Branch:,,;
        push (@branch_lines, split);
        next;
    }
#    next if (/^[ \t]+Tag:/ && $state != $STATE_LOG);
    if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
    if (/^Added Files/)    { $state = $STATE_ADDED;   next; }
    if (/^Removed Files/)  { $state = $STATE_REMOVED; next; }
    if (/^Log Message/)    { $state = $STATE_LOG;     next; }
    s/\s+$//;
    
    push (@changed_files, split) if ($state == $STATE_CHANGED);
    push (@added_files,   split) if ($state == $STATE_ADDED);
    push (@removed_files, split) if ($state == $STATE_REMOVED);
    if ($state == $STATE_LOG) {
		if (/^PR:$/i ||
			/^Reviewed by:$/i ||
			/^Submitted by:$/i ||
			/^Obtained from:$/i) {
			next;
		}
		push (@log_lines, $_);
    }
}

#
# Strip leading and trailing blank lines from the log message.  Also
# compress multiple blank lines in the body of the message down to a
# single blank line.
# (Note, this only does the mail and changes log, not the rcs log).
#
while ($#log_lines > -1) {
    last if ($log_lines[0] ne "");
    shift(@log_lines);
}
while ($#log_lines > -1) {
    last if ($log_lines[$#log_lines] ne "");
    pop(@log_lines);
}
for ($i = $#log_lines; $i > 0; $i--) {
    if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
		splice(@log_lines, $i, 1);
    }
}

#
# Find the log file that matches this log message
#
for ($i = 0; ; $i++) {
    last if (! -e "$LOG_FILE.$i.$id");
    @text = &read_logfile("$LOG_FILE.$i.$id", "");
    last if ($#text == -1);
    last if (join(" ", @log_lines) eq join(" ", @text));
}

#
# Spit out the information gathered in this pass.
#
&write_logfile("$LOG_FILE.$i.$id", @log_lines);
&append_to_file("$BRANCH_FILE.$i.$id",  $dir, @branch_lines);
&append_to_file("$ADDED_FILE.$i.$id",   $dir, @added_files);
&append_to_file("$CHANGED_FILE.$i.$id", $dir, @changed_files);
&append_to_file("$REMOVED_FILE.$i.$id", $dir, @removed_files);
if ($rcsidinfo) {
    &change_summary("$SUMMARY_FILE.$i.$id", (@changed_files, @added_files));
}

#
# Check whether this is the last directory.  If not, quit.
#
if (-e "$LAST_FILE.$id") {
	$_ = &read_line("$LAST_FILE.$id");
	$tmpfiles = $files[0];
	$tmpfiles =~ s,([^a-zA-Z0-9_/]),\\$1,g;
	if (! grep(/$tmpfiles$/, $_)) {
		print "More commits to come...\n";
		exit 0
	}
}

#
# This is it.  The commits are all finished.  Lump everything together
# into a single message, fire a copy off to the mailing list, and drop
# it on the end of the Changes file.
#
$header = &build_header();

#
# Produce the final compilation of the log messages
#
@text = ();
push(@text, $header);
for ($i = 0; ; $i++) {
    last if (! -e "$LOG_FILE.$i.$id");
    push(@text, &read_file("$BRANCH_FILE.$i.$id", "Branch:"));
    push(@text, &read_file("$CHANGED_FILE.$i.$id", "Modified:"));
    push(@text, &read_file("$ADDED_FILE.$i.$id", "Added:"));
    push(@text, &read_file("$REMOVED_FILE.$i.$id", "Removed:"));
    push(@text, "Log:");
    push(@text, &read_logfile("$LOG_FILE.$i.$id", ""));
    if ($rcsidinfo == 2) {
		if (-e "$SUMMARY_FILE.$i.$id") {
			push(@text, "");
			push(@text, "Revision  Changes    Path");
			push(@text, &read_logfile("$SUMMARY_FILE.$i.$id", ""));
		}
    }
    push(@text, "");
}
#
# Append the log message to the commitlogs/<module> file
#
&do_changes_file($mlist, @text);
#
# Now generate the extra info for the mail message..
#
if ($rcsidinfo == 1) {
    $revhdr = 0;
    for ($i = 0; ; $i++) {
		last if ! -e "$LOG_FILE.$i.$id";
		if (-e "$SUMMARY_FILE.$i.$id") {
			if (!$revhdr++) {
				push @text, "Revision  Changes    Path";
			}
			push @text, &read_logfile("$SUMMARY_FILE.$i.$id", "");
		}
    }
	if ($revhdr) {
		push @text, "";	# consistency...
    }
}
#
# Mail out the notification.
#
&mail_notification(@text) if defined $MAIL_TO;
&cleanup_tmpfiles;
exit 0;
