1#! @PERL@ 2# -*-Perl-*- 3# 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 7# the commit. 8# 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. 11# 12# Contributed by David Hampton <hampton@cisco.com> 13# 14# hacked greatly by Greg A. Woods <woods@planix.com> 15 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 25 26# 27# Configurable options 28# 29 30# set this to something that takes a whole message on stdin 31$MAILER = "/usr/lib/sendmail -t"; 32 33# 34# End user configurable options. 35# 36 37# Constants (don't change these!) 38# 39$STATE_NONE = 0; 40$STATE_CHANGED = 1; 41$STATE_ADDED = 2; 42$STATE_REMOVED = 3; 43$STATE_LOG = 4; 44 45$LAST_FILE = "/tmp/#cvs.lastdir"; 46 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"; 51 52$FILE_PREFIX = "#cvs.files"; 53 54# 55# Subroutines 56# 57 58sub cleanup_tmpfiles { 59 local($wd, @files); 60 61 $wd = `pwd`; 62 chdir("/tmp") || die("Can't chdir('/tmp')\n"); 63 opendir(DIR, "."); 64 push(@files, grep(/^$FILE_PREFIX\..*\.$id$/, readdir(DIR))); 65 closedir(DIR); 66 foreach (@files) { 67 unlink $_; 68 } 69 unlink $LAST_FILE . "." . $id; 70 71 chdir($wd); 72} 73 74sub write_logfile { 75 local($filename, @lines) = @_; 76 77 open(FILE, ">$filename") || die("Cannot open log file $filename.\n"); 78 print FILE join("\n", @lines), "\n"; 79 close(FILE); 80} 81 82sub append_to_logfile { 83 local($filename, @lines) = @_; 84 85 open(FILE, ">$filename") || die("Cannot open log file $filename.\n"); 86 print FILE join("\n", @lines), "\n"; 87 close(FILE); 88} 89 90sub format_names { 91 local($dir, @files) = @_; 92 local(@lines); 93 94 $format = "\t%-" . sprintf("%d", length($dir)) . "s%s "; 95 96 $lines[0] = sprintf($format, $dir, ":"); 97 98 if ($debug) { 99 print STDERR "format_names(): dir = ", $dir, "; files = ", join(":", @files), ".\n"; 100 } 101 foreach $file (@files) { 102 if (length($lines[$#lines]) + length($file) > 65) { 103 $lines[++$#lines] = sprintf($format, " ", " "); 104 } 105 $lines[$#lines] .= $file . " "; 106 } 107 108 @lines; 109} 110 111sub format_lists { 112 local(@lines) = @_; 113 local(@text, @files, $lastdir); 114 115 if ($debug) { 116 print STDERR "format_lists(): ", join(":", @lines), "\n"; 117 } 118 @text = (); 119 @files = (); 120 $lastdir = shift @lines; # first thing is always a directory 121 if ($lastdir !~ /.*\/$/) { 122 die("Damn, $lastdir doesn't look like a directory!\n"); 123 } 124 foreach $line (@lines) { 125 if ($line =~ /.*\/$/) { 126 push(@text, &format_names($lastdir, @files)); 127 $lastdir = $line; 128 @files = (); 129 } else { 130 push(@files, $line); 131 } 132 } 133 push(@text, &format_names($lastdir, @files)); 134 135 @text; 136} 137 138sub append_names_to_file { 139 local($filename, $dir, @files) = @_; 140 141 if (@files) { 142 open(FILE, ">>$filename") || die("Cannot open file $filename.\n"); 143 print FILE $dir, "\n"; 144 print FILE join("\n", @files), "\n"; 145 close(FILE); 146 } 147} 148 149sub read_line { 150 local($line); 151 local($filename) = @_; 152 153 open(FILE, "<$filename") || die("Cannot open file $filename.\n"); 154 $line = <FILE>; 155 close(FILE); 156 chop($line); 157 $line; 158} 159 160sub read_logfile { 161 local(@text); 162 local($filename, $leader) = @_; 163 164 open(FILE, "<$filename"); 165 while (<FILE>) { 166 chop; 167 push(@text, $leader.$_); 168 } 169 close(FILE); 170 @text; 171} 172 173sub build_header { 174 local($header); 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", 177 $cvsroot, 178 $modulename, 179 $dir, 180 $login, $hostdomain, 181 $year%100, $mon+1, $mday, 182 $hour, $min, $sec); 183} 184 185sub mail_notification { 186 local(@text) = @_; 187 188 # if only we had strftime()... stuff stolen from perl's ctime.pl: 189 local($[) = 0; 190 191 @DoW = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat'); 192 @MoY = ('Jan','Feb','Mar','Apr','May','Jun', 193 'Jul','Aug','Sep','Oct','Nov','Dec'); 194 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. 198 # 199 $TZ = defined($ENV{'TZ'}) ? ( $ENV{'TZ'} ? $ENV{'TZ'} : 'GMT' ) : ''; 200 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]] 204 # 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); 208 } 209 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); 213 214 $year += ($year < 70) ? 2000 : 1900; 215 216 if ($gmtoff != 0) { 217 $tzoff = sprintf("%05d", ($gmtoff / 60) * 100); 218 } 219 if ($zone ne '') { 220 $TZ = $zone; 221 } 222 223 # ok, let's try.... 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); 227 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"; 233 print MAIL "\n"; 234 print MAIL join("\n", @text), "\n"; 235 close(MAIL); 236} 237 238sub write_commitlog { 239 local($logfile, @text) = @_; 240 241 open(FILE, ">>$logfile"); 242 print FILE join("\n", @text), "\n"; 243 close(FILE); 244} 245 246# 247# Main Body 248# 249 250# Initialize basic variables 251# 252$debug = 0; 253$id = getpgrp(); # note, you *must* use a shell which does setpgrp() 254$state = $STATE_NONE; 255chop($hostname = `hostname`); 256chop($domainname = `domainname`); 257if ($domainname !~ '^\..*') { 258 $domainname = '.' . $domainname; 259} 260$hostdomain = $hostname . $domainname; 261$cvsroot = $ENV{'CVSROOT'}; 262$do_status = 1; # moderately useful 263$show_wd = 0; # useless in client/server 264$modulename = ""; 265 266# parse command line arguments (file list is seen as one arg) 267# 268while (@ARGV) { 269 $arg = shift @ARGV; 270 271 if ($arg eq '-d') { 272 $debug = 1; 273 print STDERR "Debug turned on...\n"; 274 } elsif ($arg eq '-m') { 275 if ($mailto eq '') { 276 $mailto = shift @ARGV; 277 } else { 278 $mailto = $mailto . ", " . shift @ARGV; 279 } 280 } elsif ($arg eq '-R') { 281 if ($replyto eq '') { 282 $replyto = shift @ARGV; 283 } else { 284 $replyto = $replyto . ", " . shift @ARGV; 285 } 286 } elsif ($arg eq '-M') { 287 $modulename = shift @ARGV; 288 } elsif ($arg eq '-u') { 289 $login = shift @ARGV; 290 } elsif ($arg eq '-s') { 291 $do_status = 0; 292 } elsif ($arg eq '-w') { 293 $show_wd = 1; 294 } elsif ($arg eq '-f') { 295 ($commitlog) && die("Too many '-f' args\n"); 296 $commitlog = shift @ARGV; 297 } else { 298 ($donefiles) && die("Too many arguments! Check usage.\n"); 299 $donefiles = 1; 300 @files = split(/ /, $arg); 301 } 302} 303if ($login eq '') { 304 $login = getlogin || (getpwuid($<))[0] || "nobody"; 305} 306($mailto) || die("No mail recipient specified (use -m)\n"); 307if ($replyto eq '') { 308 $replyto = $login; 309} 310 311# for now, the first "file" is the repository directory being committed, 312# relative to the $CVSROOT location 313# 314@path = split('/', $files[0]); 315 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. 320# XXX 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. 324# 325if ($modulename eq "") { 326 $modulename = $path[0]; # I.e. the module name == top-level dir 327} 328if ($#path == 0) { 329 $dir = "."; 330} else { 331 $dir = join('/', @path); 332} 333$dir = $dir . "/"; 334 335if ($debug) { 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"; 341} 342 343# Check for a new directory first. This appears with files set as follows: 344# 345# files[0] - "path/name/newdir" 346# files[1] - "-" 347# files[2] - "New" 348# files[3] - "directory" 349# 350if ($files[2] =~ /New/ && $files[3] =~ /directory/) { 351 local(@text); 352 353 @text = (); 354 push(@text, &build_header()); 355 push(@text, ""); 356 push(@text, $files[0]); 357 push(@text, ""); 358 359 while (<STDIN>) { 360 chop; # Drop the newline 361 push(@text, $_); 362 } 363 364 &mail_notification($mailto, @text); 365 366 exit 0; 367} 368 369# Check for an import command. This appears with files set as follows: 370# 371# files[0] - "path/name" 372# files[1] - "-" 373# files[2] - "Imported" 374# files[3] - "sources" 375# 376if ($files[2] =~ /Imported/ && $files[3] =~ /sources/) { 377 local(@text); 378 379 @text = (); 380 push(@text, &build_header()); 381 push(@text, ""); 382 push(@text, $files[0]); 383 push(@text, ""); 384 385 while (<STDIN>) { 386 chop; # Drop the newline 387 push(@text, $_); 388 } 389 390 &mail_notification(@text); 391 392 exit 0; 393} 394 395# Iterate over the body of the message collecting information. 396# 397while (<STDIN>) { 398 chop; # Drop the newline 399 400 if (/^In directory/) { 401 if ($show_wd) { # useless in client/server mode 402 push(@log_lines, $_); 403 push(@log_lines, ""); 404 } 405 next; 406 } 407 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; } 412 413 s/^[ \t\n]+//; # delete leading whitespace 414 s/[ \t\n]+$//; # delete trailing whitespace 415 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, $_); } 420} 421 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 424# single blank line. 425# 426while ($#log_lines > -1) { 427 last if ($log_lines[0] ne ""); 428 shift(@log_lines); 429} 430while ($#log_lines > -1) { 431 last if ($log_lines[$#log_lines] ne ""); 432 pop(@log_lines); 433} 434for ($i = $#log_lines; $i > 0; $i--) { 435 if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) { 436 splice(@log_lines, $i, 1); 437 } 438} 439 440if ($debug) { 441 print STDERR "Searching for log file index..."; 442} 443# Find an index to a log file that matches this log message 444# 445for ($i = 0; ; $i++) { 446 local(@text); 447 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 452} 453if ($debug) { 454 print STDERR " found log file at $i.$id, now writing tmp files.\n"; 455} 456 457# Spit out the information gathered in this pass. 458# 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); 463 464# Check whether this is the last directory. If not, quit. 465# 466if ($debug) { 467 print STDERR "Checking current dir against last dir.\n"; 468} 469$_ = &read_line("$LAST_FILE.$id"); 470 471if ($_ ne $cvsroot . "/" . $files[0]) { 472 if ($debug) { 473 print STDERR sprintf("Current directory %s is not last directory %s.\n", $cvsroot . "/" .$files[0], $_); 474 } 475 exit 0; 476} 477if ($debug) { 478 print STDERR sprintf("Current directory %s is last directory %s -- all commits done.\n", $files[0], $_); 479} 480 481# 482# End Of Commits! 483# 484 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. 488# 489 490# 491# Produce the final compilation of the log messages 492# 493@text = (); 494@status_txt = (); 495push(@text, &build_header()); 496push(@text, ""); 497 498for ($i = 0; ; $i++) { 499 last if (! -e "$LOG_FILE.$i.$id"); # we're done them all! 500 @lines = &read_logfile("$CHANGED_FILE.$i.$id", ""); 501 if ($#lines >= 0) { 502 push(@text, "Modified files:"); 503 push(@text, &format_lists(@lines)); 504 } 505 @lines = &read_logfile("$ADDED_FILE.$i.$id", ""); 506 if ($#lines >= 0) { 507 push(@text, "Added files:"); 508 push(@text, &format_lists(@lines)); 509 } 510 @lines = &read_logfile("$REMOVED_FILE.$i.$id", ""); 511 if ($#lines >= 0) { 512 push(@text, "Removed files:"); 513 push(@text, &format_lists(@lines)); 514 } 515 if ($#text >= 0) { 516 push(@text, ""); 517 } 518 @lines = &read_logfile("$LOG_FILE.$i.$id", "\t"); 519 if ($#lines >= 0) { 520 push(@text, "Log message:"); 521 push(@text, @lines); 522 push(@text, ""); 523 } 524 if ($do_status) { 525 local(@changed_files); 526 527 @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", "")); 531 532 if ($debug) { 533 print STDERR "main: pre-sort changed_files = ", join(":", @changed_files), ".\n"; 534 } 535 sort(@changed_files); 536 if ($debug) { 537 print STDERR "main: post-sort changed_files = ", join(":", @changed_files), ".\n"; 538 } 539 540 foreach $dofile (@changed_files) { 541 if ($dofile =~ /\/$/) { 542 next; # ignore the silly "dir" entries 543 } 544 if ($debug) { 545 print STDERR "main(): doing 'cvs -nQq status -v $dofile'\n"; 546 } 547 open(STATUS, "-|") || exec 'cvs', '-nQq', 'status', '-v', $dofile; 548 while (<STATUS>) { 549 chop; 550 push(@status_txt, $_); 551 } 552 } 553 } 554} 555 556# Write to the commitlog file 557# 558if ($commitlog) { 559 &write_commitlog($commitlog, @text); 560} 561 562if ($#status_txt >= 0) { 563 push(@text, @status_txt); 564} 565 566# Mailout the notification. 567# 568&mail_notification(@text); 569 570# cleanup 571# 572if (! $debug) { 573 &cleanup_tmpfiles(); 574} 575 576exit 0; 577