Go to the first, previous, next, last section, table of contents.


Advanced CVS

Now that we've covered the basic concepts of CVS usage and repository administration, we'll look at how CVS can be incorporated into the entire process of development. The fundamental CVS working cycle -- checkout, update, commit, update, commit, and so on -- was demonstrated by the examples in section An Overview of CVS. This chapter elaborates on the cycle and discusses how CVS can be used to help developers communicate, give overviews of project activity and history, isolate and reunite different branches of development, and automate frequently performed tasks. Some of the techniques covered introduce new CVS commands, but many merely explain better ways to use commands that you already know.

Watches (CVS As Telephone)

A major benefit of using CVS on a project is that it can function as a communications device as well as a record-keeper. This section concentrates on how CVS can be used to keep participants informed about what's going on in a project. As is true with other aspects of CVS, these features reward cooperation. The participants must want to be informed; if people choose not to use the communications features, there's nothing CVS can do about it.

How Watches Work

In its default behavior, CVS treats each working copy as an isolated sandbox. No one knows what you're doing in your working copy until you commit your changes. In turn, you don't know what others are doing in theirs -- except via the usual methods of communication, such as shouting down the hallway, "Hey, I'm going to work on parse.c now. Let me know if you're editing it so we can avoid conflicts!"

This informality works for projects where people have a general idea of who's responsible for what. However, this process can break down when a large number of developers are active in all parts of a code base and want to avoid conflicts. In such cases, they frequently have to cross each others' areas of responsibility but can't shout down the hallway at each other because they're geographically distributed.

A feature of CVS called watches provides developers with a way to notify each other about who is working on what files at a given time. By "setting a watch" on a file, a developer can have CVS notify her if anyone else starts to work on that file. The notifications are normally sent via email, although it is possible to set up other notification methods.

To use watches, you must modify one or two files in the repository administrative area, and developers must add some extra steps to the usual checkout/update/commit cycle. The changes on the repository side are fairly simple: You may need to edit the `CVSROOT/notify' file so that CVS knows how notifications are to be performed. You may also have to add lines to the `CVSROOT/users' file, which supplies external email addresses.

On the working copy side, developers have to tell CVS which files they want to watch so that CVS can send them notifications when someone else starts editing those files. They also need to tell CVS when they start or stop editing a file, so CVS can send out notifications to others who may be watching. The following commands are used to implement these extra steps:

The command watch differs from the usual CVS command pattern in that it requires further subcommands, such as cvs watch add..., cvs watch remove..., and so on.

In the following example, we'll look at how to turn on watches in the repository and then how to use watches from the developer's side. The two example users, jrandom and qsmith, each have their own separate working copies of the same project; the working copies may even be on different machines. As usual, all examples assume that the $CVSROOT environment variable has already been set, so there's no need to pass -d <REPOS> to any CVS commands.

Enabling Watches In The Repository

First, the CVSROOT/notify file must be edited to turn on email notification. One of the developers can do this, or the repository administrator can if the developers don't have permission to change the repository's administrative files. In any case, the first thing to do is check out the administrative area and edit the notify file:

floss$ cvs -q co CVSROOT 
U CVSROOT/checkoutlist 
U CVSROOT/commitinfo 
U CVSROOT/config 
U CVSROOT/cvswrappers 
U CVSROOT/editinfo 
U CVSROOT/loginfo 
U CVSROOT/modules 
U CVSROOT/notify 
U CVSROOT/rcsinfo 
U CVSROOT/taginfo 
U CVSROOT/verifymsg 
floss$ cd CVSROOT
floss$ emacs notify 
... 

When you edit the notify file for the first time, you'll see something like this:

# The "notify" file controls where notifications from watches set by 
# "cvs watch add" or "cvs edit" are sent. The first entry on a line is 
# a regular expression which is tested against the directory that the 
# change is being made to, relative to the $CVSROOT. If it matches, 
# then the remainder of the line is a filter program that should contain 
# one occurrence of %s for the user to notify, and information on its 
# standard input. 
# 
# "ALL" or "DEFAULT" can be used in place of the regular expression. 
# 
# For example: 
# ALL mail %s -s "CVS notification" 

All you really need to do is uncomment the last line by removing the initial # mark. Although the notify file provides the same flexible interface as the other administrative files, with regular expressions matching against directory names, the truth is that you almost never want to use any of that flexibility. The only reason to have multiple lines, with each line's regular expression matching a particular part of the repository, would be if you wanted to use a different notification method for each project. However, normal email is a perfectly good notification mechanism, so most projects just use that.

To specify email notification, the line

ALL mail %s -s "CVS notification" 

should work on any standard Unix machine. This command causes notifications to be sent as emails with the subject line CVS notification (the special expression ALL matches any directory, as usual). Having uncommented that line, commit the notify file so the repository is aware of the change:

floss$ cvs ci -m "turned on watch notification" 
cvs commit: Examining . 
Checking in notify; 
/usr/local/newrepos/CVSROOT/notify,v  <--  notify 
new revision: 1.2; previous revision: 1.1 
done 
cvs commit: Rebuilding administrative file database 
floss$  

Editing the notify file in this way may be all that you'll need to do for watches in the repository. However, if there are remote developers working on the project, you may need to edit the `CVSROOT/users' file, too. The purpose of the users file is to tell CVS where to send email notifications for those users who have external email addresses. The format of each line in the users file is:

CVS_USERNAME:EMAIL_ADDRESS 

For example,

qsmith:quentinsmith@farawayplace.com 

The CVS username at the beginning of the line corresponds to a CVS username in `CVSROOT/password' (if present and the pserver access method is being used), or failing that, the server-side system username of the person running CVS. Following the colon is an external email address to which CVS should send watch notifications for that user.

Unfortunately, as of this writing, the users file does not exist in the stock CVS distribution. Because it's an administrative file, you must not only create, cvs add, and commit it in the usual way, but also add it to `CVSROOT/checkoutlist' so that a checked-out copy is always maintained in the repository.

Here is a sample session demonstrating this:

floss$ emacs checkoutlist 
  ... (add the line for the users file) ... 
floss$ emacs users 
  ... (add the line for qsmith) ... 
floss$ cvs add users 
floss$ cvs ci -m "added users to checkoutlist, qsmith to users" 
cvs commit: Examining . 
Checking in checkoutlist; 
/usr/local/newrepos/CVSROOT/checkoutlist,v  <--  checkoutlist 
new revision: 1.2; previous revision: 1.1 
done 
Checking in users; 
/usr/local/newrepos/CVSROOT/users,v  <--  users 
new revision: 1.2; previous revision: 1.1 
done 
cvs commit: Rebuilding administrative file database 
floss$  

It's possible to use expanded-format email addresses in `CVSROOT/users', but you have to be careful to encapsulate all whitespace within quotes. For example, the following will work

qsmith:"Quentin Q. Smith <quentinsmith@farawayplace.com>"

or

qsmith:'Quentin Q. Smith <quentinsmith@farawayplace.com>' 

However, this will not work:

qsmith:"Quentin Q. Smith" <quentinsmith@farawayplace.com> 

When in doubt, you should test by running the command line given in the notify file manually. Just replace the %s in

mail %s -s "CVS notification" 

with what you have following the colon in users. If it works when you run it at a command prompt, it should work in the users file, too.

When it's over, the checkout file will look like this:

# The "checkoutlist" file is used to support additional version controlled 
# administrative files in $CVSROOT/CVSROOT, such as template files. 
# 
# The first entry on a line is a filename which will be checked out from 
# the corresponding RCS file in the $CVSROOT/CVSROOT directory. 
# The remainder of the line is an error message to use if the file cannot 
# be checked out. 
# 
# File format: 
# 
#       [<whitespace>]<filename><whitespace><error message><end-of-line> 
# 
# comment lines begin with '#' 

users   Unable to check out 'users' file in CVSROOT. 

The users file will look like this:

qsmith:quentinsmith@farawayplace.com 

Now that the repository is set up for watches, let's look at what developers need to do in their working copies.

Using Watches In Development

First, a developer checks out a working copy and adds herself to the list of watchers for one of the files in the project:

floss$ whoami 
jrandom 
floss$ cvs -q co myproj 
U myproj/README.txt 
U myproj/foo.gif 
U myproj/hello.c 
U myproj/a-subdir/whatever.c 
U myproj/a-subdir/subsubdir/fish.c 
U myproj/b-subdir/random.c 
floss$ cd myproj 
floss$ cvs watch add hello.c 
floss$ 

The last command, cvs watch add hello.c, tells CVS to notify jrandom if anyone else starts working on hello.c (that is, it adds jrandom to hello.c's watch list). For CVS to send notifications as soon as a file is being edited, the user who is editing it has to announce the fact by running cvs edit on the file first. CVS has no other way of knowing when someone starts working on a file. Once checkout is done, CVS isn't usually invoked until the next update or commit, which happens after the file has already been edited:

paste$ whoami 
qsmith 
paste$ cvs -q co myproj 
U myproj/README.txt 
U myproj/foo.gif 
U myproj/hello.c 
U myproj/a-subdir/whatever.c 
U myproj/a-subdir/subsubdir/fish.c 
U myproj/b-subdir/random.c 
paste$ cd myproj 
paste$ cvs edit hello.c 
paste$ emacs hello.c 
... 

When qsmith runs cvs edit hello.c, CVS looks at the watch list for hello.c, sees that jrandom is on it, and sends email to jrandom telling her that qsmith has started editing the file. The email even appears to come from qsmith:

From: qsmith 
Subject: CVS notification 
To: jrandom 
Date: Sat, 17 Jul 1999 22:14:43 -0500 

myproj hello.c 
-- 
Triggered edit watch on /usr/local/newrepos/myproj 
By qsmith 

Furthermore, every time that qsmith (or anyone) commits a new revision of hello.c, jrandom will receive another email: 

myproj hello.c 
-- 
Triggered commit watch on /usr/local/newrepos/myproj 
By qsmith 

After receiving these emails, jrandom may want to update hello.c immediately to see what qsmith has done, or perhaps she'll email qsmith to find out why he's working on that file. Note that nothing forced qsmith to remember to run cvs edit -- presumably he did it because he wanted jrandom to know what he was up to (anyway, even if he forgot to do cvs edit, his commits would still trigger notifications). The reason to use cvs edit is that it notifies watchers before you start to work on a file. The watchers can contact you if they think there may be a conflict, before you've wasted a lot of time.

CVS assumes that anyone who runs cvs edit on a file wants to be added to the file's watch list, at least temporarily, in case someone else starts to edit it. When qsmith ran cvs edit, he became a watcher of hello.c. Both he and jrandom would have received notification if a third party had run cvs edit on that file (or committed it).

However, CVS also assumes that the person editing the file only wants to be on its watch list while he or she is editing it. Such users are taken off the watch list when they're done editing. If they prefer to be permanent watchers of the file, they would have to run cvs watch add. CVS makes a default assumption that someone is done editing when he or she commits a file (until the next time, anyway).

Anyone who gets on a file's watch list solely by virtue of having run cvs edit on that file is known as a temporary watcher and is taken off the watch list as soon as she commits a change to the file. If she wants to edit it again, she has to rerun cvs edit.

CVS's assumption that the first commit ends the editing session is only a best guess, of course, because CVS doesn't know how many commits the person will need to finish their changes. The guess is probably accurate for one-off changes -- changes where someone just needs to make one quick fix to a file and commit it. For more prolonged editing sessions involving several commits, users should add themselves permanently to the file's watch list:

paste$ cvs watch add hello.c 
paste$ cvs edit hello.c 
paste$ emacs hello.c 
... 
paste$ cvs commit -m "print hello in Sanskrit" 

Even after the commit, qsmith remains a watcher of hello.c because he ran watch add on it. (By the way, qsmith will not receive notification of his own edits; only other watchers will. CVS is smart enough not to notify you about actions that you took.)

Ending An Editing Session

If you don't want to commit but want to explicitly end an editing session, you can do so by running cvs unedit:

paste$ cvs unedit hello.c 

But beware! This does more than just notify all watchers that you're done editing -- it also offers to revert any uncommitted changes that you've made to the file:

paste$ cvs unedit hello.c 
hello.c has been modified; revert changes? y 
paste$  

If you answer y, CVS undoes all your changes and notifies watchers that you're not editing the file anymore. If you answer n, CVS keeps your changes and also keeps you registered as an editor of the file (so no notification goes out -- in fact, it's as if you never ran cvs unedit at all). The possibility of CVS undoing all of your changes at a single keystroke is a bit scary, but the rationale is easy to understand: If you declare to the world that you're ending an editing session, then any changes you haven't committed are probably changes you don't mean to keep. At least, that's the way CVS sees it. Needless to say, be careful!

Controlling What Actions Are Watched

By default, watchers are notified about three kinds of action: edits, commits, and unedits. However, if you only want to be notified about, say, commits, you can restrict notifications by adjusting your watch with the -a flag (a for action):

floss$ cvs watch add -a commit hello.c

Or if you want to watch edits and commits but don't care about unedits, you could pass the -a flag twice:

floss$ cvs watch add -a edit -a commit hello.c 

Adding a watch with the -a flag will never cause any of your existing watches to be removed. If you were watching for all three kinds of actions on hello.c, running

floss$ cvs watch add -a commit hello.c 

has no effect -- you'll still be a watcher for all three actions. To remove watches, you should run

floss$ cvs watch remove hello.c 

which is similar to add in that, by default, it removes your watches for all three actions. If you pass -a arguments, it removes only the watches you specify:

floss$ cvs watch remove -a commit hello.c 

This means that you want to stop receiving notifications about commits but continue to receive notifications about edits and unedits (assuming you were watching edits and unedits to begin with, that is).

There are two special actions you can pass to the -a flag: all or none. The former means all actions that are eligible for watching (edits, commits, and unedits, as of this writing), and the latter means none of these. Because CVS's default behavior, in the absence of -a, is to watch all actions, and because watching none is the same as removing yourself from the watch list entirely, it's hard to imagine a situation in which it would be useful to specify either of these two special actions. However, cvs edit also takes the -a option, and in this case, it can be useful to specify all or none. For example, someone working on a file very briefly may not want to receive any notifications about what other people do with the file. Thus, this command

paste$ whoami 
qsmith 
paste$ cvs edit -a none README.txt 

causes watchers of README.txt to be notified that qsmith is about to work on it, but qsmith would not be added as a temporary watcher of README.txt during his editing session (which he normally would have been), because he asked not to watch any actions.

Remember that you can only affect your own watches with the cvs watch command. You may stop watching a certain file yourself, but that won't change anyone else's watches.

Finding Out Who Is Watching What

Sometimes you may want to know who's watching before you even run cvs edit or want to see who is editing what without adding yourself to any watch lists. Or you may have forgotten exactly what your own status is. After setting and unsetting a few watches and committing some files, it's easy to lose track of what you're watching and editing.

CVS provides two commands to show who's watching and who's editing files -- cvs watchers and cvs editors:

floss$ whoami 
jrandom 
floss$ cvs watch add hello.c 
floss$ cvs watchers hello.c 
hello.c jrandom  edit unedit  commit 
floss$ cvs watch remove -a unedit hello.c 
floss$ cvs watchers hello.c 
hello.c jrandom  edit commit 
floss$ cvs watch add README.txt 
floss$ cvs watchers 
README.txt      jrandom edit    unedit  commit 
hello.c jrandom edit    commit 
floss$  

Notice that the last cvs watchers command doesn't specify any files and, therefore, shows watchers for all files (all those that have watchers, that is).

All of the watch and edit commands have this behavior in common with other CVS commands. If you specify file names, they act on those files. If you specify directory names, they act on everything in that directory and its subdirectories. If you don't specify anything, they act on the current directory and everything underneath it, to as many levels of depth as are available. For example (continuing with the same session):

floss$ cvs watch add a-subdir/whatever.c  
floss$ cvs watchers 
README.txt      jrandom edit    unedit  commit 
hello.c jrandom edit    commit 
a-subdir/whatever.c     jrandom edit    unedit  commit 
floss$ cvs watch add 
floss$ cvs watchers 
README.txt      jrandom edit    unedit  commit 
foo.gif jrandom edit    unedit  commit 
hello.c jrandom edit    commit  unedit 
a-subdir/whatever.c     jrandom edit    unedit  commit 
a-subdir/subsubdir/fish.c       jrandom edit    unedit  commit 
b-subdir/random.c       jrandom edit    unedit  commit 
floss$  

The last two commands made jrandom a watcher of every file in the project and then showed the watch list for every file in the project, respectively. The output of cvs watchers doesn't always line up perfectly in columns because it mixes tab stops with information of varying length, but the lines are consistently formatted:

[FILENAME] [whitespace] WATCHER [whitespace] ACTIONS-BEING-WATCHED... 

Now watch what happens when qsmith starts to edit one of the files:

paste$ cvs edit hello.c 
paste$ cvs watchers 
README.txt      jrandom edit    unedit  commit 
foo.gif jrandom edit    unedit  commit 
hello.c jrandom edit    commit  unedit 
       qsmith  tedit   tunedit tcommit 
a-subdir/whatever.c     jrandom edit    unedit  commit 
a-subdir/subsubdir/fish.c       jrandom edit    unedit  commit 
b-subdir/random.c       jrandom edit    unedit  commit 

The file hello.c has acquired another watcher: qsmith himself (note that the file name is not repeated but is left as white space at the beginning of the line -- this would be important if you ever wanted to write a program that parses watchers output). Because he's editing hello.c, qsmith has a temporary watch on the file; it goes away as soon as he commits a new revision of hello.c. The prefix t in front of each of the actions indicates that these are temporary watches. If qsmith adds himself as a regular watcher of hello.c as well

paste$ cvs watch add hello.c 
README.txt      jrandom edit    unedit  commit 
foo.gif jrandom edit    unedit  commit 
hello.c jrandom edit    commit  unedit 
       qsmith  tedit   tunedit tcommit edit    unedit  commit 
a-subdir/whatever.c     jrandom edit    unedit  commit 
a-subdir/subsubdir/fish.c       jrandom edit    unedit  commit 
b-subdir/random.c       jrandom edit    unedit  commit 

he is listed as both a temporary watcher and a permanent watcher. You may think that the permanent watch status would simply override the temporary, so that the line would look like this:

        qsmith  edit    unedit  commit 

However, CVS can't just replace the temporary watches because it doesn't know in what order things happen. Will qsmith remove himself from the permanent watch list before ending his editing session, or will he finish the edits while still remaining a watcher? If the former, the edit/unedit/commit actions disappear while the tedit/tunedit/tcommit ones remain; if the latter, the reverse would happen.

Anyway, that side of the watch list is usually not of great concern. Most of the time, what you want to do is run

floss$ cvs watchers 

or

floss$ cvs editors 

from the top level of a project and see who's doing what. You don't really need to know the details of who cares about what actions: the important things are people and files.

Reminding People To Use Watches

You've probably noticed that the watch features are utterly dependent on the cooperation of all the developers. If someone just starts editing a file without first running cvs edit, no one else will know about it until the changes get committed. Because cvs edit is an additional step, not part of the normal development routine, people can easily forget to do it.

Although CVS can't force someone to use cvs edit, it does have a mechanism for reminding people to do so -- the watch on command:

floss$ cvs -q co myproj 
U myproj/README.txt 
U myproj/foo.gif 
U myproj/hello.c 
U myproj/a-subdir/whatever.c 
U myproj/a-subdir/subsubdir/fish.c 
U myproj/b-subdir/random.c 
floss$ cd myproj 
floss$ cvs watch on hello.c 
floss$ 

By running cvs watch on hello.c, jrandom causes future checkouts of myproj to create hello.c read-only in the working copy. When qsmith tries to work on it, he'll discover that it's read-only and be reminded to run cvs edit first:

paste$ cvs -q co myproj 
U myproj/README.txt 
U myproj/foo.gif 
U myproj/hello.c 
U myproj/a-subdir/whatever.c 
U myproj/a-subdir/subsubdir/fish.c 
U myproj/b-subdir/random.c 
paste$ cd myproj 
paste$ ls -l 
total 6 
drwxr-xr-x   2 qsmith    users        1024 Jul 19 01:06 CVS/ 
-rw-r--r--   1 qsmith    users          38 Jul 12 11:28 README.txt 
drwxr-xr-x   4 qsmith    users        1024 Jul 19 01:06 a-subdir/ 
drwxr-xr-x   3 qsmith    users        1024 Jul 19 01:06 b-subdir/ 
-rw-r--r--   1 qsmith    users         673 Jun 20 22:47 foo.gif 
-r--r--r--   1 qsmith    users         188 Jul 18 01:20 hello.c 
paste$ 

When he does so, the file becomes read-write. He can then edit it, and when he commits, it becomes read-only again:

paste$ cvs edit hello.c 
paste$ ls -l hello.c 
-rw-r--r--   1 qsmith    users         188 Jul 18 01:20 hello.c 
paste$ emacs hello.c 
  ...  
paste$ cvs commit -m "say hello in Aramaic" hello.c 
Checking in hello.c;  
/usr/local/newrepos/myproj/hello.c,v  <--  hello.c 
new revision: 1.12; previous revision: 1.11 
done 
paste$ ls -l hello.c 
-r--r--r--   1 qsmith    users         210 Jul 19 01:12 hello.c 
paste$  

His edit and commit will send notification to all watchers of hello.c. Note that jrandom isn't necessarily one of them. By running cvs watch on hello.c, jrandom did not add herself to the watch list for that file; she merely specified that it should be checked out read-only. People who want to watch a file must remember to add themselves to its watch list -- CVS cannot help them with that.

Turning on watches for a single file may be the exception. Generally, it's more common to turn on watches project-wide:

floss$ cvs -q co myproj 
U myproj/README.txt 
U myproj/foo.gif 
U myproj/hello.c 
U myproj/a-subdir/whatever.c 
U myproj/a-subdir/subsubdir/fish.c 
U myproj/b-subdir/random.c 
floss$ cd myproj 
floss$ cvs watch on 
floss$  

This action amounts to announcing a policy decision for the entire project: "Please use cvs edit to tell watchers what you're working on, and feel free to watch any file you're interested in or responsible for." Every file in the project will be checked out read-only, and thus people will be reminded that they're expected to use cvs edit before working on anything.

Curiously, although checkouts of watched files make them read-only, updates do not. If qsmith had checked out his working copy before jrandom ran cvs watch on, his files would have stayed read-write, remaining so even after updates. However, any file he commits after jrandom turns watching on will become read-only. If jrandom turns off watches

floss$ cvs watch off 

qsmith's read-only files do not magically become read-write. On the other hand, after he commits one, it will not revert to read-only again (as it would have if watches were still on).

It's worth noting that qsmith could, were he truly devious, make files in his working copy writeable by using the standard Unix chmod command, bypassing cvs edit entirely

paste$ chmod u+w hello.c 

or if he wanted to get everything in one fell swoop:

paste$ chmod -R u+w . 

There is nothing CVS can do about this. Working copies by their nature are private sandboxes -- the watch features can open them up to public scrutiny a little bit, but only as far as the developer permits. Only when a developer does something that affects the repository (such as commits) is her privacy unconditionally lost.

The relationship among watch add, watch remove, watch on, and watch off probably seems a bit confusing. It may help to summarize the overall scheme: add and remove are about adding or removing users from a file's watch list; they don't have anything to do with whether files are read-only on checkout or after commits. on and off are only about file permissions. They don't have anything to do with who is on a file's watch list; rather, they are tools to help remind developers of the watch policy by causing working-copy files to become read-only.

All of this may seem a little inconsistent. In a sense, using watches works against the grain of CVS. It deviates from the idealized universe of multiple developers editing freely in their working copies, hidden from each other until they choose to commit. With watches, CVS gives developers convenient shortcuts for informing each other of what's going on in their working copies; however, it has no way to enforce observation policies, nor does it have a definitive concept of what constitutes an editing session. Nevertheless, watches can be helpful in certain circumstances if developers work with them.

What Watches Look Like In The Repository

In the interests of stamping out black boxes and needless mystery, let's take a quick look at how watches are implemented in the repository. We'll only take a quick look, though, because it's not pretty.

When you set a watch

floss$ pwd 
/home/jrandom/myproj 
floss$ cvs watch add hello.c 
floss$ cvs watchers 
hello.c jrandom edit    unedit  commit 
floss$ 

CVS records it in the special file, `CVS/fileattr', in the appropriate repository subdirectory:

floss$ cd /usr/local/newrepos 
floss$ ls 
CVSROOT/   myproj/ 
floss$ cd myproj 
floss$ ls 
CVS/          a-subdir/     foo.gif,v 
README.txt,v  b-subdir/     hello.c,v 
floss$ cd CVS 
floss$ ls 
fileattr 
floss$ cat fileattr 
Fhello.c        _watchers=jrandom>edit+unedit+commit 
floss$  

The fact that fileattr is stored in a CVS subdirectory in the repository does not mean that the repository has become a working copy. It's simply that the name CVS was already reserved for bookkeeping in the working copy, so CVS can be sure no project will ever need a subdirectory of that name in the repository.

I won't describe the format of `fileattr' formally; you can probably grok it pretty well just by watching it change from command to command:

floss$ cvs watch add hello.c 
floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
Fhello.c        _watchers=jrandom>edit+unedit+commit 
floss$ cvs watch add README.txt 
floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
Fhello.c        _watchers=jrandom>edit+unedit+commit 
FREADME.txt     _watchers=jrandom>edit+unedit+commit 
floss$ cvs watch on hello.c 
floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
Fhello.c        _watchers=jrandom>edit+unedit+commit;_watched= 
FREADME.txt     _watchers=jrandom>edit+unedit+commit 
floss$ cvs watch remove hello.c 
floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
Fhello.c        _watched= 
FREADME.txt     _watchers=jrandom>edit+unedit+commit 
floss$ cvs watch off hello.c 
floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
FREADME.txt     _watchers=jrandom>edit+unedit+commit 
floss$  

Edit records are stored in fileattr, too. Here's what happens when qsmith adds himself as an editor:

paste$ cvs edit hello.c 

floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
Fhello.c        _watched=;_editors=qsmith>Tue Jul 20 04:53:23 1999 GMT+floss\
+/home/qsmith/myproj;_watchers=qsmith>tedit+tunedit+tcommit 
FREADME.txt     _watchers=jrandom>edit+unedit+commit 

Finally, note that CVS removes fileattr and the CVS subdirectory when there are no more watchers or editors for any of the files in that directory:

paste$ cvs unedit 

floss$ cvs watch off 
floss$ cvs watch remove 
floss$ cat /usr/local/newrepos/myproj/CVS/fileattr 
cat: /usr/local/newrepos/myproj/CVS/fileattr: No such file or directory 
floss$  

It should be clear after this brief exposure that the details of parsing fileattr format are better left to CVS. The main reason to have a basic understanding of the format -- aside from the inherent satisfaction of knowing what's going on behind the curtain -- is if you try to write an extension to the CVS watch features or debug some problem in them. It's sufficient to know that you shouldn't be alarmed if you see CVS/ subdirectories popping up in your repository. They're the only safe place CVS has to store meta-information such as watch lists.

Log Messages And Commit Emails

Commit emails are notices sent out at commit time, showing the log message and files involved in the commit. They usually go to all project participants and sometimes to other interested parties. The details of setting up commit emails were covered in section Repository Administration, so I won't repeat them here. I have noticed, however, that commit emails can sometimes result in unexpected side effects to projects, effects that you may want to take into account if you set up commit emails for your project.

First, be prepared for the messages to be mostly ignored. Whether people read them depends, at least partly, on the frequency of commits in your project. Do developers tend to commit one big change at the end of the day, or many small changes throughout the day? The closer your project is to the latter, the thicker the barrage of tiny commit notices raining down on the developers all day long, and the less inclined they will be to pay attention to each message.

This doesn't mean the notices aren't useful, just that you shouldn't count on every person reading every message. It's still a convenient way for people to keep tabs on who's doing what (without the intrusiveness of watches). When the emails go to a publicly subscribable mailing list, they are a wonderful mechanism for giving interested users (and future developers!) a chance to see what happens in the code on a daily basis.

You may want to consider having a designated developer who watches all log messages and has an overview of activity across the entire project (of course, a good project leader will probably be doing this anyway). If there are clear divisions of responsibility -- say, certain developers are "in charge of" certain subdirectories of the project -- you could do some fancy scripting in CVSROOT/loginfo to see that each responsible party receives specially marked notices of changes made in their area. This will help ensure that the developers will at least read the email that pertains to their subdirectories.

A more interesting side effect happens when commit emails aren't ignored. People start to use them as a realtime communications method. Here's the kind of log message that can result:

Finished feedback form; fixed the fonts and background colors on the
home page.  Whew!  Anyone want to go to Mon Lung for lunch?

There's nothing wrong with this, and it makes the logs more fun to read over later. However, people need to be aware that log messages, such as the following, are not only distributed by email but is also preserved forever in the project's history. For example, griping about customer specifications is a frequent pastime among programmers; it's not hard to imagine someone committing a log message like this one, knowing that the other programmers will soon see it in their email:

Truncate four-digit years to two-digits in input.  What the customer
wants, the customer gets, no matter how silly & wrong.  Sigh.

This makes for an amusing email, but what happens if the customer reviews the logs someday? (I'll bet similar concerns have led more than one site to set up CVSROOT/loginfo so that it invokes scripts to guard against offensive words in log messages!)

The overall effect of commit emails seems to be that people become less willing to write short or obscure log messages, which is probably a good thing. However, they may need to be reminded that their audience is anyone who might ever read the logs, not just the people receiving commit emails.

Changing A Log Message After Commit

Just in case someone does commit a regrettable log message, CVS enables you to rewrite logs after they've been committed. It's done with the -m option to the admin command (this command is covered in more detail later in this chapter) and allows you to change one log message (per revision, per file) at a time. Here's how it works:

floss$ cvs admin -m 1.7:"Truncate four-digit years to two in input." date.c 
RCS file: /usr/local/newrepos/someproj/date.c,v 
done 
floss$  

The original, offensive log message that was committed with revision 1.7 has been replaced with a perfectly innocent -- albeit duller -- message. (Don't forget the colon separating the revision number from the new log message.)

If the bad message was committed into multiple files, you'll have to run cvs admin separately for each one, because the revision number is different for each file. Therefore, this is one of the few commands in CVS that requires you to pass a single file name as argument:

floss$ cvs admin -m 1.2:"very boring log message" hello.c README.txt foo.gif
cvs admin: while processing more than one file: 
cvs [admin aborted]: attempt to specify a numeric revision 
floss$  

Confusingly, you get the same error if you pass no file names (because CVS then assumes all the files in the current directory and below are implied arguments):

floss$ cvs admin -m 1.2:"very boring log message" 
cvs admin: while processing more than one file: 
cvs [admin aborted]: attempt to specify a numeric revision 
floss$  

(As is unfortunately often the case with CVS error messages, you have to see things from CVS's point of view before the message makes sense!)

Invoking admin -m actually changes the project's history, so use it with care. There will be no record that the log message was ever changed -- it will simply appear as if that revision had been originally committed with the new log message. No trace of the old message will be left anywhere (unless you saved the original commit email).

Although its name might seem to imply that only the designated CVS administrator can use it, in fact anyone can run cvs admin, as long as they have write access to the project in question. Nevertheless, it is best used with caution; the ability to change a project's history is mild compared with other potentially damaging things it can do. See section CVS Reference for more about admin, as well as a way to restrict its use.

Getting Rid Of A Working Copy

In typical CVS usage, the way to get rid of a working copy directory tree is to remove it like any other directory tree:

paste$ rm -rf myproj 

However, if you eliminate your working copy this way, other developers will not know that you have stopped using it. CVS provides a command to relinquish a working copy explicitly. Think of release as the opposite of checkout -- you're telling the repository that you're done with the working copy now. Like checkout, release is invoked from the parent directory of the tree:

paste$ pwd 
/home/qsmith/myproj 
paste$ cd .. 
paste$ ls 
myproj 
paste$ cvs release myproj 
You have [0] altered files in this repository. 
Are you sure you want to release directory 'myproj': y 
paste$  

If there are any uncommitted changes in the repository, the release fails, meaning that it just lists the modified files and otherwise has no effect. Assuming the tree is clean (totally up to date), release records in the repository that the working copy has been released.

You can also have release automatically delete the working tree for you, by passing the -d flag:

paste$ ls 
myproj 
paste$ cvs release -d myproj 
You have [0] altered files in this repository. 
Are you sure you want to release (and delete) directory 'myproj: y 
paste$ ls 
paste$  

As of CVS version 1.10.6, the release command is not able to deduce the repository's location by examining the working copy (this is because release is invoked from above the working copy, not within it). You must pass the -d <REPOS> global option or make sure that your CVSROOT environment variable is set correctly. (This bug may be fixed in future versions of CVS.)

The Cederqvist claims that if you use release instead of just deleting the working tree, people with watches set on the released files will be notified just as if you had run unedit. However, I tried to verify this experimentally, and it does not seem to be true.

History -- A Summary Of Repository Activity

In section Repository Administration, I briefly mentioned the cvs history command. This command displays a summary of all checkouts, commits, updates, rtags, and releases done in the repository (at least, since logging was enabled by the creation of the CVSROOT/history file in the repository). You can control the format and contents of the summary with various options.

The first step is to make sure that logging is enabled in your repository. The repository administrator should first make sure there is a history file

floss$ cd /usr/local/newrepos/CVSROOT 
floss$ ls -l history 
ls: history: No such file or directory 
floss$  

and if there isn't one, create it, as follows:

floss$ touch history 
floss$ ls -l history 
-rw-r--r--   1 jrandom   cvs           0 Jul 22 14:57 history 
floss$  

This history file also needs to be writeable by everyone who uses the repository, otherwise they'll get an error every time they try to run a CVS command that modifies that file. The easiest way is simply to make the file world-writeable:

floss$ chmod a+rw history 
floss$ ls -l history 
-rw-rw-rw-   1 jrandom   cvs           0 Jul 22 14:57 history 
floss$  

If the repository was created with the cvs init command, the history file already exists. You may still have to fix its permissions, however.

The rest of these examples assume that history logging has been enabled for a while, so that data has had time to accumulate in the history file.

The output of cvs history is somewhat terse (it's probably intended to be parsed by programs rather than humans, although it is readable with a little study). Let's run it once and see what we get:

paste$ pwd 
/home/qsmith/myproj 
paste$ cvs history -e -a 
O 07/25 15:14 +0000 qsmith  myproj =mp=     ~/* 
M 07/25 15:16 +0000 qsmith  1.14 hello.c    myproj == ~/mp 
U 07/25 15:21 +0000 qsmith  1.14 README.txt myproj == ~/mp 
G 07/25 15:21 +0000 qsmith  1.15 hello.c    myproj == ~/mp 
A 07/25 15:22 +0000 qsmith  1.1  goodbye.c  myproj == ~/mp 
M 07/25 15:23 +0000 qsmith  1.16 hello.c    myproj == ~/mp 
M 07/25 15:26 +0000 qsmith  1.17 hello.c    myproj == ~/mp 
U 07/25 15:29 +0000 qsmith  1.2  goodbye.c  myproj == ~/mp 
G 07/25 15:29 +0000 qsmith  1.18 hello.c    myproj == ~/mp 
M 07/25 15:30 +0000 qsmith  1.19 hello.c    myproj == ~/mp 
O 07/23 03:45 +0000 jrandom myproj =myproj= ~/src/* 
F 07/23 03:48 +0000 jrandom        =myproj= ~/src/* 
F 07/23 04:06 +0000 jrandom        =myproj= ~/src/* 
M 07/25 15:12 +0000 jrandom 1.13 README.txt myproj == ~/src/myproj 
U 07/25 15:17 +0000 jrandom 1.14 hello.c    myproj == ~/src/myproj 
M 07/25 15:18 +0000 jrandom 1.14 README.txt myproj == ~/src/myproj 
M 07/25 15:18 +0000 jrandom 1.15 hello.c    myproj == ~/src/myproj 
U 07/25 15:23 +0000 jrandom 1.1  goodbye.c  myproj == ~/src/myproj 
U 07/25 15:23 +0000 jrandom 1.16 hello.c    myproj == ~/src/myproj 
U 07/25 15:26 +0000 jrandom 1.1  goodbye.c  myproj == ~/src/myproj 
G 07/25 15:26 +0000 jrandom 1.17 hello.c    myproj == ~/src/myproj 
M 07/25 15:27 +0000 jrandom 1.18 hello.c    myproj == ~/src/myproj 
C 07/25 15:30 +0000 jrandom 1.19 hello.c    myproj == ~/src/myproj 
M 07/25 15:31 +0000 jrandom 1.20 hello.c    myproj == ~/src/myproj 
M 07/25 16:29 +0000 jrandom 1.3  whatever.c myproj/a-subdir == ~/src/myproj 
paste$  

There, isn't that clear?

Before we examine the output, notice that the invocation included two options: -e and -a. When you run history, you almost always want to pass options telling it what data to report and how to report it. In this respect, it differs from most other CVS commands, which usually do something useful when invoked without any options. In this example, the two flags meant "everything" (show every kind of event that happened) and "all" (for all users), respectively.

Another way that history differs from other commands is that, although it is usually invoked from within a working copy, it does not restrict its output to that working copy's project. Instead, it shows all history events from all projects in the repository -- the working copy merely serves to tell CVS from which repository to retrieve the history data. (In the preceding example, the only history data in that repository is for the myproj project, so that's all we see.)

The general format of the output is:

CODE DATE USER [REVISION] [FILE] PATH_IN_REPOSITORY ACTUAL_WORKING_COPY_NAME

The code letters refer to various CVS operations, as shown in Table 6.1.

For operations (such as checkout) that are about the project as a whole rather than about individual files, the revision and file are omitted, and the repository path is placed between the equal signs.

Although the output of the history command was designed to be compact, parseable input for other programs, CVS still gives you a lot of control over its scope and content. The options shown in Table 6.2 control what types of events get reported.

Table 6.1  The meaning of the code letters.

Letter	        Meaning
======          =========================================================
O		Checkout
T		Tag
F		Release
W		Update (no user file, remove from entries file)
U		Update (file overwrote unmodified user file)
G		Update (file was merged successfully into modified user file)
C		Update (file was merged, but conflicts w/ modified user file)
M		Commit (from modified file)
A		Commit (an added file)
R		Commit (the removal of a file)
E		Export 
Table 6.2  Options to filter by event type.

Option	        Meaning
==========      =========================================================
-m MODULE	Show historical events affecting MODULE.
-c		Show commit events.
-o		Show checkout events.
-T		Show tag events.
-x CODE(S)	Show all events of type CODE (one or more of OTFWUGCMARE).
-e		Show all types of events, period.  Once you have
                selected what type of events you want reported, you can
                filter further with the options shown in Table 6.3.
Table 6.3  Options to filter by user.

Option	        Meaning
==========      =========================================================
-a		Show actions taken by all users 
-w		Show only actions taken from within this working copy 
-l		Show only the last time this user took the action 
-u USER 	Show records for USER 

Annotations -- A Detailed View Of Project Activity

The annotate Command

If the history command gives an overview of project activity, the annotate command is a way of attaching a zoom lens to the view. With annotate, you can see who was the last person to touch each line of a file, and at what revision they touched it:

floss$ cvs annotate
Annotations for README.txt 
*************** 
1.14         (jrandom  25-Jul-99): blah 
1.13         (jrandom  25-Jul-99): test 3 for history 
1.12         (qsmith   19-Jul-99): test 2 
1.11         (qsmith   19-Jul-99): test 
1.10         (jrandom  12-Jul-99): blah 
1.1          (jrandom  20-Jun-99): Just a test project. 
1.4          (jrandom  21-Jun-99): yeah. 
1.5          (jrandom  21-Jun-99): nope. 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.15         (jrandom  25-Jul-99):   /* another test for history */ 
1.13         (qsmith   19-Jul-99):   /* random change number two */ 
1.10         (jrandom  12-Jul-99):   /* test */ 
1.21         (jrandom  25-Jul-99):   printf ("Hellooo, world!\n"); 
1.3          (jrandom  21-Jun-99):   printf ("hmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.11         (qsmith   18-Jul-99):   /* added this comment */ 
1.16         (qsmith   25-Jul-99):   /* will merge these changes */ 
1.18         (jrandom  25-Jul-99):   /* will merge these changes too */ 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
Annotations for a-subdir/whatever.c 
*************** 
1.3          (jrandom  25-Jul-99): /* A completely non-empty C file. */
Annotations for a-subdir/subsubdir/fish.c 
*************** 
1.2          (jrandom  25-Jul-99): /* An almost completely empty C file. */ 
Annotations for b-subdir/random.c 
*************** 
1.1          (jrandom  20-Jun-99): /* A completely empty C file. */ 
floss$  

The output of annotate is pretty intuitive. On the left are the revision number, developer, and date on which the line in question was added or last modified. On the right is the line itself, as of the current revision. Because every line is annotated, you can actually see the entire contents of the file, pushed over to the right by the annotation information.

If you specify a revision number or tag, the annotations are given as of that revision, meaning that it shows the most recent modification to each line at or before that revision. This is probably the most common way to use annotations -- examining a particular revision of a single file to determine which developers were active in which parts of the file.

For example, in the output of the previous example, you can see that the most recent revision of hello.c is 1.21, in which jrandom did something to the line:

printf ("Hellooo, world!\n"); 

One way to find out what she did is to diff that revision against the previous one:

floss$ cvs diff -r 1.20 -r 1.21 hello.c 
Index: hello.c 
=================================================================== 
RCS file: /usr/local/newrepos/myproj/hello.c,v 
retrieving revision 1.20 
retrieving revision 1.21 
diff -r1.20 -r1.21 
9c9 
<   printf ("Hello, world!\n"); 
-- 
>   printf ("Hellooo, world!\n"); 
floss$  

Another way to find out, while still retaining a file-wide view of everyone's activity, is to compare the current annotations with the annotations from a previous revision:

floss$ cvs annotate -r 1.20 hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.15         (jrandom  25-Jul-99):   /* another test for history */ 
1.13         (qsmith   19-Jul-99):   /* random change number two */ 
1.10         (jrandom  12-Jul-99):   /* test */ 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.3          (jrandom  21-Jun-99):   printf ("hmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.11         (qsmith   18-Jul-99):   /* added this comment */ 
1.16         (qsmith   25-Jul-99):   /* will merge these changes */ 
1.18         (jrandom  25-Jul-99):   /* will merge these changes too */ 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$  

Although the diff reveals the textual facts of the change more concisely, the annotation may be preferable because it places them in their historical context by showing how long the previous incarnation of the line had been present (in this case, all the way since revision 1.1). That knowledge can help you decide whether to look at the logs to find out the motivation for the change:

floss$ cvs log -r 1.21 hello.c 
RCS file: /usr/local/newrepos/myproj/hello.c,v 
Working file: hello.c 
head: 1.21 
branch: 
locks: strict 
access list: 
symbolic names: 
       random-tag: 1.20 
       start: 1.1.1.1 
       jrandom: 1.1.1 
keyword substitution: kv 
total revisions: 22;    selected revisions: 1 
description: 
---------------------------- 
revision 1.21 
date: 1999/07/25 20:17:42;  author: jrandom;  state: Exp;  lines: +1 -1 
say hello with renewed enthusiasm 
============================================================================
floss$  

In addition to -r, you can also filter annotations using the -D DATE option:

floss$ cvs annotate -D "5 weeks ago" hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$ cvs annotate -D "3 weeks ago" hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.3          (jrandom  21-Jun-99):   printf ("hmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$  

Annotations And Branches

By default, annotation always shows activity on the main trunk of development. Even when invoked from a branch working copy, it shows annotations for the trunk unless you specify otherwise. (This tendency to favor the trunk is either a bug or a feature, depending on your point of view.) You can force CVS to annotate a branch by passing the branch tag as an argument to -r. Here is an example from a working copy in which hello.c is on a branch named Brancho_Gratuito, with at least one change committed on that branch:

floss$ cvs status hello.c 
=================================================================== 
File: hello.c           Status: Up-to-date 

  Working revision:    1.10.2.2        Sun Jul 25 21:29:05 1999 
  Repository revision: 1.10.2.2        /usr/local/newrepos/myproj/hello.c,v
  Sticky Tag:          Brancho_Gratuito (branch: 1.10.2) 
  Sticky Date:         (none) 
  Sticky Options:      (none) 

floss$ cvs annotate hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.10         (jrandom  12-Jul-99):   /* test */ 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.3          (jrandom  21-Jun-99):   printf ("hmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$ cvs annotate -r Brancho_Gratuito hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.10         (jrandom  12-Jul-99):   /* test */ 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.10.2.2     (jrandom  25-Jul-99):   printf ("hmmmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.10.2.1     (jrandom  25-Jul-99):   printf ("added this line"); 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$  

You can also pass the branch number itself:

floss$ cvs annotate -r 1.10.2 hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.10         (jrandom  12-Jul-99):   /* test */ 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.10.2.2     (jrandom  25-Jul-99):   printf ("hmmmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.10.2.1     (jrandom  25-Jul-99):   printf ("added this line"); 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$  

or a full revision number from the branch:

floss$ cvs annotate -r 1.10.2.1 hello.c 
Annotations for hello.c 
*************** 
1.1          (jrandom  20-Jun-99): #include <stdio.h> 
1.1          (jrandom  20-Jun-99):  
1.1          (jrandom  20-Jun-99): void 
1.1          (jrandom  20-Jun-99): main () 
1.1          (jrandom  20-Jun-99): { 
1.10         (jrandom  12-Jul-99):   /* test */ 
1.1          (jrandom  20-Jun-99):   printf ("Hello, world!\n"); 
1.3          (jrandom  21-Jun-99):   printf ("hmmm\n"); 
1.4          (jrandom  21-Jun-99):   printf ("double hmmm\n"); 
1.10.2.1     (jrandom  25-Jul-99):   printf ("added this line"); 
1.2          (jrandom  21-Jun-99):   printf ("Goodbye, world!\n"); 
1.1          (jrandom  20-Jun-99): } 
floss$  

If you do this, remember that the numbers are only valid for that particular file. In general, it's probably better to use the branch name wherever possible.

Using Keyword Expansion

You may recall a brief mention of keyword expansion in section An Overview of CVS. RCS keywords are special words, surrounded by dollar signs, that CVS looks for in text files and expands into revision-control information. For example, if a file contains

$Author$ 

then when updating the file to a given revision, CVS will expand it to the username of the person who committed that revision:

$Author: jrandom $ 

CVS is also sensitive to keywords in their expanded form, so that once expanded, they continue to be updated as appropriate.

Although keywords don't actually offer any information that's not available by other means, they give people a convenient way to see revision control facts embedded in the text of the file itself, rather than by invoking some arcane CVS operation.

Here are a few other commonly used keywords:

$Date$       ==>  date of last commit, expands to ==> 
$Date: 1999/07/26 06:39:46 $ 

$Id$         ==>  filename, revision, date, and author; expands to ==> 
$Id: hello.c,v 1.11 1999/07/26 06:39:46 jrandom Exp $ 

$Revision$   ==>  exactly what you think it is, expands to ==> 
$Revision: 1.11 $ 

$Source$     ==> path to corresponding repository file, expands to ==> 
$Source: /usr/local/newrepos/tossproj/hello.c,v $ 

$Log$        ==>  accumulating log messages for the file, expands to ==> 
$Log: hello.c,v $ 
Revision 1.2  1999/07/26 06:47:52  jrandom 
...and this is the second log message. 

Revision 1.1  1999/07/26 06:39:46  jrandom 
This is the first log message... 

The $Log$ keyword is the only one of these that expands to cover multiple lines, so its behavior is unique. Unlike the others, it does not replace the old expansion with the new one, but instead inserts the latest expansion, plus an additional blank line, right after the keyword (thereby pushing any previous expansions downward). Furthermore, any text between the beginning of the line and $Log is used as a prefix for the expansions (this is done to ensure that the log messages stay commented in program code). For example, if you put this into the file

// $Log$ 

it will expand to something like this on the first commit:

// $Log: hello.c,v $ 
// Revision 1.14  1999/07/26 07:03:20  jrandom 
// this is the first log message... 
// 

this on the second:

// $Log: hello.c,v $ 
// Revision 1.15  1999/07/26 07:04:40  jrandom 
// ...and this is the second log message... 
// 
// Revision 1.14  1999/07/26 07:03:20  jrandom 
// this is the first log message... 
// 

and so on:

// $Log: hello.c,v $ 
// Revision 1.16  1999/07/26 07:05:34  jrandom 
// ...and this is the third!
// 
// Revision 1.15  1999/07/26 07:04:40  jrandom 
// ...and this is the second log message... 
// 
// Revision 1.14  1999/07/26 07:03:20  jrandom 
// this is the first log message... 
// 

You may not want to keep your entire log history in the file all the time; if you do, you can always remove the older sections when it starts to get too lengthy. It's certainly more convenient than running cvs log, and it may be worthwhile in projects where people must constantly read over the logs.

A more common technique may be to include $Revision$ in a file and use it as the version number for the program. This can work if the project consists of essentially one file or undergoes frequent releases and has at least one file that is guaranteed to be modified between every release. You can even use an RCS keyword as a value in program code:

VERSION = "$Revision: 1.114 $"; 

CVS expands that keyword just like any other; it has no concept of the programming language's semantics and does not assume that the double quotes protect the string in any way.

A complete list of keywords (there are a few more, rather obscure ones) is given in section CVS Reference.

Going Out On A Limb (How To Work With Branches And Survive)

Branches are simultaneously one of the most important and most easily misused features of CVS. Isolating risky or disruptive changes onto a separate line of development until they stabilize can be immensely helpful. If not properly managed, however, branches can quickly propel a project into confusion and cascading chaos, as people lose track of what changes have been merged when.

Some Principles For Working With Branches

To work successfully with branches, your development group should adhere to these principles:

With those principles in mind, let's take a look at a typical branch development scenario. We'll have jrandom on the trunk and qsmith on the branch, but note that there could just as well be multiple developers on the trunk and/or on the branch. Regular development along either line can involve any number of people; however, the tagging and merging are best done by one person on each side, as you'll see.

Merging Repeatedly Into The Trunk

Let's assume qsmith needs to do development on a branch for a while, to avoid destabilizing the trunk that he shares with jrandom. The first step is to create the branch. Notice how qsmith creates a regular (non-branch) tag at the branch point first, and then creates the branch:

paste$ pwd 
/home/qsmith/myproj 
paste$ cvs tag Root-of-Exotic_Greetings 
cvs tag: Tagging . 
T README.txt 
T foo.gif 
T hello.c 
cvs tag: Tagging a-subdir 
T a-subdir/whatever.c 
cvs tag: Tagging a-subdir/subsubdir 
T a-subdir/subsubdir/fish.c 
cvs tag: Tagging b-subdir 
T b-subdir/random.c 
paste$ cvs tag -b Exotic_Greetings-branch 
cvs tag: Tagging . 
T README.txt 
T foo.gif 
T hello.c 
cvs tag: Tagging a-subdir 
T a-subdir/whatever.c 
cvs tag: Tagging a-subdir/subsubdir 
T a-subdir/subsubdir/fish.c 
cvs tag: Tagging b-subdir 
T b-subdir/random.c 
paste$  

The point of tagging the trunk first is that it may be necessary someday to retrieve the trunk as it was the moment the branch was created. If you ever need to do that, you'll have to have a way of referring to the trunk snapshot without referring to the branch itself. Obviously, you can't use the branch tag because that would retrieve the branch, not the revisions in the trunk that form the root of the branch. The only way to do it is to make a regular tag at the same revisions the branch sprouts from. (Some people stick to this rule so faithfully that I considered listing it as "Branching Principle Number 4: Always create a non-branch tag at the branch point." However, many sites don't do it, and they generally seem to do okay, so it's really a matter of taste.) From here on, I will refer to this non-branch tag as the branch point tag.

Notice also that a naming convention is being adhered to: The branch point tag begins with Root-of-, then the actual branch name, which uses underscores instead of hyphens to separate words. When the actual branch is created, its tag ends with the suffix -branch so that you can identify it as a branch tag just by looking at the tag name. (The branch point tag Root-of-Exotic_Greetings does not include the -branch because it is not a branch tag.) You don't have to use this particular naming convention, of course, but you should use some convention.

Of course, I'm being extra pedantic here. In smallish projects, where everyone knows who's doing what and confusion is easy to recover from, these conventions don't have to be used. Whether you use a branch point tag or have a strict naming convention for your tags depends on the complexity of the project and the branching scheme. (Also, don't forget that you can always go back later and update old tags to use new conventions by retrieving an old tagged version, adding the new tag, and then deleting the old tag.)

Now, qsmith is ready to start working on the branch:

paste$ cvs update -r Exotic_Greetings-branch 
cvs update: Updating . 
cvs update: Updating a-subdir 
cvs update: Updating a-subdir/subsubdir 
cvs update: Updating b-subdir 
paste$  

He makes some changes to a couple of files and commits them on the branch:

paste$ emacs README.txt a-subdir/whatever.c b-subdir/random.c 
... 
paste$ cvs ci -m "print greeting backwards, etc" 
cvs commit: Examining . 
cvs commit: Examining a-subdir 
cvs commit: Examining a-subdir/subsubdir 
cvs commit: Examining b-subdir 
Checking in README.txt; 
/usr/local/newrepos/myproj/README.txt,v  <--  README.txt 
new revision: 1.14.2.1; previous revision: 1.14 
done 
Checking in a-subdir/whatever.c; 
/usr/local/newrepos/myproj/a-subdir/whatever.c,v  <--  whatever.c 
new revision: 1.3.2.1; previous revision: 1.3 
done 
Checking in b-subdir/random.c; 
/usr/local/newrepos/myproj/b-subdir/random.c,v  <--  random.c 
new revision: 1.1.1.1.2.1; previous revision: 1.1.1.1 
done 
paste$  

Meanwhile, jrandom is continuing to work on the trunk. She modifies two of the three files that qsmith touched. Just for kicks, we'll have her make changes that conflict with qsmith's work:

floss$ emacs README.txt whatever.c 
 ... 
floss$ cvs ci -m "some very stable changes indeed" 
cvs commit: Examining . 
cvs commit: Examining a-subdir 
cvs commit: Examining a-subdir/subsubdir 
cvs commit: Examining b-subdir 
Checking in README.txt; 
/usr/local/newrepos/myproj/README.txt,v  <--  README.txt 
new revision: 1.15; previous revision: 1.14 
done 
Checking in a-subdir/whatever.c; 
/usr/local/newrepos/myproj/a-subdir/whatever.c,v  <--  whatever.c 
new revision: 1.4; previous revision: 1.3 
done 
floss$  

The conflict is not apparent yet, of course, because neither developer has tried to merge branch and trunk. Now, jrandom does the merge:

floss$ cvs update -j Exotic_Greetings-branch 
cvs update: Updating . 
RCS file: /usr/local/newrepos/myproj/README.txt,v 
retrieving revision 1.14 
retrieving revision 1.14.2.1 
Merging differences between 1.14 and 1.14.2.1 into README.txt 
rcsmerge: warning: conflicts during merge 
cvs update: Updating a-subdir 
RCS file: /usr/local/newrepos/myproj/a-subdir/whatever.c,v 
retrieving revision 1.3 
retrieving revision 1.3.2.1 
Merging differences between 1.3 and 1.3.2.1 into whatever.c 
rcsmerge: warning: conflicts during merge 
cvs update: Updating a-subdir/subsubdir 
cvs update: Updating b-subdir 
RCS file: /usr/local/newrepos/myproj/b-subdir/random.c,v 
retrieving revision 1.1.1.1 
retrieving revision 1.1.1.1.2.1 
Merging differences between 1.1.1.1 and 1.1.1.1.2.1 into random.c 
floss$ cvs update 
cvs update: Updating . 
C README.txt 
cvs update: Updating a-subdir 
C a-subdir/whatever.c 
cvs update: Updating a-subdir/subsubdir 
cvs update: Updating b-subdir 
M b-subdir/random.c 
floss$  

Two of the files conflict. No big deal; with her usual savoir-faire, jrandom resolves the conflicts, commits, and tags the trunk as successfully merged:

floss$ emacs README.txt a-subdir/whatever.c 
 ... 
floss$ cvs ci -m "merged from Exotic_Greetings-branch (conflicts resolved)"
cvs commit: Examining . 
cvs commit: Examining a-subdir 
cvs commit: Examining a-subdir/subsubdir 
cvs commit: Examining b-subdir 
Checking in README.txt; 
/usr/local/newrepos/myproj/README.txt,v  <--  README.txt 
new revision: 1.16; previous revision: 1.15 
done 
Checking in a-subdir/whatever.c; 
/usr/local/newrepos/myproj/a-subdir/whatever.c,v  <--  whatever.c 
new revision: 1.5; previous revision: 1.4 
done 
Checking in b-subdir/random.c; 
/usr/local/newrepos/myproj/b-subdir/random.c,v  <--  random.c 
new revision: 1.2; previous revision: 1.1 
done 
floss$ cvs tag merged-Exotic_Greetings 
cvs tag: Tagging . 
T README.txt 
T foo.gif 
T hello.c 
cvs tag: Tagging a-subdir 
T a-subdir/whatever.c 
cvs tag: Tagging a-subdir/subsubdir 
T a-subdir/subsubdir/fish.c 
cvs tag: Tagging b-subdir 
T b-subdir/random.c 
floss$  

Meanwhile, qsmith needn't wait for the merge to finish before continuing development, as long as he makes a tag for the batch of changes from which jrandom merged (later, jrandom will need to know this tag name; in general, branches depend on frequent and thorough developer communications):

paste$ cvs tag Exotic_Greetings-1 
cvs tag: Tagging . 
T README.txt 
T foo.gif 
T hello.c 
cvs tag: Tagging a-subdir 
T a-subdir/whatever.c 
cvs tag: Tagging a-subdir/subsubdir 
T a-subdir/subsubdir/fish.c 
cvs tag: Tagging b-subdir 
T b-subdir/random.c 
paste$ emacs a-subdir/whatever.c 
 ... 
paste$ cvs ci -m "print a randomly capitalized greeting" 
cvs commit: Examining . 
cvs commit: Examining a-subdir 
cvs commit: Examining a-subdir/subsubdir 
cvs commit: Examining b-subdir 
Checking in a-subdir/whatever.c; 
/usr/local/newrepos/myproj/a-subdir/whatever.c,v  <--  whatever.c 
new revision: 1.3.2.2; previous revision: 1.3.2.1 
done 
paste$  

And of course, qsmith should tag those changes once he's done:

paste$ cvs -q tag Exotic_Greetings-2 
T README.txt 
T foo.gif 
T hello.c 
T a-subdir/whatever.c 
T a-subdir/subsubdir/fish.c 
T b-subdir/random.c 
paste$  

While all this is going on, jrandom makes a change in a different file, one that qsmith hasn't touched in his new batch of edits:

floss$ emacs README.txt 
 ... 
floss$ cvs ci -m "Mention new Exotic Greeting features" README.txt 
Checking in README.txt; 
/usr/local/newrepos/myproj/README.txt,v  <--  README.txt 
new revision: 1.17; previous revision: 1.16 
done 
floss$  

At this point, qsmith has committed a new change on the branch, and jrandom has committed a nonconflicting change in a different file on the trunk. Watch what happens when jrandom tries to merge from the branch again:

floss$ cvs -q update -j Exotic_Greetings-branch 
RCS file: /usr/local/newrepos/myproj/README.txt,v 
retrieving revision 1.14 
retrieving revision 1.14.2.1 
Merging differences between 1.14 and 1.14.2.1 into README.txt 
rcsmerge: warning: conflicts during merge 
RCS file: /usr/local/newrepos/myproj/a-subdir/whatever.c,v 
retrieving revision 1.3 
retrieving revision 1.3.2.2 
Merging differences between 1.3 and 1.3.2.2 into whatever.c 
rcsmerge: warning: conflicts during merge 
RCS file: /usr/local/newrepos/myproj/b-subdir/random.c,v 
retrieving revision 1.1 
retrieving revision 1.1.1.1.2.1 
Merging differences between 1.1 and 1.1.1.1.2.1 into random.c 
floss$ cvs -q update 
C README.txt 
C a-subdir/whatever.c 
floss$  

There are conflicts! Is that what you expected?

The problem lies in the semantics of merging. Back in section An Overview of CVS, I explained that when you run

floss$ cvs update -j BRANCH 

in a working copy, CVS merges into the working copy the differences between BRANCH's root and its tip. The trouble with that behavior, in this situation, is that most of those changes had already been incorporated into the trunk the first time that jrandom did a merge. When CVS tried to merge them in again (over themselves, as it were), it naturally registered a conflict.

What jrandom really wanted to do was merge into her working copy the changes between the branch's most recent merge and its current tip. You can do this by using two -j flags to update, as you may recall from section An Overview of CVS, as long as you know what revision to specify with each flag. Fortunately, qsmith made a tag at exactly the last merge point (hurrah for planning ahead!), so this will be no problem. First, let's have jrandom restore her working copy to a clean state, from which she can redo the merge:

floss$ rm README.txt a-subdir/whatever.c 
floss$ cvs -q update 
cvs update: warning: README.txt was lost 
U README.txt 
cvs update: warning: a-subdir/whatever.c was lost 
U a-subdir/whatever.c 
floss$  

Now she's ready to do the merge, this time using qsmith's conveniently placed tag:

floss$ cvs -q update -j Exotic_Greetings-1 -j Exotic_Greetings-branch 
RCS file: /usr/local/newrepos/myproj/a-subdir/whatever.c,v 
retrieving revision 1.3.2.1 
retrieving revision 1.3.2.2 
Merging differences between 1.3.2.1 and 1.3.2.2 into whatever.c 
floss$ cvs -q update 
M a-subdir/whatever.c 
floss$  

Much better. The change from qsmith has been incorporated into whatever.c; jrandom can now commit and tag:

floss$ cvs -q ci -m "merged again from Exotic_Greetings (1)"
Checking in a-subdir/whatever.c; 
/usr/local/newrepos/myproj/a-subdir/whatever.c,v  <--  whatever.c 
new revision: 1.6; previous revision: 1.5 
done 
floss$ cvs -q tag merged-Exotic_Greetings-1 
T README.txt 
T foo.gif 
T hello.c 
T a-subdir/whatever.c 
T a-subdir/subsubdir/fish.c 
T b-subdir/random.c 
floss$  

Even if qsmith had forgotten to tag at the merge point, all hope would not be lost. If jrandom knew approximately when qsmith's first batch of changes had been committed, she could try filtering by date:

floss$ cvs update -j Exotic_Greetings-branch:3pm -j Exotic_Greetings_branch 

Although useful as a last resort, filtering by date is less than ideal because it selects the changes based on people's recollections rather than dependable developer designations. If qsmith's first mergeable set of changes had happened over several commits instead of in one commit, jrandom may mistakenly choose a date or time that would catch some of the changes, but not all of them.

There's no reason why each taggable point in qsmith's changes needs to be sent to the repository in a single commit -- it just happens to have worked out that way in these examples. In real life, qsmith may make several commits between tags. He can work on the branch in isolation, as he pleases. The point of the tags is to record successive points on the branch where he considers the changes to be mergeable into the trunk. As long as jrandom always merges using two -j flags and is careful to use qsmith's merge tags in the right order and only once each, the trunk should never experience the double-merge problem. Conflicts may occur, but they will be the unavoidable kind that requires human resolution -- situations in which both branch and trunk made changes to the same area of code.

The Dovetail Approach -- Merging In And Out Of The Trunk

Merging repeatedly from branch to trunk is good for the people on the trunk, because they see all of their own changes and all the changes from the branch. However, the developer on the branch never gets to incorporate any of the work being done on the trunk.

To allow that, the branch developer needs to add an extra step every now and then (meaning whenever he feels like merging in recent trunk changes and dealing with the inevitable conflicts):

paste$ cvs update -j HEAD 

The special reserved tag HEAD means the tip of the trunk. The preceding command merges in all of the trunk changes between the root of the current branch (Exotic_Greetings-branch) and the current highest revisions of each file on the trunk. Of course, qsmith should tag again after doing this, so that the trunk developers can avoid accidentally merging in their own changes when they're trying to get qsmith's.

The branch developer can likewise use the trunk's merge tags as boundaries, allowing the branch to merge exactly those trunk changes between the last merge and the trunk's current state (the same way the trunk does merges). For example, supposing jrandom had made some changes to hello.c after merging from the branch:

floss$ emacs hello.c 
 ... 
floss$ cvs ci -m "clarify algorithm" hello.c 
Checking in hello.c; 
/usr/local/newrepos/myproj/hello.c,v  <--  hello.c 
new revision: 1.22; previous revision: 1.21 
done 
floss$  

Then, qsmith can merge those changes into his branch, commit, and, of course, tag:

paste$ cvs -q update -j merged-Exotic_Greetings-1 -j HEAD 
RCS file: /usr/local/newrepos/myproj/hello.c,v 
retrieving revision 1.21 
retrieving revision 1.22 
Merging differences between 1.21 and 1.22 into hello.c 
paste$ cvs -q update 
M hello.c 
paste$ cvs -q ci -m "merged trunk, from merged-Exotic_Greetings-1 to HEAD" 
Checking in hello.c; 
/usr/local/newrepos/myproj/hello.c,v  <--  hello.c 
new revision: 1.21.2.1; previous revision: 1.21 
done 
paste$ cvs -q tag merged-merged-Exotic_Greetings-1 
T README.txt 
T foo.gif 
T hello.c 
T a-subdir/whatever.c 
T a-subdir/subsubdir/fish.c 
T b-subdir/random.c 
paste$  

Notice that jrandom did not bother to tag after committing the changes to hello.c, but qsmith did. The principle at work here is that although you don't need to tag after every little change, you should always tag after a merge or after committing your line of development up to a mergeable state. That way, other people -- perhaps on other branches -- have a reference point against which to base their own merges.

The Flying Fish Approach -- A Simpler Way To Do It

There is a simpler, albeit slightly limiting, variant of the preceding. In it, the branch developers freeze while the trunk merges, and then the trunk developers create an entirely new branch, which replaces the old one. The branch developers move onto that branch and continue working. The cycle continues until there is no more need for branch development. It goes something like this (in shorthand -- we'll assume jrandom@floss has the trunk and qsmith@paste has the branch, as usual):

floss$ cvs tag -b BRANCH-1 
paste$ cvs checkout -r BRANCH-1 myproj 

Trunk and branch both start working; eventually, the developers confer and decide it's time to merge the branch into the trunk:

paste$ cvs ci -m "committing all uncommitted changes" 
floss$ cvs update -j BRANCH-1 

All the changes from the branch merge in; the branch developers stop working while the trunk developers resolve any conflicts, commit, tag, and create a new branch:

floss$ cvs ci -m "merged from BRANCH-1" 
floss$ cvs tag merged-from-BRANCH-1 
floss$ cvs tag -b BRANCH-2 

Now the branch developers switch their working copies over to the new branch; they know they won't lose any uncommitted changes by doing so, because they were up-to-date when the merge happened, and the new branch is coming out of a trunk that has incorporated the changes from the old branch:

paste$ cvs update -r BRANCH-2 

And the cycle continues in that way, indefinitely; just substitute BRANCH-2 for BRANCH-1 and BRANCH-3 for BRANCH-2.

I call this the Flying Fish technique, because the branch is constantly emerging from the trunk, traveling a short distance, then rejoining it. The advantages of this approach are that it's simple (the trunk always merges in all the changes from a given branch) and the branch developers never need to resolve conflicts (they're simply handed a new, clean branch on which to work each time). The disadvantage, of course, is that the branch people must sit idle while the trunk is undergoing merge (which can take an arbitrary amount of time, depending on how many conflicts need to be resolved). Another minor disadvantage is that it results in many little, unused branches laying around instead of many unused non-branch tags. However, if having millions of tiny, obsolete branches doesn't bother you, and you anticipate fairly trouble-free merges, Flying Fish may be the easiest way to go in terms of mental bookkeeping.

Whichever way you do it, you should try to keep the separations as short as possible. If the branch and the trunk go too long without merging, they could easily begin to suffer not just from textual drift, but semantic drift as well. Changes that conflict textually are the easiest ones to resolve. Changes that conflict conceptually, but not textually, often prove hardest to find and fix. The isolation of a branch, so freeing to the developers, is dangerous precisely because it shields each side from the effects of others' changes...for a time. When you use branches, communication becomes more vital than ever: Everyone needs to make extra sure to review each others' plans and code to ensure that they're all staying on the same track.

Branches And Keyword Expansion -- Natural Enemies

If your files contain RCS keywords that expand differently on branch and trunk, you're almost guaranteed to get spurious conflicts on every merge. Even if nothing else changed, the keywords are overlapping, and their expansions won't match. For example, if README.txt contains this on the trunk

$Revision: 1.14 $ 

and this on the branch

$Revision: 1.14.2.1 $ 

then when the merge is performed, you'll get the following conflict:

floss$ cvs update -j Exotic_Greetings-branch
RCS file: /usr/local/newrepos/myproj/README.txt,v
retrieving revision 1.14
retrieving revision 1.14.2.1
Merging differences between 1.14 and 1.14.2.1 into README.txt
rcsmerge: warning: conflicts during merge
floss$ cat README.txt
 ... 
<<<<<<< README.txt 
key $Revision: 1.14 $ 
======= 
key $Revision: 1.14.2.1 $ 
>>>>>>> 1.14.2.1 
 ... 
floss$  

To avoid this, you can temporarily disable expansion by passing the -kk option (I don't know what it stands for; "kill keywords" maybe?) when you do the merge:

floss$ cvs update -kk -j Exotic_Greetings-branch 
RCS file: /usr/local/newrepos/myproj/README.txt,v 
retrieving revision 1.14 
retrieving revision 1.14.2.1 
Merging differences between 1.14 and 1.14.2.1 into README.txt 
floss$ cat README.txt 
 ... 
$Revision$ 
 ... 
floss$  

There is one thing to be careful of, however: If you use -kk, it overrides whatever other keyword expansion mode you may have set for that file. Specifically, this is a problem for binary files, which are normally -kb (which suppresses all keyword expansion and line-end conversion). So if you have to merge binary files in from a branch, don't use -kk. Just deal with the conflicts by hand instead.

Tracking Third-Party Sources (Vendor Branches)

Sometimes a site will make local changes to a piece of software received from an outside source. If the outside source does not incorporate the local changes (and there might be many legitimate reasons why it can't), the site has to maintain its changes in each received upgrade of the software.

CVS can help with this task, via a feature known as vendor branches. In fact, vendor branches are the explanation behind the puzzling (until now) final two arguments to cvs import: the vendor tag and release tag that I glossed over in section An Overview of CVS.

Here's how it works. The initial import is just like any other initial import of a CVS project (except that you'll want to choose the vendor tag and release tag with a little care):

floss$ pwd 
/home/jrandom/theirproj-1.0 
floss$ cvs import -m "Import of TheirProj 1.0" theirproj Them THEIRPROJ_1_0
N theirproj/INSTALL 
N theirproj/README 
N theirproj/src/main.c 
N theirproj/src/parse.c 
N theirproj/src/digest.c 
N theirproj/doc/random.c 
N theirproj/doc/manual.txt 

No conflicts created by this import 

floss$  

Then you check out a working copy somewhere, make your local modifications, and commit:

floss$ cvs -q co theirproj 
U theirproj/INSTALL 
U theirproj/README 
U theirproj/doc/manual.txt 
U theirproj/doc/random.c 
U theirproj/src/digest.c 
U theirproj/src/main.c 
U theirproj/src/parse.c 
floss$ cd theirproj 
floss$ emacs src/main.c src/digest.c 
 ... 
floss$ cvs -q update 
M src/digest.c 
M src/main.c 
floss$ cvs -q ci -m "changed digestion algorithm; added comment to main" 
Checking in src/digest.c; 
/usr/local/newrepos/theirproj/src/digest.c,v  <--  digest.c 
new revision: 1.2; previous revision: 1.1 
done 
Checking in src/main.c; 
/usr/local/newrepos/theirproj/src/main.c,v  <--  main.c 
new revision: 1.2; previous revision: 1.1 
done 
floss$  

A year later, the next version of the software arrives from Them, Inc., and you must incorporate your local changes into it. Their changes and yours overlap slightly. They've added one new file, modified a couple of files that you didn't touch, but also modified two files that you modified.

First you must do another import, this time from the new sources. Almost everything is the same as it was in the initial import -- you're importing to the same project in the repository, and on the same vendor branch. The only thing different is the release tag:

floss$ pwd 
/home/jrandom/theirproj-2.0 
floss$ cvs -q import -m "Import of TheirProj 2.0" theirproj Them THEIRPROJ_2_0
U theirproj/INSTALL 
N theirproj/TODO 
U theirproj/README 
cvs import: Importing /usr/local/newrepos/theirproj/src 
C theirproj/src/main.c 
U theirproj/src/parse.c 
C theirproj/src/digest.c 
cvs import: Importing /usr/local/newrepos/theirproj/doc 
U theirproj/doc/random.c 
U theirproj/doc/manual.txt 

2 conflicts created by this import. 
Use the following command to help the merge: 

       cvs checkout -jThem:yesterday -jThem theirproj 

floss$  

My goodness -- we've never seen CVS try to be so helpful. It's actually telling us what command to run to merge the changes. And it's almost right, too! Actually, the command as given works (assuming that you adjust yesterday to be any time interval that definitely includes the first import but not the second), but I mildly prefer to do it by release tag instead:

floss$ cvs checkout -j THEIRPROJ_1_0 -j THEIRPROJ_2_0 theirproj 
cvs checkout: Updating theirproj 
U theirproj/INSTALL 
U theirproj/README 
U theirproj/TODO 
cvs checkout: Updating theirproj/doc 
U theirproj/doc/manual.txt 
U theirproj/doc/random.c 
cvs checkout: Updating theirproj/src 
U theirproj/src/digest.c 
RCS file: /usr/local/newrepos/theirproj/src/digest.c,v 
retrieving revision 1.1.1.1 
retrieving revision 1.1.1.2 
Merging differences between 1.1.1.1 and 1.1.1.2 into digest.c 
rcsmerge: warning: conflicts during merge 
U theirproj/src/main.c 
RCS file: /usr/local/newrepos/theirproj/src/main.c,v 
retrieving revision 1.1.1.1 
retrieving revision 1.1.1.2 
Merging differences between 1.1.1.1 and 1.1.1.2 into main.c 
U theirproj/src/parse.c 
floss$  

Notice how the import told us that there were two conflicts, but the merge only seems to claim one conflict. It seems that CVS's idea of a conflict is a little different when importing than at other times. Basically, import reports a conflict if both you and the vendor modified a file between the last import and this one. However, when it comes time to merge, update sticks with the usual definition of "conflict" -- overlapping changes. Changes that don't overlap are merged in the usual way, and the file is simply marked as modified.

A quick diff verifies that only one of the files actually has conflict markers:

floss$ cvs -q update 
C src/digest.c 
M src/main.c 
floss$ cvs diff -c 
Index: src/digest.c 
===================================================================
RCS file: /usr/local/newrepos/theirproj/src/digest.c,v 
retrieving revision 1.2 
diff -c -r1.2 digest.c 
*** src/digest.c        1999/07/26 08:02:18     1.2 
-- src/digest.c        1999/07/26 08:16:15 
*************** 
*** 3,7 **** 
-- 3,11 ---- 
 void 
 digest () 
 { 
+ <<<<<<< digest.c 
   printf ("gurgle, slorp\n"); 
+ ======= 
+   printf ("mild gurgle\n"); 
+ >>>>>>> 1.1.1.2 
 } 
Index: src/main.c 
=================================================================== 
RCS file: /usr/local/newrepos/theirproj/src/main.c,v 
retrieving revision 1.2 
diff -c -r1.2 main.c 
*** src/main.c  1999/07/26 08:02:18     1.2 
-- src/main.c  1999/07/26 08:16:15 
*************** 
*** 7,9 **** 
-- 7,11 ---- 
 { 
   printf ("Goodbye, world!\n"); 
 } 
+  
+ /* I, the vendor, added this comment for no good reason. */ 
floss$  

From here, it's just a matter of resolving the conflicts as with any other merge:

floss$ emacs  src/digest.c  src/main.c 
 ... 
floss$ cvs -q update 
M src/digest.c 
M src/main.c 
floss$ cvs diff src/digest.c 
cvs diff src/digest.c  
Index: src/digest.c 
=================================================================== 
RCS file: /usr/local/newrepos/theirproj/src/digest.c,v 
retrieving revision 1.2 
diff -r1.2 digest.c 
6c6 
<   printf ("gurgle, slorp\n"); 
-- 
>   printf ("mild gurgle, slorp\n"); 
floss$  

Then commit the changes

floss$ cvs -q ci -m "Resolved conflicts with import of 2.0" 
Checking in src/digest.c; 
/usr/local/newrepos/theirproj/src/digest.c,v  <--  digest.c 
new revision: 1.3; previous revision: 1.2 
done 
Checking in src/main.c; 
/usr/local/newrepos/theirproj/src/main.c,v  <--  main.c 
new revision: 1.3; previous revision: 1.2 
done 
floss$  

and wait for the next release from the vendor. (Of course, you'll also want to test that your local modifications still work!)

The Humble Guru

If you read and understood (and better yet, experimented with) everything in this chapter, you may rest assured that there are no big surprises left for you in CVS -- at least until someone adds a major new feature to CVS. Everything you need to know to use CVS on a major project has been presented.

Before that goes to your head, let me reiterate the suggestion, first made in section Repository Administration, that you subscribe to the info-cvs@gnu.org mailing list. Despite having the impoverished signal-to-noise ratio common to most Internet mailing lists, the bits of signal that do come through are almost always worth the wait. I was subscribed during the entire time I wrote this chapter (indeed, for all previous chapters as well), and you would be amazed to know how many important details I learned about CVS's behavior from reading other people's posts. If you're going to be using CVS seriously, and especially if you're the CVS administrator for a group of developers, you can benefit a lot from the shared knowledge of all the other serious users out there.


Go to the first, previous, next, last section, table of contents.