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.
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.
The CVS commands cvs watch and cvs notify are used, usually in combination, by project participants to set up the notification mechanism.
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:
remove the read-only attribute for the file
send out a notification that the participant has commenced editing it.
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.
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.
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.
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.
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.
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:
it's likely to take up less screen real-estate
no amount of tweaking of the mail client's auto-check frequency, if available (which will log in, check for, and pull emails from the mail server) will match the immediacy of IM-style message push
in extreme cases, the higher the auto-check frequency, the higher effect on overall system performance
depending on the configuration, an incoming Jabber message can be made to pop up, with greater effect
a Jabber user is more likely to have a Jabber client running permanently than an email client
it's more fun!
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.
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.
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.
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()| [1] | You can find out more about CVS at http://www.cvshome.org. |
| [2] | |
| [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." |