Tom Lane <[EMAIL PROTECTED]> writes:

> "Larry Rosenman" <[EMAIL PROTECTED]> writes:
> > I'm referring to the actual commit messages.  
> 
> It *would* be awfully nice if the pgsql-committers traffic were one
> message per commit, instead of one per directory touched per commit.
> This has been suggested before, but nothing got done ...

This is easy to set up, for an appropriate definition of ``easy''.

I have included two files below, commit_prep and log_accum.  Do
    cvs co CVSROOT
Copy the files into the CVSROOT directory.  Check the definition of
$MAILER near the top of log_accum.  Everything else should be fine.
Do this:
    cvs add commit_prep
    cvs add log_accum

Edit the file checkoutlist, and add these lines:

commit_prep     Won't be able to do mail logging.
log_accum       Won't be able to do mail logging.

Edit the file commitinfo, and add something like this line:

DEFAULT  /usr/bin/perl $CVSROOT/CVSROOT/commit_prep -r

Edit the file loginfo, and add something like this line (replace
MAILINGLIST with the mailing address to which you want log messages to
be sent):

DEFAULT  /usr/bin/perl $CVSROOT/CVSROOT/log_accum -m MAILINGLIST -s %s

Then:
    cvs commit

Then test it.

Good luck.  I didn't write these scripts, but I've used them in a
number of places.

Ian

commit_prep:
==================================================
#!/usr/bin/perl
# -*-Perl-*-
#
# $Id: commit_prep,v 1.1 1998/12/01 03:18:27 ian Exp $
#
# Perl filter to handle pre-commit checking of files.  This program
# records the last directory where commits will be taking place for
# use by the log_accum.pl script.  For new files, it forces the
# existence of a RCS "Id" keyword in the first ten lines of the file.
# For existing files, it checks version number in the "Id" line to
# prevent losing changes because an old version of a file was copied
# into the direcory.
#
# Possible future enhancements:
#
#    Check for cruft left by unresolved conflicts.  Search for
#    "^<<<<<<<$", "^-------$", and "^>>>>>>>$".
#
#    Look for a copyright and automagically update it to the
#    current year.  [[ bad idea!  -- woods ]]
#
#
# Contributed by David Hampton <[EMAIL PROTECTED]>
#
# Hacked on lots by Greg A. Woods <[EMAIL PROTECTED]>

#
#       Configurable options
#

# Constants (remember to protect strings from RCS keyword substitution)
#
$LAST_FILE     = "/tmp/#egcscvs.lastdir"; # must match name in log_accum.pl
$ENTRIES       = "CVS/Entries";

# Patterns to find $Log keywords in files
#
$LogString1 = "\\\$\\Log: .* \\\$";
$LogString2 = "\\\$\\Log\\\$";
$NoLog = "%s - contains an RCS \$Log keyword.  It must not!\n";

# pattern to match an RCS Id keyword line with an existing ID
#
$IDstring = "\"@\\(#\\)[^:]*:.*\\\$\Id: .*\\\$\"";
$NoId = "
%s - Does not contain a properly formatted line with the keyword \"Id:\".
        I.e. no lines match \"" . $IDstring . "\".
        Please see the template files for an example.\n";

# pattern to match an RCS Id keyword line for a new file (i.e. un-expanded)
#
$NewId = "\"@(#)[^:]*:.*\\$\Id\\$\"";

$NoName = "
%s - The ID line should contain only \"@(#)module/path:\$Name\$:\$\Id\$\"
        for a newly created file.\n";

$BadName = "
%s - The file name '%s' in the ID line does not match
        the actual filename.\n";

$BadVersion = "
%s - How dare you!!!  You replaced your copy of the file '%s',
        which was based upon version %s, with an %s version based
        upon %s.  Please move your '%s' out of the way, perform an
        update to get the current version, and them merge your changes
        into that file, then try the commit again.\n";

#
#       Subroutines
#

sub write_line {
    local($filename, $line) = @_;
    open(FILE, ">$filename") || die("Cannot open $filename, stopped");
    print(FILE $line, "\n");
    close(FILE);
}

sub check_version {
    local($i, $id, $rname, $version);
    local($filename, $cvsversion) = @_;

    open(FILE, "<$filename") || return(0);

    @all_lines = ();
    $idpos = -1;
    $newidpos = -1;
    for ($i = 0; <FILE>; $i++) {
        chop;
        push(@all_lines, $_);
        if ($_ =~ /$IDstring/) {
            $idpos = $i;
        }
        if ($_ =~ /$NewId/) {
            $newidpos = $i;
        }
    }

    if (grep(/$LogString1/, @all_lines) || grep(/$LogString2/, @all_lines)) {
        print STDERR sprintf($NoLog, $filename);
        return(1);
    }

    if ($debug != 0) {
        print STDERR sprintf("file = %s, version = %d.\n", $filename, 
$cvsversion{$filename});
    }

    if ($cvsversion{$filename} == 0) {
        if ($newidpos != -1 && $all_lines[$newidpos] !~ /$NewId/) {
            print STDERR sprintf($NoName, $filename);
            return(1);
        }
        return(0);
    }

    if ($idpos == -1) {
        print STDERR sprintf($NoId, $filename);
        return(1);
    }

    $line = $all_lines[$idpos];
    $pos = index($line, "Id: ");
    if ($debug != 0) {
        print STDERR sprintf("%d in '%s'.\n", $pos, $line);
    }
    ($id, $rname, $version) = split(' ', substr($line, $pos));
    if ($rname ne "$filename,v") {
        print STDERR sprintf($BadName, $filename, substr($rname, 0, length($rname)-2));
        return(1);
    }
    if ($cvsversion{$filename} < $version) {
        print STDERR sprintf($BadVersion, $filename, $filename, $cvsversion{$filename},
                             "newer", $version, $filename);
        return(1);
    }
    if ($cvsversion{$filename} > $version) {
        print STDERR sprintf($BadVersion, $filename, $filename, $cvsversion{$filename},
                             "older", $version, $filename);
        return(1);
    }
    return(0);
}

#
#       Main Body       
#

$id = getpgrp();                # You *must* use a shell that does setpgrp()!

# Check each file (except dot files) for an RCS "Id" keyword.
#
$check_id = 0;

# Record the directory for later use by the log_accumulate stript.
#
$record_directory = 0;

# parse command line arguments
#
while (@ARGV) {
    $arg = shift @ARGV;

    if ($arg eq '-d') {
        $debug = 1;
        print STDERR "Debug turned on...\n";
    } elsif ($arg eq '-c') {
        $check_id = 1;
    } elsif ($arg eq '-r') {
        $record_directory = 1;
    } else {
        push(@files, $arg);
    }
}

$directory = shift @files;

if ($debug != 0) {
    print STDERR "dir   - ", $directory, "\n";
    print STDERR "files - ", join(":", @files), "\n";
    print STDERR "id    - ", $id, "\n";
}

# Suck in the CVS/Entries file
#
open(ENTRIES, $ENTRIES) || die("Cannot open $ENTRIES.\n");
while (<ENTRIES>) {
    local($filename, $version) = split('/', substr($_, 1));
    $cvsversion{$filename} = $version;
}

# Now check each file name passed in, except for dot files.  Dot files
# are considered to be administrative files by this script.
#
if ($check_id != 0) {
    $failed = 0;
    foreach $arg (@files) {
        if (index($arg, ".") == 0) {
            next;
        }
        $failed += &check_version($arg);
    }
    if ($failed) {
        print STDERR "\n";
        exit(1);
    }
}

# Record this directory as the last one checked.  This will be used
# by the log_accumulate script to determine when it is processing
# the final directory of a multi-directory commit.
#
if ($record_directory != 0) {
    &write_line("$LAST_FILE.$id", $directory);
}
exit(0);
==================================================

log_accum:
==================================================
#!/usr/bin/perl
# -*-Perl-*-
#
# Perl filter to handle the log messages from the checkin of files in
# a directory.  This script will group the lists of files by log
# message, and mail a single consolidated log message at the end of
# the commit.
#
# This file assumes a pre-commit checking program that leaves the
# names of the first and last commit directories in a temporary file.
#
# Contributed by David Hampton <[EMAIL PROTECTED]>
#
# hacked greatly by Greg A. Woods <[EMAIL PROTECTED]>

# Usage: log_accum.pl [-d] [-s] [-M module] [[-m mailto] ...] [-f logfile]
#       -d              - turn on debugging
#       -m mailto       - send mail to "mailto" (multiple)
#       -M modulename   - set module name to "modulename"
#       -f logfile      - write commit messages to logfile too
#       -s              - *don't* run "cvs status -v" for each file

#
#       Configurable options
#

# Set this to something that takes "-s"
$MAILER        = "/bin/mail";

# Constants (don't change these!)
#
$STATE_NONE    = 0;
$STATE_CHANGED = 1;
$STATE_ADDED   = 2;
$STATE_REMOVED = 3;
$STATE_LOG     = 4;

$LAST_FILE     = "/tmp/#egcscvs.lastdir";

$CHANGED_FILE  = "/tmp/#egcscvs.files.changed";
$ADDED_FILE    = "/tmp/#egcscvs.files.added";
$REMOVED_FILE  = "/tmp/#egcscvs.files.removed";
$LOG_FILE      = "/tmp/#egcscvs.files.log";

$FILE_PREFIX   = "#egcscvs.files";

#
#       Subroutines
#

sub cleanup_tmpfiles {
    local($wd, @files);

    $wd = `pwd`;
    chdir("/tmp") || die("Can't chdir('/tmp')\n");
    opendir(DIR, ".");
    push(@files, grep(/^$FILE_PREFIX\..*\.$id$/, readdir(DIR)));
    closedir(DIR);
    foreach (@files) {
        unlink $_;
    }
    unlink $LAST_FILE . "." . $id;

    chdir($wd);
}

sub write_logfile {
    local($filename, @lines) = @_;

    open(FILE, ">$filename") || die("Cannot open log file $filename.\n");
    print FILE join("\n", @lines), "\n";
    close(FILE);
}

sub format_names {
    local($dir, @files) = @_;
    local(@lines);

    if ($dir =~ /^\.\//) {
        $dir = $';
    }
    if ($dir =~ /\/$/) {
        $dir = $`;
    }
    if ($dir eq "") {
        $dir = ".";
    }

    $format = "\t%-" . sprintf("%d", length($dir) > 15 ? length($dir) : 15) . "s%s ";

    $lines[0] = sprintf($format, $dir, ":");

    if ($debug) {
        print STDERR "format_names(): dir = ", $dir, "; files = ", join(":", @files), 
".\n";
    }
    foreach $file (@files) {
        if (length($lines[$#lines]) + length($file) > 65) {
            $lines[++$#lines] = sprintf($format, " ", " ");
        }
        $lines[$#lines] .= $file . " ";
    }

    @lines;
}

sub format_lists {
    local(@lines) = @_;
    local(@text, @files, $lastdir);

    if ($debug) {
        print STDERR "format_lists(): ", join(":", @lines), "\n";
    }
    @text = ();
    @files = ();
    $lastdir = shift @lines;    # first thing is always a directory
    if ($lastdir !~ /.*\/$/) {
        die("Damn, $lastdir doesn't look like a directory!\n");
    }
    foreach $line (@lines) {
        if ($line =~ /.*\/$/) {
            push(@text, &format_names($lastdir, @files));
            $lastdir = $line;
            @files = ();
        } else {
            push(@files, $line);
        }
    }
    push(@text, &format_names($lastdir, @files));

    @text;
}

sub accum_subject {
    local(@lines) = @_;
    local(@files, $lastdir);

    $lastdir = shift @lines;    # first thing is always a directory
    @files = ($lastdir);
    if ($lastdir !~ /.*\/$/) {
        die("Damn, $lastdir doesn't look like a directory!\n");
    }
    foreach $line (@lines) {
        if ($line =~ /.*\/$/) {
            $lastdir = $line;
            push(@files, $line);
        } else {
            push(@files, $lastdir . $line);
        }
    }

    @files;
}

sub compile_subject {
    local(@files) = @_;
    local($text, @a, @b, @c, $dir, $topdir);

    # find the highest common directory
    $dir = '-';
    do {
        $topdir = $dir;
        foreach $file (@files) {
            if ($file =~ /.*\/$/) {
                if ($dir eq '-') {
                    $dir = $file;
                } else {
                    if (index($dir,$file) == 0) {
                        $dir = $file;
                    } elsif (index($file,$dir) != 0) {
                        @a = split /\//,$file;
                        @b = split /\//,$dir;
                        @c = ();
                        CMP: while ($#a > 0 && $#b > 0) {
                            if ($a[0] eq $b[0]) {
                                push(@c, $a[0]);
                                shift @a;
                                shift @b;
                            } else {
                                last CMP;
                            }
                        }
                        $dir = join('/',@c) . '/';
                    }
                }
            }
        }
    } until $dir eq $topdir;

    # strip out directories and the common prefix topdir.
    chop $topdir;
    @c = ($modulename . '/' . $topdir);
    foreach $file (@files) {
        if (!($file =~ /.*\/$/)) {
            push(@c, substr($file, length($topdir)+1));
        }
    }

    # put it together and limit the length.
    $text = join(' ',@c);
    if (length($text) > 50) {
        $text = substr($text, 0, 46) . ' ...';
    }

    $text;
}

sub append_names_to_file {
    local($filename, $dir, @files) = @_;

    if (@files) {
        open(FILE, ">>$filename") || die("Cannot open file $filename.\n");
        print FILE $dir, "\n";
        print FILE join("\n", @files), "\n";
        close(FILE);
    }
}

sub read_line {
    local($line);
    local($filename) = @_;

    open(FILE, "<$filename") || die("Cannot open file $filename.\n");
    $line = <FILE>;
    close(FILE);
    chop($line);
    $line;
}

sub read_logfile {
    local(@text);
    local($filename, $leader) = @_;

    open(FILE, "<$filename");
    while (<FILE>) {
        chop;
        push(@text, $leader.$_);
    }
    close(FILE);
    @text;
}

sub build_header {
    local($header);
    local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
    $header = sprintf("CVSROOT:\t%s\nModule name:\t%s\n",
                      $cvsroot,
                      $modulename);
    if (defined($branch)) {
        $header .= sprintf("Branch: \t%s\n",
                      $branch);
    }
    $header .= sprintf("Changes by:\t%s@%s\t%02d/%02d/%02d %02d:%02d:%02d",
                      $login, $hostdomain,
                      $year%100, $mon+1, $mday,
                      $hour, $min, $sec);
}

sub mail_notification {
    local($name, $subject, @text) = @_;
    open(MAIL, "| $MAILER -s \"$subject\" $name");
    print MAIL join("\n", @text), "\n";
    close(MAIL);
}

sub write_commitlog {
    local($logfile, @text) = @_;

    open(FILE, ">>$logfile");
    print FILE join("\n", @text), "\n\n";
    close(FILE);
}

#
#       Main Body
#

# Initialize basic variables
#
$debug = 0;
$id = getpgrp();                # note, you *must* use a shell which does setpgrp()
$state = $STATE_NONE;
$login = $ENV{'USER'} || (getpwuid($<))[0] || "nobody";
chop($hostname = `hostname`);
if ($hostname !~ /\./) {
    chop($domainname = `domainname`);
    $hostdomain = $hostname . "." . $domainname;
} else {
    $hostdomain = $hostname;
}
$cvsroot = $ENV{'CVSROOT'};
$do_status = 1;
$modulename = "";

# parse command line arguments (file list is seen as one arg)
#
while (@ARGV) {
    $arg = shift @ARGV;

    if ($arg eq '-d') {
        $debug = 1;
        print STDERR "Debug turned on...\n";
    } elsif ($arg eq '-m') {
        $mailto = "$mailto " . shift @ARGV;
    } elsif ($arg eq '-M') {
        $modulename = shift @ARGV;
    } elsif ($arg eq '-s') {
        $do_status = 0;
    } elsif ($arg eq '-f') {
        ($commitlog) && die("Too many '-f' args\n");
        $commitlog = shift @ARGV;
    } else {
        ($donefiles) && die("Too many arguments!  Check usage.\n");
        $donefiles = 1;
        @files = split(/ /, $arg);
    }
}
($mailto) || die("No -m mail recipient specified\n");

# for now, the first "file" is the repository directory being committed,
# relative to the $CVSROOT location
#
@path = split('/', $files[0]);

# XXX there are some ugly assumptions in here about module names and
# XXX directories relative to the $CVSROOT location -- really should
# XXX read $CVSROOT/CVSROOT/modules, but that's not so easy to do, since
# XXX we have to parse it backwards.
#
if ($modulename eq "") {
    $modulename = $path[0];     # I.e. the module name == top-level dir
}
if ($commitlog ne "") {
    $commitlog = $cvsroot . "/" . $modulename . "/" . $commitlog unless ($commitlog =~ 
/^\//);
}
if ($#path == 0) {
    $dir = ".";
} else {
    $dir = join('/', @path[1..$#path]);
}
$dir = $dir . "/";

if ($debug) {
    print STDERR "module - ", $modulename, "\n";
    print STDERR "dir    - ", $dir, "\n";
    print STDERR "path   - ", join(":", @path), "\n";
    print STDERR "files  - ", join(":", @files), "\n";
    print STDERR "id     - ", $id, "\n";
}

# Check for a new directory first.  This appears with files set as follows:
#
#    files[0] - "path/name/newdir"
#    files[1] - "-"
#    files[2] - "New"
#    files[3] - "directory"
#
if ($files[2] =~ /New/ && $files[3] =~ /directory/) {
    local(@text);

    @text = ();
    push(@text, &build_header());
    push(@text, "");
    push(@text, $files[0]);
    push(@text, "");

    while (<STDIN>) {
        chop;                   # Drop the newline
        push(@text, $_);
    }

    &mail_notification($mailto, $files[0], @text);

    if ($commitlog) {
        &write_commitlog($commitlog, @text);
    }

    exit 0;
}

# Iterate over the body of the message collecting information.
#
while (<STDIN>) {
    chop;                       # Drop the newline

    if (/^Modified Files/) { $state = $STATE_CHANGED; next; }
    if (/^Added Files/)    { $state = $STATE_ADDED;   next; }
    if (/^Removed Files/)  { $state = $STATE_REMOVED; next; }
    if (/^Log Message/)    { $state = $STATE_LOG;     next; }
    if (/^Revision\/Branch/) { /^[^:]+:\s*(.*)/; $branch = $+; next; }

    s/^[ \t\n]+//;              # delete leading whitespace
    s/[ \t\n]+$//;              # delete trailing whitespace
    
    if ($state == $STATE_CHANGED) { push(@changed_files, split); }
    if ($state == $STATE_ADDED)   { push(@added_files,   split); }
    if ($state == $STATE_REMOVED) { push(@removed_files, split); }
    if ($state == $STATE_LOG)     { push(@log_lines,     $_); }
}

# Strip leading and trailing blank lines from the log message.  Also
# compress multiple blank lines in the body of the message down to a
# single blank line.
#
while ($#log_lines > -1) {
    last if ($log_lines[0] ne "");
    shift(@log_lines);
}
while ($#log_lines > -1) {
    last if ($log_lines[$#log_lines] ne "");
    pop(@log_lines);
}
for ($i = $#log_lines; $i > 0; $i--) {
    if (($log_lines[$i - 1] eq "") && ($log_lines[$i] eq "")) {
        splice(@log_lines, $i, 1);
    }
}

# Check for an import command.  This appears with files set as follows:
#
#    files[0] - "path/name"
#    files[1] - "-"
#    files[2] - "Imported"
#    files[3] - "sources"
#
if ($files[2] =~ /Imported/ && $files[3] =~ /sources/) {
    local(@text);

    @text = ();
    push(@text, &build_header());
    push(@text, "");

    push(@text, "Log message:");
    while ($#log_lines > -1) {
        push (@text, "    " . $log_lines[0]);
        shift(@log_lines);
    }

    &mail_notification($mailto, "Import $file[0]", @text);

    if ($commitlog) {
        &write_commitlog($commitlog, @text);
    }

    exit 0;
}

if ($debug) {
    print STDERR "Searching for log file index...";
}
# Find an index to a log file that matches this log message
#
for ($i = 0; ; $i++) {
    local(@text);

    last if (! -e "$LOG_FILE.$i.$id"); # the next available one
    @text = &read_logfile("$LOG_FILE.$i.$id", "");
    last if ($#text == -1);     # nothing in this file, use it
    last if (join(" ", @log_lines) eq join(" ", @text)); # it's the same log message 
as another
}
if ($debug) {
    print STDERR " found log file at $i.$id, now writing tmp files.\n";
}

# Spit out the information gathered in this pass.
#
&append_names_to_file("$CHANGED_FILE.$i.$id", $dir, @changed_files);
&append_names_to_file("$ADDED_FILE.$i.$id",   $dir, @added_files);
&append_names_to_file("$REMOVED_FILE.$i.$id", $dir, @removed_files);
&write_logfile("$LOG_FILE.$i.$id", @log_lines);

# Check whether this is the last directory.  If not, quit.
#
if ($debug) {
    print STDERR "Checking current dir against last dir.\n";
}
$_ = &read_line("$LAST_FILE.$id");

if ($_ ne $cvsroot . "/" . $files[0]) {
    if ($debug) {
        print STDERR sprintf("Current directory %s is not last directory %s.\n", 
$cvsroot . "/" .$files[0], $_);
    }
    exit 0;
}
if ($debug) {
    print STDERR sprintf("Current directory %s is last directory %s -- all commits 
done.\n", $files[0], $_);
}

#
#       End Of Commits!
#

# This is it.  The commits are all finished.  Lump everything together
# into a single message, fire a copy off to the mailing list, and drop
# it on the end of the Changes file.
#

#
# Produce the final compilation of the log messages
#
@text = ();
@status_txt = ();
@subject_files = ();
push(@text, &build_header());
push(@text, "");

for ($i = 0; ; $i++) {
    last if (! -e "$LOG_FILE.$i.$id"); # we're done them all!
    @lines = &read_logfile("$CHANGED_FILE.$i.$id", "");
    if ($#lines >= 0) {
        push(@text, "Modified files:");
        push(@text, &format_lists(@lines));
        push(@subject_files, &accum_subject(@lines));
    }
    @lines = &read_logfile("$ADDED_FILE.$i.$id", "");
    if ($#lines >= 0) {
        push(@text, "Added files:");
        push(@text, &format_lists(@lines));
        push(@subject_files, &accum_subject(@lines));
    }
    @lines = &read_logfile("$REMOVED_FILE.$i.$id", "");
    if ($#lines >= 0) {
        push(@text, "Removed files:");
        push(@text, &format_lists(@lines));
        push(@subject_files, &accum_subject(@lines));
    }
    if ($#text >= 0) {
        push(@text, "");
    }
    @lines = &read_logfile("$LOG_FILE.$i.$id", "\t");
    if ($#lines >= 0) {
        push(@text, "Log message:");
        push(@text, @lines);
        push(@text, "");
    }
    if ($do_status) {
        local(@changed_files);

        @changed_files = ();
        push(@changed_files, &read_logfile("$CHANGED_FILE.$i.$id", ""));
        push(@changed_files, &read_logfile("$ADDED_FILE.$i.$id", ""));
        push(@changed_files, &read_logfile("$REMOVED_FILE.$i.$id", ""));

        if ($debug) {
            print STDERR "main: pre-sort changed_files = ", join(":", @changed_files), 
".\n";
        }
        sort(@changed_files);
        if ($debug) {
            print STDERR "main: post-sort changed_files = ", join(":", 
@changed_files), ".\n";
        }

        foreach $dofile (@changed_files) {
            if ($dofile =~ /\/$/) {
                next;           # ignore the silly "dir" entries
            }
            if ($debug) {
                print STDERR "main(): doing status on $dofile\n";
            }
            open(STATUS, "-|") || exec 'cvs', '-n', 'status', '-Qqv', $dofile;
            while (<STATUS>) {
                chop;
                push(@status_txt, $_);
            }
        }
    }
}

$subject_txt = &compile_subject(@subject_files);

# Write to the commitlog file
#
if ($commitlog) {
    &write_commitlog($commitlog, @text);
}

if ($#status_txt >= 0) {
    push(@text, @status_txt);
}

# Mailout the notification.
#
&mail_notification($mailto, $subject_txt, @text);

# cleanup
#
if (! $debug) {
    &cleanup_tmpfiles();
}

exit 0;
==================================================

Reply via email to