reduce diff against previous version to only fixing #858769
[alioth/cvs.git] / contrib / log_accum.in
1 #! @PERL@ -T
2 # -*-Perl-*-
3
4 # Copyright (C) 1994-2005 The Free Software Foundation, Inc.
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2, or (at your option)
9 # any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 ###############################################################################
17 ###############################################################################
18 ###############################################################################
19 #
20 # THIS SCRIPT IS PROBABLY BROKEN.  REMOVING THE -T SWITCH ON THE #! LINE ABOVE
21 # WOULD FIX IT, BUT THIS IS INSECURE.  WE RECOMMEND FIXING THE ERRORS WHICH THE
22 # -T SWITCH WILL CAUSE PERL TO REPORT BEFORE RUNNING THIS SCRIPT FROM A CVS
23 # SERVER TRIGGER.  PLEASE SEND PATCHES CONTAINING THE CHANGES YOU FIND
24 # NECESSARY TO RUN THIS SCRIPT WITH THE TAINT-CHECKING ENABLED BACK TO THE
25 # <@PACKAGE_BUGREPORT@> MAILING LIST.
26 #
27 # For more on general Perl security and taint-checking, please try running the
28 # `perldoc perlsec' command.
29 #
30 ###############################################################################
31 ###############################################################################
32 ###############################################################################
33
34 # Perl filter to handle the log messages from the checkin of files in
35 # a directory.  This script will group the lists of files by log
36 # message, and mail a single consolidated log message at the end of
37 # the commit.
38 #
39 # This file assumes a pre-commit checking program that leaves the
40 # names of the first and last commit directories in a temporary file.
41 #
42 # IMPORTANT: what the above means is, this script interacts with
43 # commit_prep, in that they have to agree on the tmpfile name to use.
44 # See $LAST_FILE below. 
45 #
46 # How this works: CVS triggers this script once for each directory
47 # involved in the commit -- in other words, a single commit can invoke
48 # this script N times.  It knows when it's on the last invocation by
49 # examining the contents of $LAST_FILE.  Between invocations, it
50 # caches information for its future incarnations in various temporary
51 # files in /tmp, which are named according to the process group and
52 # the committer (by themselves, neither of these are unique, but
53 # together they almost always are, unless the same user is doing two
54 # commits simultaneously).  The final invocation is the one that
55 # actually sends the mail -- it gathers up the cached information,
56 # combines that with what it found out on this pass, and sends a
57 # commit message to the appropriate mailing list.
58 #
59 # (Ask Karl Fogel <kfogel@collab.net> if questions.)
60 #
61 # Contributed by David Hampton <hampton@cisco.com>
62 # Roy Fielding removed useless code and added log/mail of new files
63 # Ken Coar added special processing (i.e., no diffs) for binary files
64 #
65
66 ############################################################
67 #
68 # Configurable options
69 #
70 ############################################################
71 #
72 # The newest versions of CVS have UseNewInfoFmtStrings=yes
73 # to change the arguments being passed on the command line.
74 # If you are using %1s on the command line, then set this
75 # value to 0.
76 # 0 = old-style %1s format. use split(' ') to separate ARGV into filesnames.
77 # 1 = new-style %s format. Note: allows spaces in filenames.
78 my $UseNewInfoFmtStrings = 0;
79
80 #
81 # Where do you want the RCS ID and delta info?
82 # 0 = none,
83 # 1 = in mail only,
84 # 2 = in both mail and logs.
85 #
86 $rcsidinfo = 2;
87
88 #if you are using CVS web then set this to some value... if not set it to ""
89 #
90 # When set properly, this will cause links to aspects of the project to
91 # print in the commit emails.
92 #$CVSWEB_SCHEME = "http";
93 #$CVSWEB_DOMAIN = "nongnu.org";
94 #$CVSWEB_PORT = "80";
95 #$CVSWEB_URI = "source/browse/";
96 #$SEND_URL = "true";
97 $SEND_DIFF = "true";
98
99
100 # Set this to a domain to have CVS pretend that all users who make
101 # commits have mail accounts within that domain.
102 #$EMULATE_LOCAL_MAIL_USER="nongnu.org"; 
103
104 # Set this to '-c' for context diffs; defaults to '-u' for unidiff format.
105 $difftype = '-uN';
106
107 ############################################################
108 #
109 # Constants
110 #
111 ############################################################
112 $STATE_NONE    = 0;
113 $STATE_CHANGED = 1;
114 $STATE_ADDED   = 2;
115 $STATE_REMOVED = 3;
116 $STATE_LOG     = 4;
117
118 $TMPDIR        = $ENV{'TMPDIR'} || '/tmp';
119 $FILE_PREFIX   = '#cvs.';
120
121 $LAST_FILE     = "$TMPDIR/${FILE_PREFIX}lastdir";  # Created by commit_prep!
122 $ADDED_FILE    = "$TMPDIR/${FILE_PREFIX}files.added";
123 $REMOVED_FILE  = "$TMPDIR/${FILE_PREFIX}files.removed";
124 $LOG_FILE      = "$TMPDIR/${FILE_PREFIX}files.log";
125 $BRANCH_FILE   = "$TMPDIR/${FILE_PREFIX}files.branch";
126 $MLIST_FILE    = "$TMPDIR/${FILE_PREFIX}files.mlist";
127 $SUMMARY_FILE  = "$TMPDIR/${FILE_PREFIX}files.summary";
128
129 $CVSROOT       = $ENV{'CVSROOT'};
130
131 $MAIL_CMD      = "| /usr/lib/sendmail -i -t";
132 #$MAIL_CMD      = "| /var/qmail/bin/qmail-inject";
133 $MAIL_FROM     = 'commitlogger';  #not needed if EMULATE_LOCAL_MAIL_USER
134 $SUBJECT_PRE   = 'CVS update:';
135
136
137 ############################################################
138 #
139 # Subroutines
140 #
141 ############################################################
142
143 sub format_names {
144     local($dir, @files) = @_;
145     local(@lines);
146
147     $lines[0] = sprintf(" %-08s", $dir);
148     foreach $file (@files) {
149         if (length($lines[$#lines]) + length($file) > 60) {
150             $lines[++$#lines] = sprintf(" %8s", " ");
151         }
152         $lines[$#lines] .= " ".$file;
153     }
154     @lines;
155 }
156
157 sub cleanup_tmpfiles {
158     local(@files);
159
160     opendir(DIR, $TMPDIR);
161     push(@files, grep(/^${FILE_PREFIX}.*\.${id}\.${cvs_user}$/, readdir(DIR)));
162     closedir(DIR);
163     foreach (@files) {
164         unlink "$TMPDIR/$_";
165     }
166 }
167
168 sub write_logfile {
169     local($filename, @lines) = @_;
170
171     open(FILE, ">$filename") || die ("Cannot open log file $filename: $!\n");
172     print(FILE join("\n", @lines), "\n");
173     close(FILE);
174 }
175
176 sub append_to_file {
177     local($filename, $dir, @files) = @_;
178
179     if (@files) {
180         local(@lines) = &format_names($dir, @files);
181         open(FILE, ">>$filename") || die ("Cannot open file $filename: $!\n");
182         print(FILE join("\n", @lines), "\n");
183         close(FILE);
184     }
185 }
186
187 sub write_line {
188     local($filename, $line) = @_;
189
190     open(FILE, ">$filename") || die("Cannot open file $filename: $!\n");
191     print(FILE $line, "\n");
192     close(FILE);
193 }
194
195 sub append_line {
196     local($filename, $line) = @_;
197
198     open(FILE, ">>$filename") || die("Cannot open file $filename: $!\n");
199     print(FILE $line, "\n");
200     close(FILE);
201 }
202
203 sub read_line {
204     local($filename) = @_;
205     local($line);
206
207     open(FILE, "<$filename") || die("Cannot open file $filename: $!\n");
208     $line = <FILE>;
209     close(FILE);
210     chomp($line);
211     $line;
212 }
213
214 sub read_line_nodie {
215     local($filename) = @_;
216     local($line);
217     open(FILE, "<$filename") || return ("");
218
219     $line = <FILE>;
220     close(FILE);
221     chomp($line);
222     $line;
223 }
224
225 sub read_file_lines {
226     local($filename) = @_;
227     local(@text) = ();
228
229     open(FILE, "<$filename") || return ();
230     while (<FILE>) {
231         chomp;
232         push(@text, $_);
233     }
234     close(FILE);
235     @text;
236 }
237
238 sub read_file {
239     local($filename, $leader) = @_;
240     local(@text) = ();
241
242     open(FILE, "<$filename") || return ();
243     while (<FILE>) {
244         chomp;
245         push(@text, sprintf("  %-10s  %s", $leader, $_));
246         $leader = "";
247     }
248     close(FILE);
249     @text;
250 }
251
252 sub read_logfile {
253     local($filename, $leader) = @_;
254     local(@text) = ();
255
256     open(FILE, "<$filename") || die ("Cannot open log file $filename: $!\n");
257     while (<FILE>) {
258         chomp;
259         push(@text, $leader.$_);
260     }
261     close(FILE);
262     @text;
263 }
264
265 #
266 # do an 'cvs -Qn status' on each file in the arguments, and extract info.
267 #
268 sub change_summary {
269     local($out, @filenames) = @_;
270     local(@revline);
271     local($file, $rev, $rcsfile, $line, $vhost, $cvsweb_base);
272
273     while (@filenames) {
274         $file = shift @filenames;
275
276         if ("$file" eq "") {
277             next;
278         }
279
280         open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'status', '--', $file;
281
282         $rev = "";
283         $delta = "";
284         $rcsfile = "";
285
286
287         while (<RCS>) {
288             if (/^[ \t]*Repository revision/) {
289                 chomp;
290                 @revline = split(' ', $_);
291                 $rev = $revline[2];
292                 $rcsfile = $revline[3];
293                 $rcsfile =~ s,^$CVSROOT/,,;
294                 $rcsfile =~ s/,v$//;
295             }
296         }
297         close(RCS);
298
299
300         if ($rev ne '' && $rcsfile ne '') {
301             open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'log', "-r$rev",
302                                     '--', $file;
303             while (<RCS>) {
304                 if (/^date:.*lines:([^;]+);.*/) {
305                     $delta = $1;
306                     last;
307                 }
308             }
309             close(RCS);
310         }
311
312         $diff = "\n\n";
313         $vhost = $path[0];
314         if ($CVSWEB_PORT eq "80") {
315           $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN/$CVSWEB_URI";
316         }
317         else {
318           $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN:$CVSWEB_PORT/$CVSWEB_URI";
319         }
320         if ($SEND_URL eq "true") {
321           $diff .= $cvsweb_base . join("/", @path) . "/$file";
322         }
323
324         #
325         # If this is a binary file, don't try to report a diff; not only is
326         # it meaningless, but it also screws up some mailers.  We rely on
327         # Perl's 'is this binary' algorithm; it's pretty good.  But not
328         # perfect.
329         #
330         if (($file =~ /\.(?:pdf|gif|jpg|mpg)$/i) || (-B $file)) {
331           if ($SEND_URL eq "true") {
332             $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
333           }
334           if ($SEND_DIFF eq "true") {
335             $diff .= "\t<<Binary file>>\n\n";
336           }
337         }
338         else {
339             #
340             # Get the differences between this and the previous revision,
341             # being aware that new files always have revision '1.1' and
342             # new branches always end in '.n.1'.
343             #
344             if ($rev =~ /^(.*)\.([0-9]+)$/) {
345                 $prev = $2 - 1;
346                 $prev_rev = $1 . '.' .  $prev;
347
348                 $prev_rev =~ s/\.[0-9]+\.0$//;# Truncate if first rev on branch
349
350                 if ($rev eq '1.1') {
351                   if ($SEND_URL eq "true") {
352                     $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
353                   }
354                   if ($SEND_DIFF eq "true") {
355                     open(DIFF, "-|")
356                       || exec "$cvsbin/cvs", '-Qn', 'update', '-p', '-r1.1',
357                               '--', $file;
358                     $diff .= "Index: $file\n=================================="
359                       . "=================================\n";
360                   }
361                 }
362                 else {
363                   if ($SEND_URL eq "true") {
364                     $diff .= ".diff?r1=$prev_rev&r2=$rev\n\n";
365                   }
366                   if ($SEND_DIFF eq "true") {
367                     $diff .= "(In the diff below, changes in quantity "
368                       . "of whitespace are not shown.)\n\n";
369                     open(DIFF, "-|")
370                       || exec "$cvsbin/cvs", '-Qn', 'diff', "$difftype",
371                       '-b', "-r$prev_rev", "-r$rev", '--', $file;
372                   }
373                 }
374
375                 if ($SEND_DIFF eq "true") {
376                   while (<DIFF>) {
377                     $diff .= $_;
378                   }
379                   close(DIFF);
380                 }
381                 $diff .= "\n\n";
382             }
383         }
384
385         &append_line($out, sprintf("%-9s%-12s%s%s", $rev, $delta,
386                                    $rcsfile, $diff));
387     }
388 }
389
390
391 sub build_header {
392     local($header);
393     delete $ENV{'TZ'};
394     local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
395
396     $header = sprintf("  User: %-8s\n  Date: %02d/%02d/%02d %02d:%02d:%02d",
397                        $cvs_user, $year%100, $mon+1, $mday,
398                        $hour, $min, $sec);
399 #    $header = sprintf("%-8s    %02d/%02d/%02d %02d:%02d:%02d",
400 #                       $login, $year%100, $mon+1, $mday,
401 #                       $hour, $min, $sec);
402 }
403
404 # !!! Destination Mailing-list and history file mappings here !!!
405
406 #sub mlist_map
407 #{
408 #    local($path) = @_;
409 #    my $domain = "nongnu.org";
410 #    
411 #    if ($path =~ /^([^\/]+)/) {
412 #        return "cvs\@$1.$domain";
413 #    } else {
414 #        return "cvs\@$domain";
415 #    }
416 #}    
417
418 sub derive_subject_from_changes_file ()
419 {
420   my $subj = "";
421
422   for ($i = 0; ; $i++)
423   {
424     open (CH, "<$CHANGED_FILE.$i.$id.$cvs_user") or last;
425
426     while (my $change = <CH>)
427     {
428       # A changes file looks like this:
429       #
430       #  src      foo.c newfile.html
431       #  www      index.html project_nav.html
432       #
433       # Each line is " Dir File1 File2 ..."
434       # We only care about Dir, since the subject line should
435       # summarize. 
436       
437       $change =~ s/^[ \t]*//;
438       $change =~ /^([^ \t]+)[ \t]*/;
439       my $dir = $1;
440       # Fold to rightmost directory component
441       $dir =~ /([^\/]+)$/;
442       $dir = $1;
443       if ($subj eq "") {
444         $subj = $dir;
445       } else {
446         $subj .= ", $dir"; 
447       }
448     }
449     close (CH);
450   }
451
452   if ($subj ne "") {
453       $subj = "MODIFIED: $subj ..."; 
454   }
455   else {
456       # NPM: See if there's any file-addition notifications.
457       my $added = &read_line_nodie("$ADDED_FILE.$i.$id.$cvs_user");
458       if ($added ne "") {
459           $subj .= "ADDED: $added "; 
460       }
461     
462 #    print "derive_subject_from_changes_file().. added== $added \n";
463     
464        ## NPM: See if there's any file-removal notications.
465       my $removed = &read_line_nodie("$REMOVED_FILE.$i.$id.$cvs_user");
466       if ($removed ne "") {
467           $subj .= "REMOVED: $removed "; 
468       }
469     
470 #    print "derive_subject_from_changes_file().. removed== $removed \n";
471     
472       ## NPM: See if there's any branch notifications.
473       my $branched = &read_line_nodie("$BRANCH_FILE.$i.$id.$cvs_user");
474       if ($branched ne "") {
475           $subj .= "BRANCHED: $branched"; 
476       }
477     
478 #    print "derive_subject_from_changes_file().. branched== $branched \n";
479     
480       ## NPM: DEFAULT: DIRECTORY CREATION (c.f. "Check for a new directory first" in main mody)
481       if ($subj eq "") {
482           my $subject = join("/", @path);
483           $subj = "NEW: $subject"; 
484       }    
485   }
486
487   return $subj;
488 }
489
490 sub mail_notification
491 {
492     local($addr_list, @text) = @_;
493     local($mail_to);
494
495     my $subj = &derive_subject_from_changes_file ();
496
497     if ($EMULATE_LOCAL_MAIL_USER ne "") {
498         $MAIL_FROM = "$cvs_user\@$EMULATE_LOCAL_MAIL_USER";
499     }
500
501     $mail_to = join(", ", @{$addr_list});
502
503     print "Mailing the commit message to $mail_to (from $MAIL_FROM)\n";
504
505     $ENV{'MAILUSER'} = $MAIL_FROM;
506     # Commented out on hocus, so comment it out here.  -kff
507     # $ENV{'QMAILINJECT'} = 'f';
508
509     open(MAIL, "$MAIL_CMD -f$MAIL_FROM");
510     print MAIL "From: $MAIL_FROM\n";
511     print MAIL "To: $mail_to\n";
512     print MAIL "Subject: $SUBJECT_PRE $subj\n\n";
513     print(MAIL join("\n", @text));
514     close(MAIL);
515 #    print "Mailing the commit message to $MAIL_TO...\n";
516 #
517 #    #added by jrobbins@collab.net 1999/12/15
518 #    # attempt to get rid of anonymous
519 #    $ENV{'MAILUSER'} = 'commitlogger';
520 #    $ENV{'QMAILINJECT'} = 'f';
521 #
522 #    open(MAIL, "| /var/qmail/bin/qmail-inject");
523 #    print(MAIL "To: $MAIL_TO\n"); 
524 #    print(MAIL "Subject: cvs commit: $ARGV[0]\n"); 
525 #    print(MAIL join("\n", @text));
526 #    close(MAIL);
527 }
528
529 ## process the command line arguments sent to this script
530 ## it returns an array of files, %s, sent from the loginfo
531 ## command
532 sub process_argv
533 {
534     local(@argv) = @_;
535     local(@files);
536     local($arg);
537     print "Processing log script arguments...\n";
538
539     if ($UseNewInfoFmtStrings) {
540         while (@argv) {
541             $arg = shift @argv;
542
543             if ($arg eq '-u' && !defined($cvs_user)) {
544                 $cvs_user = shift @argv;
545             }
546             if ($arg eq '- New directory') {
547                 $new_directory = 1;
548             } elsif ($arg eq '- Imported sources') {
549                 $imported_sources = 1;
550             } else {
551                 push(@files, $arg);
552             }
553         }
554     } else {
555         while (@argv) {
556             $arg = shift @argv;
557
558             if ($arg eq '-u') {
559                 $cvs_user = shift @argv;
560             } else {
561                 ($donefiles) && die "Too many arguments!\n";
562                 $donefiles = 1;
563                 $ARGV[0] = $arg;
564                 if ($arg =~ s/ - New directory//) {
565                     $new_directory = 1;
566                 } elsif ($arg =~ s/ - Imported sources//) {
567                     $imported_sources = 1;
568                 }
569                 @files = split(' ', $arg);
570             }
571         }
572     }
573     return @files;
574 }
575
576
577 #############################################################
578 #
579 # Main Body
580 #
581 ############################################################
582 #
583 # Setup environment
584 #
585 umask (002);
586
587 # Connect to the database
588 $cvsbin = "/usr/bin";
589
590 #
591 # Initialize basic variables
592 #
593 $id = getpgrp();
594 $state = $STATE_NONE;
595 $cvs_user = $ENV{'USER'} || getlogin || (getpwuid($<))[0] || sprintf("uid#%d",$<);
596 $new_directory = 0;             # Is this a 'cvs add directory' command?
597 $imported_sources = 0;          # Is this a 'cvs import' command?
598 @files = process_argv(@ARGV);
599 @path = split('/', $files[0]);
600 if ($#path == 0) {
601     $dir = ".";
602 } else {
603     $dir = join('/', @path[1..$#path]);
604 }
605 #print("ARGV  - ", join(":", @ARGV), "\n");
606 #print("files - ", join(":", @files), "\n");
607 #print("path  - ", join(":", @path), "\n");
608 #print("dir   - ", $dir, "\n");
609 #print("id    - ", $id, "\n");
610
611 #
612 # Map the repository directory to an email address for commitlogs to be sent
613 # to.
614 #
615 #$mlist = &mlist_map($files[0]);
616
617 ##########################
618 #
619 # Check for a new directory first.  This will always appear as a
620 # single item in the argument list, and an empty log message.
621 #
622 if ($new_directory) {
623     $header = &build_header;
624     @text = ();
625     push(@text, $header);
626     push(@text, "");
627     push(@text, "  ".$files[0]." - New directory");
628     &mail_notification([ $mlist ], @text);
629     exit 0;
630 }
631
632 #
633 # Iterate over the body of the message collecting information.
634 #
635 while (<STDIN>) {
636     chomp;                      # Drop the newline
637     if (/^Revision\/Branch:/) {
638         s,^Revision/Branch:,,;
639         push (@branch_lines, split);
640         next;
641     }
642 #    next if (/^[ \t]+Tag:/ && $state != $STATE_LOG);
643     if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
644     if (/^Added Files/)    { $state = $STATE_ADDED;   next; }
645     if (/^Removed Files/)  { $state = $STATE_REMOVED; next; }
646     if (/^Log Message/)    { $state = $STATE_LOG;     last; }
647     s/[ \t\n]+$//;              # delete trailing space
648     
649     push (@changed_files, split) if ($state == $STATE_CHANGED);
650     push (@added_files,   split) if ($state == $STATE_ADDED);
651     push (@removed_files, split) if ($state == $STATE_REMOVED);
652 }
653 # Proces the /Log Message/ section now, if it exists.
654 # Do this here rather than above to deal with Log messages
655 # that include lines that confuse the state machine.
656 if (!eof(STDIN)) {
657     while (<STDIN>) {
658         next unless ($state == $STATE_LOG); # eat all STDIN
659
660         if ($state == $STATE_LOG) {
661             if (/^PR:$/i ||
662                 /^Reviewed by:$/i ||
663                 /^Submitted by:$/i ||
664                 /^Obtained from:$/i) {
665                 next;
666             }
667             push (@log_lines,     $_);
668         }
669     }
670 }
671
672 #
673 # Strip leading and trailing blank lines from the log message.  Also
674 # compress multiple blank lines in the body of the message down to a
675 # single blank line.
676 # (Note, this only does the mail and changes log, not the rcs log).
677 #
678 while ($#log_lines > -1) {
679     last if ($log_lines[0] ne "");
680     shift(@log_lines);
681 }
682 while ($#log_lines > -1) {
683     last if ($log_lines[$#log_lines] ne "");
684     pop(@log_lines);
685 }
686 for ($i = $#log_lines; $i > 0; $i--) {
687     if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
688         splice(@log_lines, $i, 1);
689     }
690 }
691
692 #
693 # Find the log file that matches this log message
694 #
695 for ($i = 0; ; $i++) {
696     last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
697     @text = &read_logfile("$LOG_FILE.$i.$id.$cvs_user", "");
698     last if ($#text == -1);
699     last if (join(" ", @log_lines) eq join(" ", @text));
700 }
701
702 #
703 # Spit out the information gathered in this pass.
704 #
705 &write_logfile("$LOG_FILE.$i.$id.$cvs_user", @log_lines);
706 &append_to_file("$BRANCH_FILE.$i.$id.$cvs_user",  $dir, @branch_lines);
707 &append_to_file("$ADDED_FILE.$i.$id.$cvs_user",   $dir, @added_files);
708 &append_to_file("$CHANGED_FILE.$i.$id.$cvs_user", $dir, @changed_files);
709 &append_to_file("$REMOVED_FILE.$i.$id.$cvs_user", $dir, @removed_files);
710 &append_line("$MLIST_FILE.$i.$id.$cvs_user", $mlist);
711 if ($rcsidinfo) {
712     &change_summary("$SUMMARY_FILE.$i.$id.$cvs_user", (@changed_files, @added_files));
713 }
714
715 #
716 # Check whether this is the last directory.  If not, quit.
717 #
718 if (-e "$LAST_FILE.$id.$cvs_user") {
719    $_ = &read_line("$LAST_FILE.$id.$cvs_user");
720    $tmpfiles = $files[0];
721    $tmpfiles =~ s,([^a-zA-Z0-9_/]),\\$1,g;
722    if (! grep(/$tmpfiles$/, $_)) {
723         print "More commits to come...\n";
724         exit 0
725    }
726 }
727
728 #
729 # This is it.  The commits are all finished.  Lump everything together
730 # into a single message, fire a copy off to the mailing list, and drop
731 # it on the end of the Changes file.
732 #
733 $header = &build_header;
734
735 #
736 # Produce the final compilation of the log messages
737 #
738 @text = ();
739 @mlist_list = ();
740 push(@text, $header);
741 push(@text, "");
742 for ($i = 0; ; $i++) {
743     last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
744     push(@text, &read_file("$BRANCH_FILE.$i.$id.$cvs_user", "Branch:"));
745     push(@text, &read_file("$CHANGED_FILE.$i.$id.$cvs_user", "Modified:"));
746     push(@text, &read_file("$ADDED_FILE.$i.$id.$cvs_user", "Added:"));
747     push(@text, &read_file("$REMOVED_FILE.$i.$id.$cvs_user", "Removed:"));
748     push(@text, "  Log:");
749     push(@text, &read_logfile("$LOG_FILE.$i.$id.$cvs_user", "  "));
750     push(@mlist_list, &read_file_lines("$MLIST_FILE.$i.$id.$cvs_user"));
751     if ($rcsidinfo == 2) {
752         if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
753             push(@text, "  ");
754             push(@text, "  Revision  Changes    Path");
755             push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", "  "));
756         }
757     }
758     push(@text, "");
759 }
760
761 #
762 # Now generate the extra info for the mail message..
763 #
764 if ($rcsidinfo == 1) {
765     $revhdr = 0;
766     for ($i = 0; ; $i++) {
767         last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
768         if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
769             if (!$revhdr++) {
770                 push(@text, "Revision  Changes    Path");
771             }
772             push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", ""));
773         }
774     }
775     if ($revhdr) {
776         push(@text, "");        # consistancy...
777     }
778 }
779
780 %mlist_hash = ();
781
782 foreach (@mlist_list) { $mlist_hash{ $_ } = 1; }
783
784 #
785 # Mail out the notification.
786 #
787 &mail_notification([ keys(%mlist_hash) ], @text);
788 &cleanup_tmpfiles;
789 exit 0;