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 # The newest versions of CVS have UseNewInfoFmtStrings=yes
43 # to change the arguments being passed on the command line.
44 # If you are using %1s on the command line, then set this
46 # 0 = old-style %1s format. use split(' ') to separate ARGV into filesnames.
47 # 1 = new-style %s format. Note: allows spaces in filenames.
48 my $UseNewInfoFmtStrings = 0;
51 # Where do you want the RCS ID and delta info?
54 # 2 = in both mail and logs.
58 #if you are using CVS web then set this to some value... if not set it to ""
60 # When set properly, this will cause links to aspects of the project to
61 # print in the commit emails.
62 #$CVSWEB_SCHEME = "http";
63 #$CVSWEB_DOMAIN = "cvshome.org";
65 #$CVSWEB_URI = "source/browse/";
70 # Set this to a domain to have CVS pretend that all users who make
71 # commits have mail accounts within that domain.
72 #$EMULATE_LOCAL_MAIL_USER="cvshome.org";
74 # Set this to '-c' for context diffs; defaults to '-u' for unidiff format.
77 ############################################################
81 ############################################################
88 $TMPDIR = $ENV{'TMPDIR'} || '/tmp';
89 $FILE_PREFIX = '#cvs.';
91 $LAST_FILE = "$TMPDIR/${FILE_PREFIX}lastdir"; # Created by commit_prep!
92 $ADDED_FILE = "$TMPDIR/${FILE_PREFIX}files.added";
93 $REMOVED_FILE = "$TMPDIR/${FILE_PREFIX}files.removed";
94 $LOG_FILE = "$TMPDIR/${FILE_PREFIX}files.log";
95 $BRANCH_FILE = "$TMPDIR/${FILE_PREFIX}files.branch";
96 $MLIST_FILE = "$TMPDIR/${FILE_PREFIX}files.mlist";
97 $SUMMARY_FILE = "$TMPDIR/${FILE_PREFIX}files.summary";
99 $CVSROOT = $ENV{'CVSROOT'};
101 $MAIL_CMD = "| /usr/lib/sendmail -i -t";
102 #$MAIL_CMD = "| /var/qmail/bin/qmail-inject";
103 $MAIL_FROM = 'commitlogger'; #not needed if EMULATE_LOCAL_MAIL_USER
104 $SUBJECT_PRE = 'CVS update:';
107 ############################################################
111 ############################################################
114 local($dir, @files) = @_;
117 $lines[0] = sprintf(" %-08s", $dir);
118 foreach $file (@files) {
119 if (length($lines[$#lines]) + length($file) > 60) {
120 $lines[++$#lines] = sprintf(" %8s", " ");
122 $lines[$#lines] .= " ".$file;
127 sub cleanup_tmpfiles {
130 opendir(DIR, $TMPDIR);
131 push(@files, grep(/^${FILE_PREFIX}.*\.${id}\.${cvs_user}$/, readdir(DIR)));
139 local($filename, @lines) = @_;
141 open(FILE, ">$filename") || die ("Cannot open log file $filename: $!\n");
142 print(FILE join("\n", @lines), "\n");
147 local($filename, $dir, @files) = @_;
150 local(@lines) = &format_names($dir, @files);
151 open(FILE, ">>$filename") || die ("Cannot open file $filename: $!\n");
152 print(FILE join("\n", @lines), "\n");
158 local($filename, $line) = @_;
160 open(FILE, ">$filename") || die("Cannot open file $filename: $!\n");
161 print(FILE $line, "\n");
166 local($filename, $line) = @_;
168 open(FILE, ">>$filename") || die("Cannot open file $filename: $!\n");
169 print(FILE $line, "\n");
174 local($filename) = @_;
177 open(FILE, "<$filename") || die("Cannot open file $filename: $!\n");
184 sub read_line_nodie {
185 local($filename) = @_;
187 open(FILE, "<$filename") || return ("");
195 sub read_file_lines {
196 local($filename) = @_;
199 open(FILE, "<$filename") || return ();
209 local($filename, $leader) = @_;
212 open(FILE, "<$filename") || return ();
215 push(@text, sprintf(" %-10s %s", $leader, $_));
223 local($filename, $leader) = @_;
226 open(FILE, "<$filename") || die ("Cannot open log file $filename: $!\n");
229 push(@text, $leader.$_);
236 # do an 'cvs -Qn status' on each file in the arguments, and extract info.
239 local($out, @filenames) = @_;
241 local($file, $rev, $rcsfile, $line, $vhost, $cvsweb_base);
244 $file = shift @filenames;
250 open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'status', '--', $file;
258 if (/^[ \t]*Repository revision/) {
260 @revline = split(' ', $_);
262 $rcsfile = $revline[3];
263 $rcsfile =~ s,^$CVSROOT/,,;
270 if ($rev ne '' && $rcsfile ne '') {
271 open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'log', "-r$rev",
278 $delta =~ s/^[\s]+lines://;
286 if ($CVSWEB_PORT eq "80") {
287 $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN/$CVSWEB_URI";
290 $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN:$CVSWEB_PORT/$CVSWEB_URI";
292 if ($SEND_URL eq "true") {
293 $diff .= $cvsweb_base . join("/", @path) . "/$file";
297 # If this is a binary file, don't try to report a diff; not only is
298 # it meaningless, but it also screws up some mailers. We rely on
299 # Perl's 'is this binary' algorithm; it's pretty good. But not
302 if (($file =~ /\.(?:pdf|gif|jpg|mpg)$/i) || (-B $file)) {
303 if ($SEND_URL eq "true") {
304 $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
306 if ($SEND_DIFF eq "true") {
307 $diff .= "\t<<Binary file>>\n\n";
312 # Get the differences between this and the previous revision,
313 # being aware that new files always have revision '1.1' and
314 # new branches always end in '.n.1'.
316 if ($rev =~ /^(.*)\.([0-9]+)$/) {
318 $prev_rev = $1 . '.' . $prev;
320 $prev_rev =~ s/\.[0-9]+\.0$//;# Truncate if first rev on branch
323 if ($SEND_URL eq "true") {
324 $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
326 if ($SEND_DIFF eq "true") {
328 || exec "$cvsbin/cvs", '-Qn', 'update', '-p', '-r1.1',
330 $diff .= "Index: $file\n=================================="
331 . "=================================\n";
335 if ($SEND_URL eq "true") {
336 $diff .= ".diff?r1=$prev_rev&r2=$rev\n\n";
338 if ($SEND_DIFF eq "true") {
339 $diff .= "(In the diff below, changes in quantity "
340 . "of whitespace are not shown.)\n\n";
342 || exec "$cvsbin/cvs", '-Qn', 'diff', "$difftype",
343 '-b', "-r$prev_rev", "-r$rev", '--', $file;
347 if ($SEND_DIFF eq "true") {
357 &append_line($out, sprintf("%-9s%-12s%s%s", $rev, $delta,
366 local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
368 $header = sprintf(" User: %-8s\n Date: %02d/%02d/%02d %02d:%02d:%02d",
369 $cvs_user, $year%100, $mon+1, $mday,
371 # $header = sprintf("%-8s %02d/%02d/%02d %02d:%02d:%02d",
372 # $login, $year%100, $mon+1, $mday,
373 # $hour, $min, $sec);
376 # !!! Destination Mailing-list and history file mappings here !!!
381 # my $domain = "cvshome.org";
383 # if ($path =~ /^([^\/]+)/) {
384 # return "cvs\@$1.$domain";
386 # return "cvs\@$domain";
390 sub derive_subject_from_changes_file ()
396 open (CH, "<$CHANGED_FILE.$i.$id.$cvs_user") or last;
398 while (my $change = <CH>)
400 # A changes file looks like this:
402 # src foo.c newfile.html
403 # www index.html project_nav.html
405 # Each line is " Dir File1 File2 ..."
406 # We only care about Dir, since the subject line should
409 $change =~ s/^[ \t]*//;
410 $change =~ /^([^ \t]+)[ \t]*/;
412 # Fold to rightmost directory component
425 $subj = "MODIFIED: $subj ...";
428 # NPM: See if there's any file-addition notifications.
429 my $added = &read_line_nodie("$ADDED_FILE.$i.$id.$cvs_user");
431 $subj .= "ADDED: $added ";
434 # print "derive_subject_from_changes_file().. added== $added \n";
436 ## NPM: See if there's any file-removal notications.
437 my $removed = &read_line_nodie("$REMOVED_FILE.$i.$id.$cvs_user");
438 if ($removed ne "") {
439 $subj .= "REMOVED: $removed ";
442 # print "derive_subject_from_changes_file().. removed== $removed \n";
444 ## NPM: See if there's any branch notifications.
445 my $branched = &read_line_nodie("$BRANCH_FILE.$i.$id.$cvs_user");
446 if ($branched ne "") {
447 $subj .= "BRANCHED: $branched";
450 # print "derive_subject_from_changes_file().. branched== $branched \n";
452 ## NPM: DEFAULT: DIRECTORY CREATION (c.f. "Check for a new directory first" in main mody)
454 my $subject = join("/", @path);
455 $subj = "NEW: $subject";
462 sub mail_notification
464 local($addr_list, @text) = @_;
467 my $subj = &derive_subject_from_changes_file ();
469 if ($EMULATE_LOCAL_MAIL_USER ne "") {
470 $MAIL_FROM = "$cvs_user\@$EMULATE_LOCAL_MAIL_USER";
473 $mail_to = join(", ", @{$addr_list});
475 print "Mailing the commit message to $mail_to (from $MAIL_FROM)\n";
477 $ENV{'MAILUSER'} = $MAIL_FROM;
478 # Commented out on hocus, so comment it out here. -kff
479 # $ENV{'QMAILINJECT'} = 'f';
481 open(MAIL, "$MAIL_CMD -f$MAIL_FROM");
482 print MAIL "From: $MAIL_FROM\n";
483 print MAIL "To: $mail_to\n";
484 print MAIL "Subject: $SUBJECT_PRE $subj\n\n";
485 print(MAIL join("\n", @text));
487 # print "Mailing the commit message to $MAIL_TO...\n";
489 # #added by jrobbins@collab.net 1999/12/15
490 # # attempt to get rid of anonymous
491 # $ENV{'MAILUSER'} = 'commitlogger';
492 # $ENV{'QMAILINJECT'} = 'f';
494 # open(MAIL, "| /var/qmail/bin/qmail-inject");
495 # print(MAIL "To: $MAIL_TO\n");
496 # print(MAIL "Subject: cvs commit: $ARGV[0]\n");
497 # print(MAIL join("\n", @text));
501 ## process the command line arguments sent to this script
502 ## it returns an array of files, %s, sent from the loginfo
509 print "Processing log script arguments...\n";
511 if ($UseNewInfoFmtStrings) {
515 if ($arg eq '-u' && !defined($cvs_user)) {
516 $cvs_user = shift @argv;
518 if ($arg eq '- New directory') {
520 } elsif ($arg eq '- Imported sources') {
521 $imported_sources = 1;
531 $cvs_user = shift @argv;
533 ($donefiles) && die "Too many arguments!\n";
536 if ($arg =~ s/ - New directory//) {
538 } elsif ($arg =~ s/ - Imported sources//) {
539 $imported_sources = 1;
541 @files = split(' ', $arg);
549 #############################################################
553 ############################################################
559 # Connect to the database
560 $cvsbin = "/usr/bin";
563 # Initialize basic variables
566 $state = $STATE_NONE;
567 $cvs_user = $ENV{'USER'} || getlogin || (getpwuid($<))[0] || sprintf("uid#%d",$<);
568 $new_directory = 0; # Is this a 'cvs add directory' command?
569 $imported_sources = 0; # Is this a 'cvs import' command?
570 @files = process_argv(@ARGV);
571 @path = split('/', $files[0]);
575 $dir = join('/', @path[1..$#path]);
577 #print("ARGV - ", join(":", @ARGV), "\n");
578 #print("files - ", join(":", @files), "\n");
579 #print("path - ", join(":", @path), "\n");
580 #print("dir - ", $dir, "\n");
581 #print("id - ", $id, "\n");
584 # Map the repository directory to an email address for commitlogs to be sent
587 #$mlist = &mlist_map($files[0]);
589 ##########################
591 # Check for a new directory first. This will always appear as a
592 # single item in the argument list, and an empty log message.
594 if ($new_directory) {
595 $header = &build_header;
597 push(@text, $header);
599 push(@text, " ".$files[0]." - New directory");
600 &mail_notification([ $mlist ], @text);
605 # Iterate over the body of the message collecting information.
608 chomp; # Drop the newline
609 if (/^Revision\/Branch:/) {
610 s,^Revision/Branch:,,;
611 push (@branch_lines, split);
614 # next if (/^[ \t]+Tag:/ && $state != $STATE_LOG);
615 if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
616 if (/^Added Files/) { $state = $STATE_ADDED; next; }
617 if (/^Removed Files/) { $state = $STATE_REMOVED; next; }
618 if (/^Log Message/) { $state = $STATE_LOG; last; }
619 s/[ \t\n]+$//; # delete trailing space
621 push (@changed_files, split) if ($state == $STATE_CHANGED);
622 push (@added_files, split) if ($state == $STATE_ADDED);
623 push (@removed_files, split) if ($state == $STATE_REMOVED);
625 # Proces the /Log Message/ section now, if it exists.
626 # Do this here rather than above to deal with Log messages
627 # that include lines that confuse the state machine.
630 next unless ($state == $STATE_LOG); # eat all STDIN
632 if ($state == $STATE_LOG) {
635 /^Submitted by:$/i ||
636 /^Obtained from:$/i) {
639 push (@log_lines, $_);
645 # Strip leading and trailing blank lines from the log message. Also
646 # compress multiple blank lines in the body of the message down to a
648 # (Note, this only does the mail and changes log, not the rcs log).
650 while ($#log_lines > -1) {
651 last if ($log_lines[0] ne "");
654 while ($#log_lines > -1) {
655 last if ($log_lines[$#log_lines] ne "");
658 for ($i = $#log_lines; $i > 0; $i--) {
659 if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
660 splice(@log_lines, $i, 1);
665 # Find the log file that matches this log message
667 for ($i = 0; ; $i++) {
668 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
669 @text = &read_logfile("$LOG_FILE.$i.$id.$cvs_user", "");
670 last if ($#text == -1);
671 last if (join(" ", @log_lines) eq join(" ", @text));
675 # Spit out the information gathered in this pass.
677 &write_logfile("$LOG_FILE.$i.$id.$cvs_user", @log_lines);
678 &append_to_file("$BRANCH_FILE.$i.$id.$cvs_user", $dir, @branch_lines);
679 &append_to_file("$ADDED_FILE.$i.$id.$cvs_user", $dir, @added_files);
680 &append_to_file("$CHANGED_FILE.$i.$id.$cvs_user", $dir, @changed_files);
681 &append_to_file("$REMOVED_FILE.$i.$id.$cvs_user", $dir, @removed_files);
682 &append_line("$MLIST_FILE.$i.$id.$cvs_user", $mlist);
684 &change_summary("$SUMMARY_FILE.$i.$id.$cvs_user", (@changed_files, @added_files));
688 # Check whether this is the last directory. If not, quit.
690 if (-e "$LAST_FILE.$id.$cvs_user") {
691 $_ = &read_line("$LAST_FILE.$id.$cvs_user");
692 $tmpfiles = $files[0];
693 $tmpfiles =~ s,([^a-zA-Z0-9_/]),\\$1,g;
694 if (! grep(/$tmpfiles$/, $_)) {
695 print "More commits to come...\n";
701 # This is it. The commits are all finished. Lump everything together
702 # into a single message, fire a copy off to the mailing list, and drop
703 # it on the end of the Changes file.
705 $header = &build_header;
708 # Produce the final compilation of the log messages
712 push(@text, $header);
714 for ($i = 0; ; $i++) {
715 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
716 push(@text, &read_file("$BRANCH_FILE.$i.$id.$cvs_user", "Branch:"));
717 push(@text, &read_file("$CHANGED_FILE.$i.$id.$cvs_user", "Modified:"));
718 push(@text, &read_file("$ADDED_FILE.$i.$id.$cvs_user", "Added:"));
719 push(@text, &read_file("$REMOVED_FILE.$i.$id.$cvs_user", "Removed:"));
720 push(@text, " Log:");
721 push(@text, &read_logfile("$LOG_FILE.$i.$id.$cvs_user", " "));
722 push(@mlist_list, &read_file_lines("$MLIST_FILE.$i.$id.$cvs_user"));
723 if ($rcsidinfo == 2) {
724 if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
726 push(@text, " Revision Changes Path");
727 push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", " "));
734 # Now generate the extra info for the mail message..
736 if ($rcsidinfo == 1) {
738 for ($i = 0; ; $i++) {
739 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
740 if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
742 push(@text, "Revision Changes Path");
744 push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", ""));
748 push(@text, ""); # consistancy...
754 foreach (@mlist_list) { $mlist_hash{ $_ } = 1; }
757 # Mail out the notification.
759 &mail_notification([ keys(%mlist_hash) ], @text);