#!/usr/bin/perl
# -*-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>
#
# hacked greatly by Greg A. Woods <woods@web.net>

# Usage: log_accum.pl [-d] [-s] [-M module] [[-m mailto] ...] [-f logfile]
#	-d		- turn on debugging
#       -G product      - interface to Bugzilla
#	-m mailto	- send mail to "mailto" (multiple)
#	-M modulename	- set module name to "modulename"
#	-f logfile	- write commit messages to logfile too
#	-s		- *don't* run "cvs status -v" for each file
#       -T text         - use TEXT in temp file names.
#       -C name         - Generate cvsweb URLS; must be run using %{sVv}
#                         format string. Set cvsroot= to "name". Use
#                         -C NONE to pass cvsroot="".
#       -U URL          - Base URL for cvsweb if -C option (above) is used.
#       -D DOMAIN       - Domain from which mail should appear
#       -l              - Update list of modified files in CVSROOT/commits.  
#                         Must be run using %{sVv}.
#       -i              - Generate SQL INSERT statements for CVSQL.

use POSIX;
use DBI;

#
#	Configurable options
#

# This MUST match TMPDIR defined in commit_prep
$TMPDIR = "/var/tmp/cvs";

# Set this to something that takes "-s"
$MAILER	       = "/usr/bin/Mail";
$BMAILER       = "/usr/sbin/sendmail";

# Parameter -D dom.ain will be appended to these e-mail addresses
$cvslist       = "prj-vcs";
$bugzilla_mail = "bugzilla";

# BZ DB database/username/password
# If you do not want to reveal these you may disable the SQL connection
# as it is only a sanity check. So it is security vs sanity. YMMV.
$database      = "dbi:mysql:bugs";
$username      = "bugs";
$password      = "bugs";

# Base name of cvsweb specification
$CVSWEB_URL = "http://www.dom.ain/cgi-bin/cvsweb.cgi/";

# Constants (don't change these!)
#
$STATE_NONE    = 0;
$STATE_CHANGED = 1;
$STATE_ADDED   = 2;
$STATE_REMOVED = 3;
$STATE_LOG     = 4;

#
#	Subroutines
#
sub see_if_bugzilla_bug_exists {    
  local ($dbh, $product, $id) = @_;
  my $sth1 = $dbh->prepare ("SELECT id from products where name = \'$product\'") or return 0;
  $sth1->execute() or return 0;
  my $product_id = $sth1->fetchrow_array ();
    if (!$product_id)
      {
	return 0;
      }
  my $sth2 = $dbh->prepare ("SELECT COUNT(*) from bugs where bug_id = $id and product_id = $product_id") or return 0;
  $sth2->execute() or return 0;
  my $count = $sth2->fetchrow_array ();
  return $count > 0;
}
sub set_temp_vars {
  local ($name) = @_;
  
  $LAST_FILE     = sprintf ("$TMPDIR/#%s.lastdir", $name);
  
  $CHANGED_FILE  = sprintf ("$TMPDIR/#%s.files.changed", $name);
  $ADDED_FILE    = sprintf ("$TMPDIR/#%s.files.added", $name);
  $REMOVED_FILE  = sprintf ("$TMPDIR/#%s.files.removed", $name);
  $LOG_FILE      = sprintf ("$TMPDIR/#%s.files.log", $name);
  $URL_FILE      = sprintf ("$TMPDIR/#%s.files.urls", $name);
  $SQL_FILE      = sprintf ("$TMPDIR/#%s.files.sql", $name);
  $COMMITS_FILE  = sprintf ("$TMPDIR/#%s.files.commits", $name);
  
  # Quote for use in a regexp.
  ($FILE_PREFIX   = sprintf ("#%s.files", $name)) =~ s/(\W)/\\$1/g;
}

sub cleanup_tmpfiles {
    local($wd, @files);

    $wd = `pwd`;
    chdir("$TMPDIR") || die("Can't chdir(\"$TMPDIR\")\n");
    opendir(DIR, ".");
    push(@files, grep(/^$FILE_PREFIX\..*\.$id$/, readdir(DIR)));
    closedir(DIR);
    foreach (@files) {
	unlink $_;
    }
    unlink $LAST_FILE . "." . $id;

    chdir($wd);
}

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 format_names {
    local($dir, @files) = @_;
    local(@lines);

    if ($dir =~ /^\.\//) {
	$dir = $';
    }
    if ($dir =~ /\/$/) {
	$dir = $`;
    }
    if ($dir eq "") {
	$dir = ".";
    }

    $format = "\t%-" . sprintf("%d", length($dir) > 15 ? length($dir) : 15) . "s%s ";

    $lines[0] = sprintf($format, $dir, ":");

    if ($debug) {
	print STDERR "format_names(): dir = ", $dir, "; files = ", join(":", @files), ".\n";
    }
    foreach $file (@files) {
	if (length($lines[$#lines]) + length($file) > 65) {
	    $lines[++$#lines] = sprintf($format, " ", " ");
	}
	$lines[$#lines] .= $file . " ";
    }

    @lines;
}

sub format_lists {
    local(@lines) = @_;
    local(@text, @files, $lastdir);

    if ($debug) {
	print STDERR "format_lists(): ", join(":", @lines), "\n";
    }
    @text = ();
    @files = ();
    $lastdir = shift @lines;	# first thing is always a directory
    if ($lastdir !~ /.*\/$/) {
	die("Damn, $lastdir doesn't look like a directory!\n");
    }
    foreach $line (@lines) {
	if ($line =~ /.*\/$/) {
	    push(@text, &format_names($lastdir, @files));
	    $lastdir = $line;
	    @files = ();
	} else {
	    push(@files, $line);
	}
    }
    push(@text, &format_names($lastdir, @files));

    @text;
}

sub accum_subject {
    local(@lines) = @_;
    local(@files, $lastdir);

    $lastdir = shift @lines;	# first thing is always a directory
    @files = ($lastdir);
    if ($lastdir !~ /.*\/$/) {
	die("Damn, $lastdir doesn't look like a directory!\n");
    }
    foreach $line (@lines) {
	if ($line =~ /.*\/$/) {
	    $lastdir = $line;
	    push(@files, $line);
	} else {
	    push(@files, $lastdir . $line);
	}
    }

    @files;
}

sub compile_subject {
    local(@files) = @_;
    local($text, @a, @b, @c, $dir, $topdir, $topdir_length);

    # find the highest common directory
    $dir = '-';
    do {
	$topdir = $dir;
	foreach $file (@files) {
	    if ($file =~ /.*\/$/) {
		if ($dir eq '-') {
		    $dir = $file;
		} else {
		    if (index($dir,$file) == 0) {
			$dir = $file;
		    } elsif (index($file,$dir) != 0) {
			@a = split /\//,$file;
			@b = split /\//,$dir;
			@c = ();
			CMP: while ($#a > 0 && $#b > 0) {
			    if ($a[0] eq $b[0]) {
				push(@c, $a[0]);
				shift @a;
				shift @b;
			    } else {
				last CMP;
			    }
			}
			$dir = join('/',@c) . '/';
		    }
		}
	    }
	}
    } until $dir eq $topdir;

    # strip out directories and the common prefix topdir.
    chop $topdir;

    if ($topdir eq "") {
        @c = $modulename;
        $topdir_length = 0;
    } else {
        if ($topdir eq ".") {
            @c = $modulename;
        } else {
            @c = ($modulename . '/' . $topdir);
        }
        $topdir_length = length ($topdir) + 1;
    }

    foreach $file (@files) {
        if ($file !~ /.*\/$/) {
            push(@c, substr($file, $topdir_length));
        }
    }

    # put it together and limit the length.
    $text = join(' ',@c);
    if (length($text) > 50) {
	$text = substr($text, 0, 46) . ' ...';
    }

    $text;
}

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

    if (@files) {
	open(FILE, ">>$filename") || die("Cannot open file $filename.\n");
	if (defined ($dir)) {
            print FILE $dir, "\n";
        }
	print FILE join("\n", @files), "\n";
	close(FILE);
    }
}

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

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

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

    open(FILE, "<$filename");
    while (<FILE>) {
	chop;
	push(@text, $leader.$_);
    }
    close(FILE);
    @text;
}

sub build_header {
    local($header);
    local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
    $header = sprintf("CVSROOT:\t%s\nModule name:\t%s\n",
		      $cvsroot,
		      $modulename);
    if (defined($branch)) {
	$header .= sprintf("Branch: \t%s\n",
		      $branch);
    }
    $header .= sprintf("Changes by:\t%s@%s\t%04d-%02d-%02d %02d:%02d:%02d",
		      $login, $hostdomain,
		      $year + 1900, $mon+1, $mday,
		      $hour, $min, $sec);
}

sub mail_notification {
    local($name, $subject, @text) = @_;
    open(MAIL, "| $MAILER -s \"$subject\" $name");
    print MAIL join("\n", @text), "\n";
    close(MAIL);
}
sub mail_bug_notification {
    local($name, $subject, @text) = @_;
    open(MAIL, "| $BMAILER -f\"$cvslist\@$hostdomain\" $name");
    print MAIL "From: $cvslist\@$hostdomain\n";
    print MAIL "Subject: $subject\n";
    print MAIL "To: $name\n";
    print MAIL "\n";
    print MAIL join("\n", @text), "\n";
    close(MAIL);
}

sub write_commitlog {
    local($logfile, @text) = @_;

    open(FILE, ">>$logfile");
    print FILE join("\n", @text), "\n\n";
    close(FILE);
}

# Return a sequence of SQL inserts.
sub generate_cvsql_inserts {
    local ($dir, $branch, @files) = @_;
    local (@sp, @result);

    if ($branch eq '') {
	$branch = 'null';
    } else {
	$branch = '\'' . $branch . '\'';
    }

    foreach (@files) {
	# List is (FILE OLD-REV NEW-REV).
	@sp = split (',');
	if ($sp[1] eq 'NONE') {
	    $sp[1] = 'null';
	} else {
	    $sp[1] = '\'' . $sp[1] . '\'';
	}
	if ($sp[2] eq 'NONE') {
	    $sp[2] = 'null';
	} else {
	    $sp[2] = '\'' . $sp[2] . '\'';
	}
	push (@result, 'INSERT INTO cvs_change (filename,prerev,postrev,branch,changeset) values (\'' .
	      $dir . '/' . $sp[0] . '\',' . $sp[1] . ',' . $sp[2] . ',' . $branch . ',:id)');
    }
    return @result;
}

# Return a list of all cvsweb URLs for this commit.
sub generate_cvsweb_urls {
    local ($dir, $branch, @files) = @_;
    local (@sp, @result);
    local ($start) = $CVSWEB_URL . $dir . '/';
    $cvsweb_name = undef if ($cvsweb_name eq 'NONE');
    local ($args) = '.diff?cvsroot=' . $cvsweb_name;
    if ($branch ne '') {
	$args .= '&only_with_tag=' . $branch;
    }
    local ($r1);
    foreach (@files) {
	# List is (FILE OLD-REV NEW-REV).
	@sp = split (',');
	$r1 = $sp[1];
	if ($r1 eq 'NONE') {
	    # This lets us make a diff corresponding to the first
	    # revision.
	    $r1 = '0';
	}
	push (@result, ($start . $sp[0] . $args
			. '&f=h' . '&r1=' . $sp[1] . '&r2=' . $sp[2]));
    }
    return @result;
}

######
## cvs commits logging for processing by other programs
##
## Everything takes place in $CVSROOT/commits - as long as that dir is
## writable and exists, the functions should handle the rest.
######

sub generate_modlist {
   my $path = shift;
   my $changed_files = shift;  # incl operation, but not what rev #
   my $added_files = shift;
   my $removed_files = shift;
   my @filerevs = @_;          # incl revision #'s but not what op changed them

   my @filelist = ();

   if ($#changed_files >= 0) {
     push @filelist, modlist_add_files ("chg", $path, $changed_files, 
                                        \@filerevs);
   }
   if ($#added_files >= 0) {
       push @filelist, modlist_add_files ("add", $path, $added_files, 
                                          \@filerevs);
   }
   if ($#removed_files >= 0) {
       push @filelist, modlist_add_files ("rm", $path, $removed_files, 
                                          \@filerevs);
   }

   return @filelist;
}

sub modlist_add_files {
   my ($type, $topdir, $filelist, $filerevs) = @_;

   my $type_print = $type;
   my @lines_to_add = ();

   $topdir =~ s#^\./##;
   $topdir =~ s#/$##;
   $topdir =~ s#^\.$##;

   if ($topdir eq "./" || $topdir eq ".") {
      $topdir = "";
   } else {
      $topdir = $topdir . "/";
   }

   foreach $entry (@$filelist) {
     my $revs = modlist_get_file_revs ($entry, $filerevs);
     my $revstr = "NA NA";
     if (defined ($revs)) {
       $revstr = "@$revs[0] @$revs[1]";
     }

     if (defined ($branch)) {
         $type_print = "${type},branch=$branch";
     }
     if ($type eq "rm") {
       push @lines_to_add, "$type_print $revstr ${topdir}Attic/" . $entry;
     }
     push @lines_to_add, "$type_print $revstr $topdir" . $entry;
  }

  return @lines_to_add;
}

sub modlist_get_file_revs {
   my $desired_filename = shift;
   my $filerevs = shift;

   my $filename = undef;
   my $oldrev = "NONE";
   my $newrev = "NONE";
   my @revpair;

   foreach (@$filerevs) {
     if (m#^\Q${desired_filename}\E#) {
       if (m/(.*),([0-9.NONE]*),([0-9.NONE]*)$/) {
          $filename = $1;
          $oldrev = $2;
          $newrev = $3;
       } elsif ($desired_filename eq $_) {
          $filename = $_;
       }
     }
   }

   if (!defined ($filename)) {
      return undef;
   }

   @revpair = ($oldrev, $newrev);
   return \@revpair;
}

sub modlist_newdir {
   my $dirname = shift;

   my @filelist = ();

   $dirname =~ s#^\./##;
   $dirname =~ s#^/##;
   $dirname =~ s#/$##;
   $dirname = $dirname . "/";
   
   push @filelist, "newdir NA NA " . $dirname;
   modlist_write_to_log (@filelist);
}

sub modlist_import {
   my $parent_dir = shift;
   my $text = shift;

   my @filelist = ();

   $parent_dir =~ s#/$##;
   foreach (@$text) {
      chomp;
      if (m/^    ([UNC]) (.*)/) {
          my $type = $1;
          my $file = $2;
          $file =~ s#^\./##;
          $file =~ s#^/##;
          push @filelist, "import,stat=$type NA NA " . $file;
      }
   }

   modlist_write_to_log (@filelist);
}

# Add lines to the commit list logfile of the format
# <EPOCH SECONDS> <USERNAME> <TYPE OF CMD> <OLDREV> <NEWREV> <FILENAME/DIRNAME>

sub modlist_write_to_log {
   my @files = @_;
   my @lines = ();
   my $now = time ();
   my $month = sprintf ("%02d", (localtime)[4] + 1);
   my $year = (localtime)[5] + 1900;

   my $root_dir = "$cvsroot/CVSROOT/commits";
   my $logdir = "$root_dir/commit-logs";
   my $logfile = "$logdir/${year}-${month}";
   my $plugins = "$root_dir/plugins";

   foreach my $dir ("$root_dir", "$logdir")
     {
       if (! -d "$dir")
         {
           mkdir ("$dir", 0777) or warn "Unable to create commit-list log dir $dir";
           chmod 0777, "$dir";
         }
     }

# Remove "./" at the beg of filenames; add ",v" at the end of filenames,
# add username/timestamp to each line for printing.

   foreach my $f (@files) {
       next if (!defined ($f) or $f eq "");
       $f =~ s#^\./##;
       $f =~ s# \./# #;
       if ($f !~ m#/$# && $f !~ m#,v$#) {
          $f = $f . ",v";
       }
       if ($f =~ m#(.*)/$#) {
          $f = $1;
       }
       push @lines, "$now $login $f\n";
   }

## Save the commit lines to the main log file
      
   if (open LOG, ">> $logfile") {
      foreach (@lines) {
         print LOG;
       }
      close (LOG);
      if (-o "$logfile") {
         chmod 0666, "$logfile";
      }
   } else {
     warn "Warning: Unable to write to $logfile !\n";
   }

## Run any plugin scripts and pass them the lines.

   if (-f "$plugins" && open PLUGINS, "< $plugins") {
     my $name;
     while (<PLUGINS>) {
       chomp;
       next if m/^\s*#/;
       next if (! -x $_);

       do { $name = tmpnam(); } until sysopen 
                                (TMP, $name, O_RDWR | O_CREAT | O_EXCL, 0600);
       foreach (@lines) { print TMP; }
       close (TMP);

       open (OUTPUT, "$_ $name 1>&2|") or
              warn "Warning: Unable to execute plugin '$_' at commit-time";
       while (<OUTPUT>) { print; }
       close (OUTPUT);
       unlink "$name";
     }
     close (PLUGINS);
   }
}

sub write_sql_batch {
  my $msg;
  $extension = time();
  do
    {
      $extension++;
      $spoolfile = $TMPDIR . "/sql-tmp." . $extension;
      if ($debug) {
	print STDERR "Spooling SQL -> " . $spoolfile . "\n";
      }
    } while (!sysopen SQL, $spoolfile, O_WRONLY | O_CREAT | O_EXCL, 0644);

  $msg = join ("\n", @unindented_log_txt);
  while ($msg =~ m,[\(<]([a-zA-Z0-9_\.+-]+@[a-zA-Z0-9\.-]+)[\)>](.*)$,s) {
    push (@authors, $1);
    $msg = $2;
  }
  if (scalar (@authors) == 0) {
    push (@authors, $login . "@" . $hostdomain);
  }
  foreach my $author (@authors) {
    my $insert = "INSERT INTO cvs_author values (:id,'" . $author . ")\n";
  }

  $msg = join ("\n", @unindented_log_txt);
  # Escape certain characters.
  $msg =~ s,(\'|\"|\\),\\$1,g;
  # Replace newlines with `\n'.
  $msg =~ s,\n,\\n,g;

  my $now = strftime ("%Y-%m-%d %H:%M:%S %Z", localtime);
  my $insert = 'INSERT INTO cvs_changeset (id,committer,date,msg,cvsroot) values (:id,\'' .
    $login . "','" . $now . "','" . $msg . "','" . $hostdomain . ":" . $cvsroot . "')\n";
  if ($debug) { print STDERR $insert; }
  print SQL $insert;

  foreach my $author (@authors) {
    my $insert = "INSERT INTO cvs_author values (:id,'" . $author . "')\n";
    if ($debug) { print STDERR $insert; }
    print SQL $insert;
  }

  if ($debug) { print STDERR join("\n", @sql_stmts), "\n"; }
  print SQL join("\n", @sql_stmts), "\n";

  close (SQL);
  $newfile = $TMPDIR . "/sql." . $extension;
  rename $spoolfile, $newfile
}

#
#	Main Body
#

# Initialize basic variables
#
$debug = 0;
$id = getpgrp();		# note, you *must* use a shell which does setpgrp()
$state = $STATE_NONE;
$login = $ENV{'USER'} || (getpwuid($<))[0] || "nobody";
chop($hostname = `hostname`);
$cvsroot = $ENV{'CVSROOT'};
$do_status = 1;
$modulename = "";
$temp_name = "temp";
$do_cvsweb = 0;
$cvsweb_name = '';
$do_modlist = 0;
$do_cvsql = 0;

# parse command line arguments (file list is seen as one arg)
#
while (@ARGV) {
    $arg = shift @ARGV;

    if ($arg eq '-d') {
	$debug = 1;
	print STDERR "Debug turned on...\n";
    } elsif ($arg eq '-m') {
	$mailto = "$mailto " . shift @ARGV;
    } elsif ($arg eq '-M') {
	$modulename = shift @ARGV;
    } elsif ($arg eq '-s') {
	$do_status = 0;
    } elsif ($arg eq '-f') {
	($commitlog) && die("Too many '-f' args\n");
	$commitlog = shift @ARGV;
    } elsif ($arg eq '-G') {
	($bugzillaproduct) && die("Too many '-G' args\n");
	$bugzillaproduct = shift @ARGV;
    } elsif ($arg eq '-T') {
	$temp_name = shift @ARGV;
    } elsif ($arg eq '-C') {
	$do_cvsweb = 1;
	$cvsweb_name = shift @ARGV;
    } elsif ($arg eq '-U') {
	$CVSWEB_URL = shift @ARGV;
    } elsif ($arg eq '-D') {
	$hostdomain = shift @ARGV;
    } elsif ($arg eq '-l') {
	$do_modlist = 1;
    } elsif ($arg eq '-i') {
	$do_cvsql = 1;
    } else {
	($donefiles) && die("Too many arguments!  Check usage.\n");
	$donefiles = 1;
	@files = split(/ /, $arg);
    }
}

if (defined ($hostdomain)) {
    # nothing
} elsif ($hostname !~ /\./) {
    chop($domainname = `domainname`);
    $hostdomain = $hostname . "." . $domainname;
} else {
    $hostdomain = $hostname;
}

# Used with sprintf to form name of notification mailing list.
# %s argument comes from -G option.
$MAIL_FORMAT = "$bugzilla_mail\@$hostdomain";

($mailto) || die("No -m mail recipient specified\n");
&set_temp_vars ($temp_name);

# for now, the first "file" is the repository directory being committed,
# relative to the $CVSROOT location
#
@path = split('/', $files[0]);

# XXX there are some ugly assumptions in here about module names and
# XXX directories relative to the $CVSROOT location -- really should
# XXX read $CVSROOT/CVSROOT/modules, but that's not so easy to do, since
# XXX we have to parse it backwards.
#
if ($modulename eq "") {
    $modulename = $path[0];	# I.e. the module name == top-level dir
}
if ($commitlog ne "") {
    $commitlog = $cvsroot . "/" . $modulename . "/" . $commitlog unless ($commitlog =~ /^\//);
}
if ($#path == 0) {
    $dir = ".";
} else {
    $dir = join('/', @path[1..$#path]);
}
$dir = $dir . "/";

if ($debug) {
    print STDERR "module - ", $modulename, "\n";
    print STDERR "dir    - ", $dir, "\n";
    print STDERR "path   - ", join(":", @path), "\n";
    print STDERR "files  - ", join(":", @files), "\n";
    print STDERR "id     - ", $id, "\n";
}

# Check for a new directory first.  This appears with files set as follows:
#
#    files[0] - "path/name/newdir"
#    files[1] - "-"
#    files[2] - "New"
#    files[3] - "directory"
#
if ($files[2] =~ /New/ && $files[3] =~ /directory/) {
    local(@text);

    @text = ();
    push(@text, &build_header());
    push(@text, "");
    push(@text, $files[0]);
    push(@text, "");

    while (<STDIN>) {
	chop;			# Drop the newline
	push(@text, $_);
    }

    &mail_notification($mailto, $files[0], @text);

    if ($commitlog) {
	&write_commitlog($commitlog, @text);
    }

    if ($do_modlist) {
	modlist_newdir ($files[0]);
    }

    exit 0;
}

# Iterate over the body of the message collecting information.
#
while (<STDIN>) {
    chop;			# Drop the newline
    if ($debug)
{
print STDERR "Line is $_\n";
print STDERR "State is currently $state\n";
}
    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; }
    if (/^\s*Tag:|Revision\/Branch/) { /^[^:]+:\s*(.*)/; $branch = $+; next; }

    s/^[ \t\n]+//;		# delete leading whitespace
    s/[ \t\n]+$//;		# delete trailing whitespace

    if ($state == $STATE_CHANGED) { push(@changed_files, split); }
    if ($state == $STATE_ADDED)   { push(@added_files,   split); }
    if ($state == $STATE_REMOVED) { push(@removed_files, split); }
    if ($state == $STATE_LOG)     { 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.
#
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);
    }
}

# Check for an import command.  This appears with files set as follows:
#
#    files[0] - "path/name"
#    files[1] - "-"
#    files[2] - "Imported"
#    files[3] - "sources"
#
if ($files[2] =~ /Imported/ && $files[3] =~ /sources/) {
    local(@text);

    @text = ();
    push(@text, &build_header());
    push(@text, "");

    push(@text, "Log message:");
    while ($#log_lines > -1) {
	push (@text, "    " . $log_lines[0]);
	shift(@log_lines);
    }

    &mail_notification($mailto, "Import $file[0]", @text);

    if ($commitlog) {
	&write_commitlog($commitlog, @text);
    }

    if ($do_modlist) {
	modlist_import ($files[0], \@text);
    }

    exit 0;
}

# Compute the list of cvsweb URLs if necessary.
if ($do_cvsweb) {
    @urls = &generate_cvsweb_urls (join ('/', @path), $branch,
				   @files[1 .. $#files]);
}

if ($do_cvsql) {
    @sql = &generate_cvsql_inserts (join ('/', @path), $branch,
				    @files[1 .. $#files]);
}

if ($do_modlist) {
    @modlist = generate_modlist (join ('/', @path),
                              \@changed_files, \@added_files, \@removed_files,
                               @files[1 .. $#files]);
}

if ($debug) {
    print STDERR "Searching for log file index...";
}
# Find an index to a log file that matches this log message
#
for ($i = 0; ; $i++) {
    local(@text);

    last if (! -e "$LOG_FILE.$i.$id"); # the next available one
    @text = &read_logfile("$LOG_FILE.$i.$id", "");
    last if ($#text == -1);	# nothing in this file, use it
    last if (join(" ", @log_lines) eq join(" ", @text)); # it's the same log message as another
}
if ($debug) {
    print STDERR " found log file at $i.$id, now writing tmp files.\n";
}

# Spit out the information gathered in this pass.
#
&append_names_to_file("$CHANGED_FILE.$i.$id", $dir, @changed_files);
&append_names_to_file("$ADDED_FILE.$i.$id",   $dir, @added_files);
&append_names_to_file("$REMOVED_FILE.$i.$id", $dir, @removed_files);
&append_names_to_file("$URL_FILE.$i.$id",     $dir, @urls);
&append_names_to_file("$SQL_FILE.$i.$id",     $dir, @sql);
&append_names_to_file("$COMMITS_FILE.$i.$id", undef, @modlist);
&write_logfile("$LOG_FILE.$i.$id", @log_lines);
    

# Check whether this is the last directory.  If not, quit.
#
if ($debug) {
    print STDERR "Checking current dir against last dir.\n";
}
$_ = &read_line("$LAST_FILE.$id");

if ($_ ne $cvsroot . "/" . $files[0]) {
    if ($debug) {
	print STDERR sprintf("Current directory %s is not last directory %s.\n", $cvsroot . "/" .$files[0], $_);
    }
    exit 0;
}
if ($debug) {
    print STDERR sprintf("Current directory %s is last directory %s -- all commits done.\n", $files[0], $_);
}

#
#	End Of Commits!
#

# 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.
#

#
# Produce the final compilation of the log messages
#
@text = ();
@status_txt = ();
@subject_files = ();
@unindented_log_txt = ();
@log_txt = ();
@modlist_txt = ();
push(@text, &build_header());
push(@text, "");

for ($i = 0; ; $i++) {
    last if (! -e "$LOG_FILE.$i.$id"); # we're done them all!
    @lines = &read_logfile("$CHANGED_FILE.$i.$id", "");
    if ($#lines >= 0) {
	push(@text, "Modified files:");
	push(@text, &format_lists(@lines));
	push(@subject_files, &accum_subject(@lines));
    }
    @lines = &read_logfile("$ADDED_FILE.$i.$id", "");
    if ($#lines >= 0) {
	push(@text, "Added files:");
	push(@text, &format_lists(@lines));
	push(@subject_files, &accum_subject(@lines));
    }
    @lines = &read_logfile("$REMOVED_FILE.$i.$id", "");
    if ($#lines >= 0) {
	push(@text, "Removed files:");
	push(@text, &format_lists(@lines));
	push(@subject_files, &accum_subject(@lines));
    }
    if ($#text >= 0) {
	push(@text, "");
    }
    @lines = &read_logfile("$LOG_FILE.$i.$id", "");
    if ($#lines >= 0) {
        push (@unindented_log_txt, @lines);
    }
    @log_txt = &read_logfile("$LOG_FILE.$i.$id", "\t");
    if ($#log_txt >= 0) {
	push(@text, "Log message:");
	push(@text, @log_txt);
	push(@text, "");
    }
    @url_txt = &read_logfile("$URL_FILE.$i.$id", "");
    if ($#url_txt >= 0) {
	push (@text, "Patches:");
	# Exclude directories listed in the file.
	push (@text, grep (! /\/$/, @url_txt));
	push (@text, "");
    }
    @sql_txt = &read_logfile("$SQL_FILE.$i.$id", "");
    if ($#sql_txt >= 0) {
	# Exclude directories listed in the file.
        push (@sql_stmts, grep (! /\/$/, @sql_txt));
    }
    if ($do_modlist) {
        push (@modlist_txt, read_logfile("$COMMITS_FILE.$i.$id", ""));
    }
    if ($do_status) {
	local(@changed_files);

	@changed_files = ();
	push(@changed_files, &read_logfile("$CHANGED_FILE.$i.$id", ""));
	push(@changed_files, &read_logfile("$ADDED_FILE.$i.$id", ""));
	push(@changed_files, &read_logfile("$REMOVED_FILE.$i.$id", ""));

	if ($debug) {
	    print STDERR "main: pre-sort changed_files = ", join(":", @changed_files), ".\n";
	}
	@changed_files = sort(@changed_files);
	if ($debug) {
	    print STDERR "main: post-sort changed_files = ", join(":", @changed_files), ".\n";
	}

	foreach $dofile (@changed_files) {
	    if ($dofile =~ /\/$/) {
		next;		# ignore the silly "dir" entries
	    }
	    if ($debug) {
		print STDERR "main(): doing status on $dofile\n";
	    }
	    open(STATUS, "-|") || exec 'cvs', '-nQq', 'status', '-v', $dofile;
	    while (<STATUS>) {
		chop;
		push(@status_txt, $_);
	    }
	}
    }
}

$subject_txt = &compile_subject(@subject_files);

# Write to the commitlog file
#
if ($commitlog) {
    &write_commitlog($commitlog, @text);
}

if ($#modlist_txt >= 0) {
    modlist_write_to_log (@modlist_txt);
}

if ($#status_txt >= 0) {
    push(@text, @status_txt);
}

# Mailout the notification.
#
&mail_notification($mailto, $subject_txt, @text);

$log_txt = join ("\n", @log_txt);
%done_ids = {};

while ($log_txt =~ m/[^Aa](?:bug|BugID:|PR|BZ)\s+\#?\s*(?:[a-z+-]+\/)?(?:\/)?(\d+)(.*)$/si) {
  $bug_id = $1;
  $log_txt = $2;
  if (!defined $done_ids{$bug_id})
    {
      $done_ids{$bug_id} = 1;
      # Send mail to Bugzilla, if required.
      if ($bugzillaproduct ne '') {
	my $dbh = undef;
	$dbh = DBI->connect ($database, $username, $password);
	if ($debug) 
	  {
	    print STDERR "Attempting to see if bug $bug_id exists\n";
	  }
	if (defined $dbh 
	    && &see_if_bugzilla_bug_exists ($dbh, $bugzillaproduct, $bug_id)) 
	  {
	    if ($debug) { print STDERR "It does\n"; }
	    &mail_bug_notification( sprintf ($MAIL_FORMAT, $bugzillaproduct),
				    "[Bug $bug_id] Bug $bug_id", @text);
	  }
	if (defined $dbh)
	  {
	    $dbh->disconnect;
	  }
      }
      if ($do_cvsql) {
	push(@sql_stmts, 'INSERT INTO cvs_bugreport (changeset,id) values (:id, \'' . $bug_id . '\')');
      }
    }
}

# Write out an SQL batch file.
#
if ($do_cvsql) {
  &write_sql_batch();
}

# cleanup
#
if (! $debug) {
    &cleanup_tmpfiles();
}

exit 0;
