CVS: Using loginfo to send mail with diffs on Win32/CVSNT

Like presumably so many other CVS administrators, I was faced with the task of making my CVS server send mail every time somebody commits something. The additional spice in the soup was the platform, Win32. Just sending the CVS log message and the files modified is trivial, but it all gets a bit more complicated when you want to include a unidiff of the changes.

In this article, I will present some of the problems involved and a Perl-based solution. If you wish to understand the specifics, some basic knowledge of Perl is necessary. If you just want to get the source, it is available for download - but please note that the code is not luxuriously documented! Much of your ability to understand the implementation (and modify its behaviour) probably depends on reading and understanding this text. You will also probably at least need the Installation Instructions.

The solution I present here is designed to work on Win32 servers with ActiveState ActivePerl and CVSNT. It is probably fairly easy to make this use another Perl distribution or the standard-class CVS server, but you don't want to try to port this to another OS. If you're running on unix, there are solutions available for you already.

A quick revision: How does loginfo work?

Each time a commit is made, CVS searches through an administrative file called loginfo. It is a text file whose each line contains a regexp and a command line, separated by a space. The lines are searched in order, and the regexp matching the directory being handled is executed. The special regexp "ALL" is always executed, and the command line with regexp "DEFAULT" when no other expression matches.

The command line is executed, and the log message (the stuff that usually pops up in a editor when committing) is piped to the executing process' stdin. Information to the logging task can also be transmitted through command-line parameters. There are two different approaches to this, actually.

First, CVS has several variables which are prefixed by $ - a list of them is available. Second, a special format string can be used as a parameter. For example, a format string of %{sVv} produces a string that contains file names (s), old revisions (V) and new revisions (v) for the changes. Missing revisions are marked as NONE. Information about each file is separated by commas, file specifications are separated by spaces. The first file specification is always the directory being accessed relative to the cvsroot. So, for example the string myproject/code a.c,1.4,1.5 b.c,NONE,1.1 c.c,1.8,NONE means that in directory myproject/code, the file a.c is updated from revision 1.4 to 1.5, file b.c is created (its old revision being NONE) and file c.c is deleted (its new revision is NONE).

So what are the problems?

It's fairly trivial to create a program that reads from stdin, parses some parameters and outputs the stuff into a log file or even sends the mail. However, it gets way more complicated when you want the diffs in. The problem starts with the fact that you don't get the diff information piped from cvs (like you get the log message) or through parameters (like you get the file modification list). Therefore, you will have to run cvs diff to get the diff and then attach it to your mail.

Running diff isn't problematic, but the cvs locking scheme is. If you run diff directly from the task you spawned off using loginfo, it will note that "oops, the repository is locked because of a commit, I'd better wait". Yes, you guessed right - it's the commit process that we're supposed to mail about right now! So, we end up locking the cvs task forever, since the commit won't certainly finish until the loginfo process is done, and it won't be done until the commit lock preventing diffing has vanished...

So let's just fork?

For those of you who don't know what fork means, it's a unixish concept of creating a nigh-exact duplicate of the current running process, which can then run totally separately from its parent. So effectively, the execution of the code forks, hence the name.

This is where the multitasking model of unix-based systems is cool. Most scripts running on unix simply fork, so that the original task can exit and let the child do the job. That's nice, but since the multitasking model of Windows is based on threads rather than forks, we're out of luck.

ActiveState Perl emulates fork, but as of version 5.6.1, the emulation is based on threaded implementation of the interpreter. Essentially, that means that the Perl script is run just fine, but in the same process context as the original parent. The process that contains both the threads terminates when both of the forks have called exit(). Thus, the parent doesn't exit and the commit lock is never released. So, forking doesn't help us here - at least until ActiveState implements a split-process fork() model.

But the problem can be solved, it's just ugly...

So essentially what you need to do is get the stuff running in two processes instead of two threads. Doing this is relatively easy, but doesn't look good. In this example, we do it by having two Perl scripts: the other is which is called from loginfo, and the other is (where be stands for backend), which is in turn called by calls by running the command interpreter, with a command line of "start perl somepathhere\ parameters". This makes Windows start a new process, a Perl interpreter, which then runs the backend. Since the backend is a separate process, it's free to do whatever it wishes with cvs - if it can't make diffs because of a lock in the repository, it will wait. The important thing here is that waiting will not permanently block the execution of anything else - exited already, and thus the cvs commit process continues. contains a "sleep 5" to wait for five seconds before doing anything. This is not strictly necessary, but it's an optimization. If we don't add it, it almost always bumps to a lock when trying to do the diff, since the cvs commit hasn't finished yet. It would work even without the delay, but then cvs would end up in the "Waiting for somebody's lock on ..." loop, and then it takes at least 15 seconds before it tries again.

There is one more issue here. When spawns the new backend task, the content waiting in stdin is not conveyed (if we did a fork(), it would - but this is not possible as already stated). Thus, writes out the data in stdin to a temporary files and passes its name as a parameter to, which then in turn reads the file, processes it and finally deletes the temporary file. Ugly, but it works.

Call syntax and installation

I warmly recommend that you put the perl files into your CVSROOT directory and add their names into the checkoutlist file. Also, you should have perl.exe in your path and .pl files associated to it. You can do otherwise, but these instructions are based on these presumptions. Improvise if necessary. takes three parameters: the CVS loginfo string with the format specifier %{sVv}, the name of the user committing the changes (whatever you want to show up in the mail) and the address the message should be sent to. takes the name of the file containing the CVS log information (see above) followed by all the params takes. Examine to understand this better.

Basically, you can just add the following line to your loginfo file:

DEFAULT perl d:\cvsnt\repository\cvsroot\ %{sVv} $USER

But before you commit, edit, particularly the end of it. The version downloadable from this page uses a Perl module called NTsendmail. I recommend you use it. That way you can only change your mail server address and the from name near the end of the Perl script and off you go. If you want to use another mailing system, replace the NTsendmail specific part of with your own implementation.

Downloading the source code

Now that you have hopefully read through the details and after you acknowledge my total lack of responsibility for any possible damage that may be caused by the use of these scripts, feel free to download the source code: and

Contacting the author

I welcome feedback and suggestions on improving this. Especially corrections for errors are gratefully accepted. You can contact me at

Jouni Heikniemi