X-Git-Url: http://demsky.eecs.uci.edu/git/?a=blobdiff_plain;f=CVSROOT%2Fcvslog;fp=CVSROOT%2Fcvslog;h=0000000000000000000000000000000000000000;hb=refs%2Ftags%2Fbuildscript;hp=373f7cd9e956ef6149ea8f3b9eec6ac403a9efaf;hpb=b9df1caacff3dbe5959bc12e0e6ba46500fcd3e2;p=IRC.git diff --git a/CVSROOT/cvslog b/CVSROOT/cvslog deleted file mode 100755 index 373f7cd9..00000000 --- a/CVSROOT/cvslog +++ /dev/null @@ -1,1417 +0,0 @@ -#!/usr/bin/perl -w -$ID = q(cvslog,v 1.51 2005/04/16 22:39:39 eagle Exp ); -# -# cvslog -- Mail CVS commit notifications. -# -# Written by Russ Allbery -# Copyright 1998, 1999, 2000, 2001, 2002, 2003, 2004 -# Board of Trustees, Leland Stanford Jr. University -# -# This program is free software; you can redistribute it and/or modify it -# under the same terms as Perl itself. - -############################################################################## -# Modules and declarations -############################################################################## - -# The path to the repository. If your platform or CVS implementation doesn't -# pass the full path to the cvslog script in $0 (or if your cvslog script -# isn't in the CVSROOT directory of your repository for some reason), you will -# need to explicitly set $REPOSITORY to the root directory of your repository -# (the same thing that you would set CVSROOT to). -($REPOSITORY) = ($0 =~ m%^(.*)/CVSROOT/cvslog$%); -$REPOSITORY ||= ''; - -require 5.004; - -use Getopt::Long qw(GetOptions); -use IPC::Open2 qw(open2); -use POSIX qw(SEEK_SET strftime); - -use strict; -use vars qw($DEBUG $ID $REPOSITORY); - -# Clean up $0 for errors. -$0 =~ s%^.*/%%; - -############################################################################## -# Utility functions -############################################################################## - -# Given a prefix and a reference to an array, return a list of all strings in -# that array with the common prefix stripped off. Also strip off any leading -# ./ if present. -sub simplify { - my ($prefix, $list) = @_; - my @stripped = @$list; - for (@stripped) { - s%^\Q$prefix\E/*%%; - s%^\./+%%; - } - return @stripped; -} - -# Return the next version for a CVS version, incrementing the last number. -sub next_version { - my $version = shift; - my @version = split (/\./, $version); - $version[-1]++; - return join ('.', @version); -} - -# Given a directory name, find the corresponding CVS module. We do this by -# looking in the modules file, finding the last "word" on each line, making -# sure it contains a / (and is therefore assumed to be a directory), and -# seeing if it's a prefix of the module path. -sub find_module { - my $module = shift; - if (open (MODULES, "$REPOSITORY/CVSROOT/modules")) { - local $_; - while () { - next if /^\s*\#/; - next if /^\s*$/; - my ($name, @rest) = split; - my $path = pop @rest; - next unless ($path =~ m%/%); - if ($module =~ s%^$path(\Z|/)%%) { - $module = '/' . $module if $module; - $module = "<$name>$module"; - last; - } - } - close MODULES; - } - return $module; -} - -############################################################################## -# Multidirectory commit I/O -############################################################################## - -# Recalculate the file prefix and module after having loaded a new set of -# data. We do this by starting with the prefix from the last set of data and -# then stripping off one directory at a time until we find something that is a -# common prefix of every affected file. -sub recalculate_prefix { - my $data = shift; - my $prefix = $$data{prefix}; - for (keys %{ $$data{files} }) { - while ($prefix && index ($_, $prefix) != 0) { - $prefix =~ s%/*([^/]+)$%%; - my $last = $1; - $$data{repository} =~ s%/*\Q$last\E$%%; - $$data{localpath} =~ s%/*\Q$last\E$%%; - } - } - $$data{prefix} = $prefix; - $$data{module} = find_module $prefix; -} - -# Build the directory in which we'll find our data. -sub build_tmpdir { - my $tmpdir = $ENV{TMPDIR} || '/tmp'; - $tmpdir .= '/cvs.' . $< . '.' . getpgrp; - return $tmpdir; -} - -# Delete all of the accumulated data for multidirectory commits. -sub cleanup_data { - my $tmpdir = build_tmpdir; - unless (opendir (D, $tmpdir)) { - warn "$0: can't open $tmpdir: $!\n"; - return; - } - for (grep { $_ ne '.' && $_ ne '..' } readdir D) { - unlink "$tmpdir/$_"; - } - closedir D; - rmdir $tmpdir or warn "$0: can't remove $tmpdir: $!\n"; -} - -# Read the file containing the last directory noticed by the commitinfo script -# and return that directory name. -sub read_lastdir { - my $tmpdir = build_tmpdir; - my $last; - if (!-l $tmpdir && -d _ && (lstat _)[4] == $<) { - if (open (LAST, $tmpdir . '/directory')) { - $last = ; - chomp $last; - close LAST; - } - } - return $last; -} - -# Read in a list of files with revisions, one per line, and fill in the -# provided hashes. The first one gets the file information put into its files -# key, and the second gets lists of added, removed, and modified files. -# Returns success or failure. -sub read_files { - my ($file, $data, $message) = @_; - unless (open (FILES, $file)) { - warn "$0: can't open $file: $!\n"; - return; - } - my (@added, @removed, @modified); - local $_; - while () { - chomp; - my ($name, $old, $new) = /^(.*),([^,]+),([^,]+)$/; - next unless $new; - $$data{files}{$name} = [ $old, $new ]; - if ($old eq 'NONE') { push (@added, $name) } - elsif ($new eq 'NONE') { push (@removed, $name) } - else { push (@modified, $name) } - } - close FILES; - $$message{added} = [ @added ]; - $$message{removed} = [ @removed ]; - $$message{modified} = [ @modified ]; - return 1; -} - -# Read in message text from a file and put it in the provided hash. -sub read_text { - my ($file, $message) = @_; - my @text; - if (open (TEXT, $file)) { - @text = ; - close TEXT; - } - $$message{text} = [ @text ]; -} - -# Given a list of message hashes and a new one, merge the new one into the -# list. This is done by checking its commit message against the existing ones -# and merging the list of affected files if a match is found. If a match -# isn't found, the new message is appended to the end of the list. -sub merge_message { - my ($list, $message) = @_; - my $done; - for (@$list) { - if ("@{ $$_{text} }" eq "@{ $$message{text} }") { - push (@{ $$_{added} }, @{ $$message{added} }); - push (@{ $$_{removed} }, @{ $$message{removed} }); - push (@{ $$_{modified} }, @{ $$message{modified} }); - $done = 1; - } - } - push (@$list, $message) unless $done; -} - -# Read in saved data from previous directories. This involves reading in its -# affected files and its commit message, merging this with the previous list -# of affected files and commit messages, and then recalculating the common -# prefix for all the files and deleting all the data we read in. -sub read_data { - my $data = shift; - my $tmpdir = build_tmpdir; - $$data{messages} = []; - for (my $i = 1; -f "$tmpdir/files.$i"; $i++) { - my %message; - read_files ("$tmpdir/files.$i", $data, \%message); - read_text ("$tmpdir/text.$i", \%message); - merge_message ($$data{messages}, \%message); - } - merge_message ($$data{messages}, $$data{message}); - recalculate_prefix ($data); - cleanup_data; -} - -# Save data for the files modified in this invocation to be picked up later. -sub save_data { - my $data = shift; - my $tmpdir = build_tmpdir; - if (-l $tmpdir || !-d _ || (lstat _)[4] != $<) { - warn "$0: invalid directory $tmpdir\n"; - return undef; - } - my $i = 1; - $i++ while -f "$tmpdir/files.$i"; - unless (open (FILES, "> $tmpdir/files.$i") - && open (TEXT, "> $tmpdir/text.$i")) { - warn "$0: can't save to $tmpdir/files: $!\n"; - return undef; - } - for (keys %{ $$data{files} }) { - my ($old, $new) = @{ $$data{files}{$_} }; - print FILES join (',', $_, $old, $new), "\n"; - } - print TEXT @{ $$data{message}{text} }; - unless (close (FILES) && close (TEXT)) { - warn "$0: can't save to $tmpdir/files: $!\n"; - return undef; - } -} - -############################################################################## -# Parsing functions -############################################################################## - -# Split apart the file names that are passed to cvslog. Unfortunately, CVS -# passes all the affected files as one string rather than as separate -# arguments, which means that file names that contain spaces and commas pose -# problems. Returns the path in the repository and then a list of files with -# attached version information; that list may be just a couple of special-case -# strings indicating a cvs add of a directory or a cvs import. -# -# The complexity here is purely the fault of CVS, which doesn't have a good -# interface to logging hooks. -sub split_files { - my ($files) = @_; - - # This ugly hack is here to deal with files at the top level of the - # repository; CVS reports those files without including a directory - # before the file list. Check to see if what would normally be the - # directory name looks more like a file with revisions. - my ($root, $rest) = split (' ', $files, 2); - if ($rest && $root !~ /(,(\d+(\.\d+)*|NONE)){2}$/) { - $files = $rest; - } else { - $root = '.'; - } - - # Special-case directory adds and imports. - if ($files =~ /^- New directory(,NONE,NONE)?$/) { - return ($root, 'directory'); - } elsif ($files =~ /^- Imported sources(,NONE,NONE)?$/) { - return ($root, 'import'); - } - - # Now, split apart $files, which contains just the files, at the spaces - # after version information. - my @files; - while ($files =~ s/^((?:.*?)(?:,(?:\d+(?:\.\d+)*|NONE)){2})( |\z)//) { - push (@files, $1); - } - push (@files, $files) if $files; - return ($root, 'commit', @files); -} - -# Given the summary line passed to the script, parse it into file names and -# version numbers (if available). Takes the log information hash and adds a -# key for the type of change (directory, import, or commit) and for commits a -# hash of file names with values being a list of the previous and the now- -# current version number. Also finds the module and stores that in the hash. -# -# The path in the repository (the first argument) is prepended to all of the -# file names; we'll pull off the common prefix later. -sub parse_files { - my ($data, @args) = @_; - my ($directory, $type, @files); - if (@args == 1) { - ($directory, $type, @files) = split_files ($args[0]); - if ($type eq 'commit') { - @files = map { [ /^(.*),([^,]+),([^,]+)$/ ] } @files; - } - } else { - $directory = shift @args; - if ($args[0] eq '- New directory') { - $type = 'directory'; - } elsif ($args[0] eq '- Imported sources') { - $type = 'import'; - } else { - $type = 'commit'; - while (@args) { - push (@files, [ splice (@args, 0, 3) ]); - } - } - } - die "$0: no module given by CVS (no \%{sVv}?)\n" unless $directory; - $$data{prefix} = $directory; - $$data{module} = find_module $directory; - $$data{message}{added} ||= []; - $$data{message}{modified} ||= []; - $$data{message}{removed} ||= []; - if ($type eq 'directory') { - $$data{type} = 'directory'; - $$data{root} = $directory; - } elsif ($type eq 'import') { - $$data{type} = 'import'; - $$data{root} = $directory; - } elsif (!@files) { - die "$0: no files given by CVS (no \%{sVv}?)\n"; - } else { - $$data{type} = 'commit'; - my $added = $$data{message}{added}; - my $modified = $$data{message}{modified}; - my $removed = $$data{message}{removed}; - for (@files) { - my ($name, $prev, $cur) = @$_; - warn "$0: no version numbers given by CVS (no \%{sVv}?)\n" - unless defined $cur; - $$data{files}{"$directory/$name"} = [ $prev, $cur ]; - if ($prev eq 'NONE') { push (@$added, "$directory/$name") } - elsif ($cur eq 'NONE') { push (@$removed, "$directory/$name") } - else { push (@$modified, "$directory/$name") } - } - } -} - -# Parse the header of the CVS log message (containing the path information) -# and puts the path information into the data hash. -sub parse_paths { - my $data = shift; - - # The first line of the log message will be "Update of ". - my $path = ; - print $path if $DEBUG; - $path =~ s/^Update of //; - $path =~ s/\s*$//; - $$data{repository} = $path; - - # Now comes the path to the local working directory. Grab it and clean it - # up, and then ignore the next blank line. - local $_ = ; - print if $DEBUG; - my ($local) = /directory (\S+)/; - $$data{localpath} = $local; - $_ = ; - print if $DEBUG; -} - -# Extract the tag. We assume that all files will be committed with the same -# tag; probably not the best assumption, but it seems workable. Note that we -# ignore all of the file lists, since we build those ourself from the version -# information (saving the hard challenge of parsing a whitespace-separated -# list that could contain filenames with whitespace). -sub parse_filelist { - my $data = shift; - my ($current, @added, @modified, @removed); - local $_; - while () { - print if $DEBUG; - last if /^Log Message/; - $$data{tag} = $1, next if /^\s*Tag: (\S+)\s*$/; - } -} - -# Extract the commit message, stripping leading and trailing whitespace. -sub parse_message { - my $data = shift; - my @message = ; - print @message if $DEBUG; - shift @message while (@message && $message[0] =~ /^\s*$/); - pop @message while (@message && $message[-1] =~ /^\s*$/); - $$data{message}{text} = [ @message ]; -} - -############################################################################## -# Formatting functions -############################################################################## - -# Determine the From header of the message. If CVSUSER is set, we're running -# from inside a CVS server, and the From header should reflect information -# from the CVS passwd file. Otherwise, pull the information from the system -# passwd file. -sub build_from { - my $cvsuser = $ENV{CVSUSER} || scalar (getpwuid $<); - my $name = ''; - my $address = ''; - if ($cvsuser) { - if (open (PASSWD, "$REPOSITORY/CVSROOT/passwd")) { - local $_; - while () { - chomp; - next unless /:/; - my @info = split ':'; - if ($info[0] eq $cvsuser) { - $name = $info[3]; - $address = $info[4]; - } - } - close PASSWD; - } - $name ||= (getpwnam $cvsuser)[6]; - } - $address ||= $cvsuser || 'cvs'; - $name =~ s/,.*//; - if ($name =~ /[^\w ]/) { - $name = '"' . $name . '"'; - } - return "From: " . ($name ? "$name <$address>" : $address) . "\n"; -} - -# Takes the data hash, a prefix to add to the subject header, and a flag -# saying whether to give a full list of files no matter how long it is. Form -# the subject line of our message. Try to keep the subject under 78 -# characters by just giving a count of files if there are a lot of them. -sub build_subject { - my ($data, $prefix, $long) = @_; - $prefix = "Subject: " . $prefix; - my $length = 78 - length ($prefix) - length ($$data{module}); - $length = 8 if $length < 8; - my $subject; - if ($$data{type} eq 'directory') { - $subject = "[new]"; - } elsif ($$data{type} eq 'import') { - $subject = "[import]"; - } else { - my @files = sort keys %{ $$data{files} }; - @files = simplify ($$data{prefix}, \@files); - my $files = join (' ', @files); - $files =~ s/[\n\r]/ /g; - if (!$long && length ($files) > $length) { - $subject = '(' . @files . (@files > 1 ? " files" : " file") . ')'; - } else { - $subject = "($files)"; - } - } - if ($$data{module}) { - $subject = "$$data{module} $subject"; - } - if ($$data{tag} && $$data{tag} =~ /[^\d.]/) { - $subject = "$$data{tag} $subject"; - } - return "$prefix$subject\n"; -} - -# Generate file lists, wrapped at 74 columns, with the right prefix for what -# type of file they are. -sub build_filelist { - my ($prefix, @files) = @_; - local $_ = join (' ', @files); - my $output = ''; - while (length > 64) { - if (s/^(.{0,64})\s+// || s/^(\S+)//) { - $output .= (' ' x 10) . $1 . "\n"; - } else { - last; - } - } - $output .= (' ' x 10) . $_; - $output =~ s/\s*$/\n/; - $prefix = (' ' x (8 - length ($prefix))) . $prefix; - $output =~ s/^ {10}/$prefix: /; - return $output; -} - -# Build the subheader of the report, listing the files changed and some other -# information about the change. Returns the header as a list. -sub build_header { - my ($data, $showdir, $showauthor) = @_; - my $user = $ENV{CVSUSER} || (getpwuid $<)[0] || $<; - my $date = strftime ('%A, %B %e, %Y @ %T', localtime time); - $date =~ s/ / /; - my @header = (" Date: $date\n"); - push (@header, " Author: $user\n") if $showauthor; - - # If the paths are too long, trim them by taking off a leading path - # component until the length is under 70 characters. - my $path = $$data{repository}; - my $local = $$data{localpath}; - while (length ($path) > 69) { - $path =~ s%^\.\.\.%%; - last unless $path =~ s%^/[^/]+%...%; - } - while (length ($local) > 69) { - $local =~ s%^([\w.-]+:)\.\.\.%$1%; - last unless $local =~ s%^([\w.-]+:)/[^/]+%$1...%; - } - - if ($showdir) { - push (@header, " Tag: $$data{tag}\n") if $$data{tag}; - push (@header, "\n", "Update of $path\n", - " from $local\n"); - } else { - push (@header, " Path: $path\n"); - push (@header, " Tag: $$data{tag}\n") if $$data{tag}; - } - return @header; -} - -# Build a report for a particular commit; this includes the list of affected -# files and the commit message. Returns the report as a list. Takes the -# data, the commit message, and a flag saying whether to add version numbers -# to the file names. -sub build_message { - my ($data, $message, $versions) = @_; - my @added = sort @{ $$message{added} }; - my @modified = sort @{ $$message{modified} }; - my @removed = sort @{ $$message{removed} }; - if ($versions) { - @added = map { "$_ ($$data{files}{$_}[1])" } @added; - @removed = map { "$_ ($$data{files}{$_}[0])" } @removed; - @modified = map { - print "$_\n"; - "$_ ($$data{files}{$_}[0] -> $$data{files}{$_}[1])" - } @modified; - } - @added = simplify ($$data{prefix}, \@added); - @modified = simplify ($$data{prefix}, \@modified); - @removed = simplify ($$data{prefix}, \@removed); - my @message; - push (@message, build_filelist ('Added', @added)) if @added; - push (@message, build_filelist ('Modified', @modified)) if @modified; - push (@message, build_filelist ('Removed', @removed)) if @removed; - if (@{ $$message{text} }) { - push (@message, "\n") if (@added || @modified || @removed); - push (@message, @{ $$message{text} }); - } - return @message; -} - -# Builds an array of -r flags to pass to CVS to get diffs between the -# appropriate versions, given a reference to the %data hash and the name of -# the file. -sub build_version_flags { - my ($data, $file) = @_; - my @versions = @{ $$data{files}{$file} }; - return unless $versions[1] && ($versions[0] ne $versions[1]); - if ($versions[0] eq 'NONE') { - @versions = ('-r', '0.0', '-r', $versions[1]); - } elsif ($versions[1] eq 'NONE') { - @versions = ('-r', $versions[0], '-r', next_version $versions[0]); - } else { - @versions = map { ('-r', $_) } @versions; - } - return @versions; -} - -# Build cvsweb diff URLs. Right now, this is very specific to cvsweb, but -# could probably be extended for other web interfaces to CVS. Takes the data -# hash and the base URL for cvsweb. -sub build_cvsweb { - my ($data, $cvsweb) = @_; - my $options = 'f=h'; - my @cvsweb = ("Diff URLs:\n"); - my $file; - for (sort keys %{ $$data{files} }) { - my @versions = @{ $$data{files}{$_} }; - next unless @versions; - my $file = $_; - for ($file, @versions) { - s{([^a-zA-Z0-9\$_.+!*\'(),/-])} {sprintf "%%%x", ord ($1)}ge; - } - my $url = "$cvsweb/$file.diff?$options&r1=$versions[0]" - . "&r2=$versions[1]\n"; - push (@cvsweb, $url); - } - return @cvsweb; -} - -# Run a cvs rdiff between the old and new versions and return the output. -# This is useful for small changes where you want to see the changes in -# e-mail, but probably creates too large of messages when the changes get -# bigger. Note that this stores the full diff output in memory. -sub build_diff { - my $data = shift; - my @difflines; - for my $file (sort keys %{ $$data{files} }) { - my @versions = build_version_flags ($data, $file); - next unless @versions; - my $pid = open (CVS, '-|'); - if (!defined $pid) { - die "$0: can't fork cvs: $!\n"; - } elsif ($pid == 0) { - open (STDERR, '>&STDOUT') or die "$0: can't reopen stderr: $!\n"; - exec ('cvs', '-fnQq', '-d', $REPOSITORY, 'rdiff', '-u', - @versions, $file) or die "$0: can't fork cvs: $!\n"; - } else { - my @diff = ; - close CVS; - if ($diff[1] =~ /failed to read diff file header/) { - @diff = ($diff[0], "<>\n"); - } - push (@difflines, @diff); - } - } - return @difflines; -} - -# Build a summary of the changes by building the patch it represents in /tmp -# and then running diffstat on it. This gives a basic idea of the order of -# magnitude of the changes. Takes the data hash and the path to diffstat as -# arguments. -sub build_summary { - my ($data, $diffstat) = @_; - $diffstat ||= 'diffstat'; - open2 (\*OUT, \*IN, $diffstat, '-w', '78') - or die "$0: can't fork $diffstat: $!\n"; - my @binary; - for my $file (sort keys %{ $$data{files} }) { - my @versions = build_version_flags ($data, $file); - next unless @versions; - my $pid = open (CVS, '-|'); - if (!defined $pid) { - die "$0: can't fork cvs: $!\n"; - } elsif ($pid == 0) { - open (STDERR, '>&STDOUT') or die "$0: can't reopen stderr: $!\n"; - exec ('cvs', '-fnQq', '-d', $REPOSITORY, 'rdiff', '-u', - @versions, $file) or die "$0: can't fork cvs: $!\n"; - } - local $_; - while () { - s%^(\*\*\*|---|\+\+\+) \Q$$data{prefix}\E/*%$1 %; - s%^Index: \Q$$data{prefix}\E/*%Index: %; - if (/^diff -c/) { s% \Q$$data{prefix}\E/*% %g } - if (/: failed to read diff file header/) { - my $short = $file; - $short =~ s%^\Q$$data{prefix}\E/*%%; - my $date = localtime; - print IN "Index: $short\n"; - print IN "--- $short\t$date\n+++ $short\t$date\n"; - print IN "@@ -1,1 +1,1 @@\n+<>\n"; - push (@binary, $short); - last; - } else { - print IN $_; - } - } - close CVS; - } - close IN; - my @stats = ; - close OUT; - my $offset = index ($stats[0], '|'); - for my $file (@binary) { - @stats = map { - s/^( +\Q$file\E +\| +).*/$1<\>/; - $_; - } @stats; - } - unshift (@stats, '-' x $offset, "+\n"); - return @stats; -} - -############################################################################## -# Configuration file handling -############################################################################## - -# Load defaults from a configuration file, if any. The syntax is keyword -# colon value, where value may be enclosed in quotes. Returns a list -# containing the address to which to send all commits (defaults to not sending -# any message), the base URL for cvsweb (defaults to not including cvsweb -# URLs), the full path to diffstat (defaults to just "diffstat", meaning the -# user's path will be searched), the subject prefix, a default host for -# unqualified e-mail addresses, additional headers to add to the mail message, -# and the full path to sendmail. -sub load_config { - my $file = $REPOSITORY . '/CVSROOT/cvslog.conf'; - my $address = ''; - my $cvsweb = ''; - my $diffstat = 'diffstat'; - my $headers = ''; - my $mailhost = ''; - my ($sendmail) = grep { -x $_ } qw(/usr/sbin/sendmail /usr/lib/sendmail); - $sendmail ||= '/usr/lib/sendmail'; - my $subject = 'CVS update of '; - if (open (CONFIG, $file)) { - local $_; - while () { - next if /^\s*\#/; - next if /^\s*$/; - chomp; - my ($key, $value) = /^\s*(\S+):\s+(.*)/; - unless ($value) { - warn "$0:$file:$.: invalid config syntax: $_\n"; - next; - } - $value =~ s/\s+$//; - $value =~ s/^\"(.*)\"$/$1/; - if (lc $key eq 'address') { $address = $value } - elsif (lc $key eq 'cvsweb') { $cvsweb = $value } - elsif (lc $key eq 'diffstat') { $diffstat = $value } - elsif (lc $key eq 'mailhost') { $mailhost = $value } - elsif (lc $key eq 'sendmail') { $sendmail = $value } - elsif (lc $key eq 'subject') { $subject = $value } - elsif (lc $key eq 'header') { $headers .= $value . "\n" } - else { warn "$0:$file:$.: unrecognized config line: $_\n" } - } - close CONFIG; - } - return ($address, $cvsweb, $diffstat, $subject, $mailhost, $headers, - $sendmail); -} - -############################################################################## -# Main routine -############################################################################## - -# Load the configuration file for defaults. -my ($address, $cvsweburl, $diffstat, $subject, $mailhost, $headers, $sendmail) - = load_config; - -# Parse command-line options. -my (@addresses, $cvsweb, $diff, $help, $longsubject, $merge, $omitauthor, - $showdir, $summary, $version, $versions); -Getopt::Long::config ('bundling', 'no_ignore_case', 'require_order'); -GetOptions ('address|a=s' => \@addresses, - 'cvsweb|c' => \$cvsweb, - 'debug|D' => \$DEBUG, - 'diff|d' => \$diff, - 'help|h' => \$help, - 'include-versions|i' => \$versions, - 'long-subject|l' => \$longsubject, - 'merge|m' => \$merge, - 'omit-author|o' => \$omitauthor, - 'show-directory|w' => \$showdir, - 'summary|s' => \$summary, - 'version|v' => \$version) or exit 1; -if ($help) { - print "Feeding myself to perldoc, please wait....\n"; - exec ('perldoc', '-t', $0); -} elsif ($version) { - my @version = split (' ', $ID); - shift @version if $ID =~ /^\$Id/; - my $version = join (' ', @version[0..2]); - $version =~ s/,v\b//; - $version =~ s/(\S+)$/($1)/; - $version =~ tr%/%-%; - print $version, "\n"; - exit; -} -die "$0: no addresses specified\n" unless ($address || @addresses); -die "$0: unable to determine the repository path\n" unless $REPOSITORY; -die "$0: no cvsweb URL specified in the configuration file\n" - if $cvsweb && !$cvsweburl; -my $showauthor = !$omitauthor; - -# Parse the input. -print "Options: ", join ('|', @ARGV), "\n" if $DEBUG; -print '-' x 78, "\n" if $DEBUG; -my %data; -parse_files (\%data, @ARGV); -parse_paths (\%data); -parse_filelist (\%data); -parse_message (\%data); -print '-' x 78, "\n" if $DEBUG; - -# Check to see if this is part of a multipart commit. If so, just save the -# data for later. Otherwise, read in any saved data and add it to our data. -if ($merge && $data{type} eq 'commit') { - my $lastdir = read_lastdir; - if ($lastdir && $data{repository} ne $lastdir) { - save_data (\%data) and exit 0; - # Fall through and send a notification if save_data fails. - } else { - read_data (\%data); - } -} -$data{messages} = [ $data{message} ] unless $data{messages}; - -# Exit if there are no addresses to send the message to. -exit 0 if (!$address && !@addresses); - -# Open our mail program. -open (MAIL, "| $sendmail -t -oi -oem") - or die "$0: can't fork $sendmail: $!\n"; -my $oldfh = select MAIL; -$| = 1; -select $oldfh; - -# Build the mail headers. -if ($mailhost) { - for ($address, @addresses) { - if ($_ && !/\@/) { - $_ .= '@' . $mailhost unless /\@/; - } - } -} -if (@addresses) { - print MAIL "To: ", join (', ', @addresses), "\n"; - print MAIL "Cc: $address\n" if $address; -} else { - print MAIL "To: $address\n"; -} -print MAIL build_from; -print MAIL $headers if $headers; -print MAIL build_subject (\%data, $subject, $longsubject), "\n"; - -# Build the message and write it out. -print MAIL build_header (\%data, $showdir, $showauthor); -for (@{ $data{messages} }) { - print MAIL "\n", build_message (\%data, $_, $versions); -} -if ($data{type} eq 'commit') { - print MAIL "\n\n", build_summary (\%data, $diffstat) if $summary; - print MAIL "\n\n", build_cvsweb (\%data, $cvsweburl) if $cvsweb; - print MAIL "\n\n", build_diff (\%data) if $diff; -} - -# Make sure sending mail succeeded. -close MAIL; -unless ($? == 0) { die "$0: sendmail exit status " . ($? >> 8) . "\n" } -exit 0; -__END__ - -############################################################################## -# Documentation -############################################################################## - -=head1 NAME - -cvslog - Mail CVS commit notifications - -=head1 SYNOPSIS - -B [B<-cDdhilmosvw>] [B<-a> I
...] %{sVv} - -=head1 REQUIREMENTS - -CVS 1.10 or later, Perl 5.004 or later, diffstat for the B<-s> option, and a -sendmail command that can accept formatted mail messages for delivery. - -=head1 DESCRIPTION - -B is intended to be run out of CVS's F administrative file. -It parses the (undocumented) format of CVS's commit notifications, cleans it -up and reformats it, and mails the notification to one or more e-mail -addresses. Optionally, a diffstat(1) summary of the changes can be added to -the notification, and a CVS commit spanning multiple directories can be -combined into a single notification (by default, CVS generates a separate -notification for each directory). - -To combine a commit spanning multiple directories into a single notification, -B needs the help of an additional program run from the F -administrative file that records the last directory affected by the commit. -See the description in L<"FILES"> for what files and directories must be -created. One such suitable program is B by the same author. - -For information on how to add B to your CVS repository, see -L<"INSTALLATION"> below. B also looks for a configuration file named -F; for details on the format of that file, see -L<"CONFIGURATION">. - -The From: header of the mail message sent by B is formed from the user -making the commit. The contents of the environment variable CVSUSER or the -name of the user doing the commit if CVSUSER isn't set is looked up in the -F file in the CVS repository, if present, and the fourth field is used -as the full name and the fifth as the user's e-mail address. If that user -isn't found in F, it's looked up in the system password file instead -to try to find a full name. Otherwise, that user is just used as an e-mail -address. - -=head1 OPTIONS - -=over 4 - -=item B<-a> I
, B<--address>=I
- -Send the commit notification to I
(possibly in addition to the -address defined in F). This option may occur more than once, -and all specified addresses will receive a copy of the notification. - -=item B<-c>, B<--cvsweb> - -Append the cvsweb URLs for all the diffs covered in the commit message to -the message. The base cvsweb URL must be set in the configuration file. -The file name will be added and the C and C parameters will be -appended with the appropriate values, along with C to request formatted -diff output. Currently, the cvsweb URLs are not further configurable. - -=item B<-D>, B<--debug> - -Prints out the information B got from CVS as it works. This option -is mostly useful for developing B and checking exactly what data CVS -provides. The first line of output will be the options passed to B, -separated by C<|>. - -=item B<-d>, B<--diff> - -Append the full diff output for each change to the notification message. -This is probably only useful if you know that all changes for which -B is run will be small. Note that the entire diff output is -temporarily stored in memory, so this could result in excessive memory usage -in B for very large changes. - -When this option is given, B needs to be able to find B in the -user's PATH. - -If one of the committed files is binary and this is detected by B, -B will suppress the diff and replace it with a note that the file is -binary. - -=item B<-h>, B<--help> - -Print out this documentation (which is done simply by feeding the script to -C). - -=item B<-i>, B<--include-versions> - -Include version numbers (in parentheses) after the file names in the lists -of added, removed, and changed files. By default, only the file names are -given. - -=item B<-l>, B<--long-subject> - -Normally, B will just list the number of changed files rather than the -complete list of them if the subject would otherwise be too long. This flag -disables that behavior and includes the full list of modified files in the -subject header of the mail, no matter how long it is. - -=item B<-m>, B<--merge> - -Merge multidirectory commits into a single notification. This requires that a -program be run from F to record the last directory affected by the -commit. Using this option will cause B to temporarily record -information about a commit in progress in TMPDIR or /tmp; see L<"FILES">. - -=item B<-o>, B<--omit-author> - -Omit the author information from the commit notification. This is useful -where all commits are done by the same person (so the author information is -just noise) or where the author information isn't actually available. - -=item B<-s>, B<--summary> - -Append to each commit notification a summary of the changes, produced by -generating diffs and feeding those diffs to diffstat(1). diffstat(1) must be -installed to use this option; see also the B configuration parameter -in L<"CONFIGURATION">. - -When this option is given, B needs to be able to find B in the -user's PATH. - -If one of the committed files is binary and this is detected by B, -B will replace the uninformative B line corresponding to -that file (B will indicate that nothing changed) with a note that -the file is binary. - -=item B<-v>, B<--version> - -Print out the version of B and exit. - -=item B<-w>, B<--show-directory> - -Show the working directory from which the commit was made. This is usually -not enlightening and when running CVS in server mode will always be some -uninteresting directory in /tmp, so the default is to not include this -information. - -=back - -=head1 CONFIGURATION - -B will look for a configuration file named F in the -CVSROOT directory of your repository. Absence of this file is not an error; -it just means that all of the defaults will be used. The syntax of this file -is one configuration parameter per line in the format: - - parameter: value - -The value may be enclosed in double-quotes and must be enclosed in -double-quotes if there is trailing whitespace that should be part of the -value. There is no way to continue a line; each parameter must be a single -line. Lines beginning with C<#> are comments. - -The following configuration parameters are supported: - -=over 4 - -=item address - -The address or comma-separated list of addresses to which all commit messages -should be sent. If this parameter is not given, the default is to send the -commit message only to those addresses specified with B<-a> options on the -command line, and there must be at least one B<-a> option on the command line. - -=item cvsweb - -The base URL for cvsweb diffs for this repository. Only used if the B<-c> -option is given; see the description of that option for more information -about how the full URL is constructed. - -=item diffstat - -The full path to the diffstat(1) program. If this parameter is not given, the -default is to look for diffstat(1) on the user's PATH. Only used if the B<-s> -option is given. - -=item header - -The value should be a valid mail header, such as "X-Ticket: cvs". This header -will be added to the mail message sent. This configuration parameter may -occur multiple times, and all of those headers will be added to the message. - -=item mailhost - -The hostname to append to unqualified addresses given on the command line with -B<-a>. If set, an C<@> and this value will be appended to any address given -with B<-a> that doesn't contain C<@>. This parameter exists solely to allow -for shorter lines in the F file. - -=item sendmail - -The full path to the sendmail binary. If not given, this setting defaults to -either C or C, whichever is found, -falling back on C. - -=item subject - -The subject prefix to use for the mailed notifications. Appended to this -prefix will be the module or path in the repository of the affected directory -and then either a list of files or a count of files depending on the available -space. The default is C<"CVS update of ">. - -=back - -=head1 INSTALLATION - -Follow these steps to add cvslog to your project: - -=over 4 - -=item 1. - -Check out CVSROOT for your repository (see the CVS manual if you're not sure -how to do this), copy this script into that directory, change the first line -to point to your installation of Perl if necessary, and cvs add and commit it. - -=item 2. - -Add a line like: - - cvslog Unable to check out CVS log notification script cvslog - -to F in CVSROOT and commit it. - -=item 3. - -If needed, create a F file as described above and cvs add and -commit it. Most installations will probably want to set B
, since the -most common CVS configuration is a single repository per project with all -commit notifications sent to the same address. If you don't set B
, -you'll need to add B<-a> options to every invocation of B in -F. - -=item 4. - -If you created a F file, add a line like: - - cvslog.conf Unable to check out cvslog configuration cvslog.conf - -to F in CVSROOT and commit it. - -=item 5. - -Set up your rules in F for those portions of the repository you want -to send CVS commit notifications for. A good starting rule is: - - DEFAULT $CVSROOT/CVSROOT/cvslog %{sVv} - -which will send notifications for every commit to your repository that doesn't -have a separate, more specific rule to the value of B
in -F. You must always invoke B as $CVSROOT/CVSROOT/cvslog; -B uses the path it was invoked as to find the root of the repository. -If you have different portions of your repository that should send -notifications to different places, you can use a series of rules like: - - ^foo/ $CVSROOT/CVSROOT/cvslog -a foo-commit %{sVv} - ^bar/ $CVSROOT/CVSROOT/cvslog -a bar-commit %{sVv} - -This will send notification of commits to anything in the C directory -tree in the repository to foo-commit (possibly qualified with B from -F) and everything in C to bar-commit. No commit -notifications will be sent for any other commits. The C<%{sVv}> string is -replaced by CVS with information about the committed files and should always -be present. - -If you are using CVS version 1.12.6 or later, the format strings for -F rules have changed. Instead of C<%{sVv}>, use C<-- %p %{sVv}>, -once you've set UseNewInfoFmtStrings=yes in F. For example: - - DEFAULT $CVSROOT/CVSROOT/cvslog -- %p %{sVv} - -Any options to B should go before C<-->. See the CVS documentation -for more details on the new F format. - -=item 6. - -If you want summaries of changes, obtain and compile diffstat and add B<-s> to -the appropriate lines in F. You may also need to set B in -F. - -diffstat is at L. - -=item 7. - -If you want merging of multidirectory commits, add B<-m> to the invocations of -B, copy B into your checked out copy of CVSROOT, change the -first line of the script if necessary to point to your installation of Perl, -and cvs add and cvs commit it. Then, a line like: - - cvsprep Unable to check out CVS log notification script cvsprep - -to F in CVSROOT and commit it. - -See L<"WARNINGS"> for some warnings about the security of multi-directory -commit merging. - -=item 8. - -If your operating system doesn't pass the full path to the B -executable to this script when it runs, you'll need to edit the beginning of -this script and set $REPOSITORY to the correct path to the root of your -repository. This should not normally be necessary. See the comments in this -script for additional explanation. - -=back - -=head1 EXAMPLES - -Send all commits under the baz directory to B
defined in -F: - - ^baz/ $CVSROOT/CVSROOT/cvslog -msw %{sVv} - -Multidirectory commits will be merged if B is also installed, a -diffstat(1) summary will be appended to the notification, and the working -directory from which the files were committed will also be included. This -line should be put in F. - -See L<"INSTALLATION"> for more examples. - -=head1 DIAGNOSTICS - -=over 4 - -=item can't fork %s: %s - -(Fatal) B was unable to run a program that it wanted to run. This may -result in no notification being sent or in information missing. Generally -this means that the program in question was missing or B couldn't find -it for some reason. - -=item can't open %s: %s - -(Warning) B was unable to open a file. For the modules file, this -means that B won't do any directory to module mapping. For files -related to multidirectory commits, this means that B can't gather -together information about such a commit and will instead send an individual -notification for the files affected in the current directory. (This means -that some information may have been lost.) - -=item can't remove %s: %s - -(Warning) B was unable to clean up after itself for some reason, and -the temporary files from a multidirectory commit have been left behind in -TMPDIR or F. - -=item can't save to %s: %s - -(Warning) B encountered an error saving information about a -multidirectory commit and will instead send an individual notification for the -files affected in the current directory. - -=item invalid directory %s - -(Warning) Something was strange about the given directory when B went -to use it to store information about a multidirectory commit, so instead a -separate notification for the affected files in the current directory will be -sent. This means that the directory was actually a symlink, wasn't a -directory, or wasn't owned by the right user. - -=item invalid config syntax: %s - -(Warning) The given line in F was syntactically invalid. See -L<"CONFIGURATION"> for the correct syntax. - -=item no %s given by CVS (no %{sVv}?) - -(Fatal) The arguments CVS passes to B should be the directory within -the repository that's being changed and a list of files being changed with -version information for each file. Something in that was missing. This error -generally means that the invocation of B in F doesn't have -the magic C<%{sVv}> variable at the end but instead has no variables or some -other variable like C<%s>, or means that you're using a version of CVS older -than 1.10. - -=item no addresses specified - -(Fatal) There was no B
parameter in F and no B<-a> -options on the command line. At least one recipient address must be specified -for the CVS commit notification. - -=item sendmail exit status %d - -(Fatal) sendmail exited with a non-zero status. This may mean that the -notification message wasn't sent. - -=item unable to determine the repository path - -(Fatal) B was unable to find the root of your CVS repository from the -path by which it was invoked. See L<"INSTALLATION"> for hints on how to fix -this. - -=item unrecognized config line: %s - -(Warning) The given configuration parameter isn't one of the ones that -B knows about. - -=back - -=head1 FILES - -All files relative to $CVSROOT will be found by looking at the full path -B was invoked as and pulling off the path before C. -If this doesn't work on your operating system, you'll need to edit this script -to set $REPOSITORY. - -=over 4 - -=item $CVSROOT/CVSROOT/cvslog.conf - -Read for configuration directives if it exists. See L<"CONFIGURATION">. - -=item $CVSROOT/CVSROOT/modules - -Read to find the module a given file is part of. Rather than always giving -the full path relative to $CVSROOT of the changed files, B tries to -find the module that that directory belongs to and replaces the path of that -module with the name of the module in angle brackets. Modules are found by -reading this file, looking at the last white-space-separated word on each -line, and if it contains a C, checking to see if it is a prefix of the path -to the files affected by a commit. If so, the first white-space-separated -word on that line of F is taken to be the affected module. The first -matching entry is used. - -=item $CVSROOT/CVSROOT/passwd - -Read to find the full name and e-mail address corresponding to a particular -user. The full name is expected to be the fourth field colon-separated field -and the e-mail address the fifth. Defaults derived from the system password -file are used if these are not provided. - -=item TMPDIR/cvs.%d.%d - -Information about multidirectory commits is read from and stored in this -directory. This script will never create this directory (the helper script -B that runs from F has to do that), but it will read and -store information in it and when the commit message is sent, it will delete -everything in this directory and remove the directory. - -The first %d is the numeric UID of the user running B. The second %d -is the process group B is part of. The process group is included in -the directory name so that if you're running a shell that calls setpgrp() (any -modern shell with job control should), multiple commits won't collide with -each other even when done from the same shell. - -If TMPDIR isn't set in the environment, F is used for TMPDIR. - -=item TMPDIR/cvs.%d.%d/directory - -B expects this file to contain the name of the final directory -affected by a multidirectory commit. Each B invocation will save the -data that it's given until B is invoked for this directory, and then -all of the saved data will be combined with the data for that directory and -sent out as a single notification. - -This file must be created by a script such as B run from -F. If it isn't present, B doesn't attempt to combine -multidirectory commits, even if B<-m> is used. - -=back - -=head1 ENVIRONMENT - -=over 4 - -=item PATH - -Used to find cvs and diffstat when the B<-s> option is in effect. If the -B configuration option is set, diffstat isn't searched for on the -user's PATH, but cvs must always be found on the user's PATH in order for -diffstat summaries to work. - -=item TMPDIR - -If set, specifies the temporary directory to use instead of F for -storing information about multidirectory commits. Setting this to some -private directory is recommended if you're doing CVS commits on a multiuser -machine with other untrusted users due to the standard troubles with safely -creating files in F. (Note that other programs besides B also -use TMPDIR.) - -=back - -=head1 WARNINGS - -Merging multidirectory commits requires creating predictably-named files to -communicate information between different processes. By default, those files -are created in F in a directory created for that purpose. While this -should be reasonably safe on systems that don't allow one to remove -directories owned by other people in F, since a directory is used rather -than an individual file and since various sanity checks are made on the -directory before using it, this is still inherently risky on a multiuser -machine with a world-writeable F directory if any of the other users -aren't trusted. - -For this reason, I highly recommend setting TMPDIR to some directory, perhaps -in your home directory, that only you have access to if you're in that -situation. Not only will this make B more secure, it may make some of -the other programs you run somewhat more secure (lots of programs will use the -value of TMPDIR if set). I really don't trust the security of creating any -predictably-named files or directories in F and neither should you. - -Multiple separate B invocations in F interact oddly with -merging of multidirectory commits. The commit notification will be sent to -the addresses and in the style configured for the last invocation of -B, even if some of the earlier directories had different notification -configurations. As a general rule, it's best not to merge multidirectory -commits that span separate portions of the repository with different -notification policies. - -B doesn't support using B (which comes with CVS) as a -F script to provide information about multidirectory commits -because it writes files directly in F rather than using a subdirectory. - -Some file names simply cannot be supported correctly in CVS versions prior -to 1.12.6 (with new-style info format strings turned on) because of -ambiguities in the output from CVS. For example, file names beginning with -spaces are unlikely to produce the correct output, and file names containing -newlines will likely result in odd-looking mail messages. - -=head1 BUGS - -There probably should be a way to specify the path to cvs for generating -summaries and diffs, to turn off the automatic module detection stuff, to -provide for transformations of the working directory (stripping the domain -off the hostname, shortening directory paths in AFS), and to configure the -maximum subject length. The cvsweb support could stand to be more -customizable. - -Many of the logging scripts out there are based on B, which comes -with CVS and uses a different output format for multidirectory commits. I -prefer the one in B, but it would be nice if B could support -either. - -File names containing spaces may be wrapped at the space in the lists of -files added, modified, or removed. The lists may also be wrapped in the -middle of the appended version information if B<-i> is used. - -Multi-directory commit merging may mishandle file names that contain -embedded newlines even with CVS version 1.12.6 or later due to the file -format that B uses to save the intermediate data. - -=head1 NOTES - -Some parts of this script are horrible hacks because the entirety of commit -notification handling in CVS is a horrible, undocumented hack. Better commit -notification support in CVS proper would be welcome, even if it would make -this script obsolete. - -=head1 SEE ALSO - -cvs(1), diffstat(1), cvsprep(1). - -diffstat is at L. - -Current versions of this program are available from its web site at -L. B is available -from this same location. - -=head1 AUTHOR - -Russ Allbery . - -=head1 COPYRIGHT AND LICENSE - -Copyright 1998, 1999, 2000, 2001, 2002, 2003, 2004 Board of Trustees, Leland -Stanford Jr. University. - -This program is free software; you can redistribute it and/or modify it -under the same terms as Perl itself. - -=cut