A Sparse Introduction to CVS

a brain dump by Karl Fogel

What Does CVS Do?

CVS is an annotating time machine and data replication server. There, that clears everything up, doesn't it?

No, Really...

CVS keeps track of a project's revision history, allowing developers to answer questions like: Every time you make a change and inform CVS of it, you have created a new revision. CVS remembers the state of the project at each successive revision. (Note that a "revision" is not the same thing as a "version number", such as "Release-1.0" or "Upgrade 2.3". Versions are incremented according to the developer's whim, or some official policy. There might be many revisions from one version to the next; CVS wisely does not attempt to enforce any relationship between the two.)

If you've used other revision control systems, you may be familiar with the lock-modify-unlock development model, wherein a developer first reserves ("locks") the file he plans to edit, makes his changes, and then releases the lock, allowing other developers access to it.

CVS is way, way mellower. It is byte-order-independent, gender-neutral, and uses the copy-modify-merge model, which works pretty much the way you think it does:

  1. Developer requests a working copy of the project from CVS. This is also known as checking out a working copy, like checking a copy of a book out of the library.
  2. Developer edits freely in her working copy. At the same time, other developers are working in their own copies. Since these are all separate copies, there is no interference --- it is as though each developer has a different copy of the same library book, and they're all scribbling their own comments in the margins or rewriting certain pages independently.
  3. Developer finishes her changes, and commits them into CVS along with some log messages, which are comments explaining the nature and purpose of the changes. This is like informing the library of the particular changes she made to the book, and why. The library will now make its "master copy" of the book reflect these changes.
  4. From time to time, the others update their working copies -- that is, they ask the library if anything's changed recently. After the developer in Step 3 has committed her changes, the next update by someone else will bring these new changes into the updater's working copy. This is magical and wonderful, and I hope you appreciate it. It is as though certain pages in his particular copy of the book are quietly rewritten to reflect the changes the first developer just committed.
I've told this story from the point of view of one particular developer, but in reality no developer is ``special''. They are all updating periodically, and committing changes when they're ready. As far as CVS is concerned, they are all equal. Exactly when to update and when to commit is largely a matter personal preference, or policy. One common strategy is to always update before starting a major edit, and to commit only when your changes are complete and tested, so that the project is always in a "runnable" state.

At this point you may be wondering, what happens when two developers edit the same part of the same page, and then both commit? This is called a conflict, and CVS of course notices it the moment the second developer tries to commit changes. Instead of allowing the second commit, CVS places "conflict markers" (which are very obvious) in the region in question, in the second developer's copy. The conflict markers delimit the conflicting region and show both sets of changes. It's up to the second committer to sort it all out and commit a new revision with the conflict resolved. Perhaps the two developers will need to talk to each other to settle the issue. All CVS can do is notice that there is a conflict; it must be human beings who actually resolve it.

What about the "master copy" mentioned above? In offical CVS terminology, it is known as the project's repository. It's just a file tree kept on a central server (in our case, cvs.onshore.com is the server, and /usr/local/cvs/ is the repository). Unless you want to be a CVS guru (you don't), you can ignore the details of the repository's history-saving techniques --- suffice it to say that it always has the most up-to-date revision of a file or set of files, but is also capable of retrieving past revisions.

A quick review of terms:

Enough Theory, I Want to Get Started!

First, check out a working copy. You'll need an account on cvs.onshore.com, and a CVS password there. If you don't already have these, email cvsadmin. Assuming you have all these things, type:
prompt$ cvs -d :pserver:username@cvs.onshore.com:/usr/local/cvs login
CVS password: [type your CVS password here]
This confirms your password with the server and saves it in your home directory, in a file called .cvspass. After this, CVS will always try to get your password from .cvspass, so you only need to log in once from a given local host to a given CVS server. The .cvspass file is human-readable; take a look at it some time (you can paste lines from one .cvspass file into another if it's ever necessary).

Next, try checking out a project (the first command is a single line without the backslash, of course; it's just wrapped in this document because it's long):

prompt$ cvs -d :pserver:username@cvs.onshore.com:/usr/local/cvs \
            co documentation
cvs server: Updating documentation
U documentation/cvs-intro.html
U documentation/README.cvs
[... etc, etc ...]
Let's decode that command line: If you find yourself using the same repository a lot, set the CVSROOT environment variable to the repository string, then you won't need to pass the -d option. For example, in your .bashrc:
export CVSROOT
Anyway, at this point you have a directory named documentation. Go into that directory:
prompt$ cd documentation
prompt$ ls
The project has two files, README.cvs and cvs-intro.html. Note also the CVS administrative subdirectory, CVS. It records revision numbers and repository information (take a look at the files in there sometime, they're human-readable). You should never need to do anything in the CVS subdir; only CVS uses it.

The file README.cvs above exists solely for playing around with CVS, so you can edit it freely. Make some changes to it, and then do an update:

prompt$ emacs README.cvs
[... edit README.cvs to your heart's content ...]
prompt$ cvs update
cvs server: Updating .
The "M" next to the filename reminds you that you have "modified" that file, and that the repository does not yet know about the change.

Notice how you don't need to specify the repository with -d anymore; the working copy knows where it came from (that information is saved in the CVS subdir), so there would be no point making you type out that long repository string again. Note also that we didn't have to tell CVS what to update -- it just knows to work on the current directory (and subdirectories, if any), examining all files. This is a general rule: in the absence of explicit filenames, "." is always an implied argument to CVS, and CVS will recurse into subdirectories automatically. CVS detects which files have been changed locally, which have changed in the repository, and does the appropriate thing in all cases.

Now, send your changes to the repository:

prompt$ cvs ci -m "edited text because i felt like it" 
cvs commit: Examining .
Checking in README.cvs;
/usr/local/cvs/internal/documentation/README.cvs,v  <--  README.cvs
new revision: 1.2; previous revision: 1.1
The -m flag, followed by a text string, gives CVS the log message for this commit. CVS requires log messages, even if they are empty strings. If you omit the -m flag, CVS will automatically start up an editor (such as vi or Emacs), with a text buffer where you're supposed to type in the log message. Only after you save the message and exit the editor does the commit continue.

Because README.cvs was the only changed file, CVS committed it and nothing else. If there had been other modified files, but you'd wanted to check in only README.cvs, you could have specified that by putting the filename after the log message. In effect, you can always replace the implied "." with a list of specific files to act on.

Now that you've committed this change, others with working copies of their own can run cvs update, and your change will be merged into their copy:

their_prompt$ cvs update
cvs server: Updating .
The "P" means "patched"; that is, the server sent down just the differences for the changed region, and the client "patched" them into the file. It's done this way to avoid wiping out any changes the second developer might have made in other regions of the file.

Many CVS commands, particularly update, list filenames prefixed by a code letter as part of their output. Here is what the code letters mean:


Adding a file is done in two steps -- add, then commit:

prompt$ cvs add foo
cvs server: scheduling file `foo' for addition
cvs server: use 'cvs commit' to add this file permanently
prompt$ cvs ci -m "new file foo, contains useless information" foo
RCS file: /usr/local/cvs/internal/documentation/foo,v
Checking in foo;
/usr/local/cvs/internal/documentation/foo,v  <--  foo
initial revision: 1.1
CVS and Binary Files

Adding a binary file is done the same way, but it's very important to use the -kb flag to tell CVS it is a binary (it's a long story):

prompt$ cvs add -kb bar.gif
cvs server: scheduling file `bar.gif' for addition
cvs server: use 'cvs commit' to add this file permanently
prompt$ cvs ci -m "added photo of lenscap" bar.gif
RCS file: /usr/local/cvs/internal/documentation/bar.gif,v
Checking in bar.gif;
/usr/local/cvs/internal/documentation/bar.gif,v  <--  bar.gif
initial revision: 1.1
CVS may corrupt binary files unless it's told they're binary. Work is under way to correct this problem. In the meantime, always tell it that a file is binary.

How do I Add a New Directory?

CVS is broken in this respect; you can add new directories, but CVS may give confusing, even inaccurate output, while you're doing so. Basically, create the directory, then run cvs add on it just like a regular file, then add files inside it the usual way. If CVS seems to complain, just ignore it.

How do I Remove Files?

Removing a file is done in three steps -- remove the file, then tell CVS it's gone, then commit the change:

prompt$ rm foo
prompt$ cvs rm foo
cvs server: scheduling `foo' for removal
cvs server: use 'cvs commit' to remove this file permanently
prompt$ cvs ci -m "realized foo was useless, removed" foo
Removing foo;
/usr/local/cvs/internal/documentation/foo,v  <--  foo
new revision: delete; previous revision: 1.1
How do I Remove a Directory?

Run the remove procedure (see above) on the files inside it, then run it on the directory itself. Again, if CVS complains, you can probably ignore it.

How do I Rename?

Glad you asked. Move the file to the new name, then run the three steps to remove the old name, then run the two steps to add the new name, then get a drink -- you deserve it.

Starting a New Project

Starting a new project usually consists of two steps: importing the file tree into the main repository, then optionally giving the project a symbolic name (in CVS terminology, a "module"). We take these steps in order:


Arrange your project's whole directory tree nice and clean like (you want to get this right the first time, as changing things around later would be painful). Any files that are not permanent parts of the project should be removed. If there are binary files, such as graphical images, take them out for now -- you should add them in later using the -kb flag.

Then you go to the top of your project's directory tree, and import the whole thing in one command. Here's how it was done for this project (Note: don't actually run this -- this project has already been imported once, if you try to re-import it, things will get messed up!):

prompt$ cd documentation
prompt$ ls
prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs   \
            import                                              \
            -m "initial import of Project for World Domination" \
            internal/documentation onshore start
N internal/documentation/README.cvs
N internal/documentation/cvs-intro.html

No conflicts created by this import
prompt$ cd ..
prompt$ mv documentation documentation.bak
prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs co documentation
[... blah blah blah, the usual checkout messages ... ]
prompt$ cd documentation
prompt$ ls
Note that what we imported was not a working copy; because it had no CVS subdirectory, CVS didn't even know it exists. After we imported, it was still not a working copy -- it had not been produced by a CVS checkout, so it had no CVS subdirectory. After we moved the original to a backup location, and then checked out the new project from CVS, we finally had a CVS working copy.

The import subcommand itself takes a log message (-m "whatever"), and then three arguments:

Import always acts on the current directory and its subdirectories, so there's no need to pass a "." argument.

Defining a Module for the Project

This requires a little knowledge about how the repository is set up, which I will deliver orally (vaccine delivery is under development). You may find it useful to look at the Repository Structure section of this document before reading about defining modules.

Defining a module name for your project is not, strictly speaking, necessary; you can always retrieve it from the repository by the path you imported it under. For example:

prompt$ cvs -d :pserver:whoever@cvs.onshore.com:/usr/local/cvs \
            co internal/documentation
cvs server: Updating internal/documentation
U internal/documentation/README.cvs
U internal/documentation/cvs-intro.html
prompt$ cd internal
prompt$ ls
CVS/	  documentation/
prompt$ cd documentation
prompt$ ls
CVS/		README.cvs		cvs-intro.html
The problem with this is that it gives you the directory you want nested inside another directory that you didn't want. Ideally, you'd be able to check out the documentation project as a unit by itself, independent of any sibling projects in the internal section of the repository. To set this up, you would need to edit the modules file in the CVSROOT administrative subdirectory of the repository. But you wouldn't edit that file directly in the repository; instead, you'd check out a working copy of CVSROOT, edit the modules file in that working copy, and check it back in. You don't need to do this for the documentation module, of course, because it's already been done. Here's how it was done:
prompt$ cvs -d :pserver:whoever@cvs.onshore.com:/usr/local/cvs co CVSROOT
cvs server: Updating CVSROOT
U CVSROOT/checkoutlist
U CVSROOT/commitinfo
U CVSROOT/cvsignore
U CVSROOT/cvswrappers
U CVSROOT/editinfo
U CVSROOT/log.pl
U CVSROOT/loginfo
U CVSROOT/modules
U CVSROOT/notify
U CVSROOT/passwd
U CVSROOT/rcsinfo
U CVSROOT/taginfo
cvs server: Updating CVSROOT/Emptydir
prompt$ cd CVSROOT
prompt$ emacs modules
[... edit it to have a line like this:
  documentation    internal/documentation

     That maps the module name `documentation' onto the repository path
prompt$ cvs ci -m "new module: documentation"
cvs commit: Examining .
cvs commit: Examining Emptydir
Checking in modules;
/usr/local/cvs/CVSROOT/modules,v  <--  modules
new revision: 1.68; previous revision: 1.67
cvs server: Rebuilding administrative file database
There! Now, you can checkout the module documentation without reference to its actual path in the repository:
prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs co documentation
cvs server: Updating documentation
U documentation/README.cvs
U documentation/cvs-intro.html
prompt$ cd documentation
prompt$ ls
CVS/		README.cvs		cvs-intro.html

How to Detect and Resolve Conflicts

Suppose two developers make changes to the same line of the file README.cvs in their working copies. The first developer checks in her changes, then the second developer updates his working copy. CVS now has the impossible job of merging two conflicting sets of changes into the second working copy. It handles this by putting conflict markers into the file showing both sets of changes, which signals the developer that he has to resolve them before committing.

The first developer commits her changes:

herprompt$ cvs ci -m "first developer changes line 7" README.cvs
Checking in README.cvs;
/usr/local/cvs/internal/documentation/README.cvs,v  <--  README.cvs
new revision: 1.6; previous revision: 1.5
The second developer has changed line 7 as well; if he blindly tries to commit, CVS will refuse, on the basis that the file is no longer up-to-date (that is, there has been a new revision created in the repository since the developer last updated or checked out):
hisprompt$ cvs ci -m "I also changed line 7" README.cvs
cvs server: Up-to-date check failed for `README.cvs'
cvs [server aborted]: correct above errors first!
Seeing that the up-to-date check has failed, the second developer updates. CVS then places the conflict markers in the file:
hisprompt$ cvs update
cvs server: Updating .
RCS file: /usr/local/cvs/internal/documentation/README.cvs,v
retrieving revision 1.5
retrieving revision 1.6
Merging differences between 1.5 and 1.6 into README.cvs
hisprompt$ cat README.cvs
[... head of file omitted ...]

<<<<<<< README.cvs
sndbox  whoo hoo, playground,  edit freely in here, la-dee-dah-dah...
s&ndbox  whoo hoo, playground,  edit freely in here, la-dee-dah-dah...
>>>>>>> 1.6

mmm, cvs is such fun!  cvs is my hero!
The two versions of the changed region are clearly marked, as you can see. He edits the file (perhaps after conferring with the first developer), removing the conflict markers, then commits the new revision:
hisprompt$ cvs ci -m "committing with conflicts resolved" README.cvs
Checking in README.cvs;
/usr/local/cvs/internal/documentation/README.cvs,v  <--  README.cvs
new revision: 1.7; previous revision: 1.6
Voila! Conflict resolved, world peace soon to follow.

Taking a "Snapshot" of the Tree

A tag is a way to assign a symbolic revision name to a particular "snapshot" of the project tree at a given point in time. A common use is to tag the project just before releasing a new version, so one can always go back to a known working version. Here we tag the project for release of version 0.1:
prompt$ cvs tag release-0-1
cvs server: Tagging .
T cvs-intro.html
You can can give a tag any name you want (although, inconveniently, it can't contain a period). Most projects have a convention for tagnames, for example "release-majornum-minornum". Also, it is normal to tag an entire project tree at once, although you could tag a subset of it by naming individual files on the command line.

Retrieving a tagged version is equally easy: use the -r ("revision") option to checkout or update:

prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs \
            co -r release-0-1 documentation
cvs server: Updating documentation
U documentation/README.cvs
U documentation/cvs-intro.html
Rocket science, huh?

Viewing a Project's History

Viewing log messages, revision numbers, and tag names

You can use the log subcommand to view the log messages for a particular file, or for multiple files at once. The output of log tends to be verbose, here's an example in full (don't say I didn't warn you):

prompt$ cvs log README.cvs

RCS file: /usr/local/cvs/internal/documentation/README.cvs,v
Working file: README.cvs
head: 1.8
locks: strict
access list:
symbolic names:
	release-0-1: 1.8
keyword substitution: kv
total revisions: 8;	selected revisions: 8
revision 1.8
date: 1998/05/08 16:17:47;  author: kfogel;  state: Exp;  lines: +1 -5
revision 1.7
date: 1998/05/08 14:36:37;  author: kfogel;  state: Exp;  lines: +4 -0
trying to merge with conflicts still present
revision 1.6
date: 1998/05/08 14:31:56;  author: kfogel;  state: Exp;  lines: +1 -1
trivial change to line 7
revision 1.5
date: 1998/05/08 14:31:21;  author: kfogel;  state: Exp;  lines: +1 -1
*** empty log message ***
revision 1.4
date: 1998/05/08 14:28:45;  author: kfogel;  state: Exp;  lines: +1 -1
trivial change to line 7
revision 1.3
date: 1998/05/07 17:29:54;  author: kfogel;  state: Exp;  lines: +0 -1
edited text because i felt like it
revision 1.2
date: 1998/05/07 17:29:41;  author: kfogel;  state: Exp;  lines: +1 -1
edited text because i felt like it
revision 1.1
date: 1998/05/07 17:08:30;  author: kfogel;  state: Exp;
As you can see from the output, log is useful not only for reading the log messages, but also for finding out what tags have been set (see the top of the output), and what the file's revision numbers are.

Viewing differences between revisions

If you need more detail than the log messages give you, you can ask CVS to show you the differences between two revisions. The differences are displayed like the output of the Unix diff program. You can choose any two revisions you want, though people usually choose to look at adjacent ones:

prompt$ cvs diff -r 1.3 -r 1.4 README.cvs
Index: README.cvs
RCS file: /usr/local/cvs/internal/documentation/README.cvs,v
retrieving revision 1.3
retrieving revision 1.4
diff -r1.3 -r1.4
< sandbox  whoo hoo, playground,  edit freely in here, la-dee-dah-dah...
> s&ndbox  whoo hoo, playground,  edit freely in here, la-dee-dah-dah...
Yes, you can use tag names in place of actual revisions numbers. Yes, you can pass the -c argument to diff to view the output in "context diff" format:
prompt$ cvs diff -c -r 1.3 -r 1.4 README.cvs
[... context diff output omitted to save space ...]
And yes, Virginia, there is a Santa Claus.

How to Make a Release

What if you want to check out a copy of a project, but not get a working copy (with its attendant CVS subdirectories)? Just do a checkout command, but substitute "export" for "checkout" (or for "co", if you've been using the shorthand):
prompt$ cvs -d :pserver:kfogel@cvs.onshore.com:/usr/local/cvs \
            export -r release-0-1 documentation
cvs export: Updating documentation
U documentation/README.cvs
U documentation/cvs-intro.html
prompt$ cd documentation
prompt$ ls
README.cvs		cvs-intro.html        (note the lack of a CVS/ subdir!)
It is common, but not required, to export a known, tagged version -- otherwise, you're getting whatever happens to be in the repository at that moment.

What if I Make a Change and Regret It?

If you make changes to a file, and then decide you just want to revert back to the repository revision, simply remove the file (or at least move it out of the way) and then run cvs update. CVS will notice that the file is missing, and bring down a new copy from the repository.

Reverting a change that you've already committed is only slightly trickier: you need to retrieve the contents of the version previous to the one you committed, passing the -p option to update, which tells to send the file contents to standard output rather than to the file itself:

prompt$ cvs update -p -r 1.7 README.cvs
[... see the contents of README.cvs, revision 1.7, fly by ...]
prompt$ cvs update -p -r 1.7 README.cvs > README.cvs
[... same thing, only redirected back into README.cvs itself.
     Ignore the message sent to standard error at the end; it's just
     confirming the filename and revision number.
prompt$ cvs ci -m "reverted to 1.7" README.cvs
Checking in README.cvs;
/usr/local/cvs/internal/documentation/README.cvs,v  <--  README.cvs
new revision: 1.9; previous revision: 1.8

Windows, Macs, and CVS

Talk to Bill.

Repository Structure

Here is an overview of the onShore repository on cvs.onshore.com:

prompt$ ls -CF /usr/local/cvs
AA_Pediatrics/     configure/         internal/          regal/
CVSROOT/           cso/               lind/              shrdlu/
aap/               design-horizons/   motorola/          timesheet/
activision/        esi/               mpct/              user-space/
ap/                fcnbd/             npc-contemporary/  vworld/
cl-http/           hochunk-localbin/  pr-system/         wwwdist/
cme/               include/           racingdigest/
prompt$ ls -CF /usr/local/cvs/CVSROOT
Emptydir/        cvsignore,v      log.pl,v*        passwd*
checkoutlist*    cvswrappers*     loginfo*         passwd,v*
checkoutlist,v*  cvswrappers,v*   loginfo,v*       rcsinfo*
commitinfo*      editinfo*        modules*         rcsinfo,v*
commitinfo,v*    editinfo,v*      modules,v*       taginfo*
commitlog        history*         notify*          taginfo,v*
cvsignore        log.pl*          notify,v*        val-tags
prompt$ ls -CF /usr/local/cvs/internal
cpio-ssh/       documentation/        local-bin/      server/
prompt$ ls -CF /usr/local/cvs/internal/documentation
README.cvs,v           cvs-intro.html,v
The CVSROOT subdirectory above is special -- it is where CVS stores administrative files, such as modules and passwd.

Sometime when you have a half-hour to spare, go log into cvs.onshore.com, cd to /usr/local/cvs, and poke around in the subdirectories there (be careful not to touch anything of course). You'll see a lot of files ending in ,v -- these are known for historical reasons as "RCS" files, and they are where the repository stores the current revision and past history of each file in a project. They are human-readable, take a look at their contents if you're interested.

Emacs and CVS

Emacs fanatics will be comforted to know that there is an Emacs interface to CVS, called pcl-cvs.el. IMHO it's infinitely preferable to using the command-line, but then again you knew I would say that. Pcl-cvs doesn't come with Emacs by default, but we can install it on your machine if you're interested. Ask Karl.

Other Resources

The best in-depth documentation for CVS are its Info pages. You can get to them on cafe: start up emacs or xemacs there, and type Control-h i to go into Info; if you don't know how to use Info, ask any Emacs user. If you find yourself referring to the Info pages a lot, it might be worthwhile to set up a local copy of them on your machine.

http://www.cyclic.com/cyclic-pages/howget.html contains pointers to CVS documentation, including a GNU-Info manual, an introductory paper by Jim Blandy, and the CVS FAQ, which is old but apparently still useful.

You should also look at a document called CVS at onShore, at http://cafe.onShore.com/internal/using_cvs.html, which describes local CVS conventions.

This document itself is available on cafe, too: http://cafe.onShore.com/internal/cvs-intro.html.

(Back to Karl Fogel's home page.)