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
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
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.
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.
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:
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 (
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.
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.
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.
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
-dP options, meaning "get new directories, and prune
empty directories", which is really the most sane thing to do in
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.
The results of
cvs -nq up give you some idea about what's
different from the repository version of the files and your local
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
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.
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
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).
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.
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
ssh-askpass program which is typically available as a
binary package under most UNIXen.
Open Source Development With CVS (long, but comprensive)
Pascal Molli's CVS Tutorial (short, a handy quick reference, but not comprehensive)