Skip to content.

plope

Personal tools
You are here: Home » Members » chrism's Home » A CVS Primer for Zope Developers
 
 

A CVS Primer for Zope Developers

"For now, we're going to assume that you've decided to write a product, and you know how to do so. The issue then becomes how best to allow multiple people to work on your Zope application, which will consist of a number of products, without too much pain. One answer is CVS."

About Zope Products

Zope's source code is divided roughly into "core code", which is code that is part of Zope proper and which all other Zope-related code can depend on, and "Products, which are add-ons which provide extended functionality.

Zope itself ships with a few products. These live in the Zope software home under "Products" ($ZOPE_HOME/lib/python/Products).

Other Zope products must be downloaded and installed. These are typically installed into a Zope "instance home", which is a directory which contains a coniguration file, some application-specific products, some application-specific "external methods", and typically a ZODB "database" file.

A Zope instance home is created with the mkzopeinstance.py script that comes as a part of Zope. When this script is run, it creates an instance home directory. For the purposes of this discussion, the most important thing to know about this directory is that it contains a Products directory. By default, there is nothing in the Products directory, it is empty. But by placing Zope products into this Products directory, you can give your Zope instance extended behavior.

Creating a product is the most natural way to extend Zope. Products are written in Python. Plone, a popular Zope application is a set of Zope products. You can create your own Zope product as well. See the Zope Developer's Guide Products chapter for more information about how to do this.

For now, we're going to assume that you've decided to write a product, and you know how to do so. The issue then becomes how best to allow multiple people to work on your Zope application, which will consist of a number of products, without too much pain. One answer is CVS.

About CVS

CVS is a version control system. It allows multiple people to simultaneously change the same set of files without inadvertently stomping on each other's changes.

A number of front-ends exist for interacting with CVS. The most common set of tools are command-line tools. I will use those in this description, concentrating mainly on the set of tools provided by the CVS implementation shipped with Linux and/or Cygwin, although you should feel free to branch out and use other frontends and CVS implementations as you see fit once you've got the general ideas down.

There is a reasonable general introduction to CVS here. I won't be explaining the more obscure CVS commands here (like creating a new CVS repository), but for that you can consult that book.

For the purposes of this article, I will assume that the reason you're using CVS is because you'd like to do collaborative development with people working on disparate machines. The easiest and safest way to do this is to use CVS over SSH (Secure SHell), so that's what I'll concentrate on describing in examples.

Creating A CVS Module (Putting your Product Under Version Control)

Let's say you've got a CVS "repository" already set up on a computer which allows remote ssh acess, and on which you have an account. Let's call that machine cvs.example.com and let's say the CVS repository you wish to check your Product in to is on the remote computer's filesystem at /cvs. First of all, before you do anything, edit your $HOME/.bash_profile (or an equivalent) and add the following line:

    export CVS_RSH=ssh

This will take care of the next login shell, but to effect the current shell, also do this interactively at a shell prompt. This tells CVS that you want to use SSH as its transport to remote repositories (this should really be the default, but it's not). After doing that, if you wished to put your Zope product files under version control in this repository, you could do the following on a UNIX command line:

    athlon:~$ cd ExampleProduct
    athlon:~/ExampleProduct$ cvs -d :ext:cvs.example.com:/cvs import ExampleProduct mycompany start

Replace "mycompany" with a abbreviated version of your company's name (don't ask, it's not really all that important). You will be asked for a password or passphrase by ssh. Type it in and you will be greeted with an invocation of the vi command on your system encouraging you to provide a comment about what you're doing. Enter a descriptive comment on the first few lines, such as "importing my product for the first time," and save the buffer (:wq):

    Importing my product for the first time.

    CVS: ----------------------------------------------------------------------
    CVS: Enter Log.  Lines beginning with `CVS:' are removed automatically
    CVS:
    CVS: ----------------------------------------------------------------------

After you save the buffer, CVS will spew some messages to the terminal indicating that the files in your ExampleProduct directory have been imported into the ExampleProduct module:

    N ExampleProduct/somepython.py
    N ExampleProduct/__init__.py

    No conflicts created by this import

This means that other people can start working with your code via CVS. You have created what is known in CVS parlance as a "module", which is simply a fancy name for a set of files that can be addressed via version control by a given name. In this case the name is "ExampleProduct".

However, before you can start working with the code under version control, you need to take one additional step. Even though you've imported the files into CVS, your existing local ExampleProduct directory has remained unaware of version control. You need to get a new copy of your own product files out of CVS for this to happen. The way I typically do this is to move my original directory "aside" and create a new directory named the same thing by checking the module I just imported out from CVS:

    athlon:~/ExampleProduct$ cd ..
    athlon:~$ mv ExampleProduct ExampleProduct_aside
    athlon:~/ExampleProduct$ cvs -d :ext:cvs.example.com:/cvs co ExampleProduct
    athlon:~$ cvs -d :ext:cvs.example.com:/cvs co ExampleProduct
    Enter passphrase for key '/home/chrism/.ssh/id_dsa':
    cvs server: Updating ExampleProduct
    U ExampleProduct/__init__.py
    U ExampleProduct/somepython.py

My CVS client has now created an ExampleProduct directory on my local filesystem for me which contains some extra "CVS" directories that keep track of the CVS administrative details:

    athlon:~$ cd ExampleProduct
    athlon:~/ExampleProduct$ ls
    CVS  __init__.py  somepython.py
    athlon:~/ExampleProduct$ ls -alR
    .:
    total 13
    drwxr-xr-x    3 chrism   chrism        136 Dec 31 15:38 .
    drwxr-xr-x   60 chrism   chrism       3920 Dec 31 15:38 ..
    drwxr-xr-x    2 chrism   chrism        128 Dec 31 15:38 CVS
    -rw-r--r--    1 chrism   chrism          8 Dec 31  2003 __init__.py
    -rw-r--r--    1 chrism   chrism         20 Dec 31  2003 somepython.py

    ./CVS:
    total 13
    drwxr-xr-x    2 chrism   chrism        128 Dec 31 15:38 .
    drwxr-xr-x    3 chrism   chrism        136 Dec 31 15:38 ..
    -rw-r--r--    1 chrism   chrism        100 Dec 31 15:38 Entries
    -rw-r--r--    1 chrism   chrism         15 Dec 31 15:38 Repository
    -rw-r--r--    1 chrism   chrism         42 Dec 31 15:38 Root

That's all there is to checking out a module from CVS.

Instructing Others On How To Check Your Module Out

Others can check out your newly-created module by issuing the same command as you did after you moved your ExampleProduct directory aside:

    another:~$ cvs -d :ext:cvs.example.com:/cvs co ExampleProduct
    Enter passphrase for key '/home/fred/.ssh/id_dsa':
    cvs server: Updating ExampleProduct
    U ExampleProduct/__init__.py
    U ExampleProduct/somepython.py

If they're having problems, make sure to remind them to add the export CVS_RSH=ssh line to their .bash_profile or equivalent.

Note that once someone checks a module out of CVS, the resulting directory can be moved around on the local disk without worrying about "breaking" future CVS actions. The location of the module directory on the local disk doesn't make any difference to CVS. It will continue to work no matter where you move the module checkout directory to. You can even typically copy the module directory to another machine and, as long as you've got CVS configured on that machine, you should be able to perform CVS actions against the checked out module on the target host.

Seeing What Has Changed Locally, Adding Files, and Checking Stuff in to CVS

As you make changes to yor product, every so often you'll need to check these changes in to the CVS repository so the other developers on your team can get your most recent version of the code. CVS manages things on a file-by-file basis, so if you create a new file (or directory) in a directory that you've created via a CVS checkout, you need to notify CVS about the existence of the file so it can be added to the module.

Let's add a file. Add somemorepython.py file to the ExampleProduct directory (I've created it via vim). Its contents dont matter for now.:

    athlon:~/ExampleProduct$ ls
    CVS  __init__.py  somemorepython.py  somepython.py

At this point, even though the file is clearly in a directory that is under CVS control. CVS knows nothing at all about the somemorepython.py file. We can verify this by using the handy cvs -nq up command, which, though arcane, can be considered the command you should use when you want to see the differences on a file-level basis between your local copy of the module and the repository's idea of what the module consists of:

    athlon:~/ExampleProduct$ cvs -nq up
    Enter passphrase for key '/home/chrism/.ssh/id_dsa':
    ? somemorepython.py

The question mark next to the filename indicates that CVS is clueless about the somemorepython.py file. To make CVS recognize the file, you need to use the cvs add command:

    athlon:~/ExampleProduct$ cvs add somemorepython.py
    Enter passphrase for key '/home/chrism/.ssh/id_dsa':
    cvs server: scheduling file `somemorepython.py' for addition
    cvs server: use 'cvs commit' to add this file permanently
    athlon:~/ExampleProduct$ cvs -nq up
    A somemorepython.py

Note that at this point, the file isn't actually added into the CVS repository, it's only scheduled for addition (that's what the "A" means). To commit the addition, invoke the "cvs commit" command in the module directory. This command commits any known local changes to the remote CVS repository:

    athlon:~/ExampleProduct$ cvs commit
    Enter passphrase for key '/home/chrism/.ssh/id_dsa':

    Adding the somemorepython.py file.
    CVS: ----------------------------------------------------------------------
    CVS: Enter Log.  Lines beginning with `CVS:' are removed automatically
    CVS:
    CVS: Committing in .
    CVS:
    CVS: Added Files:
    CVS:    somemorepython.py
    CVS: ----------------------------------------------------------------------

    cvs commit: Examining .
    RCS file: /home/chrism/cvs/ExampleProduct/somemorepython.py,v
    done
    Checking in somemorepython.py;
    /home/chrism/cvs/ExampleProduct/somemorepython.py,v  <--  somemorepython.py
    initial revision: 1.1
    done

At this point, your changes have been committed to the repository, and other people can see the change you've just made by running an "update". You can verify that your on-disk checkout is completely up to date with respect to the repository by rerunning cvs -nq up. It should produce no output, indicating that the repository and your local copy are in complete sync.

Updating A CVS Checkout

To obtain changes made to a module by other developers in a local checkout, you can run the cvs up command. I typically run this with the -dP options, meaning "get new directories, and prune empty directories", which is really the most sane thing to do in most cases:

    athlon:~/ExampleProduct$ cvs up -dP
    Enter passphrase for key '/home/chrism/.ssh/id_dsa':
    cvs server: Updating .
    U anotherfile.py

The "U" next to the anotherfile.py indicates that this file was updated in my local checkout. This means that it was either freshly created (as a result of someone else adding the file to the repository) or it was updated with a set of changes that someone else made since the last time I updated my checkout.

Diffing

The results of cvs -nq up give you some idea about what's different from the repository version of the files and your local files:

    athlon:~/ExampleProduct$ cvs -nq up
    Enter passphrase for key '/home/chrism/.ssh/id_dsa':
    M somepython.py

In this case, it's telling me that there my local copy of the somepython.py file has uncommitted changes (its been Modified, that's what the M stands for). I can find out what the differences in my local files are are before commmiting by using the cvs diff command:

    athlon:~/ExampleProduct$ cvs diff somepython.py
    Index: somepython.py
    ===================================================================
    RCS file: /home/chrism/cvs/ExampleProduct/somepython.py,v
    retrieving revision 1.2
    diff -r1.2 somepython.py
    5a6,8
    >
    > def baz():
    >    pass

This means that I've locally added the "baz" function to this file, and the three lines I've added are what show up in the result of the cvs diff command.

Dealing With Conflicts

When both you and a developer change the same file in different ways and attempt to commit or update the result to/from the CVS repository, you will likely receive a message from CVS indicating that there was a conflict between what existed in your local copy and what exists in the repository. CVS isn't omniscient, and it can't determine what changes to actually apply when two people attempt to make conflicting changes to the same file, so it punts and makes the user deal with it. This is the bane of version control of any sort, and it's sometimes pretty painful.

The way I usually deal with conflicts is to avoid them entirely. Other people do it completely differently, and I'm sure there's a number of ways that one can deal with conflicts, but here's mine.

First of all, before I try to update a checkout I've got, I'll reflexively run the cvs -nq up command and audit the resulting spew to see if there would be any conflicts were I to actually perform the update. Conflicts are identified with a "C" before the reported filename.

If I do have conflicts, I'll just move my local copy of the files in which there are conflicts aside (by renaming them to "filename_aside"), and then I'll do a cvs up -dP to get all the files that the repository thinks are the most recent versions. I'll then diff my "aside" files against the ones that came from the repository. I'll edit the "real" file (the one that came from the repository), adding in the bits of functionality from the "aside" file as necessary. Then I'll commit the result to the repository. This makes dealing with conflicts simple. There are undoubtedly more elaborate strategies (which I'd like to hear about).

Setting Up SSH Public Key Auth

Typing your ssh password in to interact with the CVS repository becomes old quickly. You can reduce the number of times you need to type your password in by using ssh public key authentication instead of password authentication.

Setting up SSH public key auth is fairly easy, if a bit arcane. I'll describe it here for recent OpenSSH versions (3.4.X). First of all, if you don't have one already, generate an ssh public/private key pair by using the ssh-keygen facility on your own system:

   chrism@james~$ ssh-keygen -t dsa
   Generating public/private dsa key pair.
   Enter file in which to save the key (/home/chrism/.ssh/id_dsa):
   Enter passphrase (empty for no passphrase):
   Enter same passphrase again:
   Your identification has been saved in /home/chrism/.ssh/id_dsa.
   Your public key has been saved in /home/chrism/.ssh/id_dsa.pub.
   The key fingerprint is:
   f8:c5:a9:3a:24:cc:1e:c1:ae:0f:46:58:95:5b:d1:b7 chrism@james

Then copy the generated "id_dsa.pub" file to the remote system which you'd like to access. Put it in the remote file $HOME/.ssh/authorized_keys2.

Now, any time to try to do ssh authentication to the remote machine (via ssh or scp or CVS-over-ssh), you will be asked for the passphrase you choose in the keygen step (as opposed to the password of your account on the remote system). At this point, on the originating system, you can likely type "ssh-add" to cause your passphrase to be remembered for the duration of your terminal session. Once this is done, you won't be prompted for the passphrase again in any scenario, at least until you quit that terminal.

Using Emacs as a CVS Frontend

Pressing CTRL-x v v at any point while visiting a buffer of a file which is kept under CVS version control will cause Emacs to attempt to perform the next "most reasonable" action against the file.

If you've changed the file and/or buffer locally, Emacs will attempt to check your changes in, asking you for a checkin comment in another buffer. If Emacs detects a conflict when performing a checkin, it will alert you and optionally run the ediff program which will allow you to resolve the conflicts. Ediff is pretty baffling but with some trial and error usage it becomes usable.

For Emacs CVS-over-ssh integration to work under X, you will likely need the ssh-askpass program which is typically available as a binary package under most UNIXen.

Further Reading

Open Source Development With CVS (long, but comprensive)

Pascal Molli's CVS Tutorial (short, a handy quick reference, but not comprehensive)

Created by chrism
Last modified 2004-01-04 12:07 AM

make_instance.py

In Zope 2.6.3 there is no mkzopeinstance.py, but there is make_instance.py.

SSH public key auth in PuTTY

(Contributed by Frank Wilder):

To use putty (version 0.54 for this writing) under MS Windows, you need to
perform the following steps because putty does not use the OpenSSH format
for the SSH private key.

Down load the tool 'puttygen' (you can find it via a web search). Make sure
you have access to your private key on you MS Windows. Run puttygen and hit
the 'load' button. Select the private key using the browse button that you
created using a OpenSSH compliant tool.

Once puttygen has converted the key, save it using the 'save private key'
to a location on your MS Windows machine. I named mine 'putty_private_key.ppk'.

Start up putty. Make sure you have selected the 'SSH' protocol (under
Session). Go to the 'Auth' category and browse for the file in the 'Private
key file for authentication:' You should select the file you created in the
previous paragraph (in my case it is 'putty_private_key.ppk').

putty should now work. When you connect to a server via SSH, you will be
asked the passphrase for your public key. Enter it and you should be
connected. If it doesn't work, bring up the event log (by clicking with the
right-mouse-button on the banner) and look for clues there.