CVS notification

CVS—the Concurrent Versions System [1] —allows you to comfortably create and manage versions of the sources of your project. The most common use for CVS is to create and manage versions of program source code, but it can be readily used for any text files. For example, this book was written using the DocBook markup language [2] and CVS was used to take versions of the manuscript at certain points in the writing's progress. The versions so taken could be compared, and old versions could be retrieved.

That's the Versions System part of the name. The Concurrent part means that this facility is given an extra dimension in the form of group collaboration. With CVS, more than one person can share work on a project, and the various chunks of work carried out by each participant are coordinated—automatically, to a large extent—by CVS. Multiple changes by different people to the same file can be merged by CVS; any unresolvable conflicts (which may for example arise when more than one person changes exactly the same line of source code) are flagged and must be resolved by the participants involved.

The general idea is that you can create a project containing files and directories and have it stored centrally in a CVS repository. Depending on what sort of access is granted to this repository, other project participants can pull down a copy of the project—those files and directories—and work on it independently. In this way, each participant's work is isolated (in time and space) from the others. When the work is done, the work can be sent back to the repository and the changes will be merged into the central copy. After that, those merged changes are available to the rest of the participants.

CVS watches and notification

While CVS automatically handles most of the tedious merging process that comes about when more than one person works on a project, it also offers a facility which allows you to set a "watch" on one or more files in the project, and be alerted when someone else starts to work on those watched files. This is useful if you wish to preempt any automatic merging process by contacting the other participant and coordinating your editing efforts with them.

There are two CVS commands involved in setting up watches and notifications. There are also a couple of CVS administrative files that determine how the notifications are carried out. Let's look at these commands and files in turn.

CVS commands

The CVS commands cvs watch and cvs notify are used, usually in combination, by project participants to set up the notification mechanism.

cvs watch on|off

Assuming we have a CVS-controlled project called 'proj1', and we're currently inside our local checked-out copy of the project's files, we first use cvs watch to tell CVS to watch a file ("turn a watch on") that we're interested in, which is file4 in this example:

yak:~/projects/proj1$ cvs watch on file4

This causes CVS to mark file4 as "watched", which means that any time a project participant checks out the file from the central repository, the checked-out working copy is created with read-only attributes. This means that the participant is (initially) prevented from saving any changes to that working copy. It is, in effect, a reminder to that participant to use the CVS command cvs edit, specifying file4, before commencing the edit session. Using cvs edit will cause CVS to:

  1. remove the read-only attribute for the file

  2. send out a notification that the participant has commenced editing it.

cvs watch add|remove

While running cvs watch on against a file will set a marker causing the file to be replicated with the read-only attribute when checked out (which has the effect of "suggesting" to the participant editing the file that he use the cvs edit command to signal that he's to start editing), the actual determination of the notification recipients is set up using the cvs watch add command.

Running the command:

yak:~/projects/proj1$ cvs watch add file4

will arrange for the CVS notification to be sent to us when someone else signals their intention (via cvs edit) to edit file4.

CVS administrative files

Kept in the central CVS repository are a number of administrative files that are used to control how CVS works. Two of these files, notify and users, are used to manage the watch-based notification process.

notify

The standard notify file contains a line like this:

ALL mail %s -s "CVS notification" 

The 'ALL' causes the formula described here to be used for any notification requirements (an alternative to ALL is a regular expression to match the directory name in which the edit causing the notification is being carried out).

The rest of the line is the formula to use to send the notification. It is a simple invocation of the mail command, specifying a subject line (-s "CVS notification"). The %s is a placeholder that CVS replaces with the address of the notification's intended recipient. The actual notification text, generated by CVS, is piped into the mail command via STDIN.

users

The users file contains a list of notification recipient addresses:

dj:dj.adams@pobox.com
piers:pxharding@ompa.net
robert:robert@shiels.com
...

This is a mapping from the user IDs (dj, piers, and robert) of the CVS participants, local to the host where the CVS repository is stored, to the actual addresses (dj.adams@pobox.com, pxharding@ompa.net, and robert@shiels.com) that are used to replace the %s in the formula described in the notify file.

The Notification

If the contents of the notify and users files have been set up correctly, a typical notification, set up by dj using the cvs watch on file4 and cvs watch add file4 commands, and triggered by piers using the cvs edit file4 command, will be received in dj's inbox looking like the one shown in Figure 7-1.

Figure 7-1. A typical email CVS notification

Date: Fri, 8 Jun 2001 13:10:55 +0100
From: piers@ompa.net
To: dj.adams@pobox.com
Subject: CVS notification
 
testproject file4
---
Triggered edit watch on /usr/local/cvsroot/testproject
By piers

CVS notifications via Jabber

While email-based notifications are useful, we can add value to this process by using a more immediate (and penetrating) form of communication: Jabber. While mail clients can be configured to check for mail automatically on a regular basis, using an IM-style client has a number of immediately obvious advantages:

The design of CVS's notification mechanism is simple and abstract enough for us to put an alternative notification system in place. If we substitute the formula in the notify configuration file with something that will call a Jabber script, we might end up with something like Example 7-1.

Example 7-1. A Jabber notification formula in the notify file

ALL python cvsmsg %s   

Like the previous formula, it will be invoked by CVS to send the notification, and the %s will be substituted by the recipient's address determined from the users file. In this case, the Python script cvsmsg is called.

But now that we're sending a notification via Jabber, we need a Jabber address - a JID - instead of an email address. No problem; just edit the users file to reflect the new addresses. Example 7-2 shows what the users file might contain if we were to use JIDs instead of email addresses.

Example 7-2. Matching users to JIDs in the notify file

dj:dj@gnu.pipetree.com
piers:piers@jabber.org
robert:shiels@jabber.org

As Jabber user JIDs in their most basic form (i.e., without a resource suffix) resemble email IDs, there doesn't appear to be that much difference. In any case, CVS doesn't really care, and simply passes the portion following the colon separator to the formula in the notify file.

The cvsmsg notification script

Let's now have a look at the cvsmsg script. It has to send a notification message, which it receives on STDIN, to a JID, which it receives as an argument passed to the script.

import Jabber, XMLStream
import sys

We're going to use the JabberPy Python libraries for Jabber [3] so we import them here. As is the way with many implementations for Jabber, we find there is a module to handle the XML stream-based connection with the Jabber server (the XMLStream module) and a module to handle the various Jabber connection mechanisms such as authentication, the sending of Jabber elements (<message/>, <presence/>, and <iq/>), and the dispatching of Jabber elements received (the Jabber module). We also import the sys module for reading from STDIN.

As the usage of the script will be fairly static, we can get away here with hardcoding a few parameters:

Server   = 'gnu.pipetree.com'
Username = 'cvsmsg'
Password = 'secret'
Resource = 'cvsmsg'

Specified here are the connection and authentication details for the cvsmsg script itself. If it's to send a message via Jabber, it must itself connect to Jabber. The Server variable specifies which Jabber server to connect to, and the Username, Password, and Resource variables contain the rest of the information for the script's own JID (cvsmsg@gnu.pipetree.com/cvsmsg) and password.

cvsuser  = sys.argv[1]
message  = ''

for line in sys.stdin.readlines(): message = message + line

The sys.argv[1] refers to the notification recipient's JID, which will be specified by the CVS notification mechanism, as it is substituted for the %s in the notify file's formula. This is saved in the cvsuser variable. We then build up the content of our message body we're going to send via Jabber by reading what's available on STDIN. Typically this will look like what we saw in the email message body in Figure 7-1:

testproject file4
---
Triggered edit watch on /usr/local/cvsroot/testproject
By piers

con = Jabber.Connection(host=Server)

Although it is the XMLStream module that handles the connection to the Jabber server, we are shielded from the details of this by the Jabber module that wraps and uses XMLStream. Hence the call to instantiate a new Jabber.Connection object into con, to lay the way for our connection to the host specified in our Server variable: gnu.pipetree.com. No port is explicitly specified here, and the method assumes a default port of 5222, on which the c2s service listens.

The instantiation causes a number of parameters and variables to be initialized, and an XMLStream.Client object is instantiated; various parameters are passed through from the Jabber.Connection object (for example for logging and debugging purposes) and an XML parser object is instantiated. This will be used to parse fragments of XML that come in over the XML stream.

try:
    con.connect()
except XMLStream.error, e:
    print "Couldn't connect: %s" % e 
    sys.exit(0)

A connection is attempted with the connect() method of the connection object in con. This is serviced by the XMLStream.Client object and a stream header, as described in the section called XML Streams in Chapter 5, is sent to gnu.pipetree.com:5222 in an a attempt to establish a client connection.

XMLStream will raise an error if the connection cannot be established, we trap this, after a fashion, with the try: ... except shown here.

con.auth(Username,Password,Resource)

Once we're connected—our client has successfully exchanged XML stream headers with the server—we need to authenticate. The auth method of the Jabber.Connection object provides us with a simple way of carrying out the authentication negotiation, qualified with the jabber:iq:auth namespace and described in detail in the section called User Authentication in Chapter 6. Although we supply our password here in the script in plaintext ('secret'), the auth method will use the IQ-get (<iq type='get'...>) to retrieve a list of authentication methods supported by the server. It will try and use the most secure, "gracefully degrading" to the least, until it finds one that is supported. [4]

Note the presence of the resource in the call. This is required for a successful client authentication regardless of the authentication method. Sending an IQ-set (<iq type='set'...>) in the jabber:iq:auth namespace without specifying a value in a<resource/> tag results in a "Not Acceptable" error 406; see Table 5-3 for a list of standard error codes and texts.

We're connected, and authenticated. The world is now our lobster, as an old friend used to say. We're not necessarily expecting to receive anything at this stage, and even if we did, we wouldn't really want to do anything with what we received anyway. At least at this stage. So we don't bother setting up any mechanism for handling elements that might appear on the stream.

con.send(Jabber.Message(cvsuser, message, subject="CVS Watch Alarm"))

All we do is send the notification message, in the message variable, to the user that was specified as an argument when the script was invoked, stored in the cvsuser variable. There are actually two calls here. The innermost—Jabber.Message()—creates a simple message element that looks like this:

<message to='[value in cvsuser variable]'>
  <subject>CVS Watch Alarm</subject>
  <body>[value in message variable]</body>
</message>

It takes two positional (and required) parameters; any other information to be passed (such as the subject in this example) must be supplied as key=value pairs. The outermost—con.send()— sends whatever it is given over the XML stream that the Jabber.Connection object con represents. In the case of the Jabber.Message call, this is the string representation of the object so created—i.e. our <message/> element.

Once the notification message has been sent, the script's work is done. We can therefore disconnect from the server before exiting the script.

con.disconnect()

Calling the disconnect() method of the Jabber.Connection sends an unavailable presence element to the server on behalf of the user that is connected:

<presence type='unavailable'/>

This is sent regardless of whether a <presence/> element was sent during the conversation, but does no harm if one wasn't.

After sending the unavailable presence information, theXML stream is closed by sending the stream's closing tag:

</stream:stream>

This signifes to the server that the client wishes to end the conversation. Finally, the socket is closed.

The script in its entirety

Here's the script in its entirety.

import Jabber, XMLStream
import sys

Server   = 'gnu.pipetree.com'
Username = 'cvsmsg'
Password = 'secret'
Resource = 'cvsmsg'

cvsuser  = sys.argv[1]
message  = ''

for line in sys.stdin.readlines(): message = message + line

con = Jabber.Connection(host=Server)

try:
    con.connect()
except XMLStream.error, e:
    print "Couldn't connect: %s" % e 
    sys.exit(0)

con.auth(Username,Password,Resource)
con.send(Jabber.Message(cvsuser, message, subject="CVS Watch Alarm"))
con.disconnect()

Notes

[1]

You can find out more about CVS at http://www.cvshome.org.

[2]

http://www.docbook.org.

[3]

http://sourceforge.net/projects/jabberpy/ available at http://sourceforge.net/projects/jabberpy/

[4]

This degradation typically will follow the pattern "zero-knowledge supported? No. Ok. How about digest? No? Ok. Then how about plaintext? No? Oh dear. This isn't going to work."