CVS Tutorial

Originally written: a long time ago, probably sometime in 2001.
by John Clarke


Introduction

Note: I wrote this tutorial many years ago (I'm not sure exactly when, but probably some time in 2001) when CVS 1.10.8 was the current version. Therefore, some of the information in this tutorial may be out of date or incorrect. I've left it here purely for any historical (hysterical?) value it may have, and I've updated the links to point to the latest (v1.11.23 at the time I made the changes) CVS documentation because I'm no longer hosting the old manual.

These notes are intended as an introduction to CVS and some of its features. The full CVS documentation can be found here.

CVS stands for Concurrent Version System. It was designed to allow multiple developers to share access to the same source code, and to handle the potential conflicts that can result when two people modify the same file at the same time. You can find out more about what CVS is and what it isn't in the CVS documentation.

These notes use the cvs command line tools. For the GUI-dependent Windows users, WinCvs provides most of the same functionality.


Starting a Project

The procedure for starting a new project depends upon whether you have an existing set of files or you're really starting from scratch. Existing files may come from another configuration management system, or they mey never have been under configuration control. The import command is used to import the source into the repository.

Starting a Project From Existing Files

There's little difference between starting with files already under configuration control and files which are uncontrolled. All you need to do is copy out a working copy from your old repository first. If you want to copy the enitre revision history, then you'll need to start with the first revisions you checked in to your old repository rather than the latest revisions.

For this example, your working files are in ~/work/test1. The project is to be called "test1", and the directory within the repository is test/test1. Here's how you start a new project:

[johnc@dropbear ~/work/test1]$ ls
hello.c
[johnc@dropbear ~/work/test1]$ cvs import -m"Imported files to start the project" test/test1 vast start
N test/test1/hello.c

No conflicts created by this import
The parameters you need to pass to import are: The vendor and release tags are rather meaningless when creating a new project, but the import command requires them. They're more useful for tracking third-party sources.

Now you need to delete your old copy (or rename the directory), and checkout a working copy from the repository, because importing files doesn't setup the CVS sub-directories. Read this for details of what CVS puts in these sub-directories in your working directory.

Starting a Project From Scratch

In this case, you create your project's directory structure, then import as above. The import creates an empty directory structure in the repository to which you can add files later. The add command will be described later.

Defining the Module

This isn't strictly necessary, but it is more convenient to have a short name which refers to all the files and directories in a project. This is how you'd define a module called test1 which consists of all the files in the repository directory test/test1:

[johnc@dropbear ~/work]$ cvs co CVSROOT
cvs checkout: Updating CVSROOT
U CVSROOT/checkoutlist
U CVSROOT/commitinfo
U CVSROOT/config
U CVSROOT/cvsignore
U CVSROOT/cvswrappers
U CVSROOT/editinfo
U CVSROOT/loginfo
U CVSROOT/modules
U CVSROOT/notify
U CVSROOT/rcsinfo
U CVSROOT/tagcheck.pl
U CVSROOT/taginfo
U CVSROOT/users
U CVSROOT/verifymsg
This gets you a working copy of the CVS admin files. You don't need to know about most of them for now, indeed you'll probably never need to know about most of them. The file we're interested in is CVSROOT/modules. You need to add this line at the end of the file:
test1           test/test1
This defines the module test1 as consisting of the repository directory test/test1. Now commit the change:
[johnc@dropbear ~/work]$ cvs commit -m"Defined test1 module" CVSROOT/modules
Checking in CVSROOT/modules;
/home/cvsroot/CVSROOT/modules,v  <--  modules
new revision: 1.14; previous revision: 1.13
done
cvs commit: Rebuilding administrative file database
[johnc@dropbear ~/work]$ cvs release -d CVSROOT
You have [0] altered files in this repository.
Are you sure you want to release (and delete) directory `CVSROOT': y
The admin files are a little different from normal files in that there's a checked out copy of each file in the CVSROOT directory in the repository. The Rebuilding ... message indicates that CVS has updated these working copies. If you don't see this message, something's wrong.

Note also that I released the CVSROOT directory as soon as I'd made my change. You'll rarely, if ever, need to changes the admin files so it's a good idea not to keep a working copy around.


Checking Out a Working Copy

The first thing you'll need to do is checkout a working copy of a module. You do this with the checkout command, like this:

[johnc@dropbear ~/work]$ cvs checkout test2
cvs checkout: Updating test2
U test2/Makefile
U test2/alloc.c
U test2/alloc.h
U test2/avr.sh
U test2/breakpoint.c
U test2/breakpoint.h
...
cvs checkout: Updating test2
U test2/op/op1.c
U test2/op/op1.h
This creates a directory with the same name as the module, in this case "test2", containing all files and directories in the module.

The letter at the beginning of each line tells you what CVS has done with each file. A "U" means that the file has been brought up to date with respect to the repository, in other words, a copy of the latest revision of the file has been placed in your working directory. A more complete list of these codes is available here.

An important point to note here. Most CVS commands operate recursively unless told otherwise. Files or directories may be given as arguments, but if no argument is given, the current directory is assumed.

When you checkout files, CVS creates a directory, called CVS, in each directory within the module. The contents of these directories are described here.


Editing Files

Since CVS allows concurrent access to files, there's no distinction between "checkout" and "copyout". Once you have a working copy of the files, you can start editing. However, like all rules, there is an exception. The exception is "watched" files. Watches will be described later, but for now, you can ignore them.

Let's start with a simple change. We'll update the version number in version.h.


Checking the Status of Files

A Modified File

CVS will tell you the status of your working copy with the status command. Because it can produce a large amount of output, I'll demonstrate it with a couple of files:

[johnc@dropbear ~/work/test2]$ cvs status version.h
===================================================================
File: version.h         Status: Locally Modified

   Working revision:    1.34    Thu Jun 14 08:10:46 2001
   Repository revision: 1.34    /home/cvsroot/test2/version.h,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
The fields of interest to us at the moment are "Status", "Working revision" and "Repository revision". Since we've changed our working copy, the file status is "Locally Modified". The revision number of our working copy is "1.34", and the latest revision in the repository is also "1.34".

An Unmodified File

Compare this with a file that hasn't been changed:

[johnc@dropbear ~/work/test2]$ cvs status alloc.c
===================================================================
File: alloc.c       Status: Up-to-date

   Working revision:    1.4     Thu Jun  7 05:45:10 2001
   Repository revision: 1.4     /home/cvsroot/test2/alloc.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
Note the status of "Up-to-date" because this file is identical to the copy in the repository.

A File Which Has Been Changed By Someone Else

Now let's see what would happen if someone else had checked in a new version of a file whilst we've been working:

[johnc@dropbear ~/work/test2]$ cvs status main.c
===================================================================
File: main.c            Status: Needs Patch

   Working revision:    1.40    Mon Jun 25 05:46:16 2001
   Repository revision: 1.41    /home/cvsroot/test2/main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
The status is now "Needs Patch" because our working version is "1.40" but the version in the repository is "1.41". We'll fix this with the update command soon.

A File Which Has Been Changed By You And By Someone Else

Here's another example, but this time we've modified the file and there's a new version in the repository:

[johnc@dropbear ~/work/test2]$ cvs status test_main.c
===================================================================
File: test_main.c     Status: Needs Merge

   Working revision:    1.25    Mon Jun 25 07:28:41 2001
   Repository revision: 1.26    /home/cvsroot/test2/test_main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
Note that the status is now "Needs Merge" and the repository revision is higher than the working revision.


Viewing File Differences

You can ask CVS to show you the differences between your working copy and the repository with the diff command. By default, it will display the differences between your working files and the revision you last checked out. Note that this is not necessarily the latest revision.

The reasoning behind this is simple. Usually you use the diff command to view the changes you've made to your working copy. In this case, you're interested in the differences between your working copy and the revision it was based on. You don't want someone else's changes getting in the way.

Here's an example:

[johnc@dropbear ~/work/test2]$ cvs diff -Bbu
Index: version.h
===================================================================
RCS file: /home/cvsroot/test2/version.h,v
retrieving revision 1.34
diff -B -b -u -r1.34 version.h
--- version.h      2001/06/14 08:10:46     1.34
+++ version.h      2001/06/25 05:37:45
@@ -162,6 +162,6 @@
**-----------------------------------------------------------------------------
*/

-#define PROJECT_VERSION           "1.2.9.0"
+#define PROJECT_VERSION           "1.2.10.0"
 #endif
 
Note that there's no output for main.c, because although our working revision of main.c is not the latest, there are no differences between our working copy and the same revision in the repository.

You can override the default behaviour and view the differences between your working copy and the latest copy in the repository by specifying a revision number or tag. To get the latest revision, you use the magic tag HEAD, which always refers to the latest revision on the main trunk. You can also view the difference between any two arbitrary revisions by specifying the two revision numbers or tags (or one of each).


Checking In Your Changes

You check your changes in to the repository using the commit command:

[johnc@dropbear ~/work/test2]$ cvs commit version.h
Checking in version.h;
/home/cvsroot/test2/version.h,v  <--  version.h
new revision: 1.35; previous revision: 1.34
done
After the Checking in ... message, you will be asked to enter the change log message. Your editor will start (as defined by the EDITOR or VISUAL environment variables) and you should write a descriptive log message. This information should be enough to outline the changes you've made. A message such as "Bug fixes" or "Changes" is clearly inadequate. For our example, an acceptable log message would be "Updated the version number".

There's no need to completely detail the changes to each line. If someone wants to know the changes to that level, they can use the diff command. Your message should be a clear and concise description of what you did, for example:

   Replaced printf with fprintf.
   Changed GP->pHcb to a union.
            


Adding a New File to a Module

Adding a new file is a two stage process. First, you create the file. Then you tell CVS that there's a new file to be added with the add command. For example, we'll add a makefile to the test1 module we created earlier:

[johnc@dropbear ~/work/test1]$ cvs add Makefile 
cvs add: scheduling file `Makefile' for addition
cvs add: use 'cvs commit' to add this file permanently
Finally, you need to commit the new file:
[johnc@dropbear ~/work/test1]$ cvs commit -m"Initial revision" Makefile 
RCS file: /home/cvsroot/test/test1/Makefile,v
done
Checking in Makefile;
/home/cvsroot/test/test1/Makefile,v  <--  Makefile
initial revision: 1.1
done
Note: Unlike most other CVS commands, add is not recursive.


Updating Your Working Copy

Your Working Copy Is Unchanged

Earlier, we noticed that our working copy of main.c was out of date, so we use the update command to get the latest version:

[johnc@dropbear ~/work/test2]$ cvs update main.c
U main.c
[johnc@dropbear ~/work/test2]$ cvs status main.c
===================================================================
File: main.c            Status: Up-to-date

   Working revision:    1.41    Mon Jun 25 07:09:31 2001
   Repository revision: 1.41    /home/cvsroot/test2/main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
Note that the working and repository revisions are now the same.

Your Working Copy Has Been Changed

Now we'll try the same thing with the file we modified, test_main.c:

[johnc@dropbear ~/work/test2]$ cvs update test_main.c
RCS file: /home/cvsroot/test2/test_main.c,v
retrieving revision 1.25
retrieving revision 1.26
Merging differences between 1.25 and 1.26 into test_main.c
rcsmerge: warning: conflicts during merge
cvs update: conflicts found in test_main.c
C test_main.c
The status command also tells us that there were conflicts:
[johnc@dropbear ~/work/test2]$ cvs status test_main.c
===================================================================
File: test_main.c     Status: File had conflicts on merge

   Working revision:    1.26    Result of merge
   Repository revision: 1.26    /home/cvsroot/test2/test_main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
Now we need to fix the conflicts before we can commit our changes to the repository. Opening the file in the editor, we find the merge conflict indicated like this (note the conflict markers):
<<<<<<< test_main.c
SETLABEL(pRunInnerLoop2);
=======
SETLABEL(GP->pRunInnerLoop);
>>>>>>> 1.26
Our original working copy has been saved as .#test_main.c.1.25, but in this case, the conflict is so simple that we can easily resolve it. We change the code to read:
SETLABEL(GP->pRunInnerLoop2);

Now that the conflict has been resolved, we can commit our changes:

[johnc@dropbear ~/work/test2]$ cvs commit test_main.c
Checking in test_main.c;
/home/cvsroot/test2/test_main.c,v  <--  test_main.c
new revision: 1.27; previous revision: 1.26
done

Abandoning Your Changes

You can remove all changes you've made to your working copy by using the update command with the -C switch. This checks out a clean copy of the working file and saves your working file with a prefix of .# and a suffix which is the version number:

[johnc@dropbear ~/work/test2]$ cvs status main.c
===================================================================
File: main.c            Status: Locally Modified

   Working revision:    1.41    Tue Jun 26 04:01:57 2001
   Repository revision: 1.41    /home/cvsroot/test2/main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)

[johnc@dropbear ~/work/test2]$ cvs update -C main.c
(Locally modified main.c moved to .#main.c.1.41)
U main.c
[johnc@dropbear ~/work/test2]$ cvs status main.c
===================================================================
File: main.c            Status: Up-to-date

   Working revision:    1.41    Tue Jun 26 04:02:27 2001
   Repository revision: 1.41    /home/cvsroot/test2/main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)


Deleting Your Working Copy

To delete your working copy, you could simply remove the directory. However, to ensure that the transaction is logged in CVS's history database, and users who are watching part or all of the module are informed, it's preferrable to use the release command:

[johnc@dropbear ~/work]$ cvs release -d test2 
You have [0] altered files in this repository.
Are you sure you want to release (and delete) directory `test2': y
The -d switch tells CVS to delete the working directory.


Tags

What Are Tags?

A tag is a symbolic name for a revision. Tags are generally applied to the entire module at once rather than to individual files, and are used to mark significant points during development. For example, a released version would typically be tagged with the version number, possibly in conjunction with the module name.

Tagging Files

By default, a tag is applied to the revisions of files in your working directory. Note also that it is applied to the checked-in revisions, not including any changes which you haven't yet committed. Here's an example of the tag command:

[johnc@dropbear ~/work/test2]$ cvs tag CVS_DEMO
cvs tag: Tagging .
T Makefile
T alloc.c
T alloc.h
T avr.sh
T breakpoint.c
T breakpoint.h
...

Listing Tags

To find out what tags gave been applied to a file, use the status command in verbose mode:

[johnc@dropbear ~/work/test2]$ cvs status -v version.h
===================================================================
File: version.h         Status: Up-to-date

   Working revision:    1.35    Mon Jun 25 06:55:30 2001
   Repository revision: 1.35    /home/cvsroot/test2/version.h,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)

   Existing Tags:
        CVS_DEMO                        (revision: 1.35)
        v1_2_8_5                        (revision: 1.28.2.6)
        v1_2_8_4                        (revision: 1.28.2.5)
        v1_2_8_3                        (revision: 1.28.2.4)
        v1_2_8_2                        (revision: 1.28.2.3)
        v1_2_8_1                        (revision: 1.28.2.2)
        v1_2_8_0                        (revision: 1.28.2.1)
        v1_2_8_branch                   (branch: 1.28.2)
        v1_2_7_0                        (revision: 1.28)
        v1_2_6_3                        (revision: 1.27)
        v1_2_6_2                        (revision: 1.26)
        v1_2_6_1                        (revision: 1.26)
        v1_2_6_0                        (revision: 1.25)
   ...

Checking Out By Tag

To checkout a module or file by tag, use the -r switch to the checkout command. For example, to get release version 1.2.8.3 of the test2 module:

[johnc@dropbear ~/work]$ cvs co -r v1_2_8_3 test2
cvs checkout: Updating test2
U test2/Makefile
U test2/alloc.c
U test2/alloc.h
...

Sticky Tags

Now that we've checked out a particular tagged version, CVS will remember that fact and any commands will be relative to the tagged revision. This is indicated by the "Sticky Tag" field in the status output:

[johnc@dropbear ~/work/test2]$ cvs status version.h 
===================================================================
File: version.h         Status: Up-to-date

   Working revision:    1.28.2.4    Fri Jun  8 00:16:46 2001
   Repository revision: 1.28.2.4    /home/cvsroot/test2/version.h,v
   Sticky Tag:          v1_2_8_3 (revision: 1.28.2.4)
   Sticky Date:         (none)
   Sticky Options:      (none)
Further information on Sticky Tags is available here.

Watches

What Are Watches?

Watches are used to notify users when someone else does something. They can help multiple developers co-ordinate their activity, but they are not a substitute for proper project management and communication.

Watches have two distinct but related uses:

Forcing Read-Only Checkouts

To force all files to be checked out as read-only, use the watch on command. This setting can be disabled by issuing the watch off command.

To edit a file which is watched, use the edit command rather than simply changing the read-only attribute of your working file. This allows CVS to notify users which have requested notification of the edit action. We can't force you to do this, but be nice to your fellow developers. One day you may need them to help you.

If watches are on and you want to abandon some changes you've made, use the unedit command. This replaces your working copy with the same revision from the repository and notifies anyone who has an unedit watch on the file. Note that this only works if watches are on (i.e. you had to use the edit command before editing the file). If watches are off you should use update -C as explained earlier.

Enabling Watches

With the watch add command, you can enable notification one or more of the following actions by others:

Also allowed are two special keywords: The latter is only really useful when using the edit command to prevent a temporary edit or commit watch being established.

For example:

[johnc@dropbear ~/work/test2]$ cvs watch add -a edit -a commit version.h
[johnc@dropbear ~/work/test2]$ cvs watchers version.h 
version.h  johnc   edit    commit
Running watch add without any arguments adds all watches to all files recursively:
[johnc@dropbear ~/work/test2]$ cvs watch add
[johnc@dropbear ~/work/test2]$ cvs watchers version.h 
version.h  johnc   edit    unedit  commit

Removing Watches

Watches are removed using the watch remove command:

[johnc@dropbear ~/work/test2]$ cvs watch remove -a commit version.h
[johnc@dropbear ~/work/test2]$ cvs watchers version.h 
version.h  johnc   edit
Note that watches can be removed independently, even if they were established with the one command. Each watch is independent of the others.

Running watch remove without any arguments removes all watches from all files recursively:

[johnc@dropbear ~/work/test2]$ cvs watch remove
[johnc@dropbear ~/work/test2]$ cvs watchers version.h 

Branches

Why Branch?

A branch allows you to isolate changes into a separate line of development. Consider our test2 project. v1.2.7.0 was released on 27 April. Assume for the sake of this argument that one customer is still running this version and have reported a bug, and that the latest development is unstable and not suitable for release yet.

So, you check out v1.2.7.0 and find the bug:

[johnc@dropbear ~/work/test2]$ cvs update -r v1_2_7_0
cvs update: Updating .
U Makefile
U alloc.c
U alloc.h
...
cvs update: target.c is no longer in the repository
...
[johnc@dropbear ~/work/test2]$ cvs status version.h
===================================================================
File: version.h         Status: Up-to-date

   Working revision:    1.28    Tue Jun 26 05:53:12 2001
   Repository revision: 1.28    /home/cvsroot/test2/version.h,v
   Sticky Tag:          v1_2_7_0 (revision: 1.28)
   Sticky Date:         (none)
   Sticky Options:      (none)
Note the message about the file which is not in the repository. This file has been added since v1.2.7.0. Your working copy of this file is deleted because it's not part of the older version.

Creating the Branch

Once you've found the bug, you create and checkout the branch before you make your changes:

[johnc@dropbear ~/work/test2]$ cvs tag -b v1_2_7_branch
cvs tag: Tagging .
T Makefile
T alloc.c
T alloc.h
...
[johnc@dropbear ~/work/test2]$ cvs update -r v1_2_7_branch
Note that you need to check out the branch after creating it. This is because the branch is created in the repository, not in the working copy.

Checking In To the Branch

Now you make your changes and commit them:

[johnc@dropbear ~/work/test2]$ cvs commit
Checking in version.h;
/home/cvsroot/test2/version.h.,v  <--  version.h
new revision: 1.28.4.1; previous revision: 1.28
done
Checking in op/op1.c;
/home/cvsroot/test2/op/op1.c,v  <--  op1.c
new revision: 1.18.8.1; previous revision: 1.18
done
Note the new revision numbers: "1.28.4.1;" and "1.18.8.1;". Revision numbers with more than two parts refer to branches. You don't need to know about them; they're assigned automatically by CVS, but if you really want the gory details, they're described here.

Merging Between the Branch and the Trunk

Now that you've made a change on the new branch, how do you make sure that the change is propagated to the main trunk? You could simply make the same changes manually, but your changes may be quite complex. So, you can use CVS's merging abilities to do it for you. First, get the latest revisions:

[johnc@dropbear ~/work/test2]$ cvs update -A
cvs update: Updating .
U Makefile
U alloc.c
U alloc.h
...
Now, merge all changes from the branch into the current working copy:
[johnc@dropbear ~/work/test2]$ cvs update -j v1_2_7_branch
cvs update: Updating .
...
RCS file: /home/cvsroot/test2/version.h,v
retrieving revision 1.28
retrieving revision 1.28.4.1
Merging differences between 1.28 and 1.28.4.1 into version.h
...
RCS file: /home/cvsroot/test2/op/op1.c,v
retrieving revision 1.18
retrieving revision 1.18.8.1
Merging differences between 1.18 and 1.18.8.1 into op1.c
...
Fix any merge conflicts and commit the changes:
[johnc@dropbear ~/work/test2]$ cvs commit -m "Included v1.2.7.1 fix"
cvs commit: Examining .
Checking in version.h;
/home/cvsroot/test2/version.h,v  <--  version.h
new revision: 1.36 previous revision: 1.35
done
cvs commit: Examining flexlm_nt
cvs commit: Examining flexlm_nt/include
cvs commit: Examining flexlm_nt/lib
cvs commit: Examining loader
Checking in op1.c;
/home/cvsroot/test2/op/op1.c,v  <--  op1.c
new revision: 1.20 previous revision: 1.19
done
You can also merge from the branch several times, rather than all at once. There's a trick to managing it: tag the branch each time you do the merge, and use that tag to specify where to merge from.

Admin Files

Each CVS repository has a directory, called CVSROOT, which contains various admin files. Users will generally only need to change these files when a module is added or deleted. The file which contains the mapping between modules and files/directories is called CVSROOT/modules.

There other files will only need to be modified by the Configuration Manager to enforce any requirements on activities within the repository. For example, our test project may enforce the rule that a tag which looks like a version number (e.g. v1_2_7_0) must match the version number in version.h, it must be unique, and it may not be deleted or moved.

There's a complete guide to the admin files here.