*** empty log message ***
[IRC.git] / CVSROOT / log_accum
1 #! /usr/bin/perl
2 # -*-Perl-*-
3 #
4 #ident  "@(#)ccvs/contrib:$Name:  $:$Id: log_accum,v 1.1 2006/05/09 00:27:49 bdemsky Exp $"
5 #
6 # Perl filter to handle the log messages from the checkin of files in
7 # a directory.  This script will group the lists of files by log
8 # message, and mail a single consolidated log message at the end of
9 # the commit.
10 #
11 # This file assumes a pre-commit checking program that leaves the
12 # names of the first and last commit directories in a temporary file.
13 #
14 # Contributed by David Hampton <hampton@cisco.com>
15 #
16 # hacked greatly by Greg A. Woods <woods@planix.com>
17 #
18 # modified by C. Scott Ananian <cananian@alumni.princeton.edu> to add
19 # support for including a diff of the changes in the commit email.
20
21 # Usage: log_accum.pl [-d] [-s] [-M module] [[-m mailto] ...] [[-R replyto] ...] [-f logfile]
22 #       -d              - turn on debugging
23 #       -m mailto       - send mail to "mailto" (multiple)
24 #       -R replyto      - set the "Reply-To:" to "replyto" (multiple)
25 #       -M modulename   - set module name to "modulename"
26 #       -f logfile      - write commit messages to logfile too
27 #       -s              - *don't* run "cvs status -v" for each file
28 #       -u              - run "cvs diff -u" for each file
29
30 #
31 #       Configurable options
32 #
33
34 # set this to something that takes a whole message on stdin
35 $MAILER        = "/usr/lib/sendmail -t";
36
37 #
38 #       End user configurable options.
39 #
40
41 # Constants (don't change these!)
42 #
43 $STATE_NONE    = 0;
44 $STATE_CHANGED = 1;
45 $STATE_ADDED   = 2;
46 $STATE_REMOVED = 3;
47 $STATE_LOG     = 4;
48
49 $LAST_FILE     = "/tmp/#cvs.lastdir";
50 $DIFF_FILE     = "/tmp/#cvs.diff";
51
52 $CHANGED_FILE  = "/tmp/#cvs.files.changed";
53 $ADDED_FILE    = "/tmp/#cvs.files.added";
54 $REMOVED_FILE  = "/tmp/#cvs.files.removed";
55 $LOG_FILE      = "/tmp/#cvs.files.log";
56
57 $FILE_PREFIX   = "#cvs.files";
58
59 #
60 #       Subroutines
61 #
62
63 sub cleanup_tmpfiles {
64     local($wd, @files);
65
66     $wd = `pwd`;
67     chdir("/tmp") || die("Can't chdir('/tmp')\n");
68     opendir(DIR, ".");
69     push(@files, grep(/^$FILE_PREFIX\..*\.$id$/, readdir(DIR)));
70     closedir(DIR);
71     foreach (@files) {
72         unlink $_;
73     }
74     unlink $DIFF_FILE . "." . $id;
75     unlink $LAST_FILE . "." . $id;
76
77     chdir($wd);
78 }
79
80 sub write_logfile {
81     local($filename, @lines) = @_;
82
83     open(FILE, ">$filename") || die("Cannot open log file $filename.\n");
84     print FILE join("\n", @lines), "\n";
85     close(FILE);
86 }
87
88 sub append_to_logfile {
89     local($filename, @lines) = @_;
90
91     open(FILE, ">$filename") || die("Cannot open log file $filename.\n");
92     print FILE join("\n", @lines), "\n";
93     close(FILE);
94 }
95
96 sub format_names {
97     local($dir, @files) = @_;
98     local(@lines);
99
100     $format = "\t%-" . sprintf("%d", length($dir)) . "s%s ";
101
102     $lines[0] = sprintf($format, $dir, ":");
103
104     if ($debug) {
105         print STDERR "format_names(): dir = ", $dir, "; files = ", join(":", @files), ".\n";
106     }
107     foreach $file (@files) {
108         if (length($lines[$#lines]) + length($file) > 65) {
109             $lines[++$#lines] = sprintf($format, " ", " ");
110         }
111         $lines[$#lines] .= $file . " ";
112     }
113
114     @lines;
115 }
116
117 sub format_lists {
118     local(@lines) = @_;
119     local(@text, @files, $lastdir);
120
121     if ($debug) {
122         print STDERR "format_lists(): ", join(":", @lines), "\n";
123     }
124     @text = ();
125     @files = ();
126     $lastdir = shift @lines;    # first thing is always a directory
127     if ($lastdir !~ /.*\/$/) {
128         die("Damn, $lastdir doesn't look like a directory!\n");
129     }
130     foreach $line (@lines) {
131         if ($line =~ /.*\/$/) {
132             push(@text, &format_names($lastdir, @files));
133             $lastdir = $line;
134             @files = ();
135         } else {
136             push(@files, $line);
137         }
138     }
139     push(@text, &format_names($lastdir, @files));
140
141     @text;
142 }
143
144 sub append_names_to_file {
145     local($filename, $dir, @files) = @_;
146
147     if (@files) {
148         open(FILE, ">>$filename") || die("Cannot open file $filename.\n");
149         print FILE $dir, "\n";
150         print FILE join("\n", @files), "\n";
151         close(FILE);
152     }
153 }
154
155 sub read_line {
156     local($line);
157     local($filename) = @_;
158
159     open(FILE, "<$filename") || die("Cannot open file $filename.\n");
160     $line = <FILE>;
161     close(FILE);
162     chop($line);
163     $line;
164 }
165
166 sub read_logfile {
167     local(@text);
168     local($filename, $leader) = @_;
169
170     open(FILE, "<$filename");
171     while (<FILE>) {
172         chop;
173         push(@text, $leader.$_);
174     }
175     close(FILE);
176     @text;
177 }
178
179 sub build_header {
180     local($header);
181     local($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
182     $header = sprintf("CVSROOT:\t%s\nModule name:\t%s\nChanges by:\t%s@%s\t%02d/%02d/%02d %02d:%02d:%02d",
183                       $cvsroot,
184                       $modulename,
185                       $login, $hostdomain,
186                       $year%100, $mon+1, $mday,
187                       $hour, $min, $sec);
188 }
189
190 sub mail_notification {
191     local(@text) = @_;
192
193     # if only we had strftime()...  stuff stolen from perl's ctime.pl:
194     local($[) = 0;
195
196     @DoW = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
197     @MoY = ('Jan','Feb','Mar','Apr','May','Jun',
198             'Jul','Aug','Sep','Oct','Nov','Dec');
199
200     # Determine what time zone is in effect.
201     # Use GMT if TZ is defined as null, local time if TZ undefined.
202     # There's no portable way to find the system default timezone.
203     #
204     $TZ = defined($ENV{'TZ'}) ? ( $ENV{'TZ'} ? $ENV{'TZ'} : 'GMT' ) : '';
205
206     # Hack to deal with 'PST8PDT' format of TZ
207     # Note that this can't deal with all the esoteric forms, but it
208     # does recognize the most common: [:]STDoff[DST[off][,rule]]
209     #
210     if ($TZ =~ /^([^:\d+\-,]{3,})([+-]?\d{1,2}(:\d{1,2}){0,2})([^\d+\-,]{3,})?/) {
211         $TZ = $isdst ? $4 : $1;
212         $tzoff = sprintf("%05d", -($2) * 100);
213     }
214
215     # perl-4.036 doesn't have the $zone or $gmtoff...
216     ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst, $zone, $gmtoff) =
217         ($TZ eq 'GMT') ? gmtime(time) : localtime(time);
218
219     $year += ($year < 70) ? 2000 : 1900;
220
221     if ($gmtoff != 0) {
222         $tzoff = sprintf("%05d", ($gmtoff / 60) * 100);
223     }
224     if ($zone ne '') {
225         $TZ = $zone;
226     }
227
228     # ok, let's try....
229     $rfc822date = sprintf("%s, %2d %s %4d %2d:%02d:%02d %s (%s)",
230                           $DoW[$wday], $mday, $MoY[$mon], $year,
231                           $hour, $min, $sec, $tzoff, $TZ);
232
233     open(MAIL, "| $MAILER");
234     print MAIL "Date:     " . $rfc822date . "\n";
235     print MAIL "Subject:  CVS Update: " . $modulename . "\n";
236     print MAIL "To:       " . $mailto . "\n";
237     print MAIL "From:     " . $login . "@" . $hostdomain . "\n";
238     print MAIL "Reply-To: " . $replyto . "\n";
239     print MAIL "\n";
240     print MAIL join("\n", @text), "\n";
241     close(MAIL);
242 }
243
244 sub write_commitlog {
245     local($logfile, @text) = @_;
246
247     open(FILE, ">>$logfile");
248     print FILE join("\n", @text), "\n";
249     close(FILE);
250 }
251
252 #
253 #       Main Body
254 #
255
256 # Initialize basic variables
257 #
258 $debug = 0;
259 $id = getpgrp();                # note, you *must* use a shell which does setpgrp()
260 $state = $STATE_NONE;
261 $login = getlogin || (getpwuid($<))[0] || "nobody";
262 chop($hostname = `hostname`);
263 #chop($domainname = `domainname`);
264 #$hostdomain = $hostname . $domainname;
265 chop($domainname = `hostname -d`);
266 chop($hostdomain = `hostname --fqdn`);
267 $cvsroot = $ENV{'CVSROOT'};
268 $do_status = 1;
269 $do_diff = 0;
270 $modulename = "";
271
272 # parse command line arguments (file list is seen as one arg)
273 #
274 while (@ARGV) {
275     $arg = shift @ARGV;
276
277     if ($arg eq '-d') {
278         $debug = 1;
279         print STDERR "Debug turned on...\n";
280     } elsif ($arg eq '-m') {
281         if ($mailto eq '') {
282             $mailto = shift @ARGV;
283         } else {
284             $mailto = $mailto . ", " . shift @ARGV;
285         }
286     } elsif ($arg eq '-R') {
287         if ($replyto eq '') {
288             $replyto = shift @ARGV;
289         } else {
290             $replyto = $replyto . ", " . shift @ARGV;
291         }
292     } elsif ($arg eq '-M') {
293         $modulename = shift @ARGV;
294     } elsif ($arg eq '-s') {
295         $do_status = 0;
296     } elsif ($arg eq '-u') {
297         $do_diff = 1;
298     } elsif ($arg eq '-f') {
299         ($commitlog) && die("Too many '-f' args\n");
300         $commitlog = shift @ARGV;
301     } else {
302         ($donefiles) && die("Too many arguments!  Check usage.\n");
303         $donefiles = 1;
304         @files = split(/ /, $arg);
305     }
306 }
307 ($mailto) || die("No mail recipient specified (use -m)\n");
308 if ($replyto eq '') {
309     $replyto = $login;
310 }
311
312 # for now, the first "file" is the repository directory being committed,
313 # relative to the $CVSROOT location
314 #
315 @path = split('/', $files[0]);
316
317 # XXX there are some ugly assumptions in here about module names and
318 # XXX directories relative to the $CVSROOT location -- really should
319 # XXX read $CVSROOT/CVSROOT/modules, but that's not so easy to do, since
320 # XXX we have to parse it backwards.
321 #
322 if ($modulename eq "") {
323     $modulename = $path[0];     # I.e. the module name == top-level dir
324 }
325 if ($#path == 0) {
326     $dir = ".";
327 } else {
328     $dir = join('/', @path);
329 }
330 $dir = $dir . "/";
331
332 if ($debug) {
333     print STDERR "module - ", $modulename, "\n";
334     print STDERR "dir    - ", $dir, "\n";
335     print STDERR "path   - ", join(":", @path), "\n";
336     print STDERR "files  - ", join(":", @files), "\n";
337     print STDERR "id     - ", $id, "\n";
338 }
339
340 # Check for a new directory first.  This appears with files set as follows:
341 #
342 #    files[0] - "path/name/newdir"
343 #    files[1] - "-"
344 #    files[2] - "New"
345 #    files[3] - "directory"
346 #
347 if ($files[2] =~ /New/ && $files[3] =~ /directory/) {
348     local(@text);
349
350     @text = ();
351     push(@text, &build_header());
352     push(@text, "");
353     push(@text, $files[0]);
354     push(@text, "");
355
356     while (<STDIN>) {
357         chop;                   # Drop the newline
358         push(@text, $_);
359     }
360
361     &mail_notification($mailto, @text);
362
363     exit 0;
364 }
365
366 # Check for an import command.  This appears with files set as follows:
367 #
368 #    files[0] - "path/name"
369 #    files[1] - "-"
370 #    files[2] - "Imported"
371 #    files[3] - "sources"
372 #
373 if ($files[2] =~ /Imported/ && $files[3] =~ /sources/) {
374     local(@text);
375
376     @text = ();
377     push(@text, &build_header());
378     push(@text, "");
379     push(@text, $files[0]);
380     push(@text, "");
381
382     while (<STDIN>) {
383         chop;                   # Drop the newline
384         push(@text, $_);
385     }
386
387     &mail_notification(@text);
388
389     exit 0;
390 }
391
392 # Iterate over the body of the message collecting information.
393 #
394 while (<STDIN>) {
395     chop;                       # Drop the newline
396
397     if (/^In directory/) {
398         push(@log_lines, $_);
399         push(@log_lines, "");
400         next;
401     }
402
403     if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
404     if (/^Added Files/)    { $state = $STATE_ADDED;   next; }
405     if (/^Removed Files/)  { $state = $STATE_REMOVED; next; }
406     if (/^Log Message/)    { $state = $STATE_LOG;     next; }
407
408     s/^[ \t\n]+//;              # delete leading whitespace
409     s/[ \t\n]+$//;              # delete trailing whitespace
410     
411     if ($state == $STATE_CHANGED) { push(@changed_files, split); }
412     if ($state == $STATE_ADDED)   { push(@added_files,   split); }
413     if ($state == $STATE_REMOVED) { push(@removed_files, split); }
414     if ($state == $STATE_LOG)     { push(@log_lines,     $_); }
415 }
416
417 # Strip leading and trailing blank lines from the log message.  Also
418 # compress multiple blank lines in the body of the message down to a
419 # single blank line.
420 #
421 while ($#log_lines > -1) {
422     last if ($log_lines[0] ne "");
423     shift(@log_lines);
424 }
425 while ($#log_lines > -1) {
426     last if ($log_lines[$#log_lines] ne "");
427     pop(@log_lines);
428 }
429 for ($i = $#log_lines; $i > 0; $i--) {
430     if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
431         splice(@log_lines, $i, 1);
432     }
433 }
434
435 if ($debug) {
436     print STDERR "Searching for log file index...";
437 }
438 # Find an index to a log file that matches this log message
439 #
440 for ($i = 0; ; $i++) {
441     local(@text);
442
443     last if (! -e "$LOG_FILE.$i.$id"); # the next available one
444     @text = &read_logfile("$LOG_FILE.$i.$id", "");
445     last if ($#text == -1);     # nothing in this file, use it
446     last if (join(" ", @log_lines) eq join(" ", @text)); # it's the same log message as another
447 }
448 if ($debug) {
449     print STDERR " found log file at $i.$id, now writing tmp files.\n";
450 }
451
452 # Spit out the information gathered in this pass.
453 #
454 &append_names_to_file("$CHANGED_FILE.$i.$id", $dir, @changed_files);
455 &append_names_to_file("$ADDED_FILE.$i.$id",   $dir, @added_files);
456 &append_names_to_file("$REMOVED_FILE.$i.$id", $dir, @removed_files);
457 &write_logfile("$LOG_FILE.$i.$id", @log_lines);
458
459 # Check whether this is the last directory.  If not, quit.
460 #
461 if ($debug) {
462     print STDERR "Checking current dir against last dir.\n";
463 }
464 $_ = &read_line("$LAST_FILE.$id");
465
466 if ($_ ne $cvsroot . "/" . $files[0]) {
467     if ($debug) {
468         print STDERR sprintf("Current directory %s is not last directory %s.\n", $cvsroot . "/" .$files[0], $_);
469     }
470     exit 0;
471 }
472 if ($debug) {
473     print STDERR sprintf("Current directory %s is last directory %s -- all commits done.\n", $files[0], $_);
474 }
475
476 #
477 #       End Of Commits!
478 #
479
480 # This is it.  The commits are all finished.  Lump everything together
481 # into a single message, fire a copy off to the mailing list, and drop
482 # it on the end of the Changes file.
483 #
484
485 #
486 # Produce the final compilation of the log messages
487 #
488 @text = ();
489 @status_txt = ();
490 push(@text, &build_header());
491 push(@text, "");
492
493 for ($i = 0; ; $i++) {
494     last if (! -e "$LOG_FILE.$i.$id"); # we're done them all!
495     @lines = &read_logfile("$CHANGED_FILE.$i.$id", "");
496     if ($#lines >= 0) {
497         push(@text, "Modified files:");
498         push(@text, &format_lists(@lines));
499     }
500     @lines = &read_logfile("$ADDED_FILE.$i.$id", "");
501     if ($#lines >= 0) {
502         push(@text, "Added files:");
503         push(@text, &format_lists(@lines));
504     }
505     @lines = &read_logfile("$REMOVED_FILE.$i.$id", "");
506     if ($#lines >= 0) {
507         push(@text, "Removed files:");
508         push(@text, &format_lists(@lines));
509     }
510     if ($#text >= 0) {
511         push(@text, "");
512     }
513     @lines = &read_logfile("$LOG_FILE.$i.$id", "\t");
514     if ($#lines >= 0) {
515         push(@text, "Log message:");
516         push(@text, @lines);
517         push(@text, "");
518     }
519     if ($do_status) {
520         local(@changed_files);
521
522         @changed_files = ();
523         push(@changed_files, &read_logfile("$CHANGED_FILE.$i.$id", ""));
524         push(@changed_files, &read_logfile("$ADDED_FILE.$i.$id", ""));
525         push(@changed_files, &read_logfile("$REMOVED_FILE.$i.$id", ""));
526
527         if ($debug) {
528             print STDERR "main: pre-sort changed_files = ", join(":", @changed_files), ".\n";
529         }
530         sort(@changed_files);
531         if ($debug) {
532             print STDERR "main: post-sort changed_files = ", join(":", @changed_files), ".\n";
533         }
534
535         foreach $dofile (@changed_files) {
536             if ($dofile =~ /\/$/) {
537                 next;           # ignore the silly "dir" entries
538             }
539             if ($debug) {
540                 print STDERR 
541                     "main(): doing 'cvs -nQq status -v $dofile'\n";
542             }
543             open(STATUS, "-|") || 
544                 exec 'cvs', '-nQq', 'status', '-v', $dofile;
545             while (<STATUS>) {
546                 chop;
547                 push(@status_txt, $_);
548             }
549             close(STATUS);
550         }
551     }
552 }
553
554 # Read in diff text.
555 @diff_txt = ();
556 @diff_txt = &read_logfile("$DIFF_FILE.$id", "") if ($do_diff);
557
558 # Write to the commitlog file
559 #
560 if ($commitlog) {
561     &write_commitlog($commitlog, @text);
562 }
563
564 if ($#diff_txt >= 0) {
565     push(@text, "-----------------------------------------------------");
566     push(@text, "Changes made in this commit:");
567     push(@text, "-----------------------------------------------------");
568     push(@text, "");
569     push(@text, @diff_txt);
570 }
571
572 if ($#status_txt >= 0) {
573     push(@text, @status_txt);
574 }
575
576 # Mailout the notification.
577 #
578 &mail_notification(@text);
579
580 # cleanup
581 #
582 if (! $debug) {
583     &cleanup_tmpfiles();
584 }
585
586 exit 0;