4 # Perl filter to handle the log messages from the checkin of files in
5 # a directory. This script will group the lists of files by log
6 # message, and mail a single consolidated log message at the end of
9 # This file assumes a pre-commit checking program that leaves the
10 # names of the first and last commit directories in a temporary file.
12 # Contributed by David Hampton <hampton@cisco.com>
14 # hacked greatly by Greg A. Woods <woods@planix.com>
16 # Usage: log_accum.pl [-d] [-s] [-w] [-M module] [-u user] [[-m mailto] ...] [[-R replyto] ...] [-f logfile]
17 # -d - turn on debugging
18 # -m mailto - send mail to "mailto" (multiple)
19 # -R replyto - set the "Reply-To:" to "replyto" (multiple)
20 # -M modulename - set module name to "modulename"
21 # -f logfile - write commit messages to logfile too
22 # -s - *don't* run "cvs status -v" for each file
23 # -w - show working directory with log message
24 # -u user - $USER passed from loginfo
27 # Configurable options
30 # set this to something that takes a whole message on stdin
31 $MAILER = "/usr/lib/sendmail -t";
34 # End user configurable options.
37 # Constants (don't change these!)
45 $LAST_FILE = "/tmp/#cvs.lastdir";
47 $CHANGED_FILE = "/tmp/#cvs.files.changed";
48 $ADDED_FILE = "/tmp/#cvs.files.added";
49 $REMOVED_FILE = "/tmp/#cvs.files.removed";
50 $LOG_FILE = "/tmp/#cvs.files.log";
52 $FILE_PREFIX = "#cvs.files";
58 sub cleanup_tmpfiles
{
62 chdir("/tmp") || die("Can't chdir('/tmp')\n");
64 push(@files, grep(/^$FILE_PREFIX\..*\.$id$/, readdir(DIR
)));
69 unlink $LAST_FILE . "." . $id;
75 local($filename, @lines) = @_;
77 open(FILE
, ">$filename") || die("Cannot open log file $filename.\n");
78 print FILE
join("\n", @lines), "\n";
82 sub append_to_logfile
{
83 local($filename, @lines) = @_;
85 open(FILE
, ">$filename") || die("Cannot open log file $filename.\n");
86 print FILE
join("\n", @lines), "\n";
91 local($dir, @files) = @_;
94 $format = "\t%-" . sprintf("%d", length($dir)) . "s%s ";
96 $lines[0] = sprintf($format, $dir, ":");
99 print STDERR
"format_names(): dir = ", $dir, "; files = ", join(":", @files), ".\n";
101 foreach $file (@files) {
102 if (length($lines[$#lines]) + length($file) > 65) {
103 $lines[++$#lines] = sprintf($format, " ", " ");
105 $lines[$#lines] .= $file . " ";
113 local(@text, @files, $lastdir);
116 print STDERR
"format_lists(): ", join(":", @lines), "\n";
120 $lastdir = shift @lines; # first thing is always a directory
121 if ($lastdir !~ /.*\/$/) {
122 die("Damn, $lastdir doesn't look like a directory!\n");
124 foreach $line (@lines) {
125 if ($line =~ /.*\/$/) {
126 push(@text, &format_names
($lastdir, @files));
133 push(@text, &format_names
($lastdir, @files));
138 sub append_names_to_file
{
139 local($filename, $dir, @files) = @_;
142 open(FILE
, ">>$filename") || die("Cannot open file $filename.\n");
143 print FILE
$dir, "\n";
144 print FILE
join("\n", @files), "\n";
151 local($filename) = @_;
153 open(FILE
, "<$filename") || die("Cannot open file $filename.\n");
162 local($filename, $leader) = @_;
164 open(FILE
, "<$filename");
167 push(@text, $leader.$_);
175 local($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
176 $header = sprintf("CVSROOT:\t%s\nModule name:\t%s\nRepository:\t%s\nChanges by:\t%s@%s\t%02d/%02d/%02d %02d:%02d:%02d",
181 $year%100, $mon+1, $mday,
185 sub mail_notification
{
188 # if only we had strftime()... stuff stolen from perl's ctime.pl:
191 @DoW = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
192 @MoY = ('Jan','Feb','Mar','Apr','May','Jun',
193 'Jul','Aug','Sep','Oct','Nov','Dec');
195 # Determine what time zone is in effect.
196 # Use GMT if TZ is defined as null, local time if TZ undefined.
197 # There's no portable way to find the system default timezone.
199 $TZ = defined($ENV{'TZ'}) ?
( $ENV{'TZ'} ?
$ENV{'TZ'} : 'GMT' ) : '';
201 # Hack to deal with 'PST8PDT' format of TZ
202 # Note that this can't deal with all the esoteric forms, but it
203 # does recognize the most common: [:]STDoff[DST[off][,rule]]
205 if ($TZ =~ /^([^:\d+\-,]{3,})([+-]?\d{1,2}(:\d{1,2}){0,2})([^\d+\-,]{3,})?/) {
206 $TZ = $isdst ?
$4 : $1;
207 $tzoff = sprintf("%05d", -($2) * 100);
210 # perl-4.036 doesn't have the $zone or $gmtoff...
211 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst, $zone, $gmtoff) =
212 ($TZ eq 'GMT') ?
gmtime(time) : localtime(time);
214 $year += ($year < 70) ?
2000 : 1900;
217 $tzoff = sprintf("%05d", ($gmtoff / 60) * 100);
224 $rfc822date = sprintf("%s, %2d %s %4d %2d:%02d:%02d %s (%s)",
225 $DoW[$wday], $mday, $MoY[$mon], $year,
226 $hour, $min, $sec, $tzoff, $TZ);
228 open(MAIL
, "| $MAILER");
229 print MAIL
"Date: " . $rfc822date . "\n";
230 print MAIL
"Subject: CVS Update: " . $modulename . "\n";
231 print MAIL
"To: " . $mailto . "\n";
232 print MAIL
"Reply-To: " . $replyto . "\n";
234 print MAIL
join("\n", @text), "\n";
238 sub write_commitlog
{
239 local($logfile, @text) = @_;
241 open(FILE
, ">>$logfile");
242 print FILE
join("\n", @text), "\n";
250 # Initialize basic variables
253 $id = getpgrp(); # note, you *must* use a shell which does setpgrp()
254 $state = $STATE_NONE;
255 chop($hostname = `hostname`);
256 chop($domainname = `domainname`);
257 if ($domainname !~ '^\..*') {
258 $domainname = '.' . $domainname;
260 $hostdomain = $hostname . $domainname;
261 $cvsroot = $ENV{'CVSROOT'};
262 $do_status = 1; # moderately useful
263 $show_wd = 0; # useless in client/server
266 # parse command line arguments (file list is seen as one arg)
273 print STDERR
"Debug turned on...\n";
274 } elsif ($arg eq '-m') {
276 $mailto = shift @ARGV;
278 $mailto = $mailto . ", " . shift @ARGV;
280 } elsif ($arg eq '-R') {
281 if ($replyto eq '') {
282 $replyto = shift @ARGV;
284 $replyto = $replyto . ", " . shift @ARGV;
286 } elsif ($arg eq '-M') {
287 $modulename = shift @ARGV;
288 } elsif ($arg eq '-u') {
289 $login = shift @ARGV;
290 } elsif ($arg eq '-s') {
292 } elsif ($arg eq '-w') {
294 } elsif ($arg eq '-f') {
295 ($commitlog) && die("Too many '-f' args\n");
296 $commitlog = shift @ARGV;
298 ($donefiles) && die("Too many arguments! Check usage.\n");
300 @files = split(/ /, $arg);
304 $login = getlogin || (getpwuid($<))[0] || "nobody";
306 ($mailto) || die("No mail recipient specified (use -m)\n");
307 if ($replyto eq '') {
311 # for now, the first "file" is the repository directory being committed,
312 # relative to the $CVSROOT location
314 @path = split('/', $files[0]);
316 # XXX There are some ugly assumptions in here about module names and
317 # XXX directories relative to the $CVSROOT location -- really should
318 # XXX read $CVSROOT/CVSROOT/modules, but that's not so easy to do, since
319 # XXX we have to parse it backwards.
321 # XXX Fortunately it's relatively easy for the user to specify the
322 # XXX module name as appropriate with a '-M' via the directory
323 # XXX matching in loginfo.
325 if ($modulename eq "") {
326 $modulename = $path[0]; # I.e. the module name == top-level dir
331 $dir = join('/', @path);
336 print STDERR
"module - ", $modulename, "\n";
337 print STDERR
"dir - ", $dir, "\n";
338 print STDERR
"path - ", join(":", @path), "\n";
339 print STDERR
"files - ", join(":", @files), "\n";
340 print STDERR
"id - ", $id, "\n";
343 # Check for a new directory first. This appears with files set as follows:
345 # files[0] - "path/name/newdir"
348 # files[3] - "directory"
350 if ($files[2] =~ /New/ && $files[3] =~ /directory/) {
354 push(@text, &build_header
());
356 push(@text, $files[0]);
360 chop; # Drop the newline
364 &mail_notification
($mailto, @text);
369 # Check for an import command. This appears with files set as follows:
371 # files[0] - "path/name"
373 # files[2] - "Imported"
374 # files[3] - "sources"
376 if ($files[2] =~ /Imported/ && $files[3] =~ /sources/) {
380 push(@text, &build_header
());
382 push(@text, $files[0]);
386 chop; # Drop the newline
390 &mail_notification
(@text);
395 # Iterate over the body of the message collecting information.
398 chop; # Drop the newline
400 if (/^In directory/) {
401 if ($show_wd) { # useless in client/server mode
402 push(@log_lines, $_);
403 push(@log_lines, "");
408 if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
409 if (/^Added Files/) { $state = $STATE_ADDED; next; }
410 if (/^Removed Files/) { $state = $STATE_REMOVED; next; }
411 if (/^Log Message/) { $state = $STATE_LOG; next; }
413 s/^[ \t\n]+//; # delete leading whitespace
414 s/[ \t\n]+$//; # delete trailing whitespace
416 if ($state == $STATE_CHANGED) { push(@changed_files, split); }
417 if ($state == $STATE_ADDED) { push(@added_files, split); }
418 if ($state == $STATE_REMOVED) { push(@removed_files, split); }
419 if ($state == $STATE_LOG) { push(@log_lines, $_); }
422 # Strip leading and trailing blank lines from the log message. Also
423 # compress multiple blank lines in the body of the message down to a
426 while ($#log_lines > -1) {
427 last if ($log_lines[0] ne "");
430 while ($#log_lines > -1) {
431 last if ($log_lines[$#log_lines] ne "");
434 for ($i = $#log_lines; $i > 0; $i--) {
435 if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
436 splice(@log_lines, $i, 1);
441 print STDERR
"Searching for log file index...";
443 # Find an index to a log file that matches this log message
445 for ($i = 0; ; $i++) {
448 last if (! -e
"$LOG_FILE.$i.$id"); # the next available one
449 @text = &read_logfile
("$LOG_FILE.$i.$id", "");
450 last if ($#text == -1); # nothing in this file, use it
451 last if (join(" ", @log_lines) eq join(" ", @text)); # it's the same log message as another
454 print STDERR
" found log file at $i.$id, now writing tmp files.\n";
457 # Spit out the information gathered in this pass.
459 &append_names_to_file
("$CHANGED_FILE.$i.$id", $dir, @changed_files);
460 &append_names_to_file
("$ADDED_FILE.$i.$id", $dir, @added_files);
461 &append_names_to_file
("$REMOVED_FILE.$i.$id", $dir, @removed_files);
462 &write_logfile
("$LOG_FILE.$i.$id", @log_lines);
464 # Check whether this is the last directory. If not, quit.
467 print STDERR
"Checking current dir against last dir.\n";
469 $_ = &read_line
("$LAST_FILE.$id");
471 if ($_ ne $cvsroot . "/" . $files[0]) {
473 print STDERR
sprintf("Current directory %s is not last directory %s.\n", $cvsroot . "/" .$files[0], $_);
478 print STDERR
sprintf("Current directory %s is last directory %s -- all commits done.\n", $files[0], $_);
485 # This is it. The commits are all finished. Lump everything together
486 # into a single message, fire a copy off to the mailing list, and drop
487 # it on the end of the Changes file.
491 # Produce the final compilation of the log messages
495 push(@text, &build_header
());
498 for ($i = 0; ; $i++) {
499 last if (! -e
"$LOG_FILE.$i.$id"); # we're done them all!
500 @lines = &read_logfile
("$CHANGED_FILE.$i.$id", "");
502 push(@text, "Modified files:");
503 push(@text, &format_lists
(@lines));
505 @lines = &read_logfile
("$ADDED_FILE.$i.$id", "");
507 push(@text, "Added files:");
508 push(@text, &format_lists
(@lines));
510 @lines = &read_logfile
("$REMOVED_FILE.$i.$id", "");
512 push(@text, "Removed files:");
513 push(@text, &format_lists
(@lines));
518 @lines = &read_logfile
("$LOG_FILE.$i.$id", "\t");
520 push(@text, "Log message:");
525 local(@changed_files);
528 push(@changed_files, &read_logfile
("$CHANGED_FILE.$i.$id", ""));
529 push(@changed_files, &read_logfile
("$ADDED_FILE.$i.$id", ""));
530 push(@changed_files, &read_logfile
("$REMOVED_FILE.$i.$id", ""));
533 print STDERR
"main: pre-sort changed_files = ", join(":", @changed_files), ".\n";
535 sort(@changed_files);
537 print STDERR
"main: post-sort changed_files = ", join(":", @changed_files), ".\n";
540 foreach $dofile (@changed_files) {
541 if ($dofile =~ /\/$/) {
542 next; # ignore the silly "dir" entries
545 print STDERR
"main(): doing 'cvs -nQq status -v $dofile'\n";
547 open(STATUS
, "-|") || exec 'cvs', '-nQq', 'status', '-v', $dofile;
550 push(@status_txt, $_);
556 # Write to the commitlog file
559 &write_commitlog
($commitlog, @text);
562 if ($#status_txt >= 0) {
563 push(@text, @status_txt);
566 # Mailout the notification.
568 &mail_notification
(@text);