4 ###############################################################################
5 ###############################################################################
6 ###############################################################################
8 # THIS SCRIPT IS PROBABLY BROKEN. REMOVING THE -T SWITCH ON THE #! LINE ABOVE
9 # WOULD FIX IT, BUT THIS IS INSECURE. WE RECOMMEND FIXING THE ERRORS WHICH THE
10 # -T SWITCH WILL CAUSE PERL TO REPORT BEFORE RUNNING THIS SCRIPT FROM A CVS
11 # SERVER TRIGGER. PLEASE SEND PATCHES CONTAINING THE CHANGES YOU FIND
12 # NECESSARY TO RUN THIS SCRIPT WITH THE TAINT-CHECKING ENABLED BACK TO THE
13 # <bug-cvs@gnu.org> MAILING LIST.
15 # For more on general Perl security and taint-checking, please try running the
16 # `perldoc perlsec' command.
18 ###############################################################################
19 ###############################################################################
20 ###############################################################################
22 # Perl filter to handle the log messages from the checkin of files in
23 # a directory. This script will group the lists of files by log
24 # message, and mail a single consolidated log message at the end of
27 # This file assumes a pre-commit checking program that leaves the
28 # names of the first and last commit directories in a temporary file.
30 # IMPORTANT: what the above means is, this script interacts with
31 # commit_prep, in that they have to agree on the tmpfile name to use.
32 # See $LAST_FILE below.
34 # How this works: CVS triggers this script once for each directory
35 # involved in the commit -- in other words, a single commit can invoke
36 # this script N times. It knows when it's on the last invocation by
37 # examining the contents of $LAST_FILE. Between invocations, it
38 # caches information for its future incarnations in various temporary
39 # files in /tmp, which are named according to the process group and
40 # the committer (by themselves, neither of these are unique, but
41 # together they almost always are, unless the same user is doing two
42 # commits simultaneously). The final invocation is the one that
43 # actually sends the mail -- it gathers up the cached information,
44 # combines that with what it found out on this pass, and sends a
45 # commit message to the appropriate mailing list.
47 # (Ask Karl Fogel <kfogel@collab.net> if questions.)
49 # Contributed by David Hampton <hampton@cisco.com>
50 # Roy Fielding removed useless code and added log/mail of new files
51 # Ken Coar added special processing (i.e., no diffs) for binary files
54 ############################################################
56 # Configurable options
58 ############################################################
60 # The newest versions of CVS have UseNewInfoFmtStrings=yes
61 # to change the arguments being passed on the command line.
62 # If you are using %1s on the command line, then set this
64 # 0 = old-style %1s format. use split(' ') to separate ARGV into filesnames.
65 # 1 = new-style %s format. Note: allows spaces in filenames.
66 my $UseNewInfoFmtStrings = 0;
69 # Where do you want the RCS ID and delta info?
72 # 2 = in both mail and logs.
76 #if you are using CVS web then set this to some value... if not set it to ""
78 # When set properly, this will cause links to aspects of the project to
79 # print in the commit emails.
80 #$CVSWEB_SCHEME = "http";
81 #$CVSWEB_DOMAIN = "cvshome.org";
83 #$CVSWEB_URI = "source/browse/";
88 # Set this to a domain to have CVS pretend that all users who make
89 # commits have mail accounts within that domain.
90 #$EMULATE_LOCAL_MAIL_USER="cvshome.org";
92 # Set this to '-c' for context diffs; defaults to '-u' for unidiff format.
95 ############################################################
99 ############################################################
106 $TMPDIR = $ENV{'TMPDIR'} || '/tmp';
107 $FILE_PREFIX = '#cvs.';
109 $LAST_FILE = "$TMPDIR/${FILE_PREFIX}lastdir"; # Created by commit_prep!
110 $ADDED_FILE = "$TMPDIR/${FILE_PREFIX}files.added";
111 $REMOVED_FILE = "$TMPDIR/${FILE_PREFIX}files.removed";
112 $LOG_FILE = "$TMPDIR/${FILE_PREFIX}files.log";
113 $BRANCH_FILE = "$TMPDIR/${FILE_PREFIX}files.branch";
114 $MLIST_FILE = "$TMPDIR/${FILE_PREFIX}files.mlist";
115 $SUMMARY_FILE = "$TMPDIR/${FILE_PREFIX}files.summary";
117 $CVSROOT = $ENV{'CVSROOT'};
119 $MAIL_CMD = "| /usr/lib/sendmail -i -t";
120 #$MAIL_CMD = "| /var/qmail/bin/qmail-inject";
121 $MAIL_FROM = 'commitlogger'; #not needed if EMULATE_LOCAL_MAIL_USER
122 $SUBJECT_PRE = 'CVS update:';
125 ############################################################
129 ############################################################
132 local($dir, @files) = @_;
135 $lines[0] = sprintf(" %-08s", $dir);
136 foreach $file (@files) {
137 if (length($lines[$#lines]) + length($file) > 60) {
138 $lines[++$#lines] = sprintf(" %8s", " ");
140 $lines[$#lines] .= " ".$file;
145 sub cleanup_tmpfiles {
148 opendir(DIR, $TMPDIR);
149 push(@files, grep(/^${FILE_PREFIX}.*\.${id}\.${cvs_user}$/, readdir(DIR)));
157 local($filename, @lines) = @_;
159 open(FILE, ">$filename") || die ("Cannot open log file $filename: $!\n");
160 print(FILE join("\n", @lines), "\n");
165 local($filename, $dir, @files) = @_;
168 local(@lines) = &format_names($dir, @files);
169 open(FILE, ">>$filename") || die ("Cannot open file $filename: $!\n");
170 print(FILE join("\n", @lines), "\n");
176 local($filename, $line) = @_;
178 open(FILE, ">$filename") || die("Cannot open file $filename: $!\n");
179 print(FILE $line, "\n");
184 local($filename, $line) = @_;
186 open(FILE, ">>$filename") || die("Cannot open file $filename: $!\n");
187 print(FILE $line, "\n");
192 local($filename) = @_;
195 open(FILE, "<$filename") || die("Cannot open file $filename: $!\n");
202 sub read_line_nodie {
203 local($filename) = @_;
205 open(FILE, "<$filename") || return ("");
213 sub read_file_lines {
214 local($filename) = @_;
217 open(FILE, "<$filename") || return ();
227 local($filename, $leader) = @_;
230 open(FILE, "<$filename") || return ();
233 push(@text, sprintf(" %-10s %s", $leader, $_));
241 local($filename, $leader) = @_;
244 open(FILE, "<$filename") || die ("Cannot open log file $filename: $!\n");
247 push(@text, $leader.$_);
254 # do an 'cvs -Qn status' on each file in the arguments, and extract info.
257 local($out, @filenames) = @_;
259 local($file, $rev, $rcsfile, $line, $vhost, $cvsweb_base);
262 $file = shift @filenames;
268 open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'status', '--', $file;
276 if (/^[ \t]*Repository revision/) {
278 @revline = split(' ', $_);
280 $rcsfile = $revline[3];
281 $rcsfile =~ s,^$CVSROOT/,,;
288 if ($rev ne '' && $rcsfile ne '') {
289 open(RCS, "-|") || exec "$cvsbin/cvs", '-Qn', 'log', "-r$rev",
296 $delta =~ s/^[\s]+lines://;
304 if ($CVSWEB_PORT eq "80") {
305 $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN/$CVSWEB_URI";
308 $cvsweb_base = "$CVSWEB_SCHEME://$vhost.$CVSWEB_DOMAIN:$CVSWEB_PORT/$CVSWEB_URI";
310 if ($SEND_URL eq "true") {
311 $diff .= $cvsweb_base . join("/", @path) . "/$file";
315 # If this is a binary file, don't try to report a diff; not only is
316 # it meaningless, but it also screws up some mailers. We rely on
317 # Perl's 'is this binary' algorithm; it's pretty good. But not
320 if (($file =~ /\.(?:pdf|gif|jpg|mpg)$/i) || (-B $file)) {
321 if ($SEND_URL eq "true") {
322 $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
324 if ($SEND_DIFF eq "true") {
325 $diff .= "\t<<Binary file>>\n\n";
330 # Get the differences between this and the previous revision,
331 # being aware that new files always have revision '1.1' and
332 # new branches always end in '.n.1'.
334 if ($rev =~ /^(.*)\.([0-9]+)$/) {
336 $prev_rev = $1 . '.' . $prev;
338 $prev_rev =~ s/\.[0-9]+\.0$//;# Truncate if first rev on branch
341 if ($SEND_URL eq "true") {
342 $diff .= "?rev=$rev&content-type=text/x-cvsweb-markup\n\n";
344 if ($SEND_DIFF eq "true") {
346 || exec "$cvsbin/cvs", '-Qn', 'update', '-p', '-r1.1',
348 $diff .= "Index: $file\n=================================="
349 . "=================================\n";
353 if ($SEND_URL eq "true") {
354 $diff .= ".diff?r1=$prev_rev&r2=$rev\n\n";
356 if ($SEND_DIFF eq "true") {
357 $diff .= "(In the diff below, changes in quantity "
358 . "of whitespace are not shown.)\n\n";
360 || exec "$cvsbin/cvs", '-Qn', 'diff', "$difftype",
361 '-b', "-r$prev_rev", "-r$rev", '--', $file;
365 if ($SEND_DIFF eq "true") {
375 &append_line($out, sprintf("%-9s%-12s%s%s", $rev, $delta,
384 local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
386 $header = sprintf(" User: %-8s\n Date: %02d/%02d/%02d %02d:%02d:%02d",
387 $cvs_user, $year%100, $mon+1, $mday,
389 # $header = sprintf("%-8s %02d/%02d/%02d %02d:%02d:%02d",
390 # $login, $year%100, $mon+1, $mday,
391 # $hour, $min, $sec);
394 # !!! Destination Mailing-list and history file mappings here !!!
399 # my $domain = "cvshome.org";
401 # if ($path =~ /^([^\/]+)/) {
402 # return "cvs\@$1.$domain";
404 # return "cvs\@$domain";
408 sub derive_subject_from_changes_file ()
414 open (CH, "<$CHANGED_FILE.$i.$id.$cvs_user") or last;
416 while (my $change = <CH>)
418 # A changes file looks like this:
420 # src foo.c newfile.html
421 # www index.html project_nav.html
423 # Each line is " Dir File1 File2 ..."
424 # We only care about Dir, since the subject line should
427 $change =~ s/^[ \t]*//;
428 $change =~ /^([^ \t]+)[ \t]*/;
430 # Fold to rightmost directory component
443 $subj = "MODIFIED: $subj ...";
446 # NPM: See if there's any file-addition notifications.
447 my $added = &read_line_nodie("$ADDED_FILE.$i.$id.$cvs_user");
449 $subj .= "ADDED: $added ";
452 # print "derive_subject_from_changes_file().. added== $added \n";
454 ## NPM: See if there's any file-removal notications.
455 my $removed = &read_line_nodie("$REMOVED_FILE.$i.$id.$cvs_user");
456 if ($removed ne "") {
457 $subj .= "REMOVED: $removed ";
460 # print "derive_subject_from_changes_file().. removed== $removed \n";
462 ## NPM: See if there's any branch notifications.
463 my $branched = &read_line_nodie("$BRANCH_FILE.$i.$id.$cvs_user");
464 if ($branched ne "") {
465 $subj .= "BRANCHED: $branched";
468 # print "derive_subject_from_changes_file().. branched== $branched \n";
470 ## NPM: DEFAULT: DIRECTORY CREATION (c.f. "Check for a new directory first" in main mody)
472 my $subject = join("/", @path);
473 $subj = "NEW: $subject";
480 sub mail_notification
482 local($addr_list, @text) = @_;
485 my $subj = &derive_subject_from_changes_file ();
487 if ($EMULATE_LOCAL_MAIL_USER ne "") {
488 $MAIL_FROM = "$cvs_user\@$EMULATE_LOCAL_MAIL_USER";
491 $mail_to = join(", ", @{$addr_list});
493 print "Mailing the commit message to $mail_to (from $MAIL_FROM)\n";
495 $ENV{'MAILUSER'} = $MAIL_FROM;
496 # Commented out on hocus, so comment it out here. -kff
497 # $ENV{'QMAILINJECT'} = 'f';
499 open(MAIL, "$MAIL_CMD -f$MAIL_FROM");
500 print MAIL "From: $MAIL_FROM\n";
501 print MAIL "To: $mail_to\n";
502 print MAIL "Subject: $SUBJECT_PRE $subj\n\n";
503 print(MAIL join("\n", @text));
505 # print "Mailing the commit message to $MAIL_TO...\n";
507 # #added by jrobbins@collab.net 1999/12/15
508 # # attempt to get rid of anonymous
509 # $ENV{'MAILUSER'} = 'commitlogger';
510 # $ENV{'QMAILINJECT'} = 'f';
512 # open(MAIL, "| /var/qmail/bin/qmail-inject");
513 # print(MAIL "To: $MAIL_TO\n");
514 # print(MAIL "Subject: cvs commit: $ARGV[0]\n");
515 # print(MAIL join("\n", @text));
519 ## process the command line arguments sent to this script
520 ## it returns an array of files, %s, sent from the loginfo
527 print "Processing log script arguments...\n";
529 if ($UseNewInfoFmtStrings) {
533 if ($arg eq '-u' && !defined($cvs_user)) {
534 $cvs_user = shift @argv;
536 if ($arg eq '- New directory') {
538 } elsif ($arg eq '- Imported sources') {
539 $imported_sources = 1;
549 $cvs_user = shift @argv;
551 ($donefiles) && die "Too many arguments!\n";
554 if ($arg =~ s/ - New directory//) {
556 } elsif ($arg =~ s/ - Imported sources//) {
557 $imported_sources = 1;
559 @files = split(' ', $arg);
567 #############################################################
571 ############################################################
577 # Connect to the database
578 $cvsbin = "/usr/bin";
581 # Initialize basic variables
584 $state = $STATE_NONE;
585 $cvs_user = $ENV{'USER'} || getlogin || (getpwuid($<))[0] || sprintf("uid#%d",$<);
586 $new_directory = 0; # Is this a 'cvs add directory' command?
587 $imported_sources = 0; # Is this a 'cvs import' command?
588 @files = process_argv(@ARGV);
589 @path = split('/', $files[0]);
593 $dir = join('/', @path[1..$#path]);
595 #print("ARGV - ", join(":", @ARGV), "\n");
596 #print("files - ", join(":", @files), "\n");
597 #print("path - ", join(":", @path), "\n");
598 #print("dir - ", $dir, "\n");
599 #print("id - ", $id, "\n");
602 # Map the repository directory to an email address for commitlogs to be sent
605 #$mlist = &mlist_map($files[0]);
607 ##########################
609 # Check for a new directory first. This will always appear as a
610 # single item in the argument list, and an empty log message.
612 if ($new_directory) {
613 $header = &build_header;
615 push(@text, $header);
617 push(@text, " ".$files[0]." - New directory");
618 &mail_notification([ $mlist ], @text);
623 # Iterate over the body of the message collecting information.
626 chomp; # Drop the newline
627 if (/^Revision\/Branch:/) {
628 s,^Revision/Branch:,,;
629 push (@branch_lines, split);
632 # next if (/^[ \t]+Tag:/ && $state != $STATE_LOG);
633 if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
634 if (/^Added Files/) { $state = $STATE_ADDED; next; }
635 if (/^Removed Files/) { $state = $STATE_REMOVED; next; }
636 if (/^Log Message/) { $state = $STATE_LOG; last; }
637 s/[ \t\n]+$//; # delete trailing space
639 push (@changed_files, split) if ($state == $STATE_CHANGED);
640 push (@added_files, split) if ($state == $STATE_ADDED);
641 push (@removed_files, split) if ($state == $STATE_REMOVED);
643 # Proces the /Log Message/ section now, if it exists.
644 # Do this here rather than above to deal with Log messages
645 # that include lines that confuse the state machine.
648 next unless ($state == $STATE_LOG); # eat all STDIN
650 if ($state == $STATE_LOG) {
653 /^Submitted by:$/i ||
654 /^Obtained from:$/i) {
657 push (@log_lines, $_);
663 # Strip leading and trailing blank lines from the log message. Also
664 # compress multiple blank lines in the body of the message down to a
666 # (Note, this only does the mail and changes log, not the rcs log).
668 while ($#log_lines > -1) {
669 last if ($log_lines[0] ne "");
672 while ($#log_lines > -1) {
673 last if ($log_lines[$#log_lines] ne "");
676 for ($i = $#log_lines; $i > 0; $i--) {
677 if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
678 splice(@log_lines, $i, 1);
683 # Find the log file that matches this log message
685 for ($i = 0; ; $i++) {
686 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
687 @text = &read_logfile("$LOG_FILE.$i.$id.$cvs_user", "");
688 last if ($#text == -1);
689 last if (join(" ", @log_lines) eq join(" ", @text));
693 # Spit out the information gathered in this pass.
695 &write_logfile("$LOG_FILE.$i.$id.$cvs_user", @log_lines);
696 &append_to_file("$BRANCH_FILE.$i.$id.$cvs_user", $dir, @branch_lines);
697 &append_to_file("$ADDED_FILE.$i.$id.$cvs_user", $dir, @added_files);
698 &append_to_file("$CHANGED_FILE.$i.$id.$cvs_user", $dir, @changed_files);
699 &append_to_file("$REMOVED_FILE.$i.$id.$cvs_user", $dir, @removed_files);
700 &append_line("$MLIST_FILE.$i.$id.$cvs_user", $mlist);
702 &change_summary("$SUMMARY_FILE.$i.$id.$cvs_user", (@changed_files, @added_files));
706 # Check whether this is the last directory. If not, quit.
708 if (-e "$LAST_FILE.$id.$cvs_user") {
709 $_ = &read_line("$LAST_FILE.$id.$cvs_user");
710 $tmpfiles = $files[0];
711 $tmpfiles =~ s,([^a-zA-Z0-9_/]),\\$1,g;
712 if (! grep(/$tmpfiles$/, $_)) {
713 print "More commits to come...\n";
719 # This is it. The commits are all finished. Lump everything together
720 # into a single message, fire a copy off to the mailing list, and drop
721 # it on the end of the Changes file.
723 $header = &build_header;
726 # Produce the final compilation of the log messages
730 push(@text, $header);
732 for ($i = 0; ; $i++) {
733 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
734 push(@text, &read_file("$BRANCH_FILE.$i.$id.$cvs_user", "Branch:"));
735 push(@text, &read_file("$CHANGED_FILE.$i.$id.$cvs_user", "Modified:"));
736 push(@text, &read_file("$ADDED_FILE.$i.$id.$cvs_user", "Added:"));
737 push(@text, &read_file("$REMOVED_FILE.$i.$id.$cvs_user", "Removed:"));
738 push(@text, " Log:");
739 push(@text, &read_logfile("$LOG_FILE.$i.$id.$cvs_user", " "));
740 push(@mlist_list, &read_file_lines("$MLIST_FILE.$i.$id.$cvs_user"));
741 if ($rcsidinfo == 2) {
742 if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
744 push(@text, " Revision Changes Path");
745 push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", " "));
752 # Now generate the extra info for the mail message..
754 if ($rcsidinfo == 1) {
756 for ($i = 0; ; $i++) {
757 last if (! -e "$LOG_FILE.$i.$id.$cvs_user");
758 if (-e "$SUMMARY_FILE.$i.$id.$cvs_user") {
760 push(@text, "Revision Changes Path");
762 push(@text, &read_logfile("$SUMMARY_FILE.$i.$id.$cvs_user", ""));
766 push(@text, ""); # consistancy...
772 foreach (@mlist_list) { $mlist_hash{ $_ } = 1; }
775 # Mail out the notification.
777 &mail_notification([ keys(%mlist_hash) ], @text);