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 # IMPORTANT: what the above means is, this script interacts with
13 # commit_prep, in that they have to agree on the tmpfile name to use.
14 # See $LAST_FILE below.
16 # How this works: CVS triggers this script once for each directory
17 # involved in the commit -- in other words, a single commit can invoke
18 # this script N times. It knows when it's on the last invocation by
19 # examining the contents of $LAST_FILE. Between invocations, it
20 # caches information for its future incarnations in various temporary
21 # files in /tmp, which are named according to the process group and
22 # the committer (by themselves, neither of these are unique, but
23 # together they almost always are, unless the same user is doing two
24 # commits simultaneously). The final invocation is the one that
25 # actually sends the mail -- it gathers up the cached information,
26 # combines that with what it found out on this pass, and sends a
27 # commit message to the appropriate mailing list.
29 # (Ask Karl Fogel <kfogel@collab.net> if questions.)
31 # Contributed by David Hampton <hampton@cisco.com>
32 # Roy Fielding removed useless code and added log/mail of new files
33 # Ken Coar added special processing (i.e., no diffs) for binary files
36 ############################################################
38 # Configurable options
40 ############################################################
42 # Where do you want the RCS ID and delta info?
45 # 2 = in both mail and logs.
49 #if you are using CVS web then set this to some value... if not set it to ""
51 # When set properly, this will cause links to aspects of the project to
52 # print in the commit emails.
53 #$CVSWEB_SCHEME = "http";
54 #$CVSWEB_DOMAIN = "cvshome.org";
56 #$CVSWEB_URI = "source/browse/";
61 # Set this to a domain to have CVS pretend that all users who make
62 # commits have mail accounts within that domain.
63 #$EMULATE_LOCAL_MAIL_USER="cvshome.org";
65 # Set this to '-c' for context diffs; defaults to '-u' for unidiff format.
68 ############################################################
72 ############################################################
79 $TMPDIR = $ENV{'TMPDIR'} || '/tmp';
80 $FILE_PREFIX = '#cvs.';
82 $LAST_FILE = "$TMPDIR/${FILE_PREFIX}lastdir"; # Created by commit_prep!
83 $ADDED_FILE = "$TMPDIR/${FILE_PREFIX}files.added";
84 $REMOVED_FILE = "$TMPDIR/${FILE_PREFIX}files.removed";
85 $LOG_FILE = "$TMPDIR/${FILE_PREFIX}files.log";
86 $BRANCH_FILE = "$TMPDIR/${FILE_PREFIX}files.branch";
87 $MLIST_FILE = "$TMPDIR/${FILE_PREFIX}files.mlist";
88 $SUMMARY_FILE = "$TMPDIR/${FILE_PREFIX}files.summary";
90 $CVSROOT = $ENV{'CVSROOT'};
92 $MAIL_CMD = "| /usr/lib/sendmail -i -t";
93 #$MAIL_CMD = "| /var/qmail/bin/qmail-inject";
94 $MAIL_FROM = 'commitlogger'; #not needed if EMULATE_LOCAL_MAIL_USER
95 $SUBJECT_PRE = 'CVS update:';
98 ############################################################
102 ############################################################
105 local($dir, @files) = @_;
108 $lines[0] = sprintf(" %-08s", $dir);
109 foreach $file (@files) {
110 if (length($lines[$#lines]) + length($file) > 60) {
111 $lines[++$#lines] = sprintf(" %8s", " ");
113 $lines[$#lines] .= " ".$file;
118 sub cleanup_tmpfiles {
121 opendir(DIR, $TMPDIR);
122 push(@files, grep(/^${FILE_PREFIX}.*\.${id}\.${cvs_user}$/, readdir(DIR)));
130 local($filename, @lines) = @_;
132 open(FILE, ">$filename") || die ("Cannot open log file $filename: $!\n");
133 print(FILE join("\n", @lines), "\n");
138 local($filename, $dir, @files) = @_;
141 local(@lines) = &format_names($dir, @files);
142 open(FILE, ">>$filename") || die ("Cannot open file $filename: $!\n");
143 print(FILE join("\n", @lines), "\n");
149 local($filename, $line) = @_;
151 open(FILE, ">$filename") || die("Cannot open file $filename: $!\n");
152 print(FILE $line, "\n");
157 local($filename, $line) = @_;
159 open(FILE, ">>$filename") || die("Cannot open file $filename: $!\n");
160 print(FILE $line, "\n");
165 local($filename) = @_;
168 open(FILE, "<$filename") || die("Cannot open file $filename: $!\n");
175 sub read_line_nodie {
176 local($filename) = @_;
178 open(FILE, "<$filename") || return ("");
186 sub read_file_lines {
187 local($filename) = @_;
190 open(FILE, "<$filename") || return ();
200 local($filename, $leader) = @_;
203 open(FILE, "<$filename") || return ();
206 push(@text, sprintf(" %-10s %s", $leader, $_));
214 local($filename, $leader) = @_;
217 open(FILE, "<$filename") || die ("Cannot open log file $filename: $!\n");
220 push(@text, $leader.$_);
227 # do an 'cvs -Qn status' on each file in the arguments, and extract info.
230 local($out, @filenames) = @_;
232 local($file, $rev, $rcsfile, $line, $vhost, $cvsweb_base);
235 $file = shift @filenames;
241 open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'status', '--', $file;
249 if (/^[ \t]*Repository revision/) {
251 @revline = split(' ', $_);
253 $rcsfile = $revline[3];
254 $rcsfile =~ s,^$CVSROOT/,,;
261 if ($rev ne '' && $rcsfile ne '') {
262 open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'log', "-r$rev",
269 $delta =~ s/^[\s]+lines://;
277 if ($CVSWEB_PORT eq "80") {
278 $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN/$CVSWEB_URI";
281 $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN:$CVSWEB_PORT/$CVSWEB_URI";
283 if ($SEND_URL eq "true") {
284 $diff .= $cvsweb_base . join("/", @path) . "/$file";
288 # If this is a binary file, don't try to report a diff; not only is
289 # it meaningless, but it also screws up some mailers. We rely on
290 # Perl's 'is this binary' algorithm; it's pretty good. But not
293 if (($file =~ /\.(?:pdf|gif|jpg|mpg)$/i) || (-B $file)) {
294 if ($SEND_URL eq "true") {
295 $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
297 if ($SEND_DIFF eq "true") {
298 $diff .= "\t<<Binary file>>\n\n";
303 # Get the differences between this and the previous revision,
304 # being aware that new files always have revision '1.1' and
305 # new branches always end in '.n.1'.
307 if ($rev =~ /^(.*)\.([0-9]+)$/) {
309 $prev_rev = $1 . '.' . $prev;
311 $prev_rev =~ s/\.[0-9]+\.0$//;# Truncate if first rev on branch
314 if ($SEND_URL eq "true") {
315 $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
317 if ($SEND_DIFF eq "true") {
319 || exec "$cvsbin/cvs", '-Qn', 'update', '-p', '-r1.1',
321 $diff .= "Index: $file\n=================================="
322 . "=================================\n";
326 if ($SEND_URL eq "true") {
327 $diff .= ".diff?r1=$prev_rev&r2=$rev\n\n";
329 if ($SEND_DIFF eq "true") {
330 $diff .= "(In the diff below, changes in quantity "
331 . "of whitespace are not shown.)\n\n";
333 || exec "$cvsbin/cvs", '-Qn', 'diff', "$difftype",
334 '-b', "-r$prev_rev", "-r$rev", '--', $file;
338 if ($SEND_DIFF eq "true") {
348 &append_line($out, sprintf("%-9s%-12s%s%s", $rev, $delta,
357 local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
359 $header = sprintf(" User: %-8s\n Date: %02d/%02d/%02d %02d:%02d:%02d",
360 $cvs_user, $year%100, $mon+1, $mday,
362 # $header = sprintf("%-8s %02d/%02d/%02d %02d:%02d:%02d",
363 # $login, $year%100, $mon+1, $mday,
364 # $hour, $min, $sec);
367 # !!! Destination Mailing-list and history file mappings here !!!
372 # my $domain = "cvshome.org";
374 # if ($path =~ /^([^\/]+)/) {
375 # return "cvs\@$1.$domain";
377 # return "cvs\@$domain";
381 sub derive_subject_from_changes_file ()
387 open (CH, "<$CHANGED_FILE.$i.$id.$cvs_user") or last;
389 while (my $change = <CH>)
391 # A changes file looks like this:
393 # src foo.c newfile.html
394 # www index.html project_nav.html
396 # Each line is " Dir File1 File2 ..."
397 # We only care about Dir, since the subject line should
400 $change =~ s/^[ \t]*//;
401 $change =~ /^([^ \t]+)[ \t]*/;
403 # Fold to rightmost directory component
416 $subj = "MODIFIED: $subj ...";
419 # NPM: See if there's any file-addition notifications.
420 my $added = &read_line_nodie("$ADDED_FILE.$i.$id.$cvs_user");
422 $subj .= "ADDED: $added ";
425 # print "derive_subject_from_changes_file().. added== $added \n";
427 ## NPM: See if there's any file-removal notications.
428 my $removed = &read_line_nodie("$REMOVED_FILE.$i.$id.$cvs_user");
429 if ($removed ne "") {
430 $subj .= "REMOVED: $removed ";
433 # print "derive_subject_from_changes_file().. removed== $removed \n";
435 ## NPM: See if there's any branch notifications.
436 my $branched = &read_line_nodie("$BRANCH_FILE.$i.$id.$cvs_user");
437 if ($branched ne "") {
438 $subj .= "BRANCHED: $branched";
441 # print "derive_subject_from_changes_file().. branched== $branched \n";
443 ## NPM: DEFAULT: DIRECTORY CREATION (c.f. "Check for a new directory first" in main mody)
445 my $subject = join("/", @path);
446 $subj = "NEW: $subject";
453 sub mail_notification
455 local($addr_list, @text) = @_;
458 my $subj = &derive_subject_from_changes_file ();
460 if ($EMULATE_LOCAL_MAIL_USER NE "") {
461 $MAIL_FROM = "$cvs_user\@$EMULATE_LOCAL_MAIL_USER";
464 $mail_to = join(", ", @{$addr_list});
466 print "Mailing the commit message to $mail_to (from $MAIL_FROM)\n";
468 $ENV{'MAILUSER'} = $MAIL_FROM;
469 # Commented out on hocus, so comment it out here. -kff
470 # $ENV{'QMAILINJECT'} = 'f';
472 open(MAIL, "$MAIL_CMD -f$MAIL_FROM");
473 print MAIL "From: $MAIL_FROM\n";
474 print MAIL "To: $mail_to\n";
475 print MAIL "Subject: $SUBJECT_PRE $subj\n\n";
476 print(MAIL join("\n", @text));
478 # print "Mailing the commit message to $MAIL_TO...\n";
480 # #added by jrobbins@collab.net 1999/12/15
481 # # attempt to get rid of anonymous
482 # $ENV{'MAILUSER'} = 'commitlogger';
483 # $ENV{'QMAILINJECT'} = 'f';
485 # open(MAIL, "| /var/qmail/bin/qmail-inject");
486 # print(MAIL "To: $MAIL_TO\n");
487 # print(MAIL "Subject: cvs commit: $ARGV[0]\n");
488 # print(MAIL join("\n", @text));
492 ## process the command line arguments sent to this script
493 ## it returns an array of files, %s, sent from the loginfo
500 print "Processing log script arguments...\n";
506 $cvs_user = shift @argv;
508 ($donefiles) && die "Too many arguments!\n";
511 @files = split(' ', $arg);
518 #############################################################
522 ############################################################
528 # Connect to the database
529 $cvsbin = "/usr/bin";
532 # Initialize basic variables
535 $state = $STATE_NONE;
536 $cvs_user = $ENV{'USER'} || getlogin || (getpwuid($<))[0] || sprintf("uid#%d",$<);
537 @files = process_argv(@ARGV);
538 @path = split('/', $files[0]);
539 $repository = $path[0];
543 $dir = join('/', @path[1..$#path]);
545 #print("ARGV - ", join(":", @ARGV), "\n");
546 #print("files - ", join(":", @files), "\n");
547 #print("path - ", join(":", @path), "\n");
548 #print("dir - ", $dir, "\n");
549 #print("id - ", $id, "\n");
552 # Map the repository directory to an email address for commitlogs to be sent
555 #$mlist = &mlist_map($files[0]);
557 ##########################
559 # Check for a new directory first. This will always appear as a
560 # single item in the argument list, and an empty log message.
562 if ($ARGV[0] =~ /New directory/) {
563 $header = &build_header;
565 push(@text, $header);
567 push(@text, " ".$ARGV[0]);
568 &mail_notification([ $mlist ], @text);
573 # Iterate over the body of the message collecting information.
576 chomp; # Drop the newline
577 if (/^Revision\/Branch:/) {
578 s,^Revision/Branch:,,;
579 push (@branch_lines, split);
582 # next if (/^[ \t]+Tag:/ && $state != $STATE_LOG);
583 if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
584 if (/^Added Files/) { $state = $STATE_ADDED; next; }
585 if (/^Removed Files/) { $state = $STATE_REMOVED; next; }
586 if (/^Log Message/) { $state = $STATE_LOG; next; }
587 s/[ \t\n]+$//; # delete trailing space
589 push (@changed_files, split) if ($state == $STATE_CHANGED);
590 push (@added_files, split) if ($state == $STATE_ADDED);
591 push (@removed_files, split) if ($state == $STATE_REMOVED);
592 if ($state == $STATE_LOG) {
595 /^Submitted by:$/i ||
596 /^Obtained from:$/i) {
599 push (@log_lines, $_);
604 # Strip leading and trailing blank lines from the log message. Also
605 # compress multiple blank lines in the body of the message down to a
607 # (Note, this only does the mail and changes log, not the rcs log).
609 while ($#log_lines > -1) {
610 last if ($log_lines[0] ne "");
613 while ($#log_lines > -1) {
614 last if ($log_lines[$#log_lines] ne "");
617 for ($i = $#log_lines; $i > 0; $i--) {
618 if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
619 splice(@log_lines, $i, 1);
624 # Find the log file that matches this log message
626 for ($i = 0; ; $i++) {
627 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
628 @text = &read_logfile("$LOG_FILE.$i.$id.$cvs_user", "");
629 last if ($#text == -1);
630 last if (join(" ", @log_lines) eq join(" ", @text));
634 # Spit out the information gathered in this pass.
636 &write_logfile("$LOG_FILE.$i.$id.$cvs_user", @log_lines);
637 &append_to_file("$BRANCH_FILE.$i.$id.$cvs_user", $dir, @branch_lines);
638 &append_to_file("$ADDED_FILE.$i.$id.$cvs_user", $dir, @added_files);
639 &append_to_file("$CHANGED_FILE.$i.$id.$cvs_user", $dir, @changed_files);
640 &append_to_file("$REMOVED_FILE.$i.$id.$cvs_user", $dir, @removed_files);
641 &append_line("$MLIST_FILE.$i.$id.$cvs_user", $mlist);
643 &change_summary("$SUMMARY_FILE.$i.$id.$cvs_user", (@changed_files, @added_files));
647 # Check whether this is the last directory. If not, quit.
649 if (-e "$LAST_FILE.$id.$cvs_user") {
650 $_ = &read_line("$LAST_FILE.$id.$cvs_user");
651 $tmpfiles = $files[0];
652 $tmpfiles =~ s,([^a-zA-Z0-9_/]),\\$1,g;
653 if (! grep(/$tmpfiles$/, $_)) {
654 print "More commits to come...\n";
660 # This is it. The commits are all finished. Lump everything together
661 # into a single message, fire a copy off to the mailing list, and drop
662 # it on the end of the Changes file.
664 $header = &build_header;
667 # Produce the final compilation of the log messages
671 push(@text, $header);
673 for ($i = 0; ; $i++) {
674 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
675 push(@text, &read_file("$BRANCH_FILE.$i.$id.$cvs_user", "Branch:"));
676 push(@text, &read_file("$CHANGED_FILE.$i.$id.$cvs_user", "Modified:"));
677 push(@text, &read_file("$ADDED_FILE.$i.$id.$cvs_user", "Added:"));
678 push(@text, &read_file("$REMOVED_FILE.$i.$id.$cvs_user", "Removed:"));
679 push(@text, " Log:");
680 push(@text, &read_logfile("$LOG_FILE.$i.$id.$cvs_user", " "));
681 push(@mlist_list, &read_file_lines("$MLIST_FILE.$i.$id.$cvs_user"));
682 if ($rcsidinfo == 2) {
683 if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
685 push(@text, " Revision Changes Path");
686 push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", " "));
693 # Now generate the extra info for the mail message..
695 if ($rcsidinfo == 1) {
697 for ($i = 0; ; $i++) {
698 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
699 if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
701 push(@text, "Revision Changes Path");
703 push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", ""));
707 push(@text, ""); # consistancy...
713 foreach (@mlist_list) { $mlist_hash{ $_ } = 1; }
716 # Mail out the notification.
718 &mail_notification([ keys(%mlist_hash) ], @text);