The CVS Source Control System

CVS is used by open projects like Apache's WWW server, Free-BSD, Net-BSD, Open-BSD, Gnome, Netscape's Mozilla, and PostgreSQL. Unlike other systems, CVS seems to have been designed (or evolved comfortably) for distributed development by a large number of developers. CVS makes it easier for developers to work on the same code at different paces. Most source control systems emphasize merging your changes into the repository safely. CVS gives equal importance to merging recent changes in the repository safely into code that you have begun to modify.

The mechanics of CVS are open. You can see how it works, and you could fix the code if you had to (you won't). With so many users and open development, you are unlikely to encounter a blatant bug, or an obviously needed feature. CVS seems to have no more complexity than necessary to do what it does. You can get all the online support you need. CVS clients are available on all platforms, and a variety of GUI's. (A server must run on Unix.)

Here are my notes on switching to Subversion (SVN).  

Quick start

Most Unix boxes will have the client command cvs already installed. You'll have to google for a Microsoft version, which seems to move around. Put the cvs.exe in C:\WINNT\system32 .

Set the environmental variable CVSROOT , for example:

  $ export CVSROOT=":pserver:username@pserver.denver.lgc.com:/cm/cvs/prowess"

Most everything can be done with a few commands: login, checkout (co), commit (ci), update (up), add, and remove (rm).

Login once with your password.

  $ cvs login
  (Logging in to username@pserver.denver.lgc.com)
  CVS password: *****
A .cvspass file should appear in your home directory. A shared read-only username like build accepts an empty password.

Get a new copy of the entire tree (your "working copy" or "sandbox"):

  $ cvs checkout -P prowess
This will create a directory prowess with the entire tree in your current directory. Substitute your project name for prowess . Do not try to write over an existing tree.

You can also checkout just a portion of the tree with a longer path:

  $ cvs checkout -P prowess/port/com/lgc
Specify an alternative name for the working directory with
  $ cvs checkout -P -d my_lgc prowess/port/com/lgc
You can avoid the environmental variable CVSROOT by using with the login and checkout commands. After these steps, you no longer need the environmental variable:
  $ cvs -d:pserver:username@pserver.denver.lgc.com:/cm/cvs/prowess login
  $ cvs -d:pserver:username@pserver.denver.lgc.com:/cm/cvs/prowess checkout -P prowess

Update your copy from the repository often to see recent changes:

  $ cd prowess
  $ cvs update -dPA
(The flag -d is necessary to see new directories, -P will prune obsolete directories, and -A will reset "sticky tags" to the latest versions. Do not use -A if you are working in a branch.)

Edit files, then commit changes to the file or to whole directories:

  $ cvs commit filename 
or 
  $ cvs commit dir
or 
  $ cvs commit .
Delete and add text files and directories with
  $ cvs remove -f file.f
  $ cvs add file.java
  $ cvs commit .
Add binary files with
  $ cvs add -kb file.jar
If you forget, you can specify binary later with
  $ cvs admin -kb *.so
(This will not work on Windows, where adding a binary as text will immediately corrupt it. You'll need to change type, then delete and add again.) Create and remove entire directories with
  $ cvs remove -f -R old_directory
  $ cvs add new_directory

Throw away your changes to a file by deleting and updating again.

  $ rm filename ; cvs update filename
  $ rm -rf subdirectory ; cvs update subdirectory
Do not delete CVS subdirectories unless you delete its entire directory as well. This is enough to start working on code. To remember syntax, type
  $ cvs -H [command]
If you are worried about what your command will do, try a dry run with
  $ cvs -n [command]

What? No file locking?

Many ask "how do I lock a file?" The short answer is "you can't." The short justification is "You don't need to, and you don't want to." File locking does not scale well, even with a small number of developers. Two developers should be able to work on different parts of a file at the same time. File locking does not prevent incompatibilities that involve more than one file. File boundaries are not good code boundaries. File locking is too coarse-grained and too fine-grained.

File locking "pessimistically" assumes that the work of different developers will be hard to reconcile. In fact, real conflicts are rare. File locking tries to avoid the problem at checkout time. CVS "optimistically" waits and fixes problems, if any, before code is checked back into the repository.

CVS remembers which version of a file you began to modify. (Most systems do not.) If someone else checks in a newer version, then you will not be allowed to commit and over-write previous changes. First, you must perform a cvs update -dP to merge recent changes into your modified version. Usually, you find those merged changes do not conflict with your own. In the rare cases that you modified the very same lines, CVS will warn you of a conflict, and insert into your file a diff of the affected lines. (Clearcase does the same thing when synchronizing.) You can edit this merged file and then commit. Conflicts happen less often than broken locks with other systems.

Developers may disagree on a design or required changes. If developers avoid talking to each other, then a source control system will not resolve their problem.

CVS does allow you to "watch" files. To receive an email when a file is changed in the repository, type

  $ cvs watch add -a commit filename
This should be more than enough notification.

A really stubborn team can emulate locking of a file with cvs watch on filename and cvs watch add -a edit filename . Checked-out copies of this file will be read-only, so others should type cvs edit filename before modification. A user can still "break" the lock by simply typing chmod +w filename . See if you can live without locking instead.


History

To see all changes committed to a repository recently, type

  $ cvs history -c -a -D "3 days ago"
The first letter on the line may be A for "file added," M for "file modified," or R for "removed file."

This history is difficult to read, and worse, users can hide their modifications from the history command. More reliably, go the the base directory and create a GNU-style ChangeLog file with the perl script cvs2cl.pl from http://www.red-bean.com/cvs2cl/. Check this ChangeLog file into the tree and update regularly. (I recommend the --revisions flag.)

To see the history and comments for a specific file, type

  $ cvs log filename
To see the log for the most recent revision of a file, type
  $ cvs log -rHEAD filename

You can change a log message after a commit with

  $ cvs admin -m 1.32:"New log message here" file
or
  $ cvs admin -m HEAD:"New log message here" file

Checking differences

To compare your working version of a file against the copy you checked out from the repository, type

  $ cvs diff filename
(Try Unix format options such as cvs diff -u filename .) To compare your copy against the most recent copy in the repository type
  $ cvs diff -D now filename
To compare against any particular version, type
  $ cvs diff -r 1.7 filename
  $ cvs diff -r tag_2003_1_3 filename
  $ cvs diff -D "2 hour ago" filename
  $ cvs diff -D "1 day ago" filename
  $ cvs diff -D "2 months ago" filename
  $ cvs diff -D "March 14 200X" filename
To see modifications between two specific versions, type
  $ cvs diff -r 1.7 -r 1.8 filename
(to see changes introduced by version 1.8).

To see any differences between your entire tree and the repository, type

  $ cvs -q diff
or
  $ cvs -q diff -D now
( -q suppresses verbosity.) All subdirectories will be examined. Any files you have changed but have not committed will be listed with a Unix-like diff of the different lines. Files that appear in your sandbox, but not in the repository, are marked with a question mark. Unix diff flags are also supported.

A diff of an entire tree can be verbose. You may prefer the more concise listing of what an update would do with

  $ cvs -nq update -dP
The -n flag ensures that no changes will be performed. New files will be marked with a "?", and changed files will be marked with capital letters (as with the history command). U marks a file that needs to be updated in your working directory. C marks a file that will require hand editing of a conflict after updating (rare).

Count the number of lines of code changed in the past week for all directories under the current one:

  $ cvs diff -b -D "1 week ago" 2>/dev/null | grep '^[<>]' | wc -l 
And for the previous week
  $ cvs diff -b -D "1 week ago" -D "2 weeks ago" 2>/dev/null | grep '^[<>]' | wc -l 
This command counts a new line or a deleted line as one, and a replaced line as two. Check only new lines of code with
  $ cvs diff -b -D "1 week ago" 2>/dev/null | grep '^>' | wc -l 
You can also specify specific dates as -D "March 13 200X" The flag -b ignores trivial changes to whitespace.

Seeing old versions

If you want to look at an old version 1.3 of a file without stepping on the new one, type

  $ cvs update -p -r 1.3 filename > file.old
or
  $ cvs update -p -D 2002/03/07 filename > file.old
I frequently throw away a working copy of the tree and checkout a fresh one. Before doing so, I type cvs -q diff in the root directory.

If I want see what will happen before an update, I type cvs -nq update -dP.

Always add comments when you commit. These will help you more than anyone else. When you add a file, add a comment saying where it came from, particularly if it was removed elsewhere in the tree. You can change the editor for comments by setting the environmental variable EDITOR.

This handy command will show the last revision where every line was modified and the user who made the change:

  $ cvs annotate filename
To see what has been removed from the current directory and subdirectories type
  $ cvs history -x R
or 
  $ cvs log .
To see inside a deleted directory, first recover the empty directory without pruning: cvs update -dA . Get the appropriate date or revision number from the log, and recover files with cvs update ... .

If you would like to undo changes of previous revisions in your working copy, type

  $ cvs update -j 1.7 -j 1.4 file.c
to remove all changes between version 1.4 and 1.7. The order of the -j 1.7 -j 1.4 flags matters. Immediately follow this command by a cvs diff file.c to see if you got the expected results.

Avoid stepping on revisions

CVS forces you to update before you commit, so rarely should you accidently remove someone else's revisions.

If you copy individual files from one sandbox to another, be very careful. Update both copies first. You might copy an old version onto a more recently updated file and lose more recent changes. Perform a diff after copying to be sure. Better, try to avoid this situation.

If you have updated to version 1.18, and you discover that changes from 1.16 to 1.17 were lost, then you can recover those changes with

  $ cvs update -j 1.16 -j 1.17 file
  $ cvs diff
    ...
  $ cvs commit
Again the order of the -j 1.16 -j 1.17 flags is important.

Never copy someone else's working copy. You can move your own working copy, intact, but do not make two copies. Do not mess around with CVS subdirectories.


Tagging snapshots

Occasionally you want to work with a stable version of your tree for a few days or weeks at a time.

First make sure you have an up-to-date working copy of the main trunk. Then mark every file with an informative tag that includes the purpose and date:

  $ cvs -q tag -c beta-200X-06-28
(The -c flag checks for uncommited modifications.)

Test by getting a new copy of the tree with the tagged version of every file:

  $ cvs -q export -d beta-200X-06-28 -r beta-200X-06-28 prowess
or
  $ cvs checkout -P -d beta-200X-06-28 -r beta-200X-06-28 prowess

Use -d to create a directory with the same name as the snapshot tag. Most of the time you want just the read-only copy of the tree with cvs export . Use cvs checkout only if you later need to modify the tag or want to branch.

If this turns out to be a useless snapshot, then you can delete the tag with

  $ cvs rtag -d beta-200X-06-28 prowess
(An rtag command can be used without a working copy.)

You can see which versions of a file have been tagged with

  $ cvs log filename
A tag is just a symbolic name for a particular version of every file.

If you later decide that you want your snapshot to point to a different version of a particular file, then type

  $ cvs checkout -P -d beta-200X-06-28 -r beta-200X-06-28 prowess
  $ cd beta-200X-06-28/subdirectory
  $ cvs tag -r 1.6 -F beta-200X-06-28 filename
  $ cvs update -dP
The -r 1.6 specifies an earlier or later version of the filename to be associated with the tag. Use -r HEAD to specify the most recent one. Update to see the revised version.

The above commands should be enough to make a stable snapshot for a short amount of time. If you need to fix bugs in the snapshot, try to fix them in the main trunk, and update the versions used by the snapshot.


Minimal branching

You may need to modify the snapshot with changes that no longer make sense in the main trunk. To do so, you must branch the code. You should NOT intend to merge these changes back into the main trunk. (This is a sound policy, not a CVS restriction.)

To create a new branch from a snapshot, type

  $ cvs rtag -b -r beta-200X-06-28 beta-200X-06-28-branch prowess
Use the suffix "-branch" so you can distinguish this new tag from ordinary snapshots. You can type this command without a working directory.

You can see a history of rtag operations in the "prowess" respository with

  $ cvs history -T -a -p prowess

You can switch an existing working copy to the new branch with

  $ cvs update -dP -r beta-200X-06-28-branch
This sets a "sticky tag" so that files will be updated from the branch instead from the main trunk. Or simply throw away the snapshot and make a new working copy for the branch.
  $ cvs checkout -P -d beta-200X-06-28-branch -r beta-200X-06-28-branch prowess

See how sticky tags are set in your working copy with

  $ cvs status -v [filename]
The -v flag also shows what other tags are available.

You can selectively update files and subdirectories in the branch with the latest versions from the main trunk:

  $ cd beta-200X-06-28-branch/subdirectory
  $ cvs update -j HEAD [filename]
( HEAD specifies the tip of the main trunk. The flag -j updates the file but leaves the sticky tag.)

You can commit this update to the branch by

  $ cvs commit -m "updated from main trunk" [filename]
Modify files directly in the branch only if the fix does not make sense in the main trunk. Otherwise, make the fix in the main trunk and update the branch from the trunk.

If someone modified a branch and the same change belongs in the main trunk, then merge into a working copy of the main trunk:

  $ cvs update -j branch_name
or
  $ cvs update -j branch_name filename

Add an entire directory

Add a directory and all its contents by
  $ cd /usr/src/your_copy_of_junit3.7
  $ cvs -n import -I! prowess/port/test/junit3.7 junit version3_7
  ... [look for conflicts]
  $ cvs    import -I! prowess/port/test/junit3.7 junit version3_7
or
  $ cd /your/path/j2re1.4.1_01
  $ cvs -n import -I! prowess/sys/linux/JRE/j2re1.4.1_01 Sun v1_4_1_01
  $ cvs    import -I! prowess/sys/linux/JRE/j2re1.4.1_01 Sun v1_4_1_01
The vendors ("junit", "Sun") and releases ("version3_7", "v1_4_1_01") are relics, and you can put anything you like. The repository path "prowess/port/test/junit3.7" specifies the subdirectory of the repository for the code. Your directory "/usr/src/your_copy_of_junit3.7" not be a working copy of a respository.

Before importing an entire directory, look for and replace any symbolic links:

  $ find . -type l -print
Also make sure all permissions are readable and writable:
  $ chmod -R a+rw .

Compression

Use the -z flag before any cvs command, to get compression of the cvs protocol.
  $ cvs -z 3 -q update -dPA
  $ cvs -z 3 checkout -P prowess
  $ cvs -z 3 -q diff
-z 9 does the most compression and uses the least bandwidth, but uses the most CPU. Values of 3 to 5 are usually found to be optimum.

Handy aliases

Here are the only aliases I seem to need.
alias cvco="cvs -z3 co -P"
alias cvex="cvs -z3 export -D now"
alias cvd="cvs -z3 -q diff -b"
alias cvup="cvs -z3 -q update -dP"
alias cvu="cvs -z3 -nq update -P" # see what changes are pending

Local repository for personal files

I also use CVS for personal files in my home directory, such as publications, login configuration, utility scripts, and this web page. Converting from RCS to CVS was easy.

Create a directory in your home directory for the repository, such as $HOME/mycvsroot . My login script sets CVSROOT to this directory as my default. When I access others' repositories, I check out with the -d flag. Initialize your personal repository with

  $ export CVSROOT=$HOME/mycvsroot
  $ cvs init 
From my home machine, which has a different mounted home directory, I can access the office repository by using ssh, and setting
  $ export CVS_RSH=/usr/bin/ssh
  $ export CVSROOT=:ext:user@hostname:/home/user/mycvsroot
Here you must be explicit with the remote path.

Here is how I created a repository called docs from a directory docs that was previously under RCS control. First identify and remove any symbolic links:

  $ find $HOME/docs -type l -print
Replace links by copies, or write a script that can regenerate them. Next import the directory into CVS
  $ export CVSROOT=$HOME/mycvsroot
  $ cd $HOME/docs
  $ cvs -n import -m "Converted from RCS" -I RCS docs user v0
  $ cvs    import -m "Converted from RCS" -I RCS docs user v0
Test once with -n to see that it will work. The last three arguments arguments are required (repository, vendor, and release), even though the last two are pretty useless. The -I RCS flag avoids checking in RCS subdirectories. You can omit that if you have no directories to ignore.

To retain previous revisions for files that used RCS, I then run a simple script called rcs2cvs.sh. This script copies any RCS/*,v files into the equivalent repository subdirectory. If you have no files using RCS, then skip this step entirely.

  $ export CVSROOT=$HOME/mycvsroot
  $ cd $HOME/docs
  $ rcs2cvs.sh
If you make a mistake, do not worry. You have not altered your original directory. Simply delete the repository subdirectory $HOME/mycvsroot/docs and start over.

Test checking out the new repository and view revisions as a sanity check.

  $ export CVSROOT=$HOME/mycvsroot
  $ cd $HOME
  $ cvs checkout -d docs_test docs
  $ cd docs_test
  $ cvs log | less
  $ tree
Finally, you can backup your original directory and replace it with the working copy.

Your big import may have included some files that you did not want to keep, such as large binary files. You don't want these files taking up space in your repository after you remove them from your working directory. To remove a file forever, first remove it from your working copy with cvs rm -f file. The repository will move the version file into a a subdirectory called Attic. You can now remove the Attic/file,v file, and the repository will not know that it ever existed. Do not attempt to remove a file,v before it has moved to the Attic. You can also remove entire subdirectories this way. Hacking directly on repository files is almost always a bad idea, but this exception seems justified. If the file is not large, leave it in the Attic. You might change your mind and want to recover it later.


WinCVS

Download a GUI wrapper for CVS from http://www.wincvs.org/ .

Like VSS and ClearCase, this has clever features that are harder to contrive from the command line. You can easily select lists of files and directories to commit as a group. Modified files have different icons. Versions are very comprehensible.

When WinCVS first starts up give the wizard your CVSROOT , specify password authentication, and point to your home directory with .cvspass. This much configuration will allow you to connect.

Go to the Change Location icon (with binoculars) to select your local working directory, which may already exist.

You should be able to figure out the rest by just playing with it.

You can specify an different default file viewer Go to Admin -> Preferences -> WinCvs tab -> Default viewer used to open files: and enter C:\emacs\bin\runemacs.exe

Or specify an external graphical diff program (or file editor) at Admin -> Preferences -> WinCvs tab -> External diff program: Enter C:\Program Files\Microsoft Visual Studio\Common Tools\windiff for example. The next time you actually do a diff, check the box on the Diff settings dialog that says Use the external diff.


Go back to Bill Harlan's homepage or hotlist.